这两个参数经常被一起提,但它们管的是两件不同的事:heartbeat.interval.ms 是消费者多久向协调者报一次"我还在",session.timeout.ms 是协调者多久没收到心跳就认定这个消费者已经不在了。这次用的进程冻结(kill -STOP,进程还在但完全不运行,没有心跳也没有 poll())来测第二个参数的效果,再验证两个参数之间的约束,以及 Broker 对超时范围的限制。
命令 · 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 这种配置会不会频繁被误判为掉线。
命令 · 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、测量接手耗时、触发边界检查得到的原始结果。