ISR机制让我们可以增加或减少副本的数量,在可用性和性能之间做出权衡。可是扩大或缩小副本的集合的副作用是增大了 丢失数据 的可能性。
DistributedLog使用的是Quorum投票复制算法,这在Zab、Raft以及Viewstamped Replication等一致性算法中都很常见。日志流的属主会并发地把数据记录写入所有存储节点,并在得到超过配置数量的存储节点投票确认之后,才认为数据已成功提交。存储节点也只在数据被显式地调用flush操作刷入磁盘之后才会响应写入请求。日志流的属主也会维护一个日志流的最新提交的数据记录的偏移量,就是大家知道的Apache BookKeeper中的LAC(LastAddConfirmed)。LAC也会保存在数据记录中(来节省额外的RPC调用开销),并不断复制到别的存储节点上。DistributedLog中复本集合的大小是在每个流的每个日志分片级别可配置的。改变复制参数只会影响新的日志分片,不会影响已有的。
存储
每个Kafka分区都以若干个文件的形式保存在代理的磁盘上。它利用文件系统的页缓存和I/O调度机制来得到高性能。Kafka也是因此利用Java的sendfile API来高效地从代理中写入读出数据的。不过,在某些情况下(比如消费者处理不及时、随机读写等),页缓存中的数据淘汰很频繁,它的性能也有很大的不确性性。
DistributedLog用的则是不同的I/O模型。图三表示了Bookie(BookKeeper的存储节点)的I/O机制。写入(蓝线)、末尾读(红线)和中间读(紫线)这三种常见的I/O操作都被隔离到了三种物理上不同的I/O子系统中。所有写入都被顺序地追加到磁盘上的日志文件,再批量提交到硬盘上。在写操作持久化到磁盘上之后,它们就会放到一个Memtable中,再向客户端发回响应。Memtable中的数据会被异步刷新到交叉存取的索引数据结构中:记录被追加到日志文件中,偏移量则在分类账目的索引文件中根据记录ID索引起来。最新的数据肯定在Memtable中,供末尾读操作使用。中间读会从记录日志文件中获取数据。由于物理隔离的存在,Bookie节点可以充分利用网络流入带宽和磁盘的顺序写入特性来满足写请求,以及利用网络流出代宽和多个磁盘共同提供的IOPS处理能力来满足读请求,彼此之间不会相互干扰。
图三:BookKeeper的I/O隔离
小结
Kafka和DistributedLog都是设计来处理日志流相关问题的。它们有相似性,但在存储和复制机制上有着不同的设计理念,因此有了不同的实现方式。希望这篇文章能从技术角度解释清楚它们的区别,回答一些问题。我们接下来也会再多写一些文章来讲讲DistributedLog的性能指标。