Kafka设计原理--日志复制,日志压缩,副本管理

概述

本文讲述了kafka在其他方面,比如:消息传递语言,数据复制,数据压缩等方面的设计思想。

消息传递语义

一般来说可以提供多种消息传递的语义保证:
(1) 最多一次:消息可能会丢失,但不会重新传递。
(2) 至少一次:消息永远不会丢失,但可能会重新传递。
(3) 保证正确的一次:这是真正想要的,每个消息都传递一次,只有一次。

从这些语义来看,这里有两个问题:
(1) 保证publish消息的耐用性
(2) 保证消息的消费

  • publish消息的耐用性

    • 当消息的分区有一个复制的副本的broker存活时,消息就不会丢失
    • 如果生产者指定要等待正在提交的消息,则可能需要10毫秒。然而,生产者也可以指定它想要完全异步地执行发送,或者它只想等到领导者(但不一定是追随者)有消息。
  • 消息的消费

    • 读完消息后,先保存position到broker,然后再处理消息。做到的at-most-once语义
    • 读完消息后,先处理,成功后保存position到broker,做到at-least-once语义
    • 引入two-phase commit协议保证exactly once

复制(Replication)

topic的一个分区(partition)是Kafka进行复制(replication)的最小单元。正常情况下,每个分区都有一个leader和0个或者多个followers。包括领导者在内的,一个分区的所有replicas的个数被称为复制因子(replication factor)。
所有的读和写操作都由分区的leader来处理。
通常而言,集群中有很多个topic,每个topic也有多个分区,数量远多于集群的broker节点数,这些分区的leader们均匀分布在各个broker节点上。一个分区在follower上的日志和leader上的完全相同,包括:内容,offset,顺序等。
这样,当集群中有broker节点失败时,Kafka能够使得受到影响的partition自动在其replicas间进行故障转移(failover),从而保证了partition的高可用性。

故障的自动转移需要知道节点是否存活,对于Kafka而言,存活的节点需要满足两个条件:
(1)节点必须能够与ZooKeeper保持会话(通过ZooKeeper的心跳机制)
(2)如果它是一个slave,它必须复制在leader上发生的写操作,而不是落后于“太远”

我们把满足这两个条件的节点称为“同步中”的节点,避免和存活/失败混淆。若slave由于网络或其他原因导致无法工作,master将会把这些节点从slave同步(in sync)队列中删除。判断slave是否能够工作是通过参数:replica.lag.time.max.ms来进行配置的。

当一个消息的所有副本(replicas)都被确认(commited),才认为该消息已经被确认(commited)了。仅仅被commited的消息才能够被消费者消费。

commited的消息不会丢失,因为至少有一个repicas的消息存活。

复制日志:仲裁,ISR和状态机(Replicated Logs: Quorums, ISRs, and State Machines)

Kafka分区的核心是日志的复制(replicaed log)。复制日志是分布式数据系统中最基本的原语之一,是对一系列值的顺序达成一致性的处理过程的建模,就是分布式一致性协议所要解决的问题,有许多方法可以实现。
最简单的实现方式就是:一个leader负责确定值和其顺序,其他的follower只是简单的copy leader达成的结果。

当leader没有宕机时,我们不需要follower。当leader死掉时,需要从这些follower选择一个新的leader。当然,follower也可能阻塞或死掉,所以要保证参加选举的follower也是最新(up-to-date)的。日志复制算法(log replication algorithm)必须提供的基本保证是:如果我们告诉Client一个消息被提交(commited),并且leader失败,我们选择的新的领导者也必须有这个消息。

这就产生了一个权衡:如果leader在声明提交(commited)之前等待更多的follower来承认信息,那么将会有更多leader候选人,当然也就意味着更长的等待时间。

一个通常的做法对commit decision和leader election都采用多数投票(majority vote)(Kafka不是这样做的,但有必要了解一下)。

具体来说:假设我们有2f+1个replicas,如果现在要求一条消息在被leader commit前必须有f+1个replicas收到了这条消息,此时我们选择一个新的leader只需要从任意f+1个followers中选择log最完整的那个follower即可,因为这个新的leader一定有所有的committed log(因为任意f+1个follower中至少有一个有up-to-date的replica),这也意味着可以容忍 (2f+1) - (f+1) = f个节点失败。
这种majority vote方法在commit decision时具有一个很好的性质:延迟取决于最快的那些节点。

和这种算法相似的算法有多种,包括ZooKeeper的ZabRaftViewstamped Replication
我们了解到Kafka实际实施的最相似的是Microsoft的PacificA

majority vote的缺点在于,它可以容忍的失败节点数有限,这就是说:为了容忍一个节点失败集群中必须要有三份数据的拷贝;容忍两个失败则要求5份数据拷贝。经验表明在生成环境中只容忍一个节点失败是不够的,至少两个,但是对于大容量的数据存储系统而言,5份数据拷贝也是很不现实的,这也是为什么这种quorum算法通常用于集群配置管理(比如Zookeeper)而不是数据分布式存储。比如在HDFS中,namenode的高可用基于majority-vote算法,而数据本身并不是。

kafka在选择commit decison的quorum set时所采取的方法有点不同,它动态地维护一个in-sync replicas(ISR)集合,集合中所有的replica都catch up着leader。只有这个集合中的节点才能被选为leader。一个对partition的写操作只有当所有ISR中节点都收到后,才会被认为是committed。ISR集被持久化在ZK中。理想情况下,ISR集中有所有的follower,但只要ISR集中有一个节点,那么集群就被认为是可用的(其他节点都fail了)。因此,对于f+1个replicas而言,kafka可以容忍f个节点失败。相比majority vote而言,“延迟取决于最快的那些节点”这个性质就没有了。

Unclean leader election: What if they all die?

假设我们运气不好,所有的replicas都fail了,而且不巧leader也挂了需要选主,此时将面临两个选择:
(1)等待ISR集中的replica恢复过来,选择这个节点作为新的leader
(2)选择最先恢复过来replica(不一定是ISR的)作为leader

这就是一个简单的consistency和availability的tradeoff。默认情况下,kafka采用的是第二个策略,这种行为被称之为“unclean leader election”。当然kafka提供配置禁用掉这个行为。

可用性和耐用性(Availability and Durability Guarantees)

在向Kafka写入数据时,生产者可以选择:是否等待消息被0,1或全部(-1)replicas确认(即:同步还是异步发送)。
请注意,“acknowledgement by all replicas(被所有副本(repicas)确认)”不能保证全部已分配的repicas已收到该消息。默认情况下,当acks = all时,只要所有当前的in-sync副本收到消息,就会发生确认。例如,如果topic仅配置了:两个副本而一个失败(即,in-sync副本中只有一个仍然存在),那么指定acks = all的写入将成功。

但是,如果剩余的副本也失败,则这些写入可能会丢失。
虽然这确保了分区的最大可用性,但是对于喜欢耐用性而非可用性的一些用户来说,这种设计是不合适的。

因此,当durability的重要性高过availability时,我们提供了两种topic级的配置:

(1) 禁用不洁的领导者选举(Disable unclean leader election) - 如果所有副本(replicas)都不可用,则分区将保持不可用,直到最近的leader再次可用。
这有效地优先于消息丢失风险的不可用性。
请参阅上一节关于Disable unclean leader election的解释。
(2) 指定一个最小的(in-sync repicas)ISR集大小,只要在ISR集大小大于这个值时kafka才提供写操作(这个配置需要和producer在ack级别是all才能真正起作用)

副本管理(replica management)

上面关于Replicated log选主过程的讨论是仅针对单个topic的某个partition而言的,事实上,Kafka集群管理着成百上千个这样的partitions,分布在不同的broker节点上。也就是说集群中有很多的leaders,那么优化选主过程对可用性也是非常重要的。

Kafka在集群的所有broker中选择一个broker作为“controller”,由这个controller在broker级别负责检测其他broker节点的fail情况,并负责给那些failed的broker上受到影响的partitions选主。

这样做的结果就是所有选主操作都由controller负责,多个partition的leadership变动通知也可以做到批量发送,选主过程变得简单快速。如果controller挂掉了,那么剩下的broker节点将进行新的controller选举(即broker层面的选主)。

日志压缩(Log Compaction)

日志压缩可确保Kafka始终至少保留单个topic分区的数据日志中每个消息key的最后已知值。它解决了应用程序崩溃或系统故障后恢复状态,或者在运行维护期间重新启动应用程序后重新加载缓存这种场景下的问题。下面详细介绍这些场景,并说明日志压缩是如何工作的。
当数据日志到达某个时间点,或某个容量大小时老数据被删除,是一种简单的数据保留方法。然而,还有一种数据流是,key对应的数据不断变化的数据流,例如:数据库中表的改变。

总结

本文描述了Kafka在消息传递语义,日志复制,日志压缩,等方面的设计考虑,为进行深入研究该开源软件打下一个基础。

参考资料

https://kafka.apache.org/documentation/#design

猜你喜欢

转载自blog.csdn.net/zg_hover/article/details/81394689