【大数据哔哔集20210125】Kafka将逐步弃用对zookeeper的依赖

2021-02-23 16:11:17 浏览数 (1)

动机

目前,Kafka 使用 ZooKeeper 来保存与分区和broker相关的元数据,并选举出一个broker作为集群控制器。不过,Kafka 开发团队想要消除对 Zookeeper 的依赖,这样就可以以更可伸缩和更健壮的方式来管理元数据,从而支持更多的分区,还能够简化 Kafka 的部署和配置。

通过事件流的方式来管理状态确实有它的好处,比如用一个数字(即偏移量)来描述消费者在事件流中的处理位置。多个消费者通过处理比当前偏移量更新的事件快速地达到最新的状态。日志在事件之间建立了清晰的顺序,并确保消费者总是沿着一个时间轴移动。

在用户享受这些好处的同时,Kafka 却被忽略了。元数据变更被视为独立的变更,彼此之间没有联系。当控制器将状态变更通知(例如 LeaderAndIsrRequest)推送给集群中的其他代理时,有些代理可能会收到,但不是全部。控制器可能会重试几次,但最终还是会放弃,这可能会让代理处于不一致的状态。

更糟糕的是,虽然 ZooKeeper 被用来保存记录,但 ZooKeeper 中的状态通常与控制器内存中的状态不一致。例如,当首领分区在 ZooKeeper 中修改了 ISR 时,控制器通常会在很长一段时间内不知道这些更新。虽然控制器可以设置一次性的 watcher,但出于性能方面的考虑,设置 watcher 的次数是有限的。watcher 在触发时不会告诉控制器当前的状态,它只会告诉控制器状态已经发生了变化。当控制器重新读取 znode 并设置新的 watcher 时,状态可能与 watcher 触发时的状态不一样。如果不设置 watcher,控制器可能根本不知道发生了什么变化。在某些情况下,只能通过重启控制器来解决状态不一致问题。

元数据不应该被保存在单独的系统中,而应该直接保存在 Kafka 集群里,这样就可以避免所有因控制器状态和 Zookeeper 状态不一致而导致的问题。代理不应该接受变更通知,而是从事件日志中获取元数据事件。这样可以确保元数据变更始终以相同的顺序到达。代理可以将元数据保存在本地文件中,在重新启动时,它们只需要读取发生变化的内容,不需要读取所有的状态,这样就可以支持更多的分区,同时减少 CPU 消耗。

简化部署和配置

ZooKeeper 是一个独立的系统,有自己的配置文件语法、管理工具和部署模式。为了部署 Kafka,系统管理员需要学习如何管理和部署两个独立的分布式系统。对于管理员来说,这可能是一项艰巨的任务,特别是如果他们不太熟悉如何部署 Java 服务。统一的系统部署和配置将极大地改善 Kafka 的运维体验,有助于扩大其应用范围。

因为 Kafka 和 Zookeeper 的配置是分开的,所以很容易出错。例如,管理员可能在 Kafka 上设置了 SASL,并错误地认为这样就可以保护所有通过网络传输的数据。但事实上,为了保证数据安全,还需要在 ZooKeeper 系统中配置安全性。统一这两个系统的配置将会得到一个统一的安全配置模型。

最后,Kafka 将来可能会支持单节点部署模式。对于那些想快速测试 Kafka 但又不想启动多个守护进程的人来说,这是非常有用的,而移除对 ZooKeeper 的依赖有助于实现这个想法。

新的架构

目前,Kafka 集群通常包含多个代理节点和 ZooKeeper 仲裁节点。上图中有 4 个代理节点和 3 个 ZooKeeper 节点。控制器(橙色)从 ZooKeeper 仲裁节点加载状态。从控制器到其他代理节点的线表示控制器向它们推送更新,比如 LeaderAndIsr 和 UpdateMetadata 消息。

注意,上图省略掉了一些东西。控制器之外的其他代理也可以与 Zookeeper 通信,所以应该从每个代理到 ZooKeeper 都画一条线,但画太多线会让图表看起来太复杂。另一个问题是,外部命令行工具可以不通过控制器直接修改 ZooKeeper 中的状态,所以很难知道控制器内存中的状态是否真正反映了 ZooKeeper 中的状态 。 在新的架构中,三个控制器节点代替了原先的三个 ZooKeeper 节点。控制器节点和代理节点运行在单独的 JVM 中。控制器节点选举出一个首领负责处理元数据。控制器不会将更新推送给代理,而是让代理从首领控制器获取元数据更新,所以箭头从代理指向了控制器,而不是从控制器指向代理。

控制器仲裁

控制器节点包含了一个 Raft 仲裁节点,负责管理元数据日志。这个日志包含了集群元数据的变更信息。原先保存在 ZooKeeper 中的所有内容,例如主题、分区、ISRs、配置等等,都将被保存在这个日志中。

控制器节点基于 Raft 算法选举首领,不依赖任何外部系统。选举出的首领叫作主控制器。主控制器处理所有来自代理的 RPC。从控制器从主控制器复制数据,并在主控制器发生故障时充当热备份。

和 ZooKeeper 一样,Raft 需要大多数节点可用才能继续运行。因此,一个三节点的控制器集群可以忍受一个节点出现故障,一个五节点的控制器集群可以允许两个节点出现故障,并以此类推。

控制器定期将元数据快照写入磁盘。虽然从概念上看这类似于压缩,但代码路径却有所不同,因为新的架构可以直接从内存中读取状态,而不是从磁盘中重新读取日志。

代理的元数据管理

代理将通过新的 MetadataFetch API 从主控制器获取更新,而不是让控制器向代理推送更新。

MetadataFetch 类似于 fetch 请求。与 fetch 请求一样,代理将跟踪上次获取数据的偏移量,并且只从主控制器获取更新的更新。

代理将获取的元数据保存到磁盘上,这样代理就可以快速启动,即使有数十万甚至数百万个分区(请注意,由于这种持久化机制是一种优化,所以有可能不会在第一个版本中出现)。

大多数情况下,代理只需要获取增量更新,而不是完整的状态更新。不过,如果代理落后主控制器太多,或者代理根本没有缓存元数据,那么主控制器将会向代理发送完整的元数据镜像,而不是增量更新。

代理将定期向主控制器请求元数据更新。这种请求同时作为心跳,让控制器知道代理还活着。

代理状态机

目前,代理在启动时会在 Zookeeper 中注册自己。这个注册动作完成了两件事:让代理知道自己是否被选为控制器,也让其他节点知道如何与被选为控制器的节点通信。

在移除 ZooKeeper 之后,代理将通过 MetadataFetch API 在控制器仲裁节点上注册自己,而不是在 ZooKeeper 中。

目前,如果代理丢失了与 ZooKeeper 之前的会话,控制器会将其从集群元数据中删除。在移除 ZooKeeper 之后,如果代理在足够长的时间内没有发送元数据心跳,主控制器将从集群元数据中删除代理。

目前,与 ZooKeeper 保持连接但与主控制器隔离的代理可以继续服务用户请求,但不会接收到任何元数据更新,这可能会导致一致性问题。例如,配置了 acks=1 的生产者可能继续向首领(但这个首领可能已经不是首领了)发送数据,而且无法接收到 LeaderAndIsrRequest 通知。

在移除了 ZooKeeper 之后,集群成员关系与元数据更新被集成在一起。如果代理无法接收元数据更新,就不能继续作为集群的成员。 代理的状态

0 人点赞