之前线上发生了一个很怪异的反常,网上各种查找、排查,都没有找到问题,给大家分享一下。

大概在 2 月份的时分,咱们的某个运用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行调查,然后就发现线上某个 Topic 发生了很多的RetriableCommitException,而且会集在灰度机器上。

E20:21:59.770RuntimeExceptionorg.apache.kafka.clients.consumer.RetriableCommitFailedExceptionERROR[ConsumerclientId=xx-xx.4-0,groupId=xx-xx-consumer_[gray]]Offsetcommitwithoffsets{xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918,leaderEpoch=4,metadata=''},xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228,leaderEpoch=5,metadata=''}}failedorg.apache.kafka.clients.consumer.RetriableCommitFailedException:Offsetcommitfailedwitharetriableexception.Youshouldretrycommittingthelatestconsumedoffsets.
Causedby:org.apache.kafka.common.errors.TimeoutException:Failedtosendrequestafter30000ms.

往往排查很久的问题,最后发现都非常简单。。。

排查

检查了这个 Topic 的流量流入、流出情况,发现并不是很高,至少和 QA 环境的压测流量比照,连零头都没有达到。

但是从发生反常的这个 Topic 的历史流量来看的话,发生问题的那几个时刻点的流量又的确比平常高出了许多。

往往排查很久的问题,最后发现都非常简单。。。

一起咱们检查 Broker 集群的负载情况,发现那几个时刻点的 CPU 负载也比平常也高出许多(也仅仅比平常高,整体并不算高)。

往往排查很久的问题,最后发现都非常简单。。。

对Broker集群的日志排查,也没发现什么特殊的当地。

然后咱们对这个运用在QA上进行了模拟,测验复现,惋惜的是,尽管咱们在QA上把出产流量放大到许多倍并测验了多次,问题仍是没能呈现。

此刻,咱们把问题归于当时的网络环境,这个结论在当时其实是站不住脚的,假如那个时刻网络环境发生了颤动的话,其它运用为什么没有这类反常?

可能其它的服务实例网络情况是好的,仅仅发生问题的这个灰实例网络发生了问题。

那问题又来了,为什么这个实例的其它 Topic 没有报出反常,偏偏问题只呈现在这个 Topic 呢?。。。。。。。。。

至此,陷入了僵局,无从下手的感觉。

从这个客户端的开发、测试到压测,假如有 bug 的话,不行能躲过前面那么多环节,偏偏爆发在了出产环境。

没办法了,咱们再次进行了一次灰度发布,假如过了一夜没有事情发生,咱们就把问题区分到环境问题,假如再次呈现问题的话,那就只能把问题区分到咱们完成的 Kafka 客户端的问题了。

果不其然,发布后的第二天凌晨1点多,又呈现了很多的RetriableCommitFailedException,仅仅这次换了个 Topic,而且反常的原因又多出了其它Caused by 。

org.apache.kafka.clients.consumer.RetriableCommitFailedException:Offsetcommitfailedwitharetriableexception.Youshouldretrycommittingthelatestconsumedoffsets.
Causedby:org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640RuntimeExceptionorg.apache.kafka.clients.consumer.RetriableCommitFailedExceptionERROR
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException:Offsetcommitfailedwitharetriableexception.Youshouldretrycommittingthelatestconsumedoffsets.
Causedby:org.apache.kafka.common.errors.TimeoutException:Therequesttimedout.

剖析

这次呈现的反常与之前反常的不同之处在于:

  1. 1.Topic 变了
  2. 2.反常Cause变了

而与之前反常又有相同之处:

  1. 1.只发生在灰度顾客组
  2. 2.都是RetriableCommitFailedException

RetriableCommitFailedException意思很清晰了,能够重试提交的反常,网上搜了一圈后仅发现StackOverFlow上有一问题描绘和咱们的现象相似度很高,惋惜的是没人回复这个问题:StackOverFlow。

咱们看下 RetriableCommitFailedException 这个反常和发生这个反常的调用层级联系。

往往排查很久的问题,最后发现都非常简单。。。

除了发生反常的具体 Cause 不同,剩下的都是让咱们再 retry,You should retry Commiting the lastest consumed offsets。

往往排查很久的问题,最后发现都非常简单。。。

从调用层级上来看,咱们能够得到几个要害的信息,commit 、 async。

再结合反常发生的实例,咱们能够得到有用要害信息: 灰度、commit 、async。

在灰度音讯的完成上,咱们的确存在着办理位移和手动提交的完成。

往往排查很久的问题,最后发现都非常简单。。。

看代码的第 62 行,假如当时批次音讯经过 filter 的过滤后一条音讯都不符合当时实例消费,那么咱们就把当时批次进行手动异步提交位移。结合咱们在出产的实际情况,在灰度实例上咱们的确会把所有的音讯都过滤掉,并异步提交位移。

为什么咱们封装的客户端提交就会报很多的报错,而运用 spring-kafka 的没有呢?

咱们看下Spring对提交位移这块的中心完成逻辑。

往往排查很久的问题,最后发现都非常简单。。。

能够同步,也能够异步提交,具体那种提交方法就要看 this.containerProperties.isSyncCommits() 这个特点的装备了,然而咱们一般也不会去装备这个东西,大部分都是在运用默认装备。

往往排查很久的问题,最后发现都非常简单。。。

人家默认运用的是同步提交方法,而咱们运用的是异步方法。

同步提交和异步提交有什么区别么?

先看下同步提交的完成:

往往排查很久的问题,最后发现都非常简单。。。

只要遇到了不是不行恢复的反常外,在 timer 参数过期时刻范围内重试到成功(这个方法的描绘感觉不是很谨慎的样子)。

往往排查很久的问题,最后发现都非常简单。。。

咱们在看下异步提交方法的中心完成:

往往排查很久的问题,最后发现都非常简单。。。

咱们不要被第 645 行的 RequestFuturefuture = sendOffsetCommitRequest(offsets) 所迷惑,它其实并不是发送位移提交的请求,它内部仅仅把当时请求包装好,放到 private final UnsentRequests unsent = new UnsentRequests(); 这个特点中,一起唤醒真正的发送线程来发送的。

往往排查很久的问题,最后发现都非常简单。。。

这里不是要点,要点是假如咱们的异步提交发生了反常,它仅仅简单的运用 RetriableCommitFailedException 给咱们包装了一层。

重试呢?为什么异步发送发生了可重试反常它不给咱们自动重试?

假如咱们对多个异步提交进行重试的话,很大可能会导致位移掩盖,从而引发重复消费的问题。

正好,咱们遇到的所有反常都是 RetriableCommitException 类型的,也就是说,咱们把灰度位移提交的方法修改成同步可重试的提交方法,就能够解决咱们遇到的问题了。