服务器使用事务日志来持久化事务。在接受一个提案之前,服务器(Follower和Leader)需要将提案中的事务持久化到事务日志中。事务日志是服务器本地磁盘的一个文件。事务按顺序附加到这个文件。服务器时不时的会关闭当前文件,创建一个新文件来滚动(Roll Over)日志 (这篇文章是Flavio Junqueria和Benjamin Reed的Zookeeper书的第9章中Local Storage/Logs and Disk use的翻译) 。
因为写事务日志是在写请求的关键路径上,所以Zookeeper需要高效地实现它。向文件附加信息可以在硬盘上高效完成,但Zookeeper使用一些其它技巧来使它更快的完成:
- 组提交(Group Commits)
- 补白(Padding)
组提交是将多个事务作为一次写附加到磁盘上。这种方式允许持久化多个事务只使用一次磁盘寻道(Disk Seek)的开销。
关于持久化事务到磁盘,这里有一个重要的告诫。现代操作系统通常缓存脏页(Dirty Page),并将它们异步写入磁盘介质。然而,我们需要在往下进行之前,确保事务已经被持久化。因此我们需要冲刷(Flush)事务到磁盘介质。冲刷在这里就是指我们告诉操作系统将脏页写入磁盘,并在操作完成后返回。因为我们在SyncRequestProcessor中持久化事务,所以这个处理器也负责冲刷。当是时候在SyncRequestProcessor中冲刷事务到磁盘,我们事实上是冲刷所有队列中的事务。这样可以实现组提交优化。如果队列中只有一个事务,这个处理器依然会执行冲刷。这个处理器不会等待更多的事务进入队列,因为如果这样做会增加执行延时。 代码参考可以查看SyncRequestProcessor.run()方法。
磁盘写缓存(Disk Write Cache)
服务器只有在强制将事务写入事务日志之后才确认对应的提案。更准确一点,服务器调用ZKDatabase的commit方法,这个方法最终会调用FileChannel.force。这样,服务器保证在确认事务之前已经将它持久化到磁盘中。 可是,这个观察有一个需要注意的地方。现代磁盘有一个保存将要写到磁盘的数据的写缓存。如果写缓存开启,force调用不能保证在返回后数据写入介质中。 反而, 它可能还在写缓存中。为了保证在FileChannel.force()方法返回后,写入的数据已经在介质上,磁盘写缓存必须关闭。操作系统有不同的关闭方式。
补白(Padding)通过预分配磁盘块到文件来实现。这样为块分配而对文件系统元数据的更新不会显著影响到顺序写文件。 如果正在高速向日志附加事务,而块没有预先分配到文件,那么无论何时到达了写入文件的结尾,文件系统都需要分配一个新块。补白至少会减少两次额外的磁盘寻道:一次是更新元数据;另一次是返回文件。
为了避免受到其它系统写操作的干扰,我们强烈推荐你将事务日志写入到一个独立的磁盘。并可以将第二块磁盘用作操作系统文件和Snapshot。