本文转载自微信公众号「HelloGitHub」,作者HelloGitHub。转载本文请联系HelloGitHub公众号。大家好,这里是HelloGitHub推出的HelloZooKeeper系列,免费开源,有趣,入门级的ZooKeeper教程,面向有编程基础的新手。项目地址:https://github.com/HelloGitHub-Team/HelloZooKeeper在上一篇文章中,我们介绍了ZK是如何进行选举的。在这篇文章中,我们开始学习ZK如何将数据持久化到磁盘。1.优秀员工S(Sync)我们在之前的文章中介绍过,S(Sync)负责办公数据的归档,那么今天他就是我们的主角,让我们来了解一下他的日常工作为了唤醒大家古老的记忆,我放图之前先重点介绍一下图中蓝色部分,不过在此之前,首先要从整体结构上介绍一下ZK的数据管理。ZK的数据大致分为两部分,一是内存,一是磁盘文件。1.1内存虽然我们今天的主角是磁盘文件,但还是稍微提一下内存,帮助大家记忆,也能更全面的了解ZK整体的数据管理。ZK在内存中的存储就是前面故事中提到的两本账本:小红书和小黄书。如果排除小黄书作为回调通知记录,那么ZK的内存只是小红书对应的hash表,但是小黄书里面的数据还是很重要的,所以需要把两者看成一个整,而我之前也说过这两个账本是小F(Final)掌管的。小S(Sync)作为业务处理的最后一个负责人,在时间上优先于小F(Final),所以ZK的设计是重中之重。将数据保存到磁盘,然后修改内存中的数据,尽可能保证数据的可靠性。接下来继续了解磁盘文件(真的随便提一下!)1.2磁盘文件的开发者ZK为ZK设计了两种磁盘文件,对应的路径分别是zoo.cfg中的dataDir和dataLogDir这两个目录配置。配置。为了后面描述清楚,我把这两个磁盘文件命名为:dataDir对应snapshot,dataLogDir对应log,log是小S(Sync)工作的archive,snapshot是小S(Sync)工作的快照.日志负责顺序记录每次对文件的写请求,而快照则是直接将整个内存对象持久化到文件中。假设我当前zoo.cfg的配置如下:dataDir=/tmp/zookeeper/snapshotdataLogDir=/tmp/zookeeper/logZK启动时,会继续根据以上两条路径创建version-2子路径,后续所有文件会在这个Create/tmp└──zookeeper├──snapshot└──version-2└──...└──log└──version-2└──...2。文件的创建和写入这两个文件是什么时候写入磁盘的?写的内容是什么?接下来,我们将对这两个文件一一进行分析。2.1日志文件名格式为log.{zxid}zxid对应文件创建时的最大zxid。假设现在创建文件时zxid为0,目录结构是这样的:/tmp└──zookeeper└──log└──version-2└──log.0log.0文件的时序创建也可以简单理解为当服务端收到第一个写请求,当创建完成后,不能直接写入数据,必须先在文件头写入一些字段,比如著名的magicnumber,version号码和其他元信息。日志文件的magicnumber是ZKLG(4字节),版本号固定为2(4字节),0(8字节)也记录了一个dbId(目前没用,以后可能有用吧),所以前16个字节是这样固定的:ZKLG20A4B4C47000000020000000000000000后面的业务数据是怎么记录的呢?每个写请求可以分为四个部分:校验和、请求头、请求数据、签名,校验和是通过以下三个字段计算出来的。小S每收到一个写请求,就会将请求对应的四个字段按顺序写入日志文件。因为不同业务请求的数据是不固定的,而且数据的长度也比较大,所以这里就不给大家展示具体的值了(想了解硬核存储过程的可以离开我一个帖子,以后单独做,尽量逐字节解释)然后zookeeper.txnLogSizeLimitInKb这个环境变量配置,默认-1,这个配置限制单个日志文件的大小(单位是KB)),并且每归档一次小S(Sync)(图中右下角粉色部分“是否归档”),数据到达磁盘后会统一刷新,如果用户手动配置该参数,它会检查当前的日志文件大小是否超过了参数大小,如果超过了,就会执行rollLog,相当于为下一次写请求创建一个新的日志文件。另外小S(Sync)每次做snapshot的时候,都会强制执行一个rollLog。2.2快照文件快照文件名格式为snapshot.{zxid}zxid对应文件创建时的最大zxid。假设最大zxid为0,目录结构会是这样:/tmp└──zookeeper└──snapshot└──version-2└──snapshot.0至于是否快照(图中粉色部分)图中中间区域为“是否快照”),之前简单介绍过它与随机数有关。这一次,让我们深入了解一下。首先有两个配置zookeeper.snapCount(默认100000)和zookeeper.snapSizeLimitInKb(默认4194304单位是KB,相当于4GB)。启动后,会根据这两个配置生成两个随机数。假设上面的配置是按照默认设置的,这两个随机数的取值范围是:randRoll=[0,50000]randSize=[0,4194304*1024/2]可以简单的认为是一半以内的随机数以上两个配置,至于randSize为什么要乘以1024,因为最终的文件大小是以字节计算的。是否快照取决于以上两个随机数,有两个条件:当前写请求数达到zookeeper.snapCount的一半加上randRoll的数量当前日志文件大小达到zookeeper.snapSizeLimitInKb的一半加上大小randSize满足上述任一条件时,将重置上述两个随机数,并生成快照。生成快照的过程就是启动一个子线程来创建它。snapshot和log的另外一个区别是,快照文件ZK提供了三种不同的压缩实现,GZIP、SNAPPY、CHECKED,通过zookeeper.snapshot.compression.method配置。默认是CHECKED,也就是原来的字节顺序Write,另外两个这里就不展开了。那么我们来看看快照文件是如何记录的。和日志文件一样,必须先记录文件的一些头域,快照文件的幻数是ZKSN(4字节),版本号固定为2(4字节),一个dbId固定为-1(8字节)(目前没用,以后可能会有用),所以前16字节固定成这样:ZKSN2-15A4B534E00000002FFFFFFFFFFFFFFFF后面是一些客户端会话信息,客户端数量,然后记录sessionId和超时时间循环每个client,然后是小红本里面的所有信息,包括但不限于ACL,节点统计,节点数据,子节点信息等,最后是校验和签名。像log,如果大家有兴趣,后面我会逐字节单独讲解。3、从文件中恢复如果只是保存文件,那么文件是没有用的,所以文件的另一个重要用途就是帮助ZK恢复服务器上的信息。ZK在启动时会尝试读取dataDir和dataLogDir这两个目录下的文件,假设这两个路径下的文件是:/tmp└──zookeeper├──snapshot└──version-2└──snapshot。5└──snapshot.37└──snapshot.100└──log└──version-2└──log.0└──log.6└──log.38└──log.90└──log.108我这里例子中文件名的后缀号只是为了说明恢复过程。记住,情况可能并非如此。现在ZK服务器启动后,会先从快照目录中找到zxid最大的文件,然后根据内容恢复小红书。恢复后会到日志文件目录中查找所有大于100和略小于100的日志文件,本例中为log.90和log.108。你可能会问为什么要找一个小于100的log.90文件?因为文件名中的90只是表示创建文件时zxid最大为90,但是文件中记录的写请求很可能大于100,所以还需要找到log.90。然后从log.90文件开始恢复,先从zxid大于100的writerequest读取并执行writerequest,然后继续读取log.108,等待所有符合条件的log文件读取完毕,整个ZK数据恢复完成。4.总结今天我们介绍了ZK持久化的相关知识:ZK会持久化到磁盘的文件有两类:log和snapshotlog,负责记录每次写请求。先读取最新的快照文件然后根据快照最大的zxid查找符合条件的日志文件,然后通过一个一个的读写请求恢复剩余的数据基础知识~下一篇开始介绍上次选举没有介绍的内容:选举完成后,Follower和Observer如何与Leader同步数据?
