作者:gfree.wind@gmail.com
博客:blog.focus-linux.net linuxfocus.blog.chinaunix.net
微博:weibo.com/glinuxer
QQ技术群:4367710

本文记录一次作者定位解决问题的一个过程,与大家分享一些经验。

问题描述

笔者在阿里云服务器上,使用Nginx搭建了一个HTTP 正向代理,但是运行一段时间后,就无法接受新连接请求了。

解决过程

测试定位问题

出现问题后,重要的不是立刻动手,而是先做初步的思考,然后再动手。

  1. 首先我排除是否为网络问题:检查了iptable等。同时思考,如果真的是网络问题,不应该运行一段时间后,才出现无法接受新连接的现象。
    为了验证,当出现问题时,我又重新启动Nginx,发现又可以接收新的请求了。也就是说出现问题时,只需要重启Nginx就可以解决,那么自然不是网络因素。

  2. 判定是否是Nginx本身的问题(不一定是指代码包括我写的配置文件):因为这个代理服务器是为了测试fastsocket项目的稳定性,所以Nginx是加载了fastsocket优化服务的。这时,就需要最纯粹的Nginx环境。我去掉了fastsocket服务,然后再用同样的配置启动Nginx。这时,就排除了Nginx本身的问题。那么,究竟是否是fastsocket的bug呢?
    这里先做一个小广告:fastsocket是新浪主导的一个开源项目,其通过封装socket套接字调用,无需改动服务程序,即可大幅提升服务程序性能。作者也是其中的维护者之一。这里小小推广一下:https://github.com/fastos/fastsocket。当使用fastsocket默认加载参数时,nginx运行一段时间就无法接受新连接请求了。

  3. 定位fastsocket问题:fastsocket的大部分优化功能都是有功能开关的,默认会使用一些功能,同时可以在加载动态模块时,使用参数指定是否打开开关。这时,先做实验,从所有功能关闭开始,逐渐打开功能开关,最后定位到enable_listen_spawn功能打开时,就会出现问题。并多次做实验,确定这是一个必现的问题。
    当确定可以重现后,我想这难道是一个fastsocket的bug吗?于是,先跟林晓峰同学说了一声,告诉他我的发现,毕竟fastsocket是他在sina时的工作,他最为熟悉代码。他说这可能是Nginx的配置使用了accept_mutex。我的配置文件虽然没有配置accept_mutex,但是没想到Nginx的accept_mutex是默认打开的。但是他忘了为什么会这样了?依稀记得是Nginx hang在了mutex中,具体原因记不清了。所以fastsocket的说明也是要求disable accept_mutex。
    因为我一直以来有这还不错的求知欲,所以一定要搞清楚这个问题。同时我认为,如果真的是一启用accept_mutex,fastsocket和nginx就会有兼容问题,那也应该算是fastsocket的bug,应该将其解决掉。

  4. 定位Nginx hang在什么位置:这个很简单,使用strace -p跟踪Nginx的每个worer进程。发现大部分worker进程是在不断的epoll_wait,而其中一个worker进程,始终停留在epoll_wait中。重试多次,每次都是停留在epoll_wait中。

现在已经确定了本次问题,当使用fastsocket的enable_listen_spawn功能时,也就是fastsocket自动为当前CPU创建本地的listen socket套接字时,就会出现问题。

解决问题

当定位到问题时,就需要一步一步的找到原因,查看为什么一个worker进程始终停留在epoll_wait中。这时候,其实思考还是要优于动手。先思考,再动手,动手之后,看到结果,再做进一步思考。

  1. 查看该worker进程停留在epoll_wait的什么位置:只能通过日志形式来判断hang在epoll_wait的哪个位置?这时,不能用内核普通的printk来打印日志,不然就会淹没于大量正常工作worker进程打印的日志中。我们需要根据pid来打印日志。
    再做一个小广告:我做了一个内核小工具。是用来定位内核代码的性能瓶颈工具,和一些辅助工具。大家觉得还可以的话,就给赞个星星。
    它提供一个宏UP_PID_INFO_LOG用于打印指定PID的日志,pid可以通过proc来指定。
    这样我在epoll_wait中增加了大量的日志。在Nginx启动后,通过proc指定就打印某个worker进程的日志。
    最后发现epoll_wait是因为指定了无限等待时间,所以该worker进程一直在hang住。

  2. Nginx让一个worker进程无限等待,这稍微颠覆了我对Nginx的认识。我认为Nginx一直都是使用无阻塞的系统调用,至少核心模块是这样处理的。那么为什么会出现这个现象呢?这时候,就需要思考,而不是动手了。
    毫无疑问,accept_mutex是一个关键。它本身是用于均衡不同worker进程的负载。稍微阅读一点Nginx相关的代码,就可以明白。在Nginx无法接收新连接请求时,一定是该轮到hang住的进程接收新连接请求。所以尽管其它进程没有hang住,但是它们是无法接受新请求,而能够接收新请求的进程却hang住,这样就导致了问题的产生。

  3. 为什么hang住的进程无法接收到新的请求呢?这时还是思考优先。首先要勾画标准的内核TCP连接的过程,然后对比启用fastsocket后,TCP连接的过程。很可能是这两者之间的区别,造成了问题。尤其是启用了spawn socket时,与标准流程的不同。spawnsocket时,实际上为每个cpu都创建了一个本地listen套接字的hash表,与全局的listen表区分开。这样一方面访问全局hash表时需要的锁,另一方面也做到了将TCP会话做到本地,可以尽量命中cache。对于同一个CPU,由于有两个listen表的存在,所以在收到新的TCP连接请求时,必须先检查本地的listen表,然后再检查全局表。根据这样的流程和现象,应该是所有的连接请求,都被发到其它的CPU,并且匹配中了其它CPU的本地listen表,所以全局表中的listensocket套接字一直没有被匹配到。
    那么hang住的进程,既没有连接请求匹配本地listen表中的套接字,而全局表也一样,因为被请求都被其它CPU命中了本地的套接字。
    所以问题更为明朗了,hang住的进程所在的CPU不能收到任何新连接请求。

  4. 这时其实已经到了冲刺的时候了。开始的时候,我还想着,是否是fastsocket影响了数据包的分发,还想检查一下代码。但一想,还是先看看RPS的设置吧——虽然我没有设置网卡的任何RPS。结果出乎我意料,原来阿里云ECS服务器默认就把网卡的RPS设置了,唯一的外网网卡的RPS设置为了0000,所以只有CPU 0能收到新连接请求,而另外的CPU1收不到任何的连接请求,这就造成了运行在CPU1上的worker进程hang住。

最后我修改了该网卡的RPS设置,使其可以将数据包分发到不同的CPU上。这样在加载了fastsocket后,即使打开了accept_mutex,Nginx也可以正常工作了。

本次过程,虽然最后发现只是服务器配置的问题,但整个儿过程还是收获不少。唯一的遗憾,是还没有定位Nginx对与epoll_wait的超时计算。开始的时候,都是500ms,后面因为什么因素变成了无限。这留到有时间的时候,再阅读Nginx源码吧。


12-17 02:23