事情是这样的,上个月我接手了一个内部项目,一个API网关服务,负责转发到后端的几个微服务。这个服务跑在Nginx 1.20.1上,配置是从前任手里继承下来的,看着挺规整,但我总觉得哪儿不对劲。直到有一天,我盯着监控面板发现请求的响应时间偶尔会跳一下,大概有1%的请求会多出200到400毫秒的延迟。这数字不大,但架不住业务方天天在群里问”最近是不是又卡了”。
我先是从应用层查起,后端服务的GC日志、数据库连接池、甚至网络延迟都排查了一遍,都没发现明显异常。然后我把目光转向了Nginx本身。我翻了翻access log,那些慢请求的upstream响应时间其实并不慢,说明后端处理没问题,那问题大概率出在Nginx自己身上。
这时候我注意到一个细节:Nginx的worker进程数量是8个,我们的服务器是4核8线程的CPU,按理说8个worker没问题。但我又看了眼worker_connections,设的是1024,再算上keepalive的超时时间,默认的65秒。这个组合在低并发场景下看起来没问题,但我们的API网关有时候会突然来一波并发请求,比如定时任务触发或者某个客户批量调用。
我决定动手改一改。第一个改动是调整worker进程数和CPU亲和性。虽然8个worker对应8个逻辑核心看起来合理,但Nginx的worker进程之间会争抢accept_mutex,尤其在高并发下,锁的竞争会导致请求排队。我先把worker_processes改成了auto,让Nginx自己检测CPU核心数,然后加上了worker_cpu_affinity auto,让每个worker绑定到固定核心。这个改动其实在生产环境里挺常见的,但之前一直没动,因为觉得”能用就行”。
然后我调整了worker_connections,从1024改成了4096。这里有个坑,很多人以为这个值设得越大越好,实际上它受限于系统文件描述符上限。我提前用ulimit -n看了一下,当前是65535,所以4096完全没问题。同时我把multi_accept改成了on,这样每个worker可以一次性接受多个新连接,而不是一次只拿一个。这个配置在Nginx 1.11.0之后默认就是on了,但我这台机器上跑的是1.20.1,安装包可能是从旧配置模板生成的,所以还是off。
改完这些,我把keepalive相关的参数也调了一下。keepalive_timeout从65秒降到了30秒,keepalive_requests从100提升到了1000。这个改动的逻辑是:长时间的空闲连接会占用worker的连接池资源,导致新连接进来时没有空闲worker处理。而提升keepalive_requests是因为我们的API请求中很多是短小但频繁的调用,每个连接复用次数多了能减少三次握手的开销。
重启Nginx之后,观察了大概两小时,慢请求的比例从1%降到了0.1%左右,效果挺明显。但我总觉得还不够彻底。
我又仔细翻了Nginx的error log,发现了一些WARN级别的日志,说”accept4() failed (24: Too many open files)”。这明显是文件描述符不够用了。虽然前面改了worker_connections,但系统的全局限制还没动。我检查了/etc/security/limits.conf,发现里面根本没有对nginx用户的配置。我加了两行:
nginx soft nofile 65535
nginx hard nofile 65535
然后重新加载了systemd的limit配置,再重启Nginx,那个WARN日志就消失了。
这时候我以为优化完了,结果过了两天,有个同事反馈说某个接口偶尔会返回502。我一看,这是upstream连接超时了。我查了一下,我们的upstream配置里只有一个server,没有配置备份节点。我加了backup节点,同时调整了proxy_connect_timeout从60秒降到了5秒,proxy_read_timeout从60秒降到了30秒。这样如果主节点挂掉,Nginx能快速切换到备用节点,不再傻等60秒。
另外,我注意到upstream里没有配置keepalive。是的,Nginx upstream也需要显式开启连接复用,否则每次请求都会新建一个TCP连接到后端。我在upstream块里加了一行keepalive 32,然后在location块里把proxy_http_version设成1.1,并且清空了proxy_set_header Connection,因为默认的proxy_set_header会覆盖掉keepalive需要的Connection头。这个细节很多人会忽略,包括我之前也是。
改完这些,我又观察了几天,慢请求基本没了,偶尔的502也消失了,监控面板上的平均响应时间稳定在12毫秒左右,比优化前的18毫秒低了三分之一。
最后说几个踩坑的点吧。第一个,改完配置一定要用nginx -t检查语法,我见过有人直接把配置文件改崩了导致服务挂掉。第二个,keepalive在upstream里设置的值不是越大越好,32到64一般就够,太大反而会占用后端连接池。第三个,调整worker_connections的时候别忘了同步改系统的文件描述符限制,否则Nginx会在日志里报错但不会直接告诉你为什么。第四个,proxy_http_version必须设置成1.1才能启用upstream keepalive,这个坑我踩过两次,一次是刚学Nginx的时候,一次是这次写博客前回忆起来的。
优化这事儿吧,有时候就是一堆小细节堆起来的。每个改动单独看都不起眼,但合在一起效果就出来了。下次如果再遇到类似的慢请求问题,我第一反应可能不再是去查应用层,而是先看看Nginx自己的配置是不是有什么历史遗留的默认值。