如何更好的设置timeout
为什么会有timeout
百度了一下timeout的字面意思,就是简单的“超时”,那么timeout为什么跟我们编程息息相关,我没有找到timeout的最初的出处,但是我自己想了一下,这个应该是跟tcp/ip协议一起出现的,timeout应该是伴随着io出现的,io又分为网络io和磁盘io,当时我们不太关注磁盘io,主要关注的是网络io,所以我感觉是跟tcp/ip协议一起出现的,这个具体还要在查一下。如果网络交互没有timeout会出现一个什么情况?我不知道对端是否存活,那么有人说了我可以通过heartbeat来保持长连接,那么问题又来了,心跳间隔要设置多长?心跳间隔设置了我怎么来确定是否有心跳过来,那么就设计到了read心跳包,read心跳包有回到了最初的问题,如果我们不设置timeout会有啥结果?
不设置timeout的危害
如果我们不设置timeout会有什么影响呢?
例子1:以前在做长连接push服务的时候遇到了一个问题,就是一台服务器的长连接服务的内存在承载了100w连接的时候内存使用非常高,当时是用golang语言开发的,每条连接会有两个goroutine(读和写)来维护长连接的交互,每个goroutine占用4KB(golang1.4以后已经变为2KB,轻量的goroutine+channel是golang语言适合并发开发的优势)的大小,那么我们可以估算一下不到4G的内存使用,加上其他的一些信息总共不应该超过5G,但是当时的内存使用都是10多G,一直比较纳闷,好在golang有比较好的runtime的pprof的监测,能够很容易的dump出来整个goroutine的运行情况(类似于java的jstack),当拿到这些信息的时候,通过分析看到有很多的goroutine停留在socket的read上,而且是刚刚建立连接,等到握手信息的read上边,由于golang的并发比较简单,我们对于read采用了阻塞read,看到这个异常以后就开始检查代码,发现read的时候没有设置timeout,如果没有设置read会有什么结果呢,这个等待read的goroutine就会一直在阻塞read(一直到操作系统层面的tcp超时,一般默认是两个小时),那么就造成了goroutine(java里边可能是thread)的泄露,从而造成了内存泄露,也就能理解为什么内存过高了。找到问题修改代码,只加了一行代码就解决了这个问题。
例子2:我们的线上系统,经常会遇到一个错误,就是redis获取连接超时,遇到这种问题,很多人就来找我说是不是redis挂了,我看到这个问题一般的反应:
在使用redis的时候取到连接用完没有放回,这也是一种连接泄露的情况
还有在并发很高,连接池的连接设置的比较小,不够用了,比如你的redis的连接设置的是10条,每个redis的请求是10ms,那么1s最多能够完成1000个请求,所以当你的qps超过1000的时候会出现什么情况?就是刚才提到的获取连接超时。
还有一种情况是redis的卡顿(redis是单线程工作的,所以决定它的一些应用场景,具体可以自己网上查看相应的资料,或者有兴趣下来再单独沟通redis的问题),redis的卡顿会导致所有的请求都会耗时比较高,那么也会遇到获取连接超时的问题。
重点要说的是最后一种情况,我们假设最后一种情况我们没有设置超时时间,那么会有一个什么样的结果,所有的连接都在等待redis的响应结果,所有的业务线程都在等待redis的连接,那么请求越堆积越多,一种是造成整个系统的雪崩,一种是如果没有控制好thread的数量,thread会一直增加最后导致oom。
例子3:某核心模块A,出现过两次获取mysql连接超时,当时看到以后由于影响比较大,直接重启服务,然后开始追查问题,首先是检查mysql的慢日志,发现没有慢日志,然后检查mysql事务里边除了mysql的操作,还有没有其他的阻塞操作,发现很多的redis的操作,而且redis的value比较大,操作比较复杂,而且当时redis没有设置超时时间,第一反应就是redis的阻塞导致了mysql的事务的阻塞,从而导致了连接不够用。商户中心改掉了redis的处理,增加了超时时间,结果没过几天又出现了,这次出现问题以后,直接先jstack了一份信息,然后重启恢复,分析jstack的信息,发现一个奇怪的现象,很多的线程都在等着写文件的锁。分析代码,发现一个问题,就是mybatis开启了debug模式,有大量的mysql日志输出,而且是输出到了console,默认的docker容器的console就是linux操作系统的messages文件,由于写的数量巨大,导致了磁盘io的卡顿,从而导致了mysql事务的阻塞,从而获取连接失败,那么找到问题,修改就是改个配置的事情,把mybatis的debug关闭。从这个问题我们看到的是磁盘io的问题,磁盘io的问题遇到的很少不过也会遇到,这种情况我们应该如何避免呢?
- debug日志慎用
- 不要乱输出日志,输出有用日志
- 学会使用异步日志(掌握的情况下可以使用,必然会出现比同步日志更恶化的情况,还可能出现丢日志)
如何更好的设置自己的timeout
如何设置好timeout,这个题目比较大,我大概结合着自己的经验提供一些参考,当时我不是说这个请求就是设置多少s,或者多少ms,这个是没有意义的,也不能拍着脑袋说。
- 中间件+存储的timeout设计,除了mysql其他的都还好,可以根据系统的运行情况来调整,mysql的相对比较麻烦,我们能设置的就是获取连接的超时时间,那么设置多长时间合适呢?这个一般来说都是10s,当然这个需要根据整个系统的吞吐和连接池的大小来做相应的设置。
- 业务超时,grpc+http的,grpc一般都是内部服务的请求,那么我们也需要根据系统的吞吐和业务的情况来做相应的调整,比如现在绝大部分设置的grpc的timeout都是30s,大部分情况下是满足需求的,但是也有风险,就是刚才提到的thread泄露,导致系统的oom,所以说整个设计需要根据系统的情况来做调整
- 请求第三方的接口的超时,我们一般请求第三方都是http的接口请求,那么http的超时设计也会设计到第三方系统的一个性能的情况,这种一般都需要自己根据系统运行情况来做统计分析,最后得出一个比较合理的值,那么如果不设计timeout会出现什么情况?必然是thread泄露,从而导致oom或者整个系统hang住,我们老的微信餐厅,就是因为请求支付宝和微信没有设置timeout导致tomcat的线程数打满,从而导致整个系统hang住,服务不可用。
总结一下:有io的地方就必须有timeout,这个可以当成是一个编程的惯例或者一个规则。好的timeout的设计可以使系统更加的健壮,对用户更加的优雅。timeout其实也是对系统的一种熔断降级。
有疑问加站长微信联系(非本文作者)