近千个 MBean,日常只看十二个
4.3.0 的一个 broker 暴露的 MBean 对象有近千个。本机对着单节点数过一次:
total mbean objects: 992
这不是说其中 980 个没用,而是说:监控系统必须先做减法。如果一上来就把所有 MBean 都采下来,结果通常是指标面板密密麻麻、告警规则几百条,真正出问题时反而没人看。
这一节的做法是:先按「出问题的时候我会先看哪个」筛出十二个,再讲怎么把它们采出来、阈值怎么定、告警怎么不吵。
十二个指标,分三层
| 层 | 指标 | MBean | 它回答什么问题 |
|---|---|---|---|
| broker | 消息速率 | BrokerTopicMetrics:MessagesInPerSec | 现在有没有流量 |
| broker | 入/出字节 | BrokerTopicMetrics:BytesInPerSec / BytesOutPerSec | 带宽是否饱和 |
| broker | 生产失败 | BrokerTopicMetrics:FailedProduceRequestsPerSec | 写入是不是在被拒 |
| broker | 副本不足分区 | ReplicaManager:UnderReplicatedPartitions | 冗余还够不够 |
| broker | 低于最小 ISR | ReplicaManager:UnderMinIsrPartitionCount | 写入是否已到临界 |
| broker | ISR 收缩 | ReplicaManager:IsrShrinksPerSec | 有没有节点反复掉队 |
| broker | 请求队列 | RequestChannel:RequestQueueSize | 处理不过来了吗 |
| broker | 处理线程空闲率 | KafkaRequestHandlerPool:RequestHandlerAvgIdlePercent | 还有多少余量 |
| broker | controller 数 | KafkaController:ActiveControllerCount | 集群有没有主 |
| broker | 离线分区 | KafkaController:OfflinePartitionsCount | 有没有分区不可写 |
| 客户端 | 缓冲可用字节 | buffer-available-bytes | 生产者是否被背压 |
| 业务 | 消费积压 | kafka-consumer-groups.sh 的 LAG | 消费者跟不跟得上 |
最后一行不在 JMX 里,必须单独说。LAG 是消费组维度的概念,broker 不认识「积压」这件事,只能由外面拿 LOG-END-OFFSET 减 CURRENT-OFFSET 算出来。这是整张表里唯一一个不能靠 JMX 拿到的指标,也是最重要的一个。
把指标取出来:JMX 是唯一的路
Kafka 没有内置的 HTTP 指标端点(那需要额外的 exporter 组件,本书不引入)。在只有 kafka-clients 和 JDK 的约束下,读 broker 指标的方式只有 JMX。
第一件要知道的事:apache/kafka 镜像里没有 jcmd、jps、jconsole。它的 /opt/java/openjdk/bin/ 只有这些:
java jfr jrunscript jwebserver keytool rmiregistry
这是一个精简过的 JRE。能用 jfr,但没有 attach 工具。所以「容器起来之后再动态开 JMX」这条路走不通,必须在启动时就打开。
第二件事:用镜像自带的 JMX_PORT 环境变量起 broker。
docker run -d --name mq-f1 -p 19251:19092 \
-e KAFKA_NODE_ID=1 -e KAFKA_PROCESS_ROLES=broker,controller \
-e KAFKA_LISTENERS='PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:19092' \
-e KAFKA_ADVERTISED_LISTENERS='PLAINTEXT://127.0.0.1:9092,EXTERNAL://127.0.0.1:19251' \
-e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP='CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT' \
-e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_CONTROLLER_QUORUM_VOTERS='1@127.0.0.1:9093' \
-e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \
-e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \
-e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \
-e JMX_PORT=9999 \
apache/kafka:4.3.0
启动后 9999 就在监听(本机实测,/proc/net/tcp 里能看到):
tcp 0 0 :::9999 :::* LISTEN
tcp 0 0 :::9092 :::* LISTEN
坑一:客户端 JVM 会继承 JMX_PORT
这是本机踩得最久的一个。容器里执行任何 Kafka 命令行工具时,docker exec 会把容器的环境变量(包括 JMX_PORT=9999)带进新进程。Kafka 的启动脚本看到 JMX_PORT 就给自己也开一个 JMX,于是两个 JVM 抢同一个端口:
Caused by: java.net.BindException: Address in use
at java.base/sun.nio.ch.Net.bind0(Native Method)
...
at jdk.management.agent/sun.management.jmxremote.ConnectorBootstrap$PermanentExporter.exportObject(Unknown Source)
报错栈里全是 jmxremote,看不出跟你要执行的那条命令有什么关系。解决办法是在执行前清掉这个变量:
docker exec mq-f1 sh -c 'unset JMX_PORT; /opt/kafka/bin/kafka-topics.sh \
--bootstrap-server 127.0.0.1:9092 --list'
凡是容器开了 JMX 端口,里面所有 CLI 都要加这一句。
坑二:JmxTool 的包名在 4.x 变了
采集工具是 JmxTool,但它不在默认 classpath 上,而且类名换了。老写法:
kafka.tools.JmxTool
在 4.3.0 上报:
Error: Could not find or load main class kafka.tools.JmxTool
它实际在 kafka-tools-4.3.0.jar 里,包名是 org.apache.kafka.tools:
docker exec mq-f1 sh -c 'unset JMX_PORT; java -cp "/opt/kafka/libs/*" \
org.apache.kafka.tools.JmxTool \
--jmx-url service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi \
--object-name "kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec" \
--attributes Count,OneMinuteRate --one-time true'
输出是两行,第一行是表头,第二行是值:
Trying to connect to JMX url: service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi
"time","kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec:Count","...:OneMinuteRate"
1793248915456,0,0.0
第一列是采集时刻的毫秒时间戳,不是指标值。解析的时候很容易把它当成第一个指标。
坑三:一次只能查一个 objectName
直觉上会想用逗号把多个 --object-name 串起来一次采完:
--object-name "kafka.server:type=A,name=X,kafka.server:type=B,name=Y"
这样会失败。因为 JmxTool 用逗号分隔多个查询,而 ObjectName 内部本身就有逗号(type=...,name=...),两套语法撞在一起,解析直接异常:
java.lang.RuntimeException: Invalid ObjectName
at javax.management.ObjectName.<init>(Unknown Source)
at org.apache.kafka.tools.JmxTool$JmxToolOptions.lambda$queries$0(JmxTool.java:411)
结论很朴素:一个 objectName 一次调用。十二个指标就是十二次 JVM 启动,每个一两秒,采一轮十几秒。作为手工排错手段完全够用;要高频采集,应当上专门的 exporter,而不是循环调 JmxTool。
一次完整的采集:从空集群到 5 万条
把十二个指标写成脚本,先采基线。空集群上的原始输出(本机实测,Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):
MessagesInPerSec.Count 1793248967861,0
BytesInPerSec.Count 1793248968152,0
BytesOutPerSec.Count 1793248968557,0
FailedProduce.Count 1793248969065,0
UnderReplicatedPartitions 1793248969458,0
UnderMinIsrPartitionCount 1793248969930,0
IsrShrinksPerSec.Count 1793248970252,0
PartitionCount 1793248970589,3
ActiveControllerCount 1793248970976,1
OfflinePartitionsCount 1793248971369,0
RequestQueueSize 1793248971877,0
RequestHandlerAvgIdlePct 1793248972377,0.9942522795907361
这一组数字是全 0 加两个常数,正是「健康的空集群」该有的样子:ActiveControllerCount=1(有且只有一个 controller),PartitionCount=3(建的 topic 有三个分区),RequestHandlerAvgIdlePercent=0.994(处理线程 99.4% 的时间在空闲)。
然后打 5 万条进去:
docker exec mq-f1 sh -c 'unset JMX_PORT; /opt/kafka/bin/kafka-producer-perf-test.sh \
--topic f1.demo --num-records 50000 --record-size 512 --throughput -1 \
--producer-props bootstrap.servers=127.0.0.1:9092 acks=1'
50000 records sent, 11839.924224 records/sec (5.78 MB/sec), 1471.39 ms avg latency, ...
这条命令会顺带打一行提示:Option --producer-props has been deprecated and will be removed in a future version. Use --command-property instead. 4.3.0 里这个参数还能用,但已经是弃用状态,写脚本时应当换成 --command-property。
(单次运行,随调度波动,不作基准。本机 load 常年在十几到二十几之间,这个吞吐数字没有参考价值。)
再采一次:
MessagesInPerSec.Count 1793248988399,50000
BytesInPerSec.Count 1793248988655,26148454
BytesOutPerSec.Count 1793248988855,0
RequestHandlerAvgIdlePct 1793248990593,0.9938321088574491
三个变化值得注意:
MessagesInPerSec从 0 变成 50000,与发送条数完全对上。BytesInPerSec变成 26,148,454,约 24.9 MB。5 万条 × 512 字节是 25.6 MB,差额来自批次头和记录格式开销。BytesOutPerSec还是 0——因为没有消费者。这一条直接回答了「我这套集群的读流量是多少」。
RequestHandlerAvgIdlePercent 几乎没动(0.9938),说明这点流量对 broker 来说微不足道。空闲率是判断「还有多少余量」最直接的数字,比任何 QPS 都直观。
最后消费一遍,BytesOutPerSec 变成 26,410,501,与写入量同量级。
一个必须学会区分:broker 级和 topic 级
上面的 MessagesInPerSec 显示 50000。但消费完再采一次,它变成了:
MessagesInPerSec.Count 1793249012093,50006
多出来的 6 条不是业务消息。消费组提交 offset 时,Kafka 会往内部 topic __consumer_offsets 写记录,这些记录也被 broker 级的计数器统计进去。
用带 topic= 标签的 objectName 分别取,一目了然:
--object-name 'kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec,topic=f1.demo'
--object-name 'kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec,topic=__consumer_offsets'
f1.demo 1793249040358,50000
__consumer_offsets 1793249042741,6
50000 + 6 = 50006,严丝合缝。
这就是为什么监控面板上「集群总消息量」经常会莫名其妙地跳。 事务 topic、消费者位点 topic、controller 的内部 topic 都在计数里。做容量规划要用 topic 级指标;判断「集群整体还在不在干活」才用 broker 级。
延迟要看百分位,不要看均值
同一个采集方式可以取请求延迟。RequestMetrics 下按请求类型分组:
--object-name "kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce" \
--attributes 99thPercentile,Mean,Count
本机在一次 5 万条的生产之后取到:
99thPercentile=28.2 Mean=6.02 Count=539
三件事要一起读:
Count=539不是 50000。这个计数器统计的是请求数,不是消息数,一个请求里可以装很多条消息(批次)。Mean=6.02ms看着很好,但99thPercentile=28.2ms是均值的近五倍。长尾被均值完全掩盖了。- 延迟告警必须基于百分位。用均值定阈值,会漏掉那些「1% 的请求慢到超时」的事故。
阈值怎么定
绝对值阈值在 Kafka 上几乎一定错。同一套指标,在小集群和中等集群上的「正常值」差几个数量级。可行的做法是把阈值分成三类。
| 类型 | 适用指标 | 定法 |
|---|---|---|
| 零容忍 | FailedProduceRequestsPerSec、OfflinePartitionsCount、ActiveControllerCount≠1 | 只要出现就告警 |
| 持续偏离 | UnderReplicatedPartitions、UnderMinIsrPartitionCount | 持续超过 N 个采集周期 |
| 相对基线 | MessagesInPerSec、RequestQueueSize、99thPercentile | 与同时间段的基线比,看倍数 |
第一类不需要阈值,因为它们本该恒为某个值。OfflinePartitionsCount 大于 0 意味着有分区根本不可写,没有任何「短暂可接受」的说法。
第二类需要去抖,原因在第 14.1 节已经用实验说明过:ISR 收缩在三种路径下的耗时完全不同——优雅关闭是 2 秒内主动注销,进程冻结要等 broker.session.timeout.ms(默认 9 秒),复制慢则要等 replica.lag.time.max.ms(默认 30 秒)。一次正常的滚动重启就会让 UnderReplicatedPartitions 短暂大于 0。如果见到非零就告警,每次发版都是一轮告警风暴。
第三类必须看相对值。本机的 RequestHandlerAvgIdlePercent 在空集群上是 0.994,在打满时可以是 0.2、0.05。没有一个通用数字能同时覆盖这两种场景。
告警分级与去抖
| 级别 | 触发条件 | 期望动作 |
|---|---|---|
| P1 | OfflinePartitionsCount > 0 持续 1 分钟;ActiveControllerCount ≠ 1 持续 1 分钟 | 立即介入,可能无法写入 |
| P2 | UnderMinIsrPartitionCount > 0;FailedProduceRequestsPerSec > 0 持续 2 分钟 | 15 分钟内处理,写入正在被拒 |
| P3 | UnderReplicatedPartitions > 0 持续 5 分钟;RequestQueueSize 超过基线 3 倍 | 当天处理,冗余或延迟在退化 |
去抖统一用「连续 N 个采集周期」而不是「一次采样超过阈值」。周期取 30 秒比较合适:比 ISR 收缩最慢的那条路径(30 秒)短,又不至于把正常的替换节点抖动算进去。
有一类告警必须配「抑制」。滚动重启期间 UnderReplicatedPartitions 一定会非零,这个时间窗里应当临时抑制 P3 的副本告警,否则真正的问题会淹没在正常的噪音里。抑制不等于关闭——窗口结束后要有一次强制复查。
三个常见的告警错误
错误一:用均值。 前面已经算过,均值 6ms 而 P99 是 28ms。事故往往就发生在被均值抹平的那 1% 里。
错误二:只看 broker,不看消费组。 broker 的所有指标都健康,LAG 却已经涨到几百万——这在线上的发生频率高于任何一种 broker 故障。原因通常是消费者那边变慢了(下游数据库慢了、某条消息处理超时重试),broker 完全无辜。LAG 必须单独采集,且必须按消费组告警,不能只看总量。
错误三:把 broker 级计数当业务量。 前面 50006 那个例子,如果按 broker 级做容量预测,会系统性地高估。内部 topic 的写入量随消费组数量增长,在大集群上能占到不可忽略的比例。
从指标到判据
监控这件事的终点不是「有个面板」,而是「出问题时能在五分钟内回答三个问题」:
- 现在能不能写?(
FailedProduceRequestsPerSec、UnderMinIsrPartitionCount) - 数据有没有危险?(
UnderReplicatedPartitions、OfflinePartitionsCount) - 消费者跟不跟得上?(
LAG及其斜率)
前两个问题由 broker 指标回答,第三个必须由消费组数据回答。把这十二个指标按这三个问题组织,面板上就不再是一堆曲线,而是一份可以直接照着念的判断清单。
下一节讲版本升级:哪些改动可以撤回,哪些不能。
阅读导航:上一节 14.3 数据不一致的取证 | 下一节 15.2 版本升级与兼容性
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。