最近在对一个项目做故障注入测试,测试的场景如下:

+-------------------+        +-----------------------+       +--------------------------+
|                   |        |                       |       |                          |
|   Test Client     +------->+    Stateless Frontend +------>+   Stateful Backend       |
|                   |        |                       |       |                          |
+-------------------+        +-----------------------+       +--------------------------+

其中一项测试是考察前端服务在后端服务宕机场景下的表现。使用Linux sysrq来模拟宕机。首先对后端服务所在系统开启sysrq:

$ sudo sh -c "echo 1 > /proc/sys/kernel/sysrq"

在测试端持续压力下,重启后端机器:

$ sudo sh -c "echo b > /proc/sysrq-trigger" 

这种方法重启机器,内核不会对文件系统进行unmount,也不会关闭网络链接等。

我们遇到的问题是,后端机器重启之后很长一段时间,前端仍然会以一定的概率报错。前端日志显示的是网络超时,但是搜索后端日志,发现请求并没有到达后端。前端日志还显示了错误的一个共性,就是这些超时请求都发生在同一个线程。从而猜想请求一定是卡在这个线程里了。

需要看Java程序运行状态,采用JProfiler对该进程生成了一个HPROF dump文件,并进行分析(也可以采用jmap生成dump,用mat进行分析)。分析发现,对应到该后端机器的连接池没有可用的连接。同时确认,有N个(连接池连接数上限)到宕机机器的请求一直在inflight队列里,等待后端返回payload。因为网络模型是基于线程池的,一旦线程池的连接全部用于inflight请求了,自然新的请求就会被饿死。但问题是当对端宕机,为什么没有相应的连接关闭或重置事件被触发?

经过查证,原因是这些网络连接在后端宕机后一直处于等待对方回包的状态。由于后端机器重启,连接也并没有被关闭,这些连接一直处于idle状态。那么linux对于处于idle状态的连接是如何处理的呢?这取决于下面三个kernel的设置:

net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9

注意到缺省的net.ipv4.tcp_keepalive_time7200秒,也就是两个小时。含义是一个连接空闲达到两个小时的时候,kernel才会发keepalive请求,确认对端是否存活。Keepalive请求以每75秒为间隔,最多发送9次。当对端一直不回应,则关闭连接。于是,我们又查看了一下客户端请求日志,发现果然在两个小时后,不再有新的错误发生了。

至此,我们已经基本确认这个问题的根源了。一个解决方案是采用setsockopt对这些连接设置TCP_KEEPIDLETCP_KEEPINTVLTCP_KEEPCNT,分别覆盖上面三个系统级别的参数。我们直接改变系统配置来解决该问题:

net.ipv4.tcp_keepalive_time = 90
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 3

这样在一个连接空闲90秒之后就开始探测对端是否存活,能够有效的避免因宕机导致的持续错误。

改完参数之后,我们做了同样的宕机测试,并用tcpdump在前端机器上抓包。能够发现在90秒之后,前端机器向后端机器发送keepalive包,在没有收到回复后,最终关闭了连接。

参考