Kafka 集群最烦人的告警,不是 Broker 挂了。
Broker 挂了还能切,分区还能迁,消费者顶多抖一下。真正让人心里发毛的是这种:
ControllerMovedException
TimeoutException: Timed out waiting for a node assignment
Metadata update failed
这类问题第一眼我一般不先看业务代码,先看控制面。Kafka 以前把这块交给 ZooKeeper:Broker 注册、Controller 选举、Topic 元数据、ISR 变化,都绕不开它。
问题也在这里。
Kafka 越长越大,ZooKeeper 就越像一根外挂的骨头。不是 ZooKeeper 不行,而是它不懂 Kafka。
Apache Kafka 4.0 已经只支持 KRaft 模式,ZooKeeper 模式被移除;老集群要升到 4.0 及以上,必须先迁到 KRaft。这个变化不是小修小补,是 Kafka 把自己的控制面收回来了。
以前的结构大概是这样:
Producer / Consumer
|
Broker
|
ZooKeeper
看着清楚,线上一复杂就不清楚了。
创建 Topic,改分区,Broker 上下线,Controller 变更,元数据要在 Broker 和 ZooKeeper 之间来回同步。这里最怕的不是慢一次,而是状态不一致。你在客户端看到一个 Controller,Broker 认为是另一个;ISR 刚变,元数据还没推完,某些请求就开始超时。
这种问题日志往往不好看,像这样:
[Controller id=2] Broker 5 failed
[Controller id=2] Resigned
[Broker id=7] Metadata delta apply timeout
[AdminClient clientId=ops-check] disconnected before response
这地方我第一眼就不太信“网络偶发”。控制面抖了,业务面再健康也白搭。
KRaft 做的事,就是把 ZooKeeper 这一层拿掉,让 Kafka 自己维护元数据日志。Controller 不再是去 ZooKeeper 里抢一个临时节点,而是由一组 Controller 节点组成 quorum,用 Raft 思路复制元数据。
结构变成这样:
Producer / Consumer
|
Broker
|
KRaft Controller Quorum
|
Metadata Log
这里有个很关键的变化:元数据不再散在外部系统里,而是变成 Kafka 自己的一条日志。
这就舒服多了。Kafka 最擅长什么?写日志、复制日志、按 offset 推进状态。KRaft 等于是把 Kafka 擅长的那套东西,用到了自己的控制面上。
以前运维排查 Kafka,要同时盯两套东西:
Kafka Broker 是否正常
ZooKeeper ensemble 是否正常
Broker 到 ZooKeeper 的 session 是否正常
Controller 是否频繁切换
现在至少少了一半心智负担。不是说 KRaft 不会出问题,而是问题边界清楚了。
代码里也能看出来这个变化。以前有些老系统会直接连 ZooKeeper 去读 Broker 列表,这种代码我现在看到基本会标红:
public final class KafkaClusterProbe {
private KafkaClusterProbe() {
}
public static ClusterView read(String bootstrapServers) {
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, "5000");
props.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "8000");
try (AdminClient admin = AdminClient.create(props)) {
DescribeClusterResult result = admin.describeCluster();
String clusterId = result.clusterId().get();
Node controller = result.controller().get();
Collection<Node> nodes = result.nodes().get();
return new ClusterView(clusterId, controller.id(), nodes.size());
} catch (Exception e) {
throw new IllegalStateException("Kafka 控制面探测失败,先别急着重启业务服务", e);
}
}
public record ClusterView(String clusterId, int controllerId, int brokerCount) {
}
}
这段代码没碰 ZooKeeper,只认bootstrap.servers。这才是新 Kafka 客户端该有的姿势。
Kafka 4.0 之后,命令行里的--zookeeper也被移除了,管理操作统一走--bootstrap-server。这个细节挺能说明问题:Kafka 不希望应用、脚本、运维工具再绕到 ZooKeeper 后门里扒元数据。
抛弃 ZooKeeper,还有一个原因是扩展性。
小集群没感觉。几十个 Topic,几百个分区,ZooKeeper 顶得住。可一旦分区数上来,Controller 重启后要恢复大量元数据,Broker 变化要推一堆通知,ZooKeeper watch 也会跟着热闹。
这类慢不是 SQL 那种慢,可以 explain 一下。它更像控制面卡顿:创建 Topic 慢、分区扩容慢、Leader 选举慢,最后业务侧看到的就是发送超时、消费组抖动、元数据刷新失败。
KRaft 的思路更直接:所有元数据变更先进元数据日志,Controller quorum 复制,Broker 再按日志应用变更。顺序清楚,恢复也清楚。
迁移时我反而不建议一上来就喊“升级 Kafka 4.0”。先查现场:
public class KafkaVersionGate {
public static void checkBeforeUpgrade(String bootstrapServers) {
KafkaClusterProbe.ClusterView view = KafkaClusterProbe.read(bootstrapServers);
if (view.brokerCount() < 3) {
throw new IllegalStateException("Broker 数量太少,别在这个状态下折腾 KRaft 迁移");
}
System.out.println("clusterId=" + view.clusterId());
System.out.println("controllerId=" + view.controllerId());
System.out.println("brokerCount=" + view.brokerCount());
}
}
真正迁移前,我会先看三件事:Controller 有没有频繁切换,分区副本有没有长期 under-replicated,运维脚本里还有没有zookeeper.connect和--zookeeper。
有些老脚本最坑,平时不跑,出事故才跑。一跑发现还在读 ZooKeeper 路径:
/brokers/ids
/controller
/admin/delete_topics
这种东西不清掉,升版本就是给自己埋雷。
所以 Kafka 抛弃 ZooKeeper,不是嫌弃 ZooKeeper 老,也不是为了赶技术潮流。
更直接点说,是 Kafka 的控制面长大了,不能再靠一个外部协调系统撑着。元数据、选主、状态复制,这些东西必须回到 Kafka 自己手里。
以前 Kafka + ZooKeeper 是两套系统一起扛事。现在 KRaft 是 Kafka 自己扛。
架构少一层,排障少一层,升级少一层风险。线上系统里,少一个关键依赖,很多时候就是少一半事故入口。