bigywater.tech
Kafka 内部原理 · 32

session.timeout.ms 和 heartbeat.interval.ms

这两个参数经常被一起提,但它们管的是两件不同的事:heartbeat.interval.ms 是消费者多久向协调者报一次"我还在",session.timeout.ms 是协调者多久没收到心跳就认定这个消费者已经不在了。这次用的进程冻结(kill -STOP,进程还在但完全不运行,没有心跳也没有 poll())来测第二个参数的效果,再验证两个参数之间的约束,以及 Broker 对超时范围的限制。

验证:冻结分区所有者后,另一个成员接手分区的耗时约等于 session.timeout.ms
接手耗时随 session.timeout.ms 线性变化:6 秒对应 4.6 到 6.6 秒,12 秒对应 11.7
验证:客户端不允许心跳间隔大于等于会话超时,但允许"只小一点"
心跳间隔大于会话超时时,消费者构造阶段就直接抛异常,报错信息是"Heartbeat must be set lower

验证:冻结分区所有者后,另一个成员接手分区的耗时约等于 session.timeout.ms

命令 · shell # 同一个消费组两个 Java 进程,topic poll-demo 只有 1 个分区 # 等两个成员稳定后,对"当前持有分区的那个进程"执行 kill -STOP,计时到另一个成员 onPartitionsAssigned 拿到分区
真实实测 · 本机执行输出
session.timeout.ms=6000   heartbeat.interval.ms=2000 : 6570ms
session.timeout.ms=6000   heartbeat.interval.ms=2000 : 4629ms
session.timeout.ms=12000  heartbeat.interval.ms=4000 : 12936ms
session.timeout.ms=12000  heartbeat.interval.ms=4000 : 12516ms
session.timeout.ms=12000  heartbeat.interval.ms=500  : 11655ms
session.timeout.ms=12000  heartbeat.interval.ms=500  : 11686ms
session.timeout.ms=20000  heartbeat.interval.ms=6000 : 20685ms

接手耗时随 session.timeout.ms 线性变化:6 秒对应 4.6 到 6.6 秒,12 秒对应 11.7 到 12.9 秒,20 秒对应 20.7 秒,和会话超时基本一致。这里的"基本一致"有误差:冻结发生在两次心跳之间的某个时刻,协调者是从"最后一次收到的心跳"开始计时,所以实测值会比会话超时早一点或晚一点(还包括 rebalance 本身的耗时)。6 秒那一组两次差了近 2 秒(4629 对 6570),正好和 2000ms 的心跳间隔量级相同;12 秒那一组,心跳 500ms 的两次结果几乎一样(11655、11686),心跳 4000ms 的两次是 12936 和 12516,略高,也符合"心跳间隔越大,冻结前最后一次心跳的位置越不确定"的方向。但每组只有两次,样本很小,这一段趋势只能说方向一致,不能说量化关系已经测准。

这个结果和《max.poll.interval.ms 为什么会导致消费者被踢出组》互相印证:那篇里消费者进程一直在运行、心跳正常,靠 max.poll.interval.ms 被踢;这篇里进程被冻结,连心跳都没有,靠 session.timeout.ms 被踢。

验证:客户端不允许心跳间隔大于等于会话超时,但允许"只小一点"

命令 · java new KafkaConsumer<>(props); // session.timeout.ms=6000, heartbeat.interval.ms=10000 new KafkaConsumer<>(props); // session.timeout.ms=6000, heartbeat.interval.ms=5000,运行 30 秒
真实实测 · 本机执行输出
session=6000, heartbeat=10000:
  KafkaException: Failed to construct kafka consumer
  Caused by: IllegalArgumentException: Heartbeat must be set lower than the session timeout

session=6000, heartbeat=5000(单个消费者运行 30 秒):
  t+0.0s ASSIGNED 1        <- 30 秒内没有任何重新分配的事件

心跳间隔大于会话超时时,消费者构造阶段就直接抛异常,报错信息是"Heartbeat must be set lower than the session timeout"。而 6000 对 5000 这种"只比会话超时小一点"的配置,客户端不拒绝,单个消费者稳定运行了 30 秒,没有出现任何重新分配。所以客户端只强制"心跳必须小于会话超时",并不强制常见建议里的"心跳不超过会话超时的三分之一";后者是经验规则。我只测了单个消费者、没有网络抖动的情形,没有验证在网络延迟波动下 6000 对 5000 这种配置会不会频繁被误判为掉线。

验证:Broker 限制了 session.timeout.ms 的取值范围,低于下限直接被拒绝

命令 · shell $ kafka-configs.sh --broker 1 --describe --all | grep -E "group.(min|max).session.timeout" # 消费者配置 session.timeout.ms=3000, heartbeat.interval.ms=1000
真实实测 · 本机执行输出
group.min.session.timeout.ms=6000
group.max.session.timeout.ms=1800000

InvalidSessionTimeoutException: The session timeout is not within the range allowed by the broker
  (as configured by group.min.session.timeout.ms and group.max.session.timeout.ms)

Broker 端 group.min.session.timeout.ms 是 6000,group.max.session.timeout.ms 是 1800000。把会话超时设成 3000,客户端能通过构造检查(心跳 1000 小于 3000),但加入消费组时被 Broker 拒绝,抛出 InvalidSessionTimeoutException。这也意味着想缩短观察时间而把会话超时调得很小的做法,在默认的 Broker 配置下下限只能到 6000。

结论

session.timeout.ms 决定"完全失联的消费者多久后被认定下线",这次用进程冻结实测,接手耗时和它基本一致(受最后一次心跳位置和 rebalance 耗时影响,有一两秒的浮动);heartbeat.interval.ms 决定汇报频率,必须小于会话超时(客户端强制),而且会影响 rebalance 的传播速度(见《Consumer Rebalance》里被移动分区的停顿等于心跳间隔);Broker 还限制了会话超时的取值范围,默认下限 6 秒。

分步演示:冻结一个进程,另一个成员多久能接手

每一步都是本文上面几节kill -STOP、测量接手耗时、触发边界检查得到的原始结果。

进程被冻结后,接手耗时和session.timeout.ms基本对上
这一步新验证的配置

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,group.protocol=classic,两个成员是两个独立的 Java 进程;冻结用 kill -STOP,接手时间是从 STOP 执行的时刻到另一个成员的 onPartitionsAssigned 回调(分区数为 1)之间的差值,两个时间戳来自同一台机器的毫秒时钟。
    • 第一次尝试时我的脚本在 zsh 里把 "6000 2000" 当成了一个参数,导致测试挂住,已改用 bash 重跑;第一版把"观察者"和"目标"固定成两个角色,也没有考虑到分区可能被重新分配给观察者,所以改成"哪个进程当前持有分区就冻结哪个"。这两版的输出都没有采用。
    • 没有验证:group.protocol=consumer(KIP-848)下会话超时由服务端配置的行为;网络抖动下心跳与会话超时的关系;kill -9(进程消失、连接断开)与 kill -STOP 的检测时间差异——前者 TCP 连接会立刻断开,协调者可能更早发现,这里没有测。
    京ICP备2026067502号-1京公网安备11010502063487号