ZooKeeper的数据模型

data tree 模型

计算机最根本的作用其实就是处理和存储数据,作为一款分布式一致性框架,ZooKeeper 也是如此。
数据模型就是 ZooKeeper 用来存储和处理数据的一种逻辑结构。就像我们用 MySQL 数据库一样,要想处理复杂业务。
前提是先学会如何往里边新增数据。ZooKeeper 数据模型最根本的功能就像一个数据库。
现在,数据模型对我们来说还是一个比较抽象的概念,接下来我们开始部署一个开发测试环境,并在上面做一些简单的操作。
来看看 ZooKeeper 的数据模型究竟是什么样的:
配置文件:
tickTime=2000 dataDir=/var/lib/zookeeper clientPort=2181
服务启动
bin/zkServer.sh start
使用客户端连接服务器
bin/zkCli.sh -server 127.0.0.1:2181
这样单机版的开发环境就已经构建完成了,接下来我们通过 ZooKeeper 提供的 create 命令来创建几个节点,分别是:“/locks”“/servers”“/works”:
create /locks create /servers create /works
最终在 ZooKeeper 服务器上会得到一个具有层级关系的数据结构,如下图所示,这个数据结构就是 ZooKeeper 中的数据模型。
notion image
ZooKeeper 中的数据模型是一种树形结构,非常像电脑中的文件系统,有一个根文件夹,下面还有很多子文件夹。
ZooKeeper 的数据模型也具有一个固定的根节点(/)
我们可以在根节点下创建子节点,并在子节点下继续创建下一级节点。ZooKeeper 树中的每一层级用斜杠(/)分隔开,且只能用绝对路径(如“get /work/task1”)的方式查询 ZooKeeper 节点,而不能使用相对路径。
具体的结构看下面这张图:
notion image
为什么 ZooKeeper 不能采用相对路径查找节点呢?
这是因为 ZooKeeper 大多是应用场景是定位数据模型上的节点,并在相关节点上进行操作。
这种查找与给定值相等的记录问题最适合用散列来解决。
因此 ZooKeeper 在底层实现的时候,使用了一个 hashtable,即hashtable ConcurrentHashMap<String, DataNode> nodes ,用节点的完整路径来作为 key存储节点数据。这样就大大提高了 ZooKeeper 的性能。

底层代码

从数据存储的角度看,ZooKeeper 的数据模型是存储在内存中的。我们可以把 ZooKeeper 的数据模型看作是存储在内存中的数据库,而这个数据库不但存储数据的节点信息,还存储每个数据节点的 ACL 权限信息以及 stat 状态信息等。
而在底层实现中,ZooKeeper 数据模型是通过 DataTree 类来定义的。如下面的代码所示,DataTree 类定义了一个 ZooKeeper 数据的内存结构。DataTree 的内部定义类 nodes 节点类型、root 根节点信息、子节点的 WatchManager 监控信息等数据模型中的相关信息。可以说,一个 DataTree 类定义了 ZooKeeper 内存数据的逻辑结构。
public class DataTree { private DataNode root private final WatchManager dataWatches private final WatchManager childWatches private static final String rootZookeeper = "/"; }

znode 节点类型与特性

ZooKeeper 中的数据是由多个数据节点最终构成的一个层级的树状结构,和我们在创建 MySOL 数据表时会定义不同类型的数据列字段,ZooKeeper 中的数据节点也分为持久节点、临时节点和有序节点三种类型:
  1. 持久节点
    1. 这种节点也是在 ZooKeeper 最为常用的,几乎所有业务场景中都会包含持久节点的创建。之所以叫作持久节点是因为一旦将节点创建为持久节点,该数据节点会一直存储在 ZooKeeper 服务器上,即使创建该节点的客户端与服务端的会话关闭了,该节点依然不会被删除。
      如果我们想删除持久节点,就要显式调用 delete 函数进行删除操作。
  1. 临时节点
    1. 该节点的一个最重要的特性就是临时性。
      所谓临时性是指,如果将节点创建为临时节点,那么该节点数据不会一直存储在 ZooKeeper 服务器上。当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在 ZooKeeper 服务器上被删除。
      同样,我们可以像删除持久节点一样主动删除临时节点。
      在平时的开发中,我们可以利用临时节点的这一特性来做服务器集群内机器运行情况的统计,将集群设置为“/servers”节点,并为集群下的每台服务器 创建一个临时节点“/servers/host”,当服务器下线时该节点自动被删除,最后统计临时节点个数就可以知道集群中的运行情况。
      如图所示
      notion image
  1. 有序节点并不算是一种单独种类的节点,而是在之前提到的持久节点和临时节点特性的基础上,增加了一个节点有序的性质。
    1. 所谓节点有序是说在我们创建有序节点的时候,ZooKeeper 服务器会自动使用一个单调递增的数字作为后缀,追加到我们创建节点的后边。
      例如一个客户端创建了一个路径为 works/task- 的有序节点,那么 ZooKeeper 将会生成一个序号并追加到该节点的路径后,最后该节点的路径为 works/task-1。通过这种方式我们可以直观的查看到节点的创建顺序。
 
上述这几种数据节点虽然类型不同,但 ZooKeeper 中的每个节点都维护有这些内容:
一个二进制数组(byte data[]),用来存储节点的数据、ACL 访问控制信息、子节点数据(因为临时节点不允许有子节点,所以其子节点字段为 null)
除此之外每个数据节点还有一个记录自身状态信息的字段 stat。
zookeeper 3.5.x 中引入了 container 节点 和 ttl 节点(不稳定)
  1. container 节点用来存放子节点,如果container节点中的子节点为0 ,则container节点在未来会被服务器删除。
  1. ttl 节点默认禁用,需要通过配置开启, 如果ttl 节点没有子节点,或者 ttl 节点在 指定的时间内没有被修改则会被服务器删除。

节点的状态信息

每个节点都有属于自己的状态信息,我们打开之前的客户端,执行 stat /zk_test,可以看到控制台输出了一些信息,这些就是节点状态信息。
notion image
每一个节点都有一个自己的状态属性,记录了节点本身的一些信息,这些属性包括的内容列在了下面这个表格里:
notion image

数据节点的版本

在 ZooKeeper 中为数据节点引入了版本的概念,每个数据节点有 3 种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。
ZooKeeper 的版本信息表示的是对节点数据内容、子节点信息或者是 ACL 信息的修改次数。

使用ZooKeeper实现锁

设想这样一个情景:一个购物网站,某个商品库存只剩一件,客户 A 搜索到这件商品并准备下单,但在这期间客户 B 也查询到了该件商品并提交了购买,于此同时,客户 A 也下单购买了此商品,这样就出现了只有一件库存的商品实际上卖出了两件的情况。
为了解决这个问题,我们可以在客户A 对商品进行操作的时候对这件商品进行锁定从而避免这种超卖的情况发生。

悲观锁

悲观锁认为进程对临界区的竞争总是会出现,为了保证进程在操作数据时,该条数据不被其他进程修改。数据会一直处于被锁定的状态。
我们假设一个具有 n 个进程的应用,同时访问临界区资源,我们通过进程创建 ZooKeeper 节点 /locks 的方式获取锁。
线程 a 通过成功创建 ZooKeeper 节点“/locks”的方式获取锁后继续执行,如下图所示:
notion image
这时进程 b 也要访问临界区资源,于是进程 b 也尝试创建“/locks”节点来获取锁,因为之前进程 a 已经创建该节点,所以进程 b 创建节点失败无法获得 锁。
notion image
这样就实现了一个简单的悲观锁,不过这也有一个隐含的问题,就是当进程 a 因为异常中断导致 /locks 节点始终存在,其他线程因为无法再次创建节点而无法获取锁,这就产生了一个死锁问题。
针对这种情况我们可以通过将节点设置为临时节点的方式避免。并通过在服务器端添加监听事件来通知其他进程重新获取锁。

惊群效应

举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。类比多线程争抢获取分布式锁。
 
悲观锁使用临时顺序节点更优雅 避免惊群效应(羊群效应)

乐观锁

乐观锁认为,进程对临界区资源的竞争不会总是出现,所以相对悲观锁而言。
加锁方式没有那么激烈,不会全程的锁定资源,而是在数据进行提交更新的时候,对数据的冲突与否进行检测,如果发现冲突了,则拒绝操作。
乐观锁基本可以分为读取、校验、写入三个步骤。
CAS(Compare-And-Swap),即比较并替换,就是一个乐观锁的实现。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
在 ZooKeeper 中的 version 属性就是用来实现乐观锁机制中的“校验”的,ZooKeeper 每个节点都有数据版本的概念,在调用更新操作的时候,假如有一个客户端试图进行更新操作,它会携带上次获取到的 version 值进行更新。
而如果在这段时间内,ZooKeeper 服务器上该节点的数值恰好已经被其他客户端更新了,那么其数据版本一定也会发生变化,因此肯定与客户端携带的 version 无法匹配,便无法成功更新,因此可以有效地避免一些分布式更新的并发问题。
在 ZooKeeper 的底层实现中,当服务端处理 setDataRequest 请求时,首先会调用 checkAndIncVersion 方法进行数据版本校验。
ZooKeeper 会从setDataRequest 请求中获取当前请求的版本 version,同时通过 getRecordForPath 方法获取服务器数据记录 nodeRecord, 从中得到当前服务器上的版本信息 currentversion。如果 version 为 -1,表示该请求操作不使用乐观锁,可以忽略版本对比;
如果 version 不是 -1,那么就对比 version 和currentversion,如果相等,则进行更新操作,否则就会抛出 BadVersionException 异常中断操作。
notion image

运行中的 ZooKeeper 服务产生的数据

事务日志

为了整个 ZooKeeper 集群中数据的一致性,Leader 服务器会向 ZooKeeper 集群中的其他角色服务发送数据同步信息,在接收到数据同步信息后, ZooKeeper 集群中的 Follow 和 Observer 服务器就会进行数据同步。
而这两种角色服务器所接收到的信息就是 Leader 服务器的事务日志
在接收到事务日志后,并在本地服务器上执行。这种数据同步的方式,避免了直接使用实际的业务数据,减少了网络传输的开销,提升了整个 ZooKeeper 集群的执行性能。
 
在我们启动一个 ZooKeeper 服务器之前,首先要创建一个 zoo.cfg 文件并进行相关配置,其中有一项配置就是 dataLogDir 。在这项配置中,我们会指定该台 ZooKeeper 服务器事务日志的存放位置。
在 ZooKeeper 服务的底层实现中,是通过 FileTxnLog 类来实现事务日志的底层操作的。如下图代码所示,在 FileTxnLog 类中定义了一些属性字段,分别是:
  • preAllocSize:可存储的日志文件大小。如用户不进行特殊设置,默认的大小为 65536*1024 字节。
  • TXNLOG_MAGIC:设置日志文件的魔数信息为ZKLG。
  • VERSION:设置日志文件的版本信息。
  • lastZxidSeen:最后一次更新日志得到的 ZXID。
notion image
定义了事务日志操作的相关指标参数后,在 FileTxnLog 类中调用 static 静态代码块,来将这些配置参数进行初始化7。比如读取 preAllocSize 参数分配给日志文件的空间大小等操作。
static { LOG = LoggerFactory.getLogger(FileTxnLog.class); String size = System.getProperty("zookeeper.preAllocSize"); if (size != null) { try { preAllocSize = Long.parseLong(size) * 1024; } catch (NumberFormatException e) { LOG.warn(size + " is not a valid value for preAllocSize"); } } Long fsyncWarningThreshold; if ((fsyncWarningThreshold = Long.getLong("zookeeper.fsync.warningthresholdms")) == null) fsyncWarningThreshold = Long.getLong("fsync.warningthresholdms", 1000); fsyncWarningThresholdMS = fsyncWarningThreshold;
经过参数定义和日志文件的初始化创建后,在 ZooKeeper 服务器的 dataDir 路径下就生成了一个用于存储事务性操作的日志文件。我们知道在 ZooKeeper 服务运行过程中,会不断地接收和处理来自客户端的事务性会话请求,这就要求每次在处理事务性请求的时候,都要记录这些信息到事务日志中。
如下面的代码所示,在 FileTxnLog 类中,实现记录事务操作的核心方法是 append。从方法的命名中可以看出,ZooKeeper 采用末尾追加的方式来维护新的事务日志数据到日志文件中。append 方法首先会解析事务请求的头信息,并根据解析出来的 zxid 字段作为事务日志的文件名,之后设置日志的文件头信息 magic、version、dbid 以及日志文件的大小 。
public synchronized boolean append(TxnHeader hdr, Record txn) throws IOException { if (hdr == null) { return false; } if (hdr.getZxid() <= lastZxidSeen) { LOG.warn("Current zxid " + hdr.getZxid() + " is <= " + lastZxidSeen + " for " + hdr.getType()); } else { lastZxidSeen = hdr.getZxid(); } if (logStream==null) { if(LOG.isInfoEnabled()){ LOG.info("Creating new log file: log." + Long.toHexString(hdr.getZxid())); } logFileWrite = new File(logDir, ("log." + Long.toHexString(hdr.getZxid()))); fos = new FileOutputStream(logFileWrite); logStream=new BufferedOutputStream(fos); oa = BinaryOutputArchive.getArchive(logStream); FileHeader fhdr = new FileHeader(TXNLOG_MAGIC,VERSION, dbId); fhdr.serialize(oa, "fileheader"); // Make sure that the magic number is written before padding. logStream.flush(); currentSize = fos.getChannel().position(); streamsToFlush.add(fos); } padFile(fos); byte[] buf = Util.marshallTxnEntry(hdr, txn); if (buf == null || buf.length == 0) { throw new IOException("Faulty serialization for header " + "and txn"); } Checksum crc = makeChecksumAlgorithm(); crc.update(buf, 0, buf.length); oa.writeLong(crc.getValue(), "txnEntryCRC"); Util.writeTxnBytes(oa, buf); return true;
从对事务日志的底底层代码分析中可以看出,在 datadir 配置参数路径下存放着 ZooKeeper 服务器所有的事务日志,所有事务日志的命名方法都是“log.+ 该条事务会话的 zxid”。

数据快照

最后,我们来介绍 ZooKeeper 服务运行过程中产生的最后一个数据文件,即事务快照。
说到快照,可能很多技术人员都不陌生。一个快照可以看作是当前系统或软件服务运行状态和数据的副本。
在 ZooKeeper 中,数据快照的作用是将内存数据结构存储到本地磁盘中。因此,从设计的角度说,数据快照与内存数据的逻辑结构一样,都使用 DataTree 结构。
在 ZooKeeper 服务运行的过程中,数据快照每间隔一段时间,就会把 ZooKeeper 内存中的数据存储到磁盘中,快照文件是间隔一段时间后对内存数据的备份。
因此,与内存数据相比,快照文件的数据具有滞后性。而与上面介绍的事务日志文件一样,在创建数据快照文件时,也是使用 zxid 作为文件名称。
在代码层面,ZooKeeper 通过 FileTxnSnapLog 类来实现数据快照的相关功能。
如下图所示,在FileTxnSnapLog 类的内部,最核心的方法是 save 方法,在 save 方法的内部,首先会创建数据快照文件,之后调用 FileSnap 类对内存数据进行序列化,并写入到快照文件中。
public void save(DataTree dataTree, ConcurrentHashMap<Long, Integer> sessionsWithTimeouts, boolean syncSnap) throws IOException { long lastZxid = dataTree.lastProcessedZxid; File snapshotFile = new File(snapDir, Util.makeSnapshotName(lastZxid)); LOG.info("Snapshotting: 0x{} to {}", Long.toHexString(lastZxid), snapshotFile); snapLog.serialize(dataTree, sessionsWithTimeouts, snapshotFile, syncSnap); }

总结

我们知道在 ZooKeeper 服务的运行过程中,会涉及内存数据事务日志数据快照这三种数据文件。从存储位置上来说,事务日志和数据快照一样,都存储在本地磁盘上;而从业务角度来讲,内存数据就是我们创建数据节点、添加监控等请求时直接操作的数据。事务日志数据主要用于记录本地事务性会话操作,用于 ZooKeeper 集群服务器之间的数据同步。事务快照则是将内存数据持久化到本地磁盘。
这里要注意的一点是,数据快照是每间隔一段时间才把内存数据存储到本地磁盘,因此数据并不会一直与内存数据保持一致。在单台 ZooKeeper 服务器运行过程中因为异常而关闭时,可能会出现数据丢失等情况。

线上系统日志清理的最佳时间和方式

几乎所有的生产系统都会产生日志文件,用来记录服务的运行状态,在服务发生异常的时候,可以用来作为分析问题原因的依据。
ZooKeeper 作为分布式系统下的重要组件,在分布式网络中会处理大量的客户端请求,因此也会产生大量的日志文件,对这些问题的维护关系到整个 ZooKeeper 服务的运行质量。

日志类型

首先,我们先来介绍线上生产环境中的 ZooKeeper 集群在对外提供服务的过程中,都会产生哪些日志类型。我们在之前的课程中也介绍过了,在 ZooKeeper 服务运行的时候,一般会产生数据快照和日志文件,数据快照用于集群服务中的数据同步,而数据日志则记录了 ZooKeeper 服务运行的相关状态信息。其中,数据日志是我们在生产环境中需要定期维护和管理的文件。

清理方案

如上面所介绍的,面对生产系统中产生的日志,一般的维护操作是备份和清理。备份是为了之后对系统的运行情况进行排查和优化,而清理主要因为随着系统日志的增加,日志会逐渐占用系统的存储空间,如果一直不进行清理,可能耗尽系统的磁盘存储空间,并最终影响服务的运行。但在实际工作中,我们不能 24 小时监控系统日志情况,因此这里我们介绍一种定时任务,可以自动清理和备份 ZooKeeper 服务运行产生的相关日志。

清理工具

corntab

首先,我们介绍的是 Linux corntab ,它是 Linux 系统下的软件,可以自动地按照我们设定的时间,周期性地执行我们编写的相关脚本。下面我们就用它来写一个定时任务,实现每周定期清理 ZooKeeper 服务日志。

创建脚本

我们通过 Linux 系统下的 Vim 文本编辑器,来创建一个叫作 “ logsCleanWeek ” 的定时脚本,该脚本是一个 shell 格式的可执行文件。如下面的代码所示,我们在 usr/bin/ 文件夹下创建该文件,该脚本的主要内容是设定 ZooKeeper 快照和数据日志的对应文件夹路径,并通过 shell 脚本和管道和 find 命令 查询对应的日志下的日志文件,这里我们保留最新的 10 条数据日志,其余的全部清理。
#!/bin/bash dataDir=/home/zk/zk_data/version-2 dataLogDir=/home/zk/zk_log/version-2 ls -t $dataLogDir/log.* | tail -n +$count | xargs rm -f ls -t $dataDir/snapshot.* | tail -n +$count | xargs rm -f ls -t $logDir/zookeeper.log.* | tail -n +$count | xargs rm -f  find /home/home/zk/zk_data/version-2 -name "snap*" -mtime +1 | xargs rm -f                              find /home/home/zk/zk_data/version-2 -name "snap*" -mtime +1 | xargs rm -f               find /home/home/zk/zk_data/logs/ -name "zookeeper.log.*" -mtime +1 | xargs rm –f

创建定时任务

创建完定时脚本后,我们接下来就利用 corntab 来设置脚本的启动时间,如下面的代码所示。corntab 命令的语法比较简单,其中 -u 表示设定指定的用户,因为 Linux 系统是一个多用户操作系统,而 crontab 的本质就是根据使用系统的用户来设定程序执行的时间计划表。因此当命令的执行者具有管理员 root 账号的权限时,可以通过 -u 为特定用户设定某一个程序的具体执行时间。
crontab [ -u user ] { -l | -r | -e }
接下来我们打开系统的控制台,并输入 crontab -e 命令,开启定时任务的编辑功能。
如下图所示,系统会显示出当前已有的定时任务列表。整个 crontab 界面的操作逻辑和 Vim 相同,为了新建一个定时任务,我们首先将光标移动到文件的最后一行,并敲击 i 键来开启编辑模式。
notion image
这个 crontab 定时脚本由两部分组成,第一部分是定时时间,第二部分是要执行的脚本。如下代码所示,脚本的执行时间是按照 f1 分、 f2 小时、f3 日、f4 月、f5 一个星期中的第几天这种固定顺序格式编写的。
f1 f2 f3 f4 f5 program
当对应的时间位上为 * 时,表示每间隔一段时间都要执行。例如,当 f1 分上设定的是 * 时,表示每分钟都要执行对应的脚本。而如果我们想在每天的特定时间执行对应的脚本,则可以通过在对应的时间位置设定一个时间段实现,以下代码所演示的就是将脚本清理时间设定为每天早上的 6 点到 8 点。
0 6-8 * * * /usr/bin/logsCleanWeek.sh>/dev/null 2>&1

查看定时任务

当我们设定完定时任务后,就可以打开控制台,并输入 crontab -l 命令查询系统当前的定时任务。
notion image
到目前为止我们就完成了用 crontab 创建定时任务来自动清理和维护 ZooKeeper 服务产生的相关日志和数据的过程。
crontab 定时脚本的方式相对灵活,可以按照我们的业务需求来设置处理日志的维护方式,比如这里我们希望定期清除 ZooKeeper 服务运行的日志,而不想清除数据快照的文件,则可以通过脚本设置,达到只对数据日志文件进行清理的目的。

PurgeTxnLog

除了上面所介绍的,通过编写 crontab 脚本定时清理 ZooKeeper 服务的相关日志外, ZooKeeper 自身还提供了 PurgeTxnLog 工具类,用来清理 snapshot 数据快照文件和系统日志。
PurgeTxnLog 清理方式和我们上面介绍的方式十分相似,也是通过定时脚本执行任务,唯一的不同是,上面提到在编写日志清除 logsCleanWeek 的时候 ,我们使用的是原生 shell 脚本自己手动编写的数据日志清理逻辑,而使用 PurgeTxnLog 则可以在编写清除脚本的时候调用 ZooKeeper 为我们提供的工具类完成日志清理工作。
如下面的代码所示,首先,我们在 /usr/bin 目录下创建一个 PurgeLogsClean 脚本。注意这里的脚本也是一个 shell 文件。在脚本中我们只需要编写 PurgeTxnLog 类的调用程序,系统就会自动通过 PurgeTxnLog 工具类为我们完成对应日志文件的清理工作。
#!/bin/sh  java -cp "$CLASSPATH" org.apache.zookeeper.server.PurgeTxnLog echo "清理完成"
PurgeTxnLog 方式与 crontab 相比,使用起来更加容易而且也更加稳定安全,不过 crontab 方式更加灵活,我们可以根据不同的业务需求编写自己的清理逻辑。

ZooKeeper 数据存储底层实现解析

文件系统布局

无论是 ZooKeeper 服务在运行时候产生的数据日志,还是在集群中进行数据同步的时候所用到的数据快照,都可以被看作一种文件系统。而文件系统的两个功能就是对文件的存储和对不同文件格式的解析。ZooKeeper 中的数据存储,可以分为两种类型:数据日志文件和快照文件,接下来我们就分别介绍这两种文件的结构信息和底层实现。

数据日志

在 ZooKeeper 服务运行的过程中,数据日志是用来记录 ZooKeeper 服务运行状态的数据文件。通过这个文件我们不但能统计 ZooKeeper 服务的运行情况,更可以在 ZooKeeper 服务发生异常的情况下,根据日志文件记录的内容来进行分析,定位问题产生的原因并找到解决异常错误的方法。
如何找到日志文件呢?在 ZooKeeper 的 zoo.cfg 配置文件中的 dataLogDir 属性字段,所指定的文件地址就是当前 ZooKeeper 服务的日志文件的存储地址。
在了解了 ZooKeeper 服务在运行的过程中所产生的日志文件的存放位置,以及日志文件的格式结构后,接下来我们就深入到 ZooKeeper 服务的底层,来看一下它是如何实现日志的搜集以及存储的。

搜集日志

我们先来看一下 ,ZooKeeper 是如何搜集程序的运行信息的。在统计操作情况的日志信息中,ZooKeeper 通过第三方开源日志服务框架 SLF4J 来实现的。
SLF4J 是一个采用门面设计模式(Facade) 的日志框架。如下图所示,门面模式也叫作外观模式,采用这种设计模式的主要作用是,对外隐藏系统内部的复杂性,并向外部调用的客户端或程序提供统一的接口。门面模式通常以接口的方式实现,可以被程序中的方法引用。
在下图中,我们用门面模式创建了一个绘制几何图形的小功能。首先,定义了一个 Shape 接口类,并分别创建了三个类 Circle、Square、Rectangle ,以继承 Shape 接口。其次,我们再来创建一个画笔类 ShapeMaker ,在该类中我定义了 shape 形状字段以及绘画函数 drawCircle等。
notion image
之后,当我们在本地项目中需要调用实现的会话功能时,直接调用 ShapeMaker 类,并传入我们要绘制的图形信息,就可以实现图形的绘制功能了。它使用起来非常简单,不必关心其底层是如何实现绘制操作的,只要将我们需要绘制的图形信息传入到接口函数中即可。
而在 ZooKeeper 中使用 SLF4J 日志框架也同样简单,如下面的代码所示,首先在类中通过工厂函数创建日志工具类 LOG,然后在需要搜集的操作流程处引入日志搜集函数 LOG.info 即可。
protected static final Logger LOG = LoggerFactory.getLogger(Learner.class); LOG.info("Revalidating client: 0x" + Long.toHexString(clientId)); LOG.warn("Couldn't find the leader with id = " + current.getId());

存储日志

接下来我们看一下搜集完的日志是什么样子的。在开头我们已经说过,系统日志的存放位置,在 zoo.cfg 文件中。假设我们的日志路径为dataDir=/var/lib/zookeeper,打开系统命令行,进入到该文件夹,就会看到如下图所示的样子,所有系统日志文件都放在了该文件夹下。
notion image

快照文件

除了上面介绍的记录系统操作日志的文件外,ZooKeeper 中另一种十分重要的文件数据是快照日志文件。快照日志文件主要用来存储 ZooKeeper 服务中的事务性操作日志,并通过数据快照文件实现集群之间服务器的数据同步功能。

快照创建

接下来我们来介绍,在 ZooKeeper 的底层实现中,一个快照文件是如何创建的。
如下面的代码所示,在 ZooKeeper 的源码中定义了一个 SnapShot 接口类,在该接口中描述了 ZooKeeper 服务的相关属性和方法。其中 serialize 函数是用来将内存中的快照文件转存到本地磁盘中时的序列化操作。而 deserialize 的作用正好与其相反,是把快照文件从本地磁盘中加载到内存中时的反序列化操作。无论是序列化还是反序列化,整个快照所操作的数据对象是 ZooKeeper 数据模型,也就是由 Znode 组成的结构树。
复制代码
public interface SnapShot { long deserialize(DataTree dt, Map<Long, Integer> sessions) throws IOException; void serialize(DataTree dt, Map<Long, Integer> sessions, File name, boolean fsync) throws IOException; File findMostRecentSnapshot() throws IOException; void close() throws IOException; }

快照存储

创建完 ZooKeeper 服务的数据快照文件后,接下来就要对数据文件进行持久化的存储操作了。其实在整个 ZooKeeper 中,随着服务的不同阶段变化,数据快照存放文件的位置也随之变化。存储位置的变化,主要是内存和本地磁盘之间的转变。当 ZooKeeper 集群处理来自客户端的事务性的会话请求的时候,会首先在服务器内存中针对本次会话生成数据快照。当整个集群可以执行该条事务会话请求后,提交该请求操作,就会将数据快照持久化到本地磁盘中,如下图所示。
存储到本地磁盘中的数据快照文件,是经过 ZooKeeper 序列化后的二进制格式文件,通常我们无法直接查看,但如果想要查看,也可以通过 ZooKeeper 自带的 SnapshotFormatter 类来实现。如下图所示,在 SnapshotFormatter 类的内部用来查看快照文件的几种函数分别是: printDetails 函数,用来打印日志中的数据节点和 Session 会话信息;printZnodeDetails 函数,用来查看日志文件中节点的详细信息,包括节点 id 编码、state 状态信息、version 节点版本信息等。
public class SnapshotFormatter { private void printDetails(DataTree dataTree, Map<Long, Integer> sessions) private void printZnodeDetails(DataTree dataTree) private void printZnode(DataTree dataTree, String name) private void printSessionDetails(DataTree dataTree, Map<Long, Integer> sessions) private void printStat(StatPersisted stat) private void printHex(String prefix, long value) }
虽然 ZooKeeper 提供了 SnapshotFormatter 类,但其实现的查询功能比较单一,我们可以通过本节课的学习,按照自己的业务需求,编写自己的快照文件查看器。
到目前位置,我们对 ZooKeeper 服务相关的数据文件都做了讲解。
无论是数据日志文件,还是数据快照文件,最终都会存储在本地磁盘中。而从文件的生成方式来看,两种日志文件的不同是:数据日志文件实施性更高,相对的产生的日志文件也不断变化,只要 ZooKeeper 服务一直运行,就会产生新的操作日志数据;而数据快照并非实时产生,它是当集群中数据发生变化后,先在内存中生成数据快照文件,经过序列化后再存储到本地磁盘中。