Kafka needs no Keeper(关于KIP-500的讨论)
写在前面的
最近看了Kafka Summit上的这个分享,觉得名字很霸气,标题直接沿用了。这个分享源于社区的KIP-500,大体的意思今后Apache Kafka不再需要ZooKeeper。整个分享大约40几分钟。完整看下来感觉干货很多,这里特意总结出来。如果你把这个分享看做是《三国志》的话,那么姑且就把我的这篇看做是裴松之注吧:)
客户端演进
首先,社区committer给出了Kafka Java客户端移除ZooKeeper依赖的演进过程。下面两张图总结了0.8.x版本和0.11.x版本(是否真的是从0.11版本开始的变化并不重要)及以后的功能变迁:在Kafka 0.8时代,Kafka有3个客户端,分别是Producer、Consumer和Admin Tool。其中Producer负责向Kafka写消息,Consumer负责从Kafka读消息,而Admin Tool执行各种运维任务,比如创建或删除主题等。其中Consumer的位移数据保存在ZooKeeper上,因此Consumer端的位移提交和位移获取操作都需要访问ZooKeeper。另外Admin Tool执行运维操作也要访问ZooKeeper,比如在对应的ZooKeeper znode上创建一个临时节点,然后由预定义的Watch触发相应的处理逻辑。
后面随着Kafka的演进,社区引入了__consumer_offsets位移主题,同时定义了OffsetFetch和OffsetCommit等新的RPC协议,这样Consumer的位移提交和位移获取操作全部转移到与位移主题进行交互,避免了对ZooKeeper的访问。同时社区引入了新的运维工具AdminClient以及相应的CreateTopics、DeleteTopics、AlterConfigs等RPC协议,替换了原先的Admin Tool,这样创建和删除主题这样的运维操作也完全移动Kafka这一端来做,就像下面右边这张图展示的:
至此, Kafka的3个客户端基本上都不需要和ZooKeeper交互了。应该说移除ZooKeeper的工作完成了大部分,但依然还有一部分工作要在ZooKeeper的帮助下完成,即Consumer的Rebalance操作。在0.8时代,Consumer Group的管理是交由ZooKeeper完成的,包括组成员的管理和订阅分区的分配。这个设计在新版Consumer中也得到了修正。全部的Group管理操作交由Kafka Broker端新引入的Coordinator组件来完成。要完成这些工作,Broker端新增了很多RPC协议,比如JoinGroup、SyncGroup、Heartbeat、LeaveGroup等。
此时,Kafka的Java客户端除了AdminClient还有一点要依赖ZooKeeper之外,所有其他的组件全部摆脱了对ZooKeeper的依赖。
之后,社区引入了Kafka安全层,实现了对用户的认证和授权。这个额外的安全层也是不需要访问ZooKeeper的,因此之前依赖ZooKeeper的客户端是无法“享用”这个安全层。一旦启用,新版Clients都需要首先接入这一层并通过审核之后才能访问到Broker,如下图所示:
这么做的好处在于统一了Clients访问Broker的模式,即定义RPC协议,比如我们熟知的PRODUCE协议、FETCH协议、METADATA协议、CreateTopics协议等。如果后面需要实现更多的功能,社区只需要定义新的RPC协议即可。同时新引入的安全层负责对这套RPC协议进行安全校验,统一了访问模式。另外这些协议都是版本化的(versioned),因此能够独立地进行演进,同时也兼顾了兼容性方面的考量。
所以后面Broker端的演进可能和Clients端的路线差不多:首先是把Broker与ZooKeeper的交互全部干掉,只让Controller与ZooKeeper进行交互,而其他所有Broker都只与Controller交互,如下图所示:
看上去这种演进路线社区已经走得轻车熟路了,但实际上还有遗留了一些问题需要解决。
Broker Liveness
首先就是Broker的liveness问题,即Kafka如何判断一个Broker到底是否存活?在目前的设计中,Broker的生存性监测完全依赖于与ZooKeeper之间的会话。一旦会话超时或断开Controller自动触发ZooKeeper端的Watch来移除该Broker,并对其上的分区做善后处理。如果移除了ZooKeeper,Kafka应该采用什么机制来判断Broker的生存性是一个问题。
Network Partition
如何防范网络分区也是一个需要讨论的话题。当前可能出现的Network Partition有4种:1、单个Broker完全与集群隔离;2、Broker间无法通讯;3、Broker与ZooKeeper无法通讯;4、Broker与Controller无法通讯。下面4张图分别展示了这4种情况:
我们分别讨论下。首先是第一种情况,单Broker与集群其他Broker隔离,这其实并不算太严重的问题。当前的设计已然能够保证很好地应对此种情况。一旦Broker被隔离,Controller会将其从集群中摘除,虽然可用性降低了,但是整个集群的一致性依然能够得到保证。第二种情况是Broker间无法通讯,可能的后果是消息的备份机制无法执行,Kafka要收缩ISR,依然是可用性上的降低,但是一致性状态并没有被破坏。情况三是Broker无法与ZooKeeper通讯。Broker能正常运转,它只是无法与ZooKeeper进行通讯。此时我们说该Broker处于僵尸状态,即所谓的Zoobie状态。因Zoobie状态引入的一致性bug社区jira中一直没有断过,社区这几年也一直在修正这方面的问题,主要对抗的机制就是fencing。比如leader epoch等。最后一类情况是Broker无法与Controller通讯,那么所有的元数据更新通道被堵死,即使这个Broker依然是healthy的,但是它保存的元数据信息可能是非常过期的。这样连接该Broker的客户端可能会看到各种非常古怪的问题。之前在知乎上回答过类似的问题:
这样做的好处在于每次元数据的变更都被当做是一条消息保存在Log中,而这个Log可以被视作是一个普通的Kafka主题被备份到多台Broker上。Log的一个好处在于它有清晰的前后顺序关系,即每个事件发生的时间是可以排序的,配合以恰当的处理逻辑,我们就能保证对元数据变更的处理是按照变更发生时间顺序处理,不出现乱序的情形。另外Log机制还有一个好处是,在Broker间同步元数据时,我们可以选择同步增量数据(delta),而非全量状态。现在Kafka Broker间同步元数据都是全量状态同步的。前面说过了,当集群分区数很大时,这个开销是很可观的。如果我们能够只同步增量状态,势必能极大地降低同步成本。最后一个好处是,我们可以很容易地量化元数据同步的进度,因为对Log的消费有位移数据,因此通过监控Log Lag就能算出当前同步的进度或是落后的进度。
采用Log机制后,其他Broker像是一个普通的Consumer,从Controller拉取元数据变更消息或事件。由于每个Broker都是一个Consumer,所以它们会维护自己的消费位移,就像下面这张图一样:
这种设计下,Controller所在的Broker必须要承担起所有元数据topic的管理工作,包括创建topic、管理topic分区的leader以及为每个元数据变更创建相应的事件等。既然社区选择和__consumer_offsets类似的处理方式,一个很自然的问题在于这个元数据topic的管理是否能够复用Kafka现有的副本机制?答案是:不可行。理由是现有的副本机制依赖于Controller,因此Kafka没法依靠现有的副本机制来实现Controller——按照我们的俗语来说,这有点鸡生蛋、蛋生鸡的问题,属于典型的循环依赖。为了实现这个,Kafka需要一套leader选举协议,而这套协议或算法是不依赖于Controller的,即它是一个自管理的集群quorum(抱歉,在分布式领域内,特别是分布式共识算法领域中,针对quorum的恰当翻译我目前还未找到,因此直接使用quorum原词了)。最终社区决定采用Raft来实现这组quorum。这就是上面我们提到的第二个解决思路:Controller quorum。