线上问题排查实例分析|关于网络超时 - 滴滴技术

相较于日常的编程工作,线上问题排查往往是较为低频但重要的场景。尽管我们可能不常遇到,但每当这时,其紧迫性和重要性都使得我们必须迅速、准确地找出解决方案。因此,对技术人来说,培养有效的线上问题排查思路和方法至关重要。

为应对这种不确定性,我们需要在平时就关注和学习他人的排查经验与技巧,以便在需要时能够迅速调用这些储备,冷静应对。滴滴技术公众号将定期分享线上问题排查的相关文章,以期为读者提供一些解决问题的思路和实战案例,期待通过这些内容与大家进行深入的交流和探讨。

最近生产环境出现了两起奇怪的超时问题,一个是调用 redis 导致程序夯住300s左右,另一个是上游请求频繁超时(120ms~200ms之间),两个问题都发生在同一个服务上(golang程序),并且比较典型,所以在这里一起说下,希望对大家有借鉴意义。

Redis超时300s

一天业务RD同学突然发现线上一个新增接口偶现耗时300s+,我第一反应觉得可能又是程序里哪里有 bug,因为最近在给各个项目做体检,发现了很多类似问题,但 RD 同学反馈说通过日志看应该是调用 redis 夯住了,而且新增的业务逻辑确实新增了 redis 调用,但调用 redis 的 sdk 没有设置超时,所以觉得应该是 redis 或者网络抖动导致的,但300s还是让人无法理解,就算是抖动也不至于耗时300s+,redis 应该也会主动关闭连接,所以我们开始了问题排查。

  • 1、首先排除是不是抖动,

通过跟 redis 同学反复确认,他们的耗时处理都是在1ms以下,没发现长时间的耗时,又跟运维确认是否有网络抖动,运维反馈说机房10G网卡确实会出现偶尔的丢包现象,正在换100G网卡,但偶尔丢包会有重试,不会导致耗时这么长,所以排除了丢包问题(后面发现确实是丢包,没想到的是重试包也丢了)。

-2、接着开始怀疑是我们使用的 sdk 有问题,

因为以前发生过sdk导致的耗时问题,于是升级到了最新的库,同时加了更详细的 debug 日志,最后发现是 read的时候阻塞了,并最终收到了 RST 包,日志如下,所以基本排除了 pkg 的问题。


connection reset by peer 
  • 3、这个时候大家开始了各种猜测:

丢包、内核参数问题、Gateway 升级等等,同时也发现了更多的现象,比如偶尔出现几台机器同时耗时长的问题,其他部门的同学也反馈有300s耗时问题,同样是调用 redis,种种猜测之后还是决定抓包来看,由于复现的概率比较低,也没有规律,同时机器也比较多,抓包成本比较大,所以抓包耗费了很长时间,直到线上复现了超时问题。

  • 4、redis proxy 机器数比较少相对好抓,

client 端机器比较多,所以只抓了个别机器,最终抓到问题流量如下:

通过分析流量,我们发现 client 端一直在超时重试(Retransmission,这里遵循以TCP_RTO_MIN=HZ/5 = 200ms 开始的退避算法,200ms、400ms,800…),一直到最后服务端返回了 RST,这个正好印证了第二步日志中报的错误"connection reset by peer",在这期间,server端一直没有返回 ack,但在09:18:12的时候收到了server端发送的 keep-alive 包,也叫 tcp 探活包,时间刚好是从上一次请求后的75s(redis 确认设置了 keep-alive 时间是75s),client 也发送了 keep-alive 的 ack 包,理论上来说 server 端在75s内就不会再发送探活包了,但实际后面依然在发送,直到RST结束,所以从整个流量来看,好像 redis 端收不到包,但 client 可以收到 redis 的包。同一时间redis也同步开启了抓包,但并没收到我们发送的PSH包,接下来开始一步步验证猜测。

  • 5、因为调用方式采用的VIP,VIP指向 Gateway,

但 Gateway 都是公用的,流量很大,很难抓包,所以我们通过排除法,选择直连真实ip,绕过 Gateway,通过两天观察,类似的问题消失了,所以基本就定位到是 Gateway 问题,但运维同学反复确认过 Gateway 确实没发现啥问题,而且也没有其他业务反馈类似的问题(有3000多个VIP),再此陷入停滞。

  • 6、既然已经通过排除定位到 Gateway 问题

那就看下 Gateway 到底怎么实现的,看是不是有转机,开始跟系统运维同学确认实现方式:

“fullnat模式”,一下感觉有了转机,fullnat相对nat模式有一个重要的区别就是session表从一个变成了两个(session表用作关联客户端和服务端的ip:port),一个IN一个OUT,如果IN的表丢失了,而OUT表正常,就有可能出现上述抓包的问题流量——client 能收到 server 包,server 收不到 client 包。

通过跟系统运维同学确认,确实有可能会出现,运维同学也开始了 Gateway 的验证和排查,直接贴运维同学的排查过程,更专业一些:

经过不断推断和验证,终于得到了路由器厂商的确认。

超时思考

到此算整个事件的结束,实际情况远比描述的复杂,也引发了我关于超时的思考:

关于超时的问题,是不计成本的增强基础设施建设吗?

基础设施当然会尽量提高自身的稳定性和可用性,但一旦达到一个限值,再提升,可能就需要付出几倍甚至上百倍的成本,比如丢包率从0.02%降低到0.01%,提升0.01%的成本可能比实现0%到99.98%的成本还要大,100%更是永远达不到,但任何一个系统都不是只有一层,如果在上层加一次超时重试,这种低成本的操作往往可以达到惊人的效果,还是以刚才的例子说明,0.02%的丢包率,如果加一次重试可以降低到0.000004%,成功率一下从99.98%提升到99.999996%,如果一次重试不够可以再加一次。

如果加重试造成雪崩怎么办?

有一种观点是不要在下游加重试,统一从最上游加一次重试就行了,这种观点无非是担心中间链路有突发流量,会导致下游雪崩,比如网络抖动导致大规模重试。

但这种观点忽略了更重要的问题,如果说都通过最上游"端"来重试,那下游任何一个抖动导致的超时,都有可能导致整个链路所有服务被重试,到时候就不是一个服务雪崩,而是整个链路雪崩了,其实解决雪崩的问题不应该是通过减少重试次数来缓解,而是通过限流和熔断,对限流来说,越上层限流越好,而对于重试,是越下层重试越好。

那到底该设置多少超时时间和重试次数呢?

其实这两个数是有互相配合的,举个例子大家就明白了,比如我调用下游的成功率要达到五个9,调用下游的耗时98分位是20ms,那如果我设置超时时间20ms就意味着有2%的概率失败,那加一次重试可以降低到0.04%,可用性达到99.96%,不满足需求,那就再加一次重试,可用性达到99.9992%,但如果超时次数太多对上下游压力也会比较大,所以最好不要超过3次,如果重试次数确定了,那就可以通过提高超时时间来保证成功率,比如设置成99分位的耗时或者更高,但最理想的方案还是依据SLA,来确定超时时间和重试次数。