《Java 消息中间件·Kafka 内核与客户端》9.3 客户端指标口径

metrics() 构造后有 77 项,跑起来变成 110 项,同一个名字在不同 group 下含义不同。本节把 producer-metrics 里与背压、批次、延迟相关的指标拆开口径:实测打满 1 MiB 缓冲时 buffer-available-bytes 归零、waiting-threads 变 1,并对比客户端 lag 为 0 而服务端 LAG 为 200 的同一条消费链路。

metrics() 返回的不是一张平表

KafkaProducer.metrics() 的实现只有一行:

public Map<MetricName, ? extends Metric> metrics() {
    return Collections.unmodifiableMap(this.metrics.metrics());
}

返回的是 Map<MetricName, Metric>。MetricName 不是一个字符串,它由四部分组成:

部分作用例子
name指标名buffer-available-bytes
group逻辑分组producer-metrics
description人类可读说明The total amount of buffer memory…
tags维度键值对{client-id=producer-1}

同一个 name 可以出现在多个 group 里,也可以带不同 tags。比如 records-lag-max 在 consumer-fetch-manager-metrics 下既有一个只带 client-id 的全局版本,也有带 topic、partition 的分区版本。只看名字会取错值。

本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29)打印出的一个条目:

MetricName 示例: name=buffer-available-bytes group=producer-metrics tags={client-id=producer-1}

JMX 导出时,group 会变成 MBean 的 type,tags 变成额外的键值对。指标名里的点号、空格在不同采集器里还要转义,所以跨系统对齐指标时,必须把 group 和 tags 一起对上,不能只对 name。

77 项不是固定值

本书的实验基线说「构造后 metrics() 有 77 项」。这个数字只在「刚构造、还没连上 broker」时成立。本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):

构造后(未发送)metrics().size() = 77
metrics().size() = 110
按 group 统计: {app-info=3, kafka-metrics-count=1, producer-metrics=73,
                producer-node-metrics=24, producer-topic-metrics=9}

多出来的 33 项主要是 producer-node-metrics(24 项)和 producer-topic-metrics(9 项):连上 broker 之后才有 node,发出消息之后才有 topic 维度。这也解释了一个常见困惑——为什么刚启动时抓不到 outgoing-byte-rate:它属于 node 组,节点建立之后才注册。

所以「77 项」应该理解成构造期的基线,不是运行期的总数。采集配置不要写死数量,也不要假设某个指标一定存在;先判空,再取值。

一组真实输出

先把一次 100 条消息发送后的关键指标原样贴出来(linger.ms=0、acks=all,本机实测 Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):

batch-size-avg                   1987.0
buffer-available-bytes           3.3554432E7
buffer-total-bytes               3.3554432E7
bufferpool-wait-ratio            0.0
bufferpool-wait-time-ns-total    0.0
record-queue-time-avg            9.0
record-queue-time-max            9.0
record-send-total                100.0
records-per-request-avg          100.0
request-latency-avg              12.0
request-rate                     0.15998464147441846
requests-in-flight               0.0
metadata-age                     1.243
waiting-threads                  0.0

这张表里有两类值要区分:

  • gauge:buffer-available-bytes、waiting-threads、requests-in-flight、metadata-age。它们是「采样那一刻」的值,可以直接和阈值比。
  • 窗口统计:batch-size-avg、record-queue-time-avg、request-latency-avg、request-rate。它们在 metrics.sample.window.ms(默认 30000 ms)内按 metrics.num.samples(默认 2)个样本计算,刚启动时样本不足,可能显示 0 或 NaN。

kafka-metrics-count 这个 group 只有一项,值是当前已注册的指标总数。它适合做自检:采集端突然少了很多指标,先看这个数是不是也掉了。

requests-in-flight=0 不代表没发过请求,只表示采样时没有在途请求。这次 100 条消息已经全部确认,所以它是 0。要判断「有请求卡住」,应该看它长期大于 0,同时 request-latency-max 一起升高。

producer-metrics 里先看这 14 项

producer-metrics 组是客户端自己维护的指标,不依赖 topic 或 node。下面这些和背压、批次、延迟直接相关:

指标含义反常时的含义
buffer-total-bytes客户端可用的缓冲上限等于 buffer.memory,变了说明配置改了
buffer-available-bytes尚未使用的缓冲字节数长期接近 0 表示缓冲被打满,send() 开始阻塞
waiting-threads阻塞等待缓冲区的业务线程数大于 0 说明应用线程已经卡在 send()
bufferpool-wait-time-ns-total线程等待缓冲区的累计纳秒数持续增长说明背压已经发生
bufferpool-wait-ratio等待时间占比接近 1 表示生产者基本在等内存
buffer-exhausted-total因缓冲区耗尽被丢弃的发送次数大于 0 必须查,通常伴随超时
record-queue-time-avg批次在累加器里的平均等待毫秒受 linger 和背压共同影响,升高说明排队变长
batch-size-avg每个分区每次请求的平均字节数远低于 batch.size 说明流量小或 linger 太短
records-per-request-avg每次请求的平均记录数太小说明批次没攒起来,请求数偏多
request-rate每秒请求数突增可能是批次变小或分区变多
request-latency-avg一次请求的平均往返毫秒受批次大小、broker、网络共同影响
record-send-rate / record-send-total发送速率 / 累计条数速率骤降要看错误和背压
record-error-total最终失败的记录数大于 0 说明有消息没发出去
record-retry-total重试的记录数持续增长说明 broker 或网络不稳

注意 record-error-total 和 record-retry-total 的区别:重试是过程,最终可能成功;错误是结果,记录已经失败。只看重试会低估问题,只看错误会漏掉正在恶化的链路。

背压:三个指标一起看

单看 buffer-available-bytes 不够,因为它下降可能是正常的批量堆积。判断背压要三件事同时成立:

  1. buffer-available-bytes 接近 0;
  2. waiting-threads 大于 0;
  3. bufferpool-wait-time-ns-total 在增长。

本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):把 buffer.memory 和 batch.size 都设成 1 MiB,linger.ms=1000,max.block.ms=500,连发 300 条 8 KiB 消息,并用采样线程每 50 ms 读一次指标:

[背压] 300 x 8KiB: 成功入队 300 条,耗时 2048ms,结果=no-error
[背压] 观测到 buffer-available-bytes 最小值=0 waiting-threads 最大值=1
       bufferpool-wait-time-ns-total 最大值=2043254666
[背压] 恢复前 record-queue-time-max=1007ms record-queue-time-avg=1003ms
[背压] 排空后 buffer-available-bytes=1048576 record-queue-time-max=1007ms

三个观察:

  • 300 条最终都入队了,但总耗时 2048 ms,远超无背压时的几十毫秒。背压不一定表现为异常,更多时候表现为「变慢」。
  • waiting-threads 最大只有 1,因为实验只有一个发送线程。生产上这个值等于同时卡在 send() 的线程数,是判断影响面的直接证据。
  • record-queue-time-max 停在 1007 ms,即使缓冲已经排空也不下降。最大值是窗口内的历史极值,不是当前值;判断恢复要看 buffer-available-bytes 和 waiting-threads,不要看 max。

还有一个名字坑:4.3.0 里没有 bufferpool-wait-time-total,实际注册的是 bufferpool-wait-time-ns-total,单位是纳秒。写成毫秒会差 6 个数量级。源码里 BufferPool 注册的就是这个名字:

MetricName totalNsMetricName = metrics.metricName("bufferpool-wait-time-ns-total",
        metricGrpName,
        "The total time in nanoseconds an appender waits for space allocation.");

如果 send() 在 max.block.ms 内等不到缓冲,抛的是 BufferExhaustedException,它是 TimeoutException 的子类,同时 buffer-exhausted-total 会加一。源码注释说明,早期版本抛的是裸 TimeoutException,保留继承关系是为了让老的 catch 块继续生效。

request-latency 与 batch-size 的因果方向

这两个指标经常被放在一张图上看,然后得出「延迟高是因为批次大」的结论。方向对了一半,但结论下反了。

batch-size-avg 是结果:它由 batch.size、linger.ms、到达速率共同决定。request-latency-avg 是单次请求的往返时间,批次越大,单次请求携带的字节越多,broker 处理和网络传输时间都可能变长。所以「批次大 → 单次请求慢」是正常现象,不代表链路坏了。

本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):同样 5000 条 200 字节消息,两组参数各跑两次:

参数总耗时batch-size-avgrequest-latency-avgrecords-per-requestrequest-ratequeue-time-avg
batch=1KiB linger=0659 ms9202.36 ms4.039.37314.8 ms
batch=64KiB linger=2045 ms63,4617.53 ms294.10.6413.8 ms
batch=1KiB linger=0(重跑)816 ms9203.15 ms4.039.14447.7 ms
batch=64KiB linger=20(重跑)48 ms63,4617.82 ms294.10.6416.3 ms

(单次运行,随调度波动,不作基准。)

读法:

  • 批次变大后,request-latency-avg 从 2.4–3.2 ms 升到 7.5–7.8 ms,单次请求确实变慢了。
  • 但 request-rate 从 39 降到 0.64,请求数少了约 60 倍;records-per-request 从 4 升到 294。
  • 端到端总耗时从 659–816 ms 降到 45–48 ms;record-queue-time-avg 也从 300–448 ms 降到 14–16 ms。

所以正确的因果链是:linger 和 batch.size 让批次变大 → 单次请求变慢、请求数变少 → 总吞吐上升、端到端变快。拿 request-latency-avg 单独告警,会把「批次调大了」误判成「网络变差了」。

反过来,如果 batch-size-avg 很小而 request-latency-avg 很高,那才是真的有问题:批次没攒起来,每次请求还很慢,通常是网络 RTT 高或 broker 排队。

消费端 lag 的三种口径

这是客户端指标里最容易吵架的地方:同一个消费组,客户端说 lag=0,服务端说 LAG=200,监控说峰值 250。三个数都对,口径不同。

先从源码看客户端指标的定义。records-lag-max 的说明是:

The maximum lag in terms of number of records for any partition in this window.
NOTE: This is based on current offset and not committed offset

关键词是 current offset:客户端用「日志末端位移 − 当前消费位置」算 lag,和有没有提交位移无关。而且它是窗口内的最大值,不是瞬时值。

本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):一个分区写 300 条,消费者读 100 条后提交一次,再读到 300 条但不提交:

[lag] 消费到 100 条后提交位移
[lag] 消费到 300 条(未再提交),客户端 records-lag-max=250
[lag] metric consumer-fetch-manager-metrics/records-lag tags={client-id=..., topic=b4.lag-lab, partition=0} = 0.0
[lag] metric consumer-fetch-manager-metrics/records-lag-max tags={client-id=...} = 250.0

关闭消费者后,用服务端命令看同一个组:

GROUP        TOPIC       PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG  CONSUMER-ID  HOST  CLIENT-ID
b4-lag-lab   b4.lag-lab  0          100             300             200  -            -     -

三个数字的来历:

口径值怎么算说明
客户端瞬时 records-lag0日志末端 300 − 当前 position 300消费者已经读完,只是没提交
客户端窗口 records-lag-max25030 秒窗口内的最大 lag第一轮 poll 后 position=50,lag=250
服务端 LAG200日志末端 300 − 已提交 offset 100反映「重启后会从哪里继续」

服务端的 LAG 才是「积压」的常用定义:它衡量还有多少消息没有被确认处理。客户端的 records-lag 衡量「当前读到哪里」,适合判断消费者是不是跟得上。两者相差 200,说明有 200 条记录已经读进内存但还没提交——这正是 at-least-once 的重复窗口,也是卷二要处理的问题。

还有两个细节:

  • records-lag-max 只带 client-id 的全局版本取所有分区的最大值;带 topic、partition 的版本才是单分区值。取错版本会把「某个热点分区」当成「全局积压」。
  • 4.3.0 同时输出 topic=b4.lag-lab 和 topic=b4_lag-lab 两套 topic 级指标,后者是把点号替换成下划线的旧格式,源码里标了 deprecated。采集时按带点号的那套为准,避免重复计算。

客户端指标不替代服务端指标

客户端指标只描述「这个客户端看到的自己」。它不知道其他消费者、不知道副本状态、也不知道 broker 的请求队列。

问题该看客户端指标该看服务端指标
我的发送是不是卡了waiting-threads、buffer-available-bytes—
是不是某个 broker 慢producer-node-metrics 里的请求/字节指标请求队列、磁盘、网络
消费组积压多少客户端 lag 只能看自己kafka-consumer-groups --describe 的 LAG
副本是不是同步看不到UnderReplicatedPartitions、IsrShrinksPerSec
是不是被限流produce-throttle-time-avgbroker 侧配额与限流指标

一个常见的误判是:客户端 record-error-total=0,就认为链路没问题。实际上消息可能还在重试、可能堆在缓冲区里没发出去、也可能发出去了但消费端积压。客户端指标证明的是「这个进程做了什么」,不是「集群健康」。

反过来,服务端指标也看不到单个客户端的阻塞。两边要配对使用:客户端看「我卡在哪」,服务端看「集群卡在哪」,中间用 topic 和 client-id 关联。

读客户端指标的四条原则

第一,先确认口径:瞬时、窗口最大值、累计计数、速率,四者不能混。 records-lag 是瞬时,records-lag-max 是窗口最大,record-send-total 是累计,record-send-rate 是速率。把 max 当当前值,会得出「已经恢复了但图表还没降」的误判。

第二,group 和 tags 一起看。 records-lag-max 在两个 group、三种 tag 组合下都存在。只按 name 取值,代码可能取到全局版本,也可能取到某个分区的版本,结果随 Map 遍历顺序变化。

第三,指标是结果,不是原因。 buffer-available-bytes=0 是现象,原因可能是 broker 慢、网络抖动、分区热点,也可能是 linger.ms 配得太大。指标能告诉你「哪里不对」,不能直接告诉你「为什么」。定位要靠 14.2 节的分层方法:先分生产/消费,再看分区倾斜,最后落到 broker 或客户端。

第四,注意标签基数。 带 topic、partition、node-id 的指标数量随分区数增长。一个 1000 分区的 topic,光 records-lag 系列就可能产生几千个时间序列。采集前先决定要全局值还是分区值;全量抓回去,存储成本会比消息本身还高。

本节判据

读完这一节,你应该能回答三个问题:

  1. 刚构造的 producer 有多少项指标?跑起来之后呢? 构造期 77 项,连上 broker 并发过消息后 110 项,多出来的是 node 和 topic 维度。
  2. 哪三个指标同时成立才说明发生了背压? buffer-available-bytes 接近 0、waiting-threads 大于 0、bufferpool-wait-time-ns-total 持续增长。
  3. 客户端 lag=0 而服务端 LAG=200,说明什么? 消费者已经读到末尾但只提交到 100;服务端按已提交位移算积压,客户端按当前 position 算,差的 200 条是未确认处理的消息。

如果有一个答不上来,回到上面的实测表再看一遍;这三个问题分别对应指标的结构、背压的判据和 lag 的口径。

下一节离开客户端,进入存储层:顺序追加、页缓存和零拷贝为什么能让 Kafka 用普通磁盘扛住高吞吐。

阅读导航:上一节 9.2 拦截器链与埋点 | 下一节 10.1 顺序写、页缓存与零拷贝

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收