kafka的集群管理

123 阅读15分钟

本章内容

  • 使用 KRaft 与 ZooKeeper 进行 Kafka 集群管理
  • 从 ZooKeeper 迁移到 KRaft
  • 客户端如何连接到 Kafka

在前面的章节中,我们已经把 Kafka 作为一个分布式系统介绍过,并多次提到在此类系统中进行协调总是充满挑战。这些问题不仅影响 Kafka 本身,也会影响我们在使用消费者组时的消费者。幸运的是,Kafka 为我们承担了大量工作,我们无需为如何可靠地将分区分配给消费者而烦恼。

但实际中 Kafka 是如何被管理的?我们为何需要集群管理?集群管理系统要解决的核心挑战是分布式系统中如何达成一致(consensus)。当我们只有一个 broker 时,这很简单:不用与任何人达成一致;该 broker 对所有分区都是 leader,总是同步,是所有消费者组的组协调者,等等。但当我们增加更多 broker 时,如何保证状态一致?谁来将分区分配到各个 broker、监控 broker 的健康状态、以及在 broker 不再汇报时将其移出集群?谁负责存储集群的当前配置?

在 Kafka 中,有一个 broker 会作为控制器(controller)来管理这些元数据并向所有 broker 发出指令。至于如何选出这个控制器,我们当然可以在启动 Kafka 时指定一个控制器,但如果该控制器失败、过慢或出现其他问题,又该如何处理?

近几年 Kafka 发生了重大变化。以前用于管理元数据与达成共识的方法经历了巨变:先前使用 Apache ZooKeeper(zookeeper.apache.org/)的做法已被新方法取代。由于我们认为基于ZooKeeper 的旧版 Kafka 还会被长期使用,下面我们仍会同时讨论这两种方法。

在本章的最后部分,我们将检查如何连接到 Kafka 集群,包括处理元数据请求与在分布式环境中建立可靠连接,确保 Kafka 生态系统内的无缝集成与通信。

7.1 Apache Kafka Raft 集群管理

从 Kafka 3.3.0 起,Kafka Raft(KRaft;kafka.apache.org/documentati…)—Kafka的新协调机制—已达到生产就绪状态;从 Kafka 3.5.0 起,ZooKeeper 在协调方面被弃用。请注意,对基于 ZooKeeper 的集群的支持将在 Kafka 4.0 中移除。

正如 KRaft 名称所暗示的那样,它基于 Raft 协议。该协议首次由 Diego Ongaro 和 John Ousterhout 在 2014 年的论文 “In Search of an Understandable Consensus Algorithm” 中描述。分布式系统中的一致性问题非常困难,Kafka 社区为实现这一新方法花费了很长时间。在 KRaft 出现之前,Kafka 使用 ZooKeeper(我们在 7.2 节中会更详细地介绍)。

ZooKeeper 曾让运维工程师寝食难安,原因有几方面。首先,运行 Kafka 的团队需要同时运维两个分布式系统:Kafka 与 ZooKeeper,这增加了运维负担。更重要的是,ZooKeeper 对 Kafka 来说太慢了,会限制 Kafka 可管理的分区数量,从而影响其可扩展性和可靠性。这并不是说 ZooKeeper 是糟糕的软件,而是 ZooKeeper 在其内部日志数据结构之上提供了许多抽象,而 Kafka 本身已经非常擅长处理日志,这些额外抽象对 Kafka 来说并非必要。

因此,Kafka 社区设计了新的架构。现在 Kafka 的 broker 可以配置为两种不同的处理器角色:controller 和 broker。我们不太喜欢这些命名,因为在 Kafka 领域它们可能含义不同。如果一个 broker 被配置为 controller 角色,它就成为协调集群(coordination cluster)的成员。与 ZooKeeper 一样,我们需要一个奇数个数量的 controller 节点。对于本地开发环境,一个 controller-broker 足够,但不提供故障转移保证。如果我们希望在一个 controller-broker 故障时仍能继续运行,就需要三个 controller-broker;如果有五个 controller-broker,则可以容忍两个节点故障。

该协调集群负责选举一个活动控制器(active controller, 在 ZooKeeper–Kafka 体系中通常简称为 controller),该控制器监控所有普通 broker、管理分区与 leader 分配,并在出现故障时更改这些分配。处于非活动状态的控制器称为备用控制器(standby controllers)。如果活动控制器失败,备用控制器中的某一个接管工作并能立即继续管理集群。在基于 ZooKeeper 的集群中,控制器故障可能会导致数分钟的中断,具体取决于集群规模。

为进行协调,现在有一个新的主题叫 __cluster_metadata。这个特殊主题存储了以前保存在 ZooKeeper 中并且对 Kafka 运行至关重要的所有元数据。该主题会在所有 broker 之间复制,无论每个 broker 的处理器角色如何,通过这种方式,所有 broker 都知道它们需要做什么。

普通 broker 则是我们之前讨论的那些执行常规工作的节点:它们接受生产者和消费者请求、复制分区,并负责在 Kafka 中存储所有数据。图 7.1 描述了生产环境下推荐的最小集群配置。

image.png

提示:我们建议使用三个或五个控制器节点以提高可靠性,并且至少配备三个普通 broker。(与三个控制器相比,四个控制器并不会带来额外好处,因为仍然只能容忍一个控制器的故障。)将控制器与普通 broker 分离可以更容易地上下扩展 broker,并且总体上简化运维。对于非生产环境,也可以使用同时具有控制器和 broker 角色的节点(即 controller-brokers)以节省资源,但这样会丧失灵活性。对于本地开发环境,甚至可以在同一个 Java 虚拟机(JVM)进程中以组合模式同时运行单个 broker 和控制器。

7.2 ZooKeeper 集群管理

在 KRaft 达到生产就绪之前,Kafka 依赖 ZooKeeper 来进行集群管理。尽管基于 ZooKeeper 的集群管理支持将在 Kafka 4.0 中移除,但我们认为在相当长一段时间内仍会有大量 Kafka 集群依赖 ZooKeeper。

提示:我们建议尽快将此类集群迁移到 KRaft。

ZooKeeper 是一个独立的 Apache 项目,被许多软件产品使用,比如 Apache Hadoop MapReduce 与 Neo4j。ZooKeeper 初看起来像是一个分层的键值存储。类似于操作系统中的目录与文件,我们可以创建、读取和修改 ZNode。

但 ZooKeeper 的关键特性在于它保证所有 ZooKeeper 节点始终具有完全相同的状态。例如,如果多个客户端同时尝试写入同一个 ZooKeeper ZNode,最终只有一个写入会被接受,所有客户端都会看到相同的结果。我们在 Kafka 中利用这一点来决定控制器。例如,如果 ZooKeeper 中没有 controller 的 ZNode,所有 broker 都会尝试向该 ZNode 写入自己的 ID,最终说服 ZooKeeper 的那个 broker 就成为新的控制器。

此外,ZooKeeper 还能监控哪些客户端当前仍保持连接,从而帮助我们检测 broker 故障。为可靠地协调这些变化,ZooKeeper 基于一种 Paxos 变体的一致性协议,称为 ZooKeeper 原子广播(ZooKeeper Atomic Broadcast,ZAB)。要达成一致,ZooKeeper 节点多数必须就某个值达成共识,而其余节点则接受这一占优意见。

Kafka 使用 ZooKeeper 来选举控制器,并将与 Kafka 集群相关的元数据存储在 ZooKeeper 中——具体包括哪个分区分配给哪个 broker、哪个 broker 是哪个分区的 leader 等信息。分区 leader 会向 ZooKeeper 写入当前哪些 follower 是 in-sync(同步)的信息。此外,由于 Kafka 也在 ZooKeeper 中存储访问控制列表(ACL)及其他安全相关信息,因此需对 ZooKeeper 进行额外加固。

图 7.2 示意了一个使用 ZooKeeper 的 Kafka 集群示例:该集群包含三个 ZooKeeper 节点的 ZooKeeper 集群(ensemble)和四个 Kafka broker。控制器只是这些 broker 中的一个,同时承担某些管理任务。这与基于 KRaft 的 Kafka 不同:在 KRaft 中,活动控制器始终属于控制器集群的一部分;若该控制器失败,另一个 broker 会接管。请注意,其他 broker 也会直接与 ZooKeeper 通信,上图中的箭头仅表示主要的通信路径。

image.png

没有 ZooKeeper,就很难想象 Apache Kafka 会以现在的形式出现——ZooKeeper 负责许多复杂的任务,独立实现这些任务代价很高。然而,依赖 ZooKeeper 也带来了显著的挑战。ZooKeeper 的设计更看重可靠性和一致性而不是速度,因此相对较慢,在某些方面会制约 Kafka 的性能。对于运维人员来说,ZooKeeper 增加了复杂度,因为维护和理解这个额外且往往不熟悉的系统需要大量精力。

其中一个主要的矛盾是:Kafka 的理念是把日志作为在系统之间交换消息的优雅数据结构来使用,但同时又依赖 ZooKeeper 来存储自身的元数据。这种依赖会引入性能瓶颈和一致性问题。为减轻对性能的影响,broker 经常在本地缓存元数据,而不是对每次操作都去查询 ZooKeeper。但这种做法存在不一致的风险:如果 broker 没有与 ZooKeeper 正确同步,本地缓存的数据可能已经过时。

当 broker 基于过期信息做出决策时,就会出现问题。例如,controller 可能会将某个分区的 leader 指派给一个其实已经不可用的 broker,或者根据 ZooKeeper 中陈旧的数据做出不恰当的副本分配。这类情况可能发生,因为某些 broker 操作(例如更新分区状态或管理 topic 配置)是直接在 ZooKeeper 中执行的,并不经过 controller。

为了维持性能,controller 并不会持续不断地监视 ZooKeeper 的变化,而是采用周期性轮询的方式。这种轮询机制会产生时间延迟,在此期间 controller 可能会错过关键更新或对集群状态的变化反应过慢。这些延迟会导致基于过期信息做出决策,进而损害 Kafka 集群的一致性和可靠性。

7.3 从 ZooKeeper 迁移到 KRaft

现在 KRaft 相对于 ZooKeeper 的优势已经很明确,下一步就是从 ZooKeeper 迁移到 KRaft。幸运的是,迁移可以在无停机的情况下完成,但仍需精心规划与执行。总体流程是先将现有的 Kafka 集群升级到仍然支持 ZooKeeper 的最新版本(按照官方升级流程进行),然后按本节所述从 ZooKeeper 迁移到 KRaft,最后再升级到最新的 Kafka 版本(同样按正常升级流程)。迁移到 KRaft 有一个限制:只支持迁移到专用的 KRaft controller 集群,因此需要操作的机器数量不会减少。但这不应阻止我们进行迁移。

官方文档中有详细的迁移步骤(mng.bz/yW6p),这里我们仅从概念层面介绍流程。

首先,需要预配将来用作 controller-broker 的机器。通常从三台或五台额外的、配置为 KRaft 控制器的 broker 开始。重要的是这些控制器要使用与 ZooKeeper 集群相同的 cluster ID,开启迁移模式(zookeeper.metadata.migration.enable=true),并提供 ZooKeeper 的连接信息(zookeeper.connect=<Zookeeper 地址>)。

接着把迁移相关的配置加入现有 broker 中——启用迁移模式并把新控制器节点的信息写入配置。所有 broker 都配置好后,迁移就会开始。新的控制器节点会从 ZooKeeper 复制元数据,运行一段时间后,broker 日志中应出现如下消息:
Completed migration of metadata from ZooKeeper to KRaft
注意,此时普通 broker 仍处于 ZooKeeper 模式,我们还可以随时回滚到 ZooKeeper 模式。具体回退步骤详见官方文档。

现在需要把普通(非 controller)broker 切换到 KRaft 模式:方法是移除迁移模式配置和 ZooKeeper 的连接,并在每个普通 broker 上设置 process.roles=broker

提示:建议在此状态下让集群运行一到两周,观察是否一切正常,此期间仍可回退到 ZooKeeper 模式。

一切验证无误后,就可以切换到 KRaft 模式。但要小心——这一步是不可回退的。对 controller 节点禁用元数据迁移模式,移除 ZooKeeper 连接配置,然后对控制器进行滚动重启,迁移即完成。此后 Kafka 将以 KRaft 模式运行。

7.4 连接到 Kafka

现在我们来研究连接到 Kafka 集群的过程。表面看起来很简单,但它与我们前面讨论的集群协调与元数据管理机制紧密相关。理解这些连接机制有助于深入把握 Kafka 的分布式工作方式,并把理论与实际操作衔接起来。

在传统数据库中,连接很直接:提供数据库连接信息就行了。但 Kafka 是个分布式系统,由多个 broker 和一个协调集群组成,而且并非所有分区的 leader 都位于同一台 broker 上。哪个 partition 的 leader 在哪台 broker 上的信息保存在协调集群中,每个 broker 会将这些信息缓存在本地。早期版本中,客户端直接向 ZooKeeper 查询这些信息;自 Kafka 0.10.0 起,客户端可以向任一 Kafka broker 发起元数据请求来获取这些信息。

例如,下面命令列出所有 topic:

$ kafka-topics.sh  
--list  
--bootstrap-server localhost:9092  

我们用 --bootstrap-server 指定其中一个 broker(例如测试环境中的 Broker 1)。在生产环境中,只指定单个 broker 并不推荐,因为如果该 broker 挂了,客户端将无法启动。推荐的做法是在 --bootstrap-server 中列出多个 broker:

$ kafka-topics.sh  
--list  
--bootstrap-server  
localhost:9092,localhost:9093,localhost:9094

随着 broker 数量增多,手工列举会变得麻烦。因为客户端初始化时连接哪台 broker 并不重要,我们也可以用负载均衡器来处理这个元数据请求,或通过 DNS 来做简单的负载分发(前提是每台 broker 都有可达的 IP 或 DNS 名称)。

客户端会向某个 bootstrap server 发出元数据请求,请求中可包含感兴趣的 topic。比如,消费者希望消费 products.prices.changelog 时,元数据响应会返回集群中所有 broker 的信息,以及该 topic 每个分区的 leader 位于哪台 broker。

随后,客户端可直接连接到各分区的 leader 去拉取消息。客户端并不需要持续不断地刷新元数据;当发生关键变化(如某个分区的 leader 宕机)且客户端在向该 leader 拉取数据时遇到错误,客户端会再向任一 bootstrap server 发起新的元数据请求。正因为哪个 broker 接收该请求并不重要,我们可以用 DNS、负载均衡器或虚拟 IP 简化连接配置,而无需在每个客户端中列出尽可能多的 broker 地址。

当某个 broker 故障时,Kafka 会选举该分区的某一副本为新 leader。集群中的 controller 检测到故障并触发 leader 选举。新 leader 当选并更新元数据后,客户端就能拿到正确的元数据并重新连接到新 leader。在 leader 选举的短暂期间,客户端可能会遇到短暂的拉取延迟,但一旦新 leader 就绪,通信会恢复正常。上述工作流在图 7.3 中有示意。

image.png

总结

  • Kafka 3.3.0 引入了基于 Raft 协议的 KRaft 作为新的协调机制,并从 Kafka 3.5.0 起逐步替代 ZooKeeper。
  • Raft 协议解决了分布式系统中达成共识的复杂性,这一职责此前由 ZooKeeper 在 Kafka 中承担。
  • KRaft 将协调功能直接集成到 Kafka broker 中,免去了独立的 ZooKeeper 系统,从而提升可扩展性并降低运维复杂度。
  • 在 KRaft 中,controller 负责分区分配、leader 选举与故障恢复,保证集群稳定并便于对 Kafka 集群进行扩展和管理。
  • KRaft 引入了特殊主题 __cluster_metadata,用于在所有 broker 之间一致地存储元数据(此前由 ZooKeeper 管理),从而简化集群操作并增强可靠性。
  • 尽管 ZooKeeper 在 Kafka 4.0 中会被移除,目前仍有大量 Kafka 集群依赖 ZooKeeper 的关键功能。
  • 建议尽快将这些集群迁移到 KRaft,以利用 Raft 协调带来的性能与管理简化优势。
  • ZooKeeper 在 Kafka 中曾承担关键角色,例如选举 controller 和存储元数据(如分区分配与 leader 信息),以保证各节点状态一致。
  • 尽管 ZooKeeper 在保持一致性方面可靠,但其性能限制与运维复杂性促使 Kafka 转向更为一体化的 KRaft 方案。
  • 将元数据从 ZooKeeper 迁移到 KRaft 需要周密规划与执行,但可以在无停机的条件下完成。迁移步骤包括先升级到仍支持 ZooKeeper 的最新 Kafka 版本。
  • 需要预配三台或五台作为 KRaft controller 的额外 broker,且与 ZooKeeper 集群使用相同的 cluster ID,开启迁移模式并配置 ZooKeeper 连接信息。
  • 通过在现有 broker 上启用迁移模式并指定新的 controller 节点,将元数据复制到 KRaft 控制器并验证迁移成功。
  • 将普通 broker 切换到 KRaft 模式需调整配置、测试稳定性,最终在控制器上禁用迁移模式并移除 ZooKeeper 连接完成全面切换。
  • 连接到 Kafka 集群涉及查询元数据以了解各 broker 的角色及分区 leader 的分布。
  • 客户端通过 bootstrap server(通常为 --bootstrap-server 参数列出的若干 broker 之一)发起初始连接以获取元数据。
  • 为保证弹性,生产环境应在 --bootstrap-server 中列出多个 broker,或通过 DNS/负载均衡器简化元数据请求。
  • 若某 broker 故障,客户端可向 bootstrap server 发起新的元数据请求以发现新的分区 leader,并与新当选的 leader 建立连接,从而继续生产或消费消息。