分布式日志系统从头实现 - 数据复制

查看原文

分布式日志系统的一个特性是要满足高可用,以避免单点故障(SPOF),而为了保障数据的高可用,我们的解决方法是复制数据。复制数据的手段不外乎两种:gossip/multicast 协议,或者consensus 协议,至于算法,则有 2PC/3PC, Paxos, Raft, Zab, 链式复制等等。在分布式系统中,由于日志的顺序是重要的,所以我们一致性是更优的做法,这就要求我们引入:Leader 负责给写操作排序;复制写操作给集群剩下的节点。至于 Leader 什么时候认为操作结束又引入了两种设计:1) 等待所有节点都同步了;2)等待多数节点都同步了。例如对于 Kafka,使用 1), 对于 NATS Streaming,使用 2)。本文就详细对比了这两种实现。

Kafka Leader 维护了一个 In-sync replica set (ISR)。有点像数据库的 binlog,Kafka 来消息了,把消息写到 Write-ahead log (WAL),相当于未提交的脏数据,之后同步到所有 replicas,完成提交。WAL 里面有一条 High-water Mark (HW) 标记最近提交的消息。数据恢复会从这个标记作为检查点开始恢复。只有当数据提交了才能是有效的,对于发消息的这方来说倒是可以自选是等提交还是发完不管提交是否成功(取决于业务场景的 latency 和 durability)。Kafka 依赖 ZooKeeper 协调 Leader 选举,特别是当 Leader 崩溃以后,而在 Failover 的过程中一些未提交的消息可能会丢失。Follower 崩溃后超过一定时间,Leader 会将其移除中 ISR,以保障整个复制过程的正常进行。另外, 我们也有可能遭遇网络分区,这个处理跟移除崩溃的 Follower 一样,会移出 ISR,等其恢复后再让其加入继续跟进数据复制的同步直到跟上当前日志的 HW。

NATS Streaming 依赖 Raft 一致性算法完成 Leader 选举和数据复制。Raft 将日志看作状态机,核心操作是 set & delete,它在一个协议里解决了 Leader 选举和数据同步。NATS 使用多个 Raft 组用于复制消息,好处是 相当有了 Kafka Replicated WAL ,也不再需要额外引入 ZooKeeper 这样的协调服务(coordination service)。在 Raft 中,Leader 用心跳维持 leadership,用 AppendEntries 消息向 Followers 发更新,用 commit index 记录提交的 high-water mark。在 NATS Streaming 中,提交到 raft log 中后还会继续提交到 NATS Streaming Log 里面。

这里面还有性能和数据持久性的考量。例如上面提到的,什么时候 publisher 完成 ack 操作呢?可以是最慢的完全提交后 ack,可以是其次的当添加进 local log,甚至可以是完全不等就 ack,当然,相应的,离数据丢失也就越来越近。这需要考虑到场景。我们可以在 broker 上禁用 fsync 操作,而依靠 replication 来保障数据持久,因为写硬盘是个很费时间的操作。批量操作可以提高性能。最后,硬盘顺序读和 zero-copy reads 也能提高很多性能。

这篇文章提到了很多数据复制要考虑的问题,也提供了很多外链,是篇很不错的导论型的文章。