我们都知道 Nginx 是一款非常优秀的 Web 服务器,不过因为其可编程性太差,我们一般都只将它作为反向代理服务器,其并发性能也十分优秀,经过优化的 Nginx 并发量能达到上万。
而 Openresty 则是针对 Nginx 可编程性做出改进的产品,使用了 Lua 脚本作为 runtime,可以让开发人员使用 Lua 调用 Nginx 的各种模块,大大增强了 Nginx 的可编程性,你完全可以只使用 Lua 开发一套高并发的 Web 服务。
但实际产品中没人使用 Lua 作为 Web 开发语言,我们公司也不例外,我们使用 Openresty 作为一个动态网关,负责权限校验、流量转发、日志记录等功能,一切都是那么美好。直到有一天,QA对我们的产品进行了压力测试。。。
1. 起因
QA要进行压力测试也是很正常的行为,虽然我们的产品是2B的,基本都是企业内部使用,但是作为 Web 开发者,总要对自己有点要求的对吧。
我们一开始也没啥好担心的,毕竟 Nginx 的优秀是全世界都知道的,我们站在巨人的肩膀上,再差能差到哪?
但结果出乎所有人的预料,并发量极其的差,大概只有500左右,一旦测试时间较长,异常率能达到 30% 左右,这根本不是一个产品!
为了不被领导喷,我们赶紧排查问题。
2. 问题
通过查看并发测试的结果,我们发现异常的状态码基本都是 502 (Bad Gatewa) 错误,第一反应“是不是后端服务挂了”?通过查看相关 pod 状态,这个可能性排除。
然后我们又想到产品是 k8s 部署的,服务间通信完全是通过域名访问,所以我们又想“会不会是 DNS 出现了问题”?通过检查 coredns 日志,发现并没有相关错误信息。虽然这次的问题不在 dns 上,但我们也间接地发现了 Nginx 在 k8s 下的一个配置错误,后面会提到。
最后排查的重点只能是 Nginx/Openresty 本身了,通过对日志的详细排查,我们发现了一个重复出现的消息“Cannot assign requested address”!通过谷歌查阅(我们就是面向搜索引擎工程师😁),我们知道了问题的真正原因。
简单来说,就是客户端频繁地通过 Nginx 代理连接服务端(废话,压测肯定会这样),又在短时间内断开了连接,虽然 socket 正常关闭了,但是端口并没有即时释放,按照系统内核默认的参数,这些端口会在 60s 之后释放,这就导致了很多的 TIME_WAIT 的出现(可通过 netstat -ae | grep TIME_WAIT | wc -l 查看数量)。
这样就会产生一个问题,系统可用的端口数量是有限的,旧端口无法即时释放,新连接又会占用新的端口,时间一长,一定会导致建立新连接时无端口可用,即上面的“Cannot assign requested address”。
3. 解决
既然知道了问题,那解决方案都是现成的了,主要有两个方向:
- 适当的调低 TIME_WAIT 的时间,让未释放的端口能即时释放出来。
- 调高可用端口范围,让新连接能使用更多的端口。
下面的操作都是操作内核参数,可通过编辑内核文件“/etc/sysctl.conf”或者“sysctl”命令使配置生效。
3.1 调整 TIME_WAIT 时间
// 表示开启 SYN Cookies。当出现 SYN 等待队列溢出时,启用 cookies 来处理,
// 可防范少量 SYN 攻击,默认为 0,表示关闭;
net.ipv4.tcp_syncookies=1
// 表示开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认为 0,表示关闭;
net.ipv4.tcp_tw_reuse=1
// 开启对于TCP时间戳的支持,若该项设置为0,则下面一项设置不起作用
net.ipv4.tcp_timestamps=1
// 表示开启 TCP 连接中 TIME-WAIT sockets 的快速回收,默认为 0,表示关闭。
net.ipv4.tcp_tw_recycle=1
// 修改系默认的 TIMEOUT 时间,默认为 60s
net.ipv4.tcp_fin_timeout=30
3.2 调高可用端口范围
// 表示用于向外连接的端口范围。设置为 1024 到 65535。
net.ipv4.ip_local_port_range = 1024 65535
4. 最后
因为宿主机的内核参数也会影响到 k8s pod,所以上述参数只要在各 node 上执行一遍即可。通过上述参数,我们顺利的将并发量从可怜的、时不时报错的 500 提升到了 2000,算是在不动任何配置代码的情况下取得的比较大的优化改进了。
还有一点上面也提到了,就是 Openresty 的 DNS 配置问题,因为我们的服务都是通过域名进行通信的,所以在 Nginx 配置中,我们都是 proxy_pass 域名的方式。但其实这里有一个需要注意的点,就是默认情况下,Nginx 并不会使用系统的 /etc/resolv.conf,所以我们一般都需要使用 resolver 指令,但是 Nginx 里 resolver 指令只能确定 DNS 服务,并不能使用系统的 resolv 文件,怎么办呢?难道要制定成 coredns 的 IP 吗?那样也不通用,好在 Openresty 提供了一个 local 参数,当我们配置 local=on 的时候,Openresty 就会去解析系统的 resolv.conf 并使用其中的 DNS 服务。
至此,关于QA并发测试引发的血案到此结束。