Offset 提交决定了消费者重启后从哪里继续读。自动提交、commitSync()、commitAsync() 三种方式的区别不只是"写起来方不方便"。这次量了它们的开销,验证了 commitSync() 不带参数时提交的到底是哪个位置,并看了异步提交失败时代码是怎么被通知的。
命令 · java // topic commit-demo: 1 分区 x 3 副本,3000 条消息;每次 poll 最多 100 条,共 30 批 // sync: 每批 commitSync();async: 每批 commitAsync();auto: enable.auto.commit=true,不显式提交 // 每种模式都新建一个消费组,从头读完 3000 条,各跑两次
模式=sync 总耗时 8.653s 提交 30 次 累计 54.3ms 平均 1.81ms/次 模式=async 总耗时 8.602s 提交 30 次 累计 6.3ms 平均 0.21ms/次 模式=auto 总耗时 8.704s 提交 0 次 模式=sync 总耗时 8.646s 提交 30 次 累计 56.8ms 平均 1.89ms/次 模式=async 总耗时 8.581s 提交 30 次 累计 6.6ms 平均 0.22ms/次 模式=auto 总耗时 9.366s 提交 0 次
两轮结果一致:同步提交每次调用要等 Coordinator 返回,约 1.8~1.9 毫秒;异步提交只是把请求发出去,约 0.21~0.22 毫秒,大约是前者的九分之一。但三种模式读完 3000 条的总耗时都在 8.6 秒左右(auto 有一次 9.4 秒),差别小于波动。原因是这个测试大部分时间花在启动、入组和等待分区分配上,30 次同步提交总共才多出 50 毫秒,在这个数据量下完全被淹没。所以结论只能是"每次调用的开销 sync 比 async 大约一个数量级",而不能说"用 sync 会让整体变慢"。
命令 · java // 同一批 poll 拿到 100 条,只处理前 10 条(最后处理的 offset=9)后提交,然后关闭消费者,再用同一个组重新启动 // noarg: c.commitSync() // explicit: c.commitSync(Map.of(tp, new OffsetAndMetadata(lastProcessed + 1)))
[noarg] 本批拿到 100 条,只真正处理了 10 条(最后处理的 offset=9) [noarg] 重启后同组消费者拿到的第一条 offset = 100 [explicit] 本批拿到 100 条,只真正处理了 10 条(最后处理的 offset=9) [explicit] 重启后同组消费者拿到的第一条 offset = 10
这是这篇文章里最需要留意的一个结果:commitSync() 不带参数,提交的是这个消费者当前的读取位置(这次是 100,也就是这批 100 条拉完之后的位置),而不是实际处理到哪里。我只处理了 offset 0 到 9 这 10 条,重启后同组的消费者直接从 offset 100 开始,offset 10 到 99 这 90 条从未被处理就永远跳过了。换成显式提交 lastProcessed + 1(也就是 10),重启后从 offset 10 继续,一条没丢。所以手动提交的价值不只是"自己决定什么时候提交",还在于可以精确提交"处理完的位置"。
命令 · java // max.poll.interval.ms=6000,每批处理 9 秒后 commitAsync(callback)
[ 3232ms] 第 1 批 100 条,模拟处理 9 秒(超过 max.poll.interval.ms=6000) [12248ms] commitAsync() 调用返回了,没有抛异常 [12248ms] commitAsync 回调: CommitFailedException [15271ms] 第 2 批 100 条,模拟处理 9 秒 [24277ms] commitAsync() 调用返回了,没有抛异常 [24277ms] commitAsync 回调: CommitFailedException
同样的"处理太慢被踢出消费组"场景,在《Consumer 为什么必须调用 poll()》里 commitSync() 会直接抛出 CommitFailedException。换成 commitAsync(),调用本身正常返回、没有异常,失败信息只出现在回调里。如果不传回调,这个失败会悄无声息(这次没有单独测不传回调时的日志表现,只验证了带回调时的行为)。所以用异步提交时,回调里要处理失败,否则程序会以为提交成功。
三种方式的区别:sync 每次提交约 1.8ms,async 约 0.2ms(这个量级的差别在小数据量下不会影响整体耗时);不带参数的 commitSync() 提交的是读取位置而非处理进度,处理一半就提交会永久跳过未处理的消息;异步提交失败只通过回调通知,不会抛异常。
每一步都是本文上面几节运行Java Consumer、重启验证offset得到的原始结果。