Apache Kafka 移除 ZK Proposals

时间:2022-12-27 17:08:24

Zookeeper 和 KRaft

这里有一篇 Kafka 功能改进的 proposal 原文。要了解移除 ZK 的原因,可以仔细看看该文章。以下是对该文章的翻译。

动机

目前,Kafka 使用 Zookeeper 保存与分区(patitions)、brokers 相关的元数据,以及选举 Kafka 控制器(某个 broker)。我们将移除对 Zookeeper 的依赖。如此一来,Kafka 在管理元数据方面,将获得更好的可扩展性和鲁棒性,同时支持更多的分区。在部署、配置 Kafka 方面,也将得到极大的简化。

将元数据视为 Event Log

我们常说将状态做为事件流管理的好处。一个在流中描述消费者位置的数字:offset。消费者通过回放 offset 之后的事件,就能获取最新状态。日志建立一套清晰、有序的事件机制,并确保每个消费者能获取到自己的时间线。

虽然我们的用户享受这些便利,但是忽略了 Kafka 本身。我们将作用到元数据的变更看作彼此孤立,互不相干。当控制器将状态变更通知到集群中的其他 broker 时,其他 broker 可能会收到一些变更,但不是全部变更。虽然控制器会重试几次,但最终会停止重试。这将导致 broker 之间处于不同步的状态。

更糟糕的是,虽然 Zookeeper 存储 record,但是 Zookeeper 中保存的状态经常与控制器保存在内存中的状态无法匹配。例如,当分区 leader 在 Zookeeper 中变更其 ISR(in-sync Replica)时,通常情况下,控制器会延误几秒钟才能获知其变更。对于控制器来说,没有通用的方法追踪 Zookeeper 的 event log。虽然控制器可以设置一次性守卫,但是守卫的数量由于性能问题会受到限制。当触发守卫时,守卫不负责通知控制器当前状态,仅仅是通知控制器状态发生了变更。同时,控制器重读 znode,然后设置一个新的守卫,但是,从最初守卫发出通知,到控制器完成重读,重新设置守卫期间,状态可能已经产生了新的变更。如果不设置守卫,控制器将永远无法得知变更。某些情况下,重启控制器是解决状态不一致的唯一手段。

元数据与其存储在独立的系统中,不如存储在 Kafka 中。这种情况下,控制器状态与 Zookeeper 状态之间和差异相关的问题将不复存在。与其挨个通知 broker,不如让 broker 们从 event log 中消费元数据事件。这样就确保了元数据变更能够按相同的顺序同步到 broker 中。broker 将元数据存储在本地文件中。当这些 broker 启动时,它们只需要从控制器中(某个 broker)中读取变更,而无需全量读取状态。在这种情况下,我们消耗更少的 CPU 资源就能获得更多分区。

简化部署与配置

Zookeeper 是一套独立的系统,有其配置文件语法,管理工具以及部署模式。这意味着系统管理员为了部署 Kafka,需要学习如何管理和部署两套独立的分布式系统。这对系统管理员来说,是非常艰巨的任务,尤其是在他们不熟悉部署 Java 服务的情况下。统一系统将极大地改善运行 Kafka 的初次体验,并有助于拓宽其应用范围。

由于 Kafka 和 Zookeeper 的配置文件是分离的,因此极易产生错误。例如,管理员在 Kafka 中设置了 SASL(Simple Authentication Security Layer,简单认证安全层),并且错误的认为对所有在网络中传输的数据都做了加密。事实上,还需要在外部系统 Zookeeper 中配置加密。统一两个系统将获得完整的加密配置模型。

最后,未来我们可能需要支持单节点 Kafka 模型。对于那些要测试 Kafka 功能的人来说,无需启动守护进程,将提供极大的便利性。移除 Zookeeper 依赖,将使其成为可能。

架构

介绍

本 KIP(Kafka Improvement Proposal,Kafka 改进 Proposal) 展现的是一个可扩展的后 Zookeeper 时代的 Kafka 系统的总体愿景。为了突出重要部分,我忽略了大多数细节,比如 RPC 格式、磁盘格式等等。在后续 KIP 中,我们将逐步深入描述细节。与 KIP-4 类似,提出总体愿景,后续的 KIP 中逐步扩充。

总览

Apache Kafka 移除 ZK Proposals

目前,一套 Kafka 集群包括几个 broker 节点,Zookeeper 节点做为一套外部 quorum (投票机制,少数服从多数)。我们画了 4 个 broker 节点和 3 个 Zookeeper 节点。这是小集群所需的正常配置。控制器(用橙色标识)在被选举后,从 Zoopeeper 的 quorum 中加载其状态。从控制器连接其他节点的线,在 broker 中代表更新控制器推送的消息,比如 LeaderAndIsr、UpdateMetadata 消息。

注意,这张图有误导的地方。除控制器以外,其他 broker 也可以与 Zookeeper 通信。因此,每个 broker 都应该画一条连接 ZK 的线。无论如何,画太多线将导致该图难以阅读。该图还忽略了,在不需要控制器介入的情况下,能够修改 Zookeeper 中的状态的外部命令行工具和工具包。正如上面讨论的那样,这些问题导致了控制器内存中状态无法真正的反映 Zookeeper 中的持久化状态。

在 Proposed 架构中,三个控制器节点取代了 Zookeeper 的 3 个节点。控制器节点和 broker 节点在不同的 JVM 中运行。控制器节点为元数据分区选举一个 leader 节点,用橙色标识。相较于控制器向各个 broker 推送元数据更新,在 Proposed 中,各个 broker 从 leader 中拉取元数据更新。这就是箭头指向控制器的原因。

注意,控制器进程与 broker 进程是逻辑隔离的,它们不必做物理隔离。在某些情况下,将部分或者全部控制器进程与 broker 进程部署在一个节点上,有其存在的意义。这和 Zookeeper 进程和 Kafka broker 部署在同一个节点上(目前小型集群的部署方式)类似。通常,各种各样的部署方式都可能出现,包括在同一个 JVM 中运行。

控制器 Quorum

控制器节点由管理元数据日志的 Raft quorum(Raft 选举机制)组成。该日志包括每次变更集群的元数据相关信息。目前,一切信息都存储在 Zookeeper 中,比如 topic、partition、ISR、配置等,在新的架构中,这些信息都将存在日志中。

通过 Raft 算法,控制器节点将在它们之间选举 leader,不需要依赖任何外部系统。元数据日志的 leader 被称作活动的(active)控制器。活动控制器处理所有来自 broker 的 RPC 调用。follower 控制器(相对 leader 控制来说)从活动控制器中复制所有写入的数据,并且当活动控制器故障时,做为热备(hot standbys)。由于控制器全量追踪最新状态,控制器故障切换将不再需要花很多时间转移最新状态到新的控制器上。

和 Zookeeper 一样,Raft 需要大多数节点能正常运行,才能正常工作。因此,3 个节点控制器集群允许一个节点失效。5 个节点的控制器集群允许两个节点失效,以此类推。

控制器将按周期将元数据快照写入磁盘。虽然在概念上和压缩相似,但是代码路径有些许不同,原因是我们从内存中读取状态,而不是从磁盘中重读日志。

管理 broker 元数据

不同于控制器将更新推送至各个 broker,这些 broker 将通过新的 MetadataFetch API 从活动控制器拉取更新。

MetadataFetch 与拉取请求类似。就和拉取请求一样,broker 将记录最近一次拉取的更新的 offset,并且只从活动控制器请求新的更新。

broker 将拉取到的元数据持久化至磁盘。这将使得 broker 启动的非常快,即使有成百上千分区,甚至上百万个分区。(注意,这种持久化是一种优化,如果忽略这种优化可以提高开发效率,那么我们可以在第一个版本中忽略它)

大多数时候,broker 只需要拉取增量状态(deltas),而不是全量状态。无论如何,如果 broker 的状态与活动控制器的状态差距过大,或者 broker 完全没有缓存元数据,控制器将返回全量元数据镜像,而不是返回一些列的增量数据。

Apache Kafka 移除 ZK Proposals

broker 按周期从活动控制器中请求元数据更新。该请求同时做为心跳发送,控制器以此得知该 broker 是存活状态。

注意,虽然本节只讨论管理 broker 的元数据,但是管理客户端的元数据对于可伸缩性也很重要。一旦发送增量元数据更新的基础设施搭建好后,这些基础设施将用于客户端和 broker。毕竟,一般情形下,客户端的数量会大于 broker 的数量。随着分区数量的增长,客户端感兴趣的分区也会越多,所以,以增量的方式将元数据更新交付给客户端将变得越来越重要。我们将在接下来的几个小节中讨论这个问题。

broker 状态机

目前,broker 在启动以后,马上在 Zookeeper 中注册自己。注册的过程完成两件事:告诉 broker 它是否被选举为控制器,让其他节点知道如何和它联系。

在后 Zookeeper 时代的世界里,broker 通过控制器 quorum 注册自己,而不是 Zookeeper。

当前,一个能够联系 Zookeeper ,但由控制器分区的 broker,能继续为用户的请求提供服务,但不会接收任何元数据更新。这将导致一些令人困惑、难以应对的情况。例如,一个 producer 通过 acks=1 继续发送数据给 leader,但实际上该 leader 已经不再是真正的 leader,但是这个失效的 leader 无法接收控制器的 LeaderAndIsrRequest,从而移除 leader 地位。

在后 ZK 时代的世界里,集群的成员关系集成在元数据更新中。如果 broker 无法接收元数据更新,将从集群的成员中移除。虽然该 broker 仍然可能被某个特殊的客户端分区,但如果该 broker 是由控制器分区的,仍将从集群中移除。

broker 状态

Apache Kafka 移除 ZK Proposals

Offline

当 broker 进程为 Offline 状态,它要么没有启动,要么在执行启动所需的单节点任务,比如,初始化 JVM 或者执行恢复日志。

Fenced

当 broker 处于 Fenced 状态,它将不再响应来自客户端的 RPC 请求。broker 在启动后,尝试拉取最新的元数据时,将处于 fenced 状态。如果无法联系活动控制器,broker 将重新进入 fenced 状态。发给客户端的元数据应该忽略状态为 fenced 的 broker。

Online

当 broker 状态为 online 时,表示该 broker 准备好响应客户端的请求了。

Stopping

broker 进入 stoppoing 状态表示它们收到 SIGINT 信号。该信号表明系统管理员要关闭 broker。

broker 在 stopping 状态时,仍在运行,但是我们尝试将分区 leader 从 broker 中移除。

最后,活动控制器在 MetadataFetchResponse 中添加一串特殊的代码,要求 broker 进入 offline 状态。或者,如果 leader 在预先定义的时间内没有动作,broker 将关闭。

将已有的 API 迁移到控制器中

之前的很多直接写入 Zookeeper 的操作将变为写入控制器。例如,变更配置、修改保存默认授权的 ACLs,等等。

新版本的客户端应该将这些操作直接发给活动控制器。这是一个向后兼容的变更:在新旧集群中都能正常工作。为了兼容老客户端,这些操作将随机发送给 broker,broker 将这些请求转发给活动控制器。

新的控制器 API

在某些情况下,我们需要创建一个新的 API 替换之前通过 Zookeeper 完成的操作。例如,当分区 leader 要修改 in-sync replica 集合时,在后 ZK 时代的世界里,它直接修改 Zookeeper,现在,leader 发起一个 RPC 请求到活动控制器。

从工具包中移除直接访问 Zookeeper

目前,一些工具和脚本直接联系 Zookeeper。在后 Zookeeper 时代的世界里,这些工具将被 Kafka API 取代。幸运的是,“KIP-4:命令行和中心化管理操作”,在几年前开始移除直接访问 Zookeeper,并且快完成了。