tcp_keepalive_time and rst Flag in NAT Environment

Here, I'm not going to explain the details of what is TCP keepalive, what are the 3 related parameters tcp_keepalive_time, tcp_keepalive_intvl, tcp_keepalive_probes mean.
You need to know, the default value of these 3 parameters with net.ipv4.tcp_keepalive_time = 7200, net.ipv4.tcp_keepalive_intvl = 75, net.ipv4.tcp_keepalive_probes = 9, especially the first one, tcp_keepalive_time may and usually will cause some nasty behavior to you in some environment like NAT.

Let's say, your client now wants to connect to server, via one or more nat in the middle way, whatever SNAT or DNAT. The nat server, no matter its' a router or a Linux based device with ip_forward on, need to maintain a TCP connections mapping table to track each incoming and outcoming connection, since the device's resource is limited, the table can't grow as large as it wants, so it needs to drop some connections which are idle for a period of time with no data change between client and server.

This phenomenon is quite common, not only in consumer oriented/low end/poor implementation router, but also in data center NAT servers. For most of these NAT server, their tcp_keepalive_time is usually set to 90s or around, so, if your client set the parameter larger than 90, and have no data communications for more than 90s, the NAT server will send a TCP package with RST flags to disconnect the both end.

In this situation, one workaround is to lower the keepalive interval than that, like 30s, if you can't control the NAT server.

Recently, when we sync data from MongoDB Master/Slave, note, the architecture is a little old, not the standard replica. We often observe a large amount of errors indicating the failure of synchronisation during creating index process after finishing synchronizing the MongoDB data. Below log is get from our slave server:

Fri Nov 14 12:52:00.356 [replslave] Socket recv() errno:110 Connection timed out 100.80.1.26:27018
Fri Nov 14 12:52:00.356 [replslave] SocketException: remote: 100.80.1.26:27018 error: 9001 socket exception [RECV_ERROR] server [100.80.1.26:27018]
Fri Nov 14 12:52:00.356 [replslave] DBClientCursor::init call() failedFri Nov 14 12:52:00.356 [replslave] repl: AssertionException socket error for mapping query
Fri Nov 14 12:52:00.356 [replslave] Socket flush send() errno:104 Connection reset by peer 100.80.1.26:27018
Fri Nov 14 12:52:00.356 [replslave]   caught exception (socket exception [SEND_ERROR] for 100.80.1.26:27018) in destructor (~PiggyBackData)
Fri Nov 14 12:52:00.356 [replslave] repl: sleep 2 sec before next pass
Fri Nov 14 12:52:02.356 [replslave] repl: syncing from host:100.80.1.26:27018
Fri Nov 14 12:52:03.180 [replslave] An earlier initial clone of 'sso_production' did not complete, now resyncing.
Fri Nov 14 12:52:03.180 [replslave] resync: dropping database sso_production
Fri Nov 14 12:52:04.272 [replslave] resync: cloning database sso_production to get an initial copy

100.80.1.26 is DNAT IP address for MongoDB master. As you can see, the slave receives reset from 100.80.1.26.

After some package capture and analysis, we get the conclusion with the root cause of tcp_keepalive_time related. If you are sensitive, you may consider TCP stack related issues when see "reset by peer" keyword at first glance.

Be careful, the same issue also occurs in some IMAP proxy, IPVS envionment, etc.  After many years hands-on online operations experience, I have to say, DNS and NAT are two potential threats of various issues, even in some so-called 100% uptime environment.

一个月的艰辛稳定服务过程

业务大了成本问题更加的凸显,最重要的之一就是带宽,为了更好的优化资源,我们决定将目前 BGP 全线的流量切换一部分到双线上去。说白了很简单,网络方面不需要动什么大手脚,全网 10G 的链路半年之前就全部升级完毕,剩下的就是加前端,10G 服务器,LVS(nat), Keepalived,Nginx。接下来就是小流量的上线测试,从 10M 到 50M 到 100M 再到 300M 前后大概有一周的观察时间,整体还是很稳定的,当然也遇到了一些小问题,比如 10G 网卡不规则的出现了一些 rx_crc_errors/rx_missed_errors,通过优化一些硬件设备基本得到了解决,虽然后期还会时不时的冒出一些,但是从各个服务以及业务的监控方面没有看到潜在的影响,暂时略过。

正所谓稳定压倒一切,从开始小流量切换到正式全量上线的这一个月,为了稳定,充满了坎坷,下面是这一段时间遇到的一些比较有挑战的事件, 「很不幸的是」,全部是人为原因。

4xx 错误飙升
一周之后的某个早上收到报警,4xx, 5xx 的错误上升了大概一倍的样子:

可以确认的是这段时间没有任何的变更,从到后端的请求来看,这段时间的异常并没有对其有很大的影响,但是本着负责任的态度还是把当时的 log 归档拿出来分析了下详情:

可以看到,这段时间的 return code 主要是由 499 以及 500 错误引起,并且 499 的错误占了大部分的,那就从 499 的分析开始。G 了一番,发现这个 code 并非是一个标准的 RFC,唯一可以确认的就是是客户端主动的关闭了链接,至于是什么原因让客户端主动关闭的,原因就太多了。除此之外,还有一部分的稳定的 408 错误,这个比较好理解,对于由于 Nginx 读取客户端发来的请求会造成超时而返回 408。
看了上面的解释,基本得不到很明确的答案。接下来,至少还可以从 debug log 以及 error log 入手,看看能否得到一些信息。
首先看看 error log,关于 Nginx 的 access, error log 不少人都有误解,这里统一说明一下:
1. access log 的非 2xx 错误只有部分的会打到 err log 里面,仅仅是部分,而 access log 是所有的 return code 的集合
2. error log 并非是一些非 2xx code 的集合,里面很多都是开发定制的一些东西,代码都是写死的
3. 所以,任何的返回码(2xx, 4xx, 5xx)的问题都可以从 access log 里面找到问题
看 err log,大部分都是下面两种情形:
1. "no live upstreams while connecting to upstream"
2. "upstream timed out(110: Connection timed out) while reading response reader from upstream"
总结起来就是连不上 upstream 了,为什么会 time out 并不知道。再看看 debug log,这个需要在编译的时候加进 –with-debug,里面能得到的信息同样比较少:

2026/08/02 15:29:14 [debug] 58443#0: *26274947 http upstream recv(): -1 (11: Resource temporarily unavailable)

2026/08/02 15:29:14 [debug] 58443#0: *26274956 recv() not ready (11: Resource temporarily unavailable)

调查到了这里,基本从已有的信息没得到什么特别的结论,考虑到对后端本身并没有影响,这事就这么被放过去了。但是,再怎么逃避也逃避不了墨菲定律。

logrotate(gzip) 的问题
几天之后的某个晚上,突然收到报警,ipvs master 上的 rs 被移除,紧接着 ipvs slave 上的 rs 也被移除,接下来就是疯狂的短信轰炸了,一会儿 add,一会儿 remove。当时我不在现场,让我们同事先临时手动把 vip 从目前的 master 切换到了 slave,但是不见好转,当机立断,直接全部切换到全线,客户端由于会 cache 以及重发机制,所以这 20min 的 outage(发到后端的请求大概跌了 2/3) 对实际的影响并不是很大。回去之后开始重点分析这次事故。
首先是把 dr, rs 包括 traffic, req, lvs conn, Nginx return code/status, 各种 rt/delay 在内的核心监控一步一步分析回溯,理清了从问题开始一直到恢复这中间发生的每一步过程,发现的最大的收获是出问题的这段时间,lvs 到 rs 的延时大大增加:

其次,keepalived 打到 messages 里面的 log 也看到了不少的东西,比如这段时间,会频繁的 add, remove rs:
Sep  6 20:20:10 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.6]:80 success.
Sep  6 20:20:10 lvs-1 Keepalived_healthcheckers[107592]: Adding service [192.168.90.6]:80 to VS [111.111.111.4]:80
Sep  6 20:20:14 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 failed !!!
Sep  6 20:20:14 lvs-1 Keepalived_healthcheckers[107592]: Removing service [192.168.90.5]:80 from VS [111.111.111.4]:80
Sep  6 20:20:20 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 success.
Sep  6 20:20:20 lvs-1 Keepalived_healthcheckers[107592]: Adding service [192.168.90.5]:80 to VS [111.111.111.4]:80

把 log 的时间扩展到三天前,再仔细的观察观察,可以发现,每一次 add, remove 的操作都是出现在整点的时候:
Sep  6 12:00:15 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 failed !!!
Sep  6 12:00:15 lvs-1 Keepalived_healthcheckers[107592]: Removing service [192.168.90.5]:80 from VS [111.111.111.4]:80

Sep  6 15:00:11 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 failed !!!
Sep  6 15:00:11 lvs-1 Keepalived_healthcheckers[107592]: Removing service [192.168.90.5]:80 from VS [111.111.111.4]:80

联想到整点 ngx 会进行 logrotate 的操作,期间会产生大量的 io,导致系统负载变高,再加上 ngx 本身处理 req 会有一定的负载,导致 lvs 监测到 rs 无法响应的状态。
通过监控可以发现,每个整点的 load 都会变大:

从上图可以发现,整点的这一小段时间,load 至少会超过 1,跟 24 核的机器比,似乎非常的小,但是需要考虑的一点是,我们的采集 load 的监控是 60s 一次,监控实现秒级的成本太大,实际在 60s 内系统的负载会比上图的大得多。
为了验证这个问题,手动的模拟一次 logrotate(gzip) 的操作,平均一小时的 log 量在 5G 左右。下面的是手动执行一次 logroate 操作得到的监控数据,可以看到高峰时段,这块普通的 SAS 盘的 io util 达到了 100%,磁盘写吞吐达到了 245M/s,load 峰值超过 10:
04:06:00 CST       DEV       tps  rd_sec/s  wr_sec/s  avgrq-sz  avgqu-sz     await     svctm     %util
04:06:01 CST    dev8-0    702.06     24.74 711958.76   1014.13    145.91    208.25      1.47    103.20
04:06:02 CST    dev8-0    701.01      8.08 704840.40   1005.48    146.79    208.00      1.44    101.01
04:06:03 CST    dev8-0    677.55      0.00 681755.10   1006.20    144.27    213.12      1.51    102.04
04:06:04 CST    dev8-0    672.45      8.16 681608.16   1013.63    141.47    210.12      1.52    102.04
04:06:05 CST    dev8-0   1364.65      0.00 694884.85    509.21    122.71     91.27      0.74    100.91

这总算是找到 lvs 频发 add, remove 的原因了,接下来改进就相对比较简单了:
1. lvs 对 ngx 不要太敏感,这个在 timeout 的一些设置上适当的加大可以缓解
2. ngx 的 log 处理问题,最初有同学提议只开一台 rs 的log,其他的全部关闭,这个看上去似乎可行,开一台基本可以代表其他 rs 的情况,但是实际情况可能并非我们所能想像,如果没有开启 access 的机器出现了问题而开启的每有任何问题,后续的问题排查将会比较的麻烦。所以,不管如何,保留一分全量的 log 还是非常有必要的。对此,比较通用的做法将 Nginx 通过 syslog 直接传到的远端的机器上,本地保留一分全量的,但是不开启 logrotate 的 gzip 压缩。
3. 除此之外,还可以考虑通过 access_log, error_log 的 ratio 来控制产生 log 的比例

Nginx open file 的问题
这个问题到此结束,这样安稳的过了四天,某天晚上 messages 里面发现 keepalived 频繁的 add, remove rs 了,这次时间不是整点了,虽然只出现了一两次,但是可以暴露出整个集群还是处于一个不稳定的状态,最初我们认为是 TCP_CHECK 的检测机制有问题,太过敏感,于是换成了 HTTP_GET 的方式,但是第二天仍然时不时的报错,并且从时间上来看完全没有规律:
Sep 10 17:15:19 lvs-2-2399 Keepalived_healthcheckers[34943]: MD5 digest error to [192.168.90.6]:80 url[/], MD5SUM [f9943d27e3420479271984e01dfe85dd].
Sep 10 17:15:19 lvs-2-2399 Keepalived_healthcheckers[34943]: Removing service [192.168.90.6]:80 from VS [124.251.33.4]:80
Sep 10 17:15:22 lvs-2-2399 Keepalived_healthcheckers[34943]: MD5 digest success to [192.168.90.6]:80 url(1).
Sep 10 17:15:28 lvs-2-2399 Keepalived_healthcheckers[34943]: Remote Web server [192.168.90.6]:80 succeed on service.
根据 keepalived_healthcheckers 检测的逻辑来看,肯定是发现 http://192.168.90.6/ 返回的不是既定的 md5sum 值,才触发了 remove 操作,很可能 rs 返回的不是 200 的错误才导致的。
抓包,为此蹲守了很长的时间抓住了重现的机会:

果真,确实是 rs 返回了 500 的错误才导致被 remove 的。之前虽然 5xx, 4xx 会增大,但是几乎并不影响 200 的数量,这次却不是这样的情况,500 的增多,200 的却异常下跌。全量分析 err log,惊人的发现,这段时间内的错误条目是平时的 100 倍,并且,出现奇怪的出现了下面的错误:
2014/09/06 20:09:55 [alert] 3164#0: *4082964565 socket() failed (24: Too many open files) while connecting to upstream, client: 122.93.211.105, server: foo.example.com, request: "POST /fk HTTP/1.1", upstream: "http://192.168.90.53:8888/fk", host: "foo.example.com"

一查 /proc 下面的 limits,Ngx 的竟然变成了 1024:

看到 1024 的那一刹那突然想起了我们之前某个产品线同样的出现过一次这个文档,当时尝试了各种办法包括直接修改 limits 文件也无法提高 open file,后来直接 reboot 机器了(有自动的 failover 策略),恢复,这事后来就被慢慢淡忘了,但是真相依然不清楚。结果,这次墨菲又找上门来了。后来我们发现还是有些用户(1, 2)  出现了跟我们类似的情况,但是依然不清楚原因。
为了确认就是 open file 造成了,对比一下 5xx(too many open file) 跟非 200 错误的比例:

完全符合。
真正的原因我也没法说清楚,基本可以肯定的是在 Nginx 运行过程中 open file 被改成了 1024。不过现在应该可以完全避免这个问题了,除了通过 worker_rlimit_nofile 指定之外,我们把这些进程的 limits 文件全部监控起来,包括 fd 的数量,这样即使再次「诡异」的变成了 1024,我们也能及时的发现采取措施。

kernel panic 的一些问题

机器稳定的运行了几天之后,某天早上 master 突然 kernel panic 了。
能得到的信息非常少,接下来继续挖坟似的分析 messages log。5min 打出的几百条 log 一条一条的对着 timestamp 分析,从 log 看,slave 是成功切换了,在 master 起来之后,流量又逐渐的恢复到了 master 上,但是至此之后,rs 上的流量由于 master 出现了问题。开始 master 看上去像是 work 的,inacctive conn 开始恢复,但是 active conn 一直到 5min 之后才开始恢复,这期间发生了什么,lvs 的 log 并没有很详细的说明,看上去像是两台 lvs 都没有工作,slave 变成了 backup 自然不再工作,挂掉的那台 master 虽然起来之后进入了 master 并且也 enable 了两台 rs,但是正常的流量却没有过来。
出现上面这种情况的可能原因是什么?keepalived 的配置出现问题了,果真,没有指明 priority。这个问题复现还算比较容易,直接把 master 的搞 kernel panic 就可以出现跟生产环境一样的现象,严格指定 priority 之后则不会出现这么长时间内无人接收请求的情况。
拿来主义是非常可怕的,当时我没有 review 下 keepalived 的配置就直接开跑了,核心服务还是要亲自验证。
除了上面这个教训之外,既然是 HA,一定要自己做一些破坏性实验,验证各种可能的情况。真正上线之前不验证,及时发现问题,真的全量上了,到时候出现问题的影响更大。我们虽然在小流量切换时,也做了一些破坏性实验验证 HA,但是并没有缜密的把所有情况都列穷举出来。

上面仅仅解决了 keepalived 的一个问题,kernel panic 似乎没有引起我们的注意,之后几天接连的出现 kernel panic,我们才意识到,这是个非常严重的问题,虽然每次 kernel panic 都完全的切换成功了。想到现在 lvs 使用的 kernel 跟官方唯一不一样的地方就是我自己重新编译了 kernel,调大了 IP_VS_TAB_BITS。既然这么一个小的改动都会引发 kernel panic,那索性还是先切回官方的版本,毕竟我们现在线上 90% 的机器都已经成功的跑了很长时间了。没想到的是,即使使用了官方的版本,依然不停的 panic,后来发现是其实是个 bug,在给 ipvs 的连接做 hash&unhashing 的时候缺少对被修改连接的锁,导致内存奔溃。理论上,我们可以对目前的 src 打 patch,但是实际的效果不得而知,加上没有之前的成功案例以及团队缺少精通内核的人,不得放弃这个想法。后来想到了我们兄弟团队,在多方求助之后,终于用上了高大上的集团内核,至此内核运行的很稳定。


4xx 问题的最终调查
还记得最上面第一次出现的 4xx 飙升问题吧,从某日起,4xx 的错误又明显的增大,通过对历史数据的分析发现增多的 4xx 错误主要集中在 499,随着高峰的来临(白天工作时间至凌晨),499 的错误也显著性的增加:

再次回顾下 499, 408 的问题。
499 实际上是客户端先关连接的,原因很多(无法确定,客户端引起),比如:
1. 刷页面的时候连按了 2 次 F5,就会导致之前页面上的请求没有收到完的就 499
2. 网络太慢
3. ngx 刚转发请求往后端,后端还没来得及应答,前端就主动关连接了

408 问题的解释:
1. 408 是客户端连完整的请求也没有传完,从 access log 里面没每发现 ua,正常的上传应该是有 ua 的,所以这个问题很可能是由于网络问题造成的
2. 408 的复现也比较简单,发送 POST /app_log HTTP/1.1 给 ngx,不发数据,过 60s 就会返回 408 的错误

除了 4xx 错误的飙升之外,upstream java 的 fd 也会随之飙升,从上面的一些数据至少可以得到下面的一些结论:
1. 本身移动网络的质量相比会比较差,再加上刚切换完双线,有一定 4xx 的增加还是可以理解的
2. 正常的应用,比较稳定的 4xx 应该是比较正常的
3. 但是,我们的情况是连续数天的 4xx 频繁不稳定,所以,可以确定有问题

想到这段期间唯一的操作就是切换了流量,于是,直接回滚,全部切换到全线 BGP,观察了一天没有任何效果。在跟开发确认一些细节的时候发现,原来在 4xx 飙升的前一天,更新过一次代码,既然切回全线没效果,那继续回滚代码吧,结果大跌眼镜的是问题依然存在。

结合 upstream 发现,有大量的 CLOSE_WAIT  状态,并且 recv-Q 均为非 0:
# cat status
established
syn-sent
syn-recv
fin-wait-1
fin-wait-2
time-wait
closed
close-wait
last-ack
closing
# for i in $(cat status);  do echo -n  "$i: " && ss -o state $i src INTERNAL_IP | wc -l; done
# ss src INTERNAL_IP:PORT  | head


上面的非 0 字段表明,upstream 没能成功的接受发过来的请求将其放到网卡的 buffer 里面,导致其 queue 一直保持在非 0 状态。
问题分析到这里,基本可以确定是用户态的应用没法正确的接收请求才导致此类结果,再结合上面 return code 的分析以及 upstream 的分析:
1. 切换完双线之后,由于本身网络质量会有一定的下降,所有 4xx 的会增多,这个是可以容忍的
2. upstream 代码层面应该还存在一些问题,结合 fd 的监控,网上高峰 fd 可能会飙升到 100+K,是不是有可能 fd 没有及时的回收?(fd 泄漏有没有这种可能)
3. 出问题前一天更新过一次代码,在其他没有变化的情况下,从前端到后端的监控都有不同程度的异常,是不是上线的有问题

上面提到的 fd 飙升问题,既然飙升,那就在出问题的时候上去看看是哪些 fd 飙升,首先需要确认是确实是 java 的进程导致:
for x in `ps -eF| awk '{ print $2 }'`;do echo `ls /proc/$x/fd 2> /dev/null | wc -l` $x `cat /proc/$x/cmdline 2> /dev/null`;done | sort -n -r | head -n 20

在某天出问题的中午 12 点,我登录到出问题的机器上统计了 fd 的情况:
出问题的时间段 fd 在 ~40k,分解下来包含
* tcp estab: ~1.7k,跟正常的比没变化
* tcp close-wait: ~9k,这两者大概 10k
* 剩余的 30k 通过 lsof 观察全部是 can't identify protocol 的状态

而正常情况是:
* tcp estab: ~1.5k
* tcp close-wait: ~2k
* 剩余的一些是 can't xxx

也就是说出问题阶段,close-wait & can't identiy protocol 飙升了原来的数倍:
1. close-wait 的很好理解,跟之前的观点一样,tcp 一直在等待 user space 调用 close(),否则会一直处于 close-wait 状态
2. can't xxx 的暂时没法解释,等到下一次出问题的时候再观察

有这么多的 close wait,很容易让人联想到是 fd 泄漏,想关闭的一方发送 tcp fin,服务端收到之后,如果程序逻辑有问题,忘记调用 close(socketfd),则会造成 fd 泄漏,表象就是有大量的 close wait 状态。不过从监控来看,这部分的飙升的 fd 很快就会直线下降到正常值(后来分析 jvm 的时候发现其实是 mgc, fgc 导致的 fd 回收):

问题分析到这里,那就继续深入下去,从 JVM 层面看看有没有问题。
首先看看 java 的进程,不管是正常状态还是异常状态,某几个 thread 都消耗了大量的 cpu 以及 fd,也就是说目前的 thread 是非常不均匀的,某一个 thread 能消耗 50% 以上的 fd:

接下来,搞个脚本看看最耗资源的那个 thread dump 是什么样的,有了这个就可以很容易的看到某些方法的异常了。这我在前一篇博客已经总结过,这里假设我们已经得到了 TID,先转换成 hex 然后 for 循环就能找到真相了。
for i in {1..100}; do echo "—$(date +'%H:%M:%S')—" >> tiddump.log; LAB=$(date '+%s'); HEX=`echo "obase=16; $TID" |bc`; jstack {PID} >> ./jstack$LAB; grep -iA 20  $HEX  ./jstack$LAB –color >> tiddump.log; sleep5; done

到了这一步,真相大白了,从 log 里面发现某些方法调用常年的 hang 在那儿,源于系统在处理某个大 List,客户端的 SDK 层面逻辑判断有问题,不做检查的把一些巨型字段经过压缩都发到前端这边来,结果到了后端解压缩处理的时候发现处理不了。

如果依然发现不了问题了?其实这时候问题的范围已经被缩的很小了,除了继续通过 mat、btrace、perf 深层次的跟踪服务情况之外,还有个重要的方面遗漏了,确认我们使用的一些软件的版本有无 bug,不要以为那些 release log, changelog, readme 文档是写着玩的,开发没养成看这些细节文档的习惯如果 如果我们一线运维也不看,那最坏的结果就是折腾了十来天最后看到最新的发型版本的 release log 出现了一些特殊的 bugfix 字段以及最后的欲哭无泪。
最初我们怀疑是服务在关闭连接的处理逻辑上有问题,后来发现我们的代码里面并不涉及这方面的逻辑处理,连接的建立关闭全部由 Finagle 框架自己处理了,所以一个重要的疑点是我们使用的该框架的版本是否存在连接方面的 bug,查一下他的下一个以及下几个版本的 release log 很快就能确认。

以上分析的这些案例,虽然影响没有大到会让业务遭受严重的损失,但是不夸张的说,每一个都是一个重量级的定时炸弹,不及时排除,指不定哪天全线就都挂了。这些事件,可能绝大多数的公司都不会遇到,但是思考问题的思路方向比这些案例重要的多,拿这次来说,其实思路在一开始就跑偏了,在确认前端没有问题的情况下还在花了大量的时间而不是一开始就从 upstream 下手。

ipvs 频繁 add/remove 后端 rs 问题追查

这个问题我们三年前就遇到过,限于当时的资源,没能分析出具体的原因。欠下的债肯定是要还的,最近又遇到了,不过解决起来倒是很快。
看 messages,发现有不少类似的 log:
Mar  6 15:00:11 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 failed !!!
Mar  6 15:00:11 lvs-1 Keepalived_healthcheckers[107592]: Removing service [192.168.90.5]:80 from VS [111.111.111.4]:80
Mar  6 15:00:17 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 success.
Mar  6 15:00:17 lvs-1 Keepalived_healthcheckers[107592]: Adding service [192.168.90.5]:80 to VS [111.111.111.4]:80
Mar  6 20:00:16 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 failed !!!
Mar  6 20:00:16 lvs-1 Keepalived_healthcheckers[107592]: Removing service [192.168.90.5]:80 from VS [111.111.111.4]:80
Mar  6 20:00:22 lvs-1 Keepalived_healthcheckers[107592]: TCP connection to [192.168.90.5]:80 success.
Mar  6 20:00:22 lvs-1 Keepalived_healthcheckers[107592]: Adding service [192.168.90.5]:80 to VS [111.111.111.4]:80

结合监控图,发现出现问题的时候都是在整点,并且正点的时候系统的 load 确实比较大,把历史的 sar 数据调出来发现正点的 IO 利用率都是在 100%,这一联想就只有 logrotate 在正点做 rotate 然后 zip 压缩了。模拟了一下发现可以复现该问题,剩下的就是解决问题了。

除了上面这个,我们还发现在非整点也会出现类似的 log
Mar 10 17:10:23 lvs-2 sshd[54526]: Accepted publickey for jaseywang from 192.168.10.254 port 56532 ssh2
Mar 10 17:15:19 lvs-2 Keepalived_healthcheckers[34943]: MD5 digest error to [192.168.90.5]:80 url[/], MD5SUM [f9943d27e3420479271984e01dfe85dd].
Mar 10 17:15:19 lvs-2 Keepalived_healthcheckers[34943]: Removing service [192.168.90.5]:80 from VS [111.111.111.4]:80
Mar 10 17:15:22 lvs-2 Keepalived_healthcheckers[34943]: MD5 digest success to [192.168.90.5]:80 url(1).
Mar 10 17:15:28 lvs-2 Keepalived_healthcheckers[34943]: Remote Web server [192.168.90.5]:80 succeed on service.


抓包发现 LVS 向 RS 发送 GET / HTTP/1.0 的请求, RS 却返回了 HTTP/1.1 500 Internal Server Error 的非 200 错误码,所以 LVS remove 掉这些 RS 确实是正常的逻辑:

同样从监控中看到,出问题的时间段由于频繁的切换导致了前端几乎不可用,在 5 点多这一时段 error log 的比平常大了 100 多倍,达到了 500M:

从上面的信息,全量分析了出问题阶段所有 err log 的类型,发现了惊人的答案,99% 以上的问题都源于 "too many open file" 的错误,/proc 一看,发现 Nginx 的确实变成了 1024:
# cat /proc/3148/limits
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            10485760             unlimited            bytes      
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             514947               514947               processes
Max open files            1024                 4096                 files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes      
Max file locks            unlimited            unlimited            locks     
Max pending signals       514947               514947               signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us      

root 用户的没有问题:
# ulimit  -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 514947
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 512000
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 512000
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

不过为什么突然由原来的系统级别的数字变成了 1024,实在值得奇怪,该问题很早之前在我们一台 Nginx 的机器上同样遇到,记得当时 restart 了 Nginx 依然没有效果,不得已只能 reboot 机器恢复。Nginx limit 变成 1024 的问题,其他用户也遇到类似的问题:
* http://serverfault.com/questions/516802/too-many-open-files-with-nginx-cant-seem-to-raise-limit
* http://anothersysadmin.wordpress.com/2013/08/09/nginx-and-the-too-many-open-files-limit/

目前还无法得知为什么,但是找到了部分原因,要避免此类问题倒是很简单,强行指定 worker_rlimit_nofile 就好了,谨慎起见,把 /proc/xxx/ 下面重要的文件都给监控上。

访问 VIP 缓慢的调查

将某个 https 的域名迁移到 lvs,部署完成,绑定 hosts 测试过程中发现 GET 请求非常的缓慢,一个 90KB 的图片耗费的时间长达几十秒甚至超过 1min,这个在未迁移前是从未出现过的现象。
为了一步一步调查,绑定了 hosts 直接访问 rs,结果跟未迁移前效果一样,但是直接访问 VIP 则十分的缓慢。看到这么多的现象,第一反应是难道经过了 VIP 之后 ssl 的处理速度会急剧下降?直接用浏览器自带的开发工具观察好了,发现包含 DNS 查询之前的响应还是很快的,从 Connecting 开始之后的每一步当然包括 SSL 了,其 RT 都是原先的 100 倍以上,Waiting 以及 Receiving 的时间更是长的惊人。
为了方便后续的分析,找了个简单的脚本,可以通过 curl 更方面的观察:

# cat curl-format.txt
    time_namelookup:  %{time_namelookup}\n
       time_connect:  %{time_connect}\n
    time_appconnect:  %{time_appconnect}\n
   time_pretransfer:  %{time_pretransfer}\n
      time_redirect:  %{time_redirect}\n
 time_starttransfer:  %{time_starttransfer}\n
                    ———-\n
         time_total:  %{time_total}\n

curl -w "@curl-format.txt" -o /dev/null -s http://wordpress.com/
连续几次的测试结果跟上面观察的到的一样。当时的第一反应是,虽然怎么都讲不通,经过了 VIP 之后质量还不到原来的 1%,但观察到的结果就是,这个 SSL 把个 VIP 搞的这么慢,为了确认确实是 https 的问题,又找了几个 http 的资源测试了一下,结果,奇怪的事情发生了,也极其的缓慢。
看来最初的判断是错误的,所有的走 VIP 的 GET 都变的极其缓慢了,那为什么之前比这个量大的多的 POST 没有任何异常了?最大的区别就是发送请求 body 的大小了,前者一个静态资源都是几十上百 K,而后者仅仅是返回一个 json 的纯文本。
直接抓包,最初仅仅是在客户端分别抓经过 VIP 跟不经过 VIP 的包,除了每一个 segment 的返回时间差距很大之外,其他的并无差异。后来经我们主管提醒,需要在服务器上也同时抓包,于是一个在原来的 Nginx 上抓,一个在 VIP 上抓,对比相面四张图应该就能看出原因所在了。



上面两张是不走 VIP 的情况,可以看到一个 90K 的文件经过 12 个 TCP segment 就重组了,看 length 这一栏也可以得出,平均一个 segment 都在8K 左右。



上面两张是走 VIP 的,所有的 segment 都变成了 1514(14B 的二层加上 MTU),导致的结果是分了 60 多个 segment。
看到这里立马就联想 2.6.32 ipvs 对 gro 的那个 bug: 2.6.32 – 2.6.36 的内核上在使用 ipvs 的情况下需要关闭 gro。这个我之前曾经单独写过一篇博客。只不过当年没有深入的研究下开启 gro 会有什么具体的影响,这次算是经历过了。
由于配置管理的问题,导致我们一个集群上部分网卡的 gro off 操作没有生效,这个其实是主因。
既然说到 gro/lro,那干脆把 tso, gso 的也一并实践一下。
tso, gso 在 Ubuntu 以及 RedHat 上默认是开启。
gro 在 Ubuntu 上默认关闭,在 Redhat 上默认开启。
lro 的在这两个发行版本上默认均是关闭。

可以看看我下面的两个抓包,第一张是 tso, gso 均关闭的情况

下面一张是单独开启 tso 或者 gso 的情况

现象是不是很明显。所以开启 tso/gso 对于绝大多数的应用来说还是很有必要的,能节约不少资源。

ref:
http://wiki.wireshark.org/CaptureSetup/Offloading

如何部署一个稳定高效可扩展的前端

一切的工程都要从实用的角度出发,排除 GLSB 在外,目前主流的开放的前端无非就那几种:
1. 最简单的就是 DNS RR,上手很快,缺点也很明显
2. web server + keepalived,相对智能些,依然有缺点,没有 health checker
3. 目前主流的方案 LVS(ipvs) + keepalived + web server(Nginx/Tengine),如果规模比较大,可能还会涉及到 OSPF

前端的地位不言而喻,出问题大家都只能大眼瞪小眼,保证稳定是其首要目标。这里总结一些我们线上的通用问题(前期的 benchmark 就不说了),其基本原理不仅仅适用于前端,中间件、DB 同样适用,举一反三非常重要。

Tengine
在调研比较了 Tengine, Nginx 之后,我们还是决定使用 Tengine,主要原因之一是其添加了一些非常诱人的功能,再加上我们后面有大量的技术支持,所以不是很需要担心 Tengine 本身的问题。后来偶然发现,@chobits 同学在全职开发 Tengine,就更不要担心了,全程也拉他帮我们 debug 了不少问题,提供了不少非常实用的建议,再次感谢。
1. 其支持的 reqstat 模块可以实时的针对不同的 location、server 统计到当前的 return code,这个对于整个 web server 的监控是最重要的一环。
2. 其次,对 log 部分做了改进,支持 syslog/rsyslog,这样可以直接打到后端,方便后续的归档分析。
3. upstream 几个对应的模块可以更好的检测后端机器的健康状况,不过我们没实用这个 module,而是自己编译的 fair module。
4. 另外,还支持一致性 hash、session sticky 以及灰度发布类似 tcpcopy(不推荐使用)/gor(推荐使用) 的 httpcopy 等特性,总的来说,确实比较实用。

LVS/keepalived
这个没什么特别要说的,首先是模式的选择了,目前主流就是 NAT/FNAT,DR,前者更安全,后者在出现某些问题的是时候切换 DNS 比较方便。有点要特别强调的是,keepalived 的 syntax 问题,默认没有任何的检查,我们之前少写了一个配对的括号竟然也能起起来,不禁让人感叹一下,后来找到了一个叫 keepalived-check 的工具,小巧方便,真应该直接编译到 keepalived 里面。除此之外,还有针对 vim  syntax 高亮插件。

debug
该开 debug 的初期最好开起 debug,这样能发现很多的问题。
keepalived,最主要就是看 messages 文件
tegine,把 debug 的 module 编译进去就可以看到更详细的信息了,包括一些非常重要的系统调用,recv() 在哪里出问题等等,我们通过此信息发现了不少的 upstream 问题;其次就是 log format 的定义,尽量定义的详细些,同样的能发现不少问题,多看看 error log 以及 access log,通过实时的 reqstat 就能一步一步的细化。

网络
直接上的万兆,涉及的方面还比较多,可以看我之写的一系列文档,概括一下包含下面的一些内容:
1. 路由的添加,/etc/sysconfig/network-scripts/route-INTERFACE,别在 rc.local 里面加了,太土鳖了
2. 中断的绑定,万兆的都是多队列,这个不做其他的没任何意义
3. ring buffer 调整,GRO 关闭等等
4. kernel 方面的,下面会提到

版本&打包
1. 自己编译,添加一些需要的 module,比如 fair,记得开启 –debug 模式
2. 实用 SysV 来管理进程,统一由 /etc/ini.d/ 下面的脚本来管理

内核
肯定是要重新编译的(CONFIG_IP_VS_TAB_BITS=20),下面包含一些基本的 TCP 优化
1. rmem_max/wmem_max
2. rmem_default/wmem_default
3. tcp_rmem/tcp_wmem/tcp_mem
4. netdev_max_backlpg
5. somaxconn/nginx backlog
6. timestamp/tcp_reuse/tcp_recycle

监控到位
zabbix 的 item,要么通过 trigger 来体现,要么通过 graph 来体现,如果存在于第三者,就没有存在的必要了,很少会到 "Latest data" 里面去挖来挖去,你自己的写的说不定都会忘记。
1. 系统层面的不细说,但是像 DNS resolver, NTP, fork rate, CPU freq(BIOS), 10000M 速率双工, ethtool -S 下面的一些 discard/dropped 很可能会忽略掉,这些 item 其实是非常重要的,关键的时候能派上大用途
2. 基本的 ping、curl 监控,为了确保 LVS 到 RS 的基本连通行,ping、curl 必不可少,这个在出问题的时候能第一时间确定是否是小范围或者大面积的网络故障
3. LVS 相关的,包括进程,链接情况(ipvsadm),rs 的情况等等,这个通过 zbx LLD 还是很方便的
4. Nginx 基本的 req, conn,以及 return code,rt 等等,不少都是直接从 Tengine 获取到的
5. proc 下面的 fd, open file, proc,Nginx 的比较诡异,目前已经遇到至少三次 open file 由系统值变为 1024 的,这方面监控必不可少
6. 全国各地的请求监控,这个一般是第三方的服务来帮助发现问题了

log 处理
1. 目前是本地 logrotate 一份,关闭了 compress,避免短时间内 IO 过高 LVS 以为 RS 出问题了
2. 通过 syslog 远端到另外一台机器上做压缩存档
3. 除此之外,Tengine 可以抽样获取 log,这样对服务器的压力可以大大减小

整个过程就是不断发现问题,解决问题的过程,及时的记录 post-mortem,及时的 review 当前完成的状况,这样整个系统才会朝更好的方向发展。之前发现有同学写完 post-mortem 就扔在一边不管了,虽然写了改进措施,但是由于没有及时的更进,导致的没有真正的完成需要改进的工作,结果是相同的问题继续出现,这就失去了本身的意义了。题目的「稳定高效」全篇都在提。可扩展如果是 10G 或者 20G 以下,可以通过加 RS 的方式完成,当然在这之前需要确定前端网卡能否以 wire rate 抗过 10G 或者 20G 甚至 40G(bonding) 的量,尤其全是小包的极端情况。当然,一般这么大的量已经很少只通过一个出口完成了,一般 OSPF 结合多 uplink 或许能够支撑住这么大的量。
任何一个子系统子模块都可以遵循上面提到的的一些方面以及思路来完善。

RH 6.2 关闭 gro

先了解几个术语。
tso(tcp segmentation offload)
利用网卡分割大数据包,减小 CPU 负荷的一种技术。这个需要硬件的支持。
gso(generic segmentation offload)
将 tso 的技术一般化,通过 gso 的技术实现。比 tso 更通用,不需要硬件的分片就可以实现。

lro(large receive offload)
它通过将多个 TCP 数据聚合在一个 skb 结构,在稍后的某个时刻作为一个大数据包交付给上层的网络协议栈,以减少上层协议栈处理 skb 的开销,提高系统接收 TCP 数据包的能力。
gro(generic receive offload)
lro 有不少问题,后续的驱动,都应该使用 GRO 的接口,而不是 LRO。

详细的解释可以看看这个文档,从 TSO 到 GSO,从 LRO 到 GRO。

6.2 的机器如果部署有 LVS,需要关闭 gro,否则其性能会异常的低:
# ethtool  -k em1
Offload parameters for em1:
rx-checksumming: off
tx-checksumming: on
scatter-gather: on
tcp-segmentation-offload: on
udp-fragmentation-offload: off
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off

# ethtool -K eth0 gro off

6.2 版本之前由于有 bug,无法在 ETHTOOL_OPTS 里面加上多个参数。包括 6.2 在内的以后的版本可以直接在 /etc/sysconfig/network-scripts/ifcfg-em1 里面指定:
ETHTOOL_OPTS="-K ${DEVICE} tso on; -G ${DEVICE} rx 256 tx 128"

而对于 6.2 之前的,方法有二:
1. 将 ethtool 放到 rc.local 里面,但此时如果使用 service network restar 就无法生效了
2. 建立  /sbin/ifup-local 文件,将需要操作的脚本写到这个里面,并将其可执行