Answers
可以用 sudo strace -p PID 查看编号的PID的进程的系统调用.
nginx master: rt_sigsuspend
nginx worker: epoll_wait
php-fpm master: epoll_wait (events.mechanism = epoll)
php-fpm worker: accept poll
可见Nginx工作进程进行了epoll_wait系统调用,epoll是Linux内核提供的异步网络IO编程接口.
http://liuxun.org/blog/nginx-gong-zuo-jin-cheng-mo-xing/
Nginx并没有像PHP-FPM那样采用master进程来分发连接,这个工作由操作系统内核机制完成,
所以可能会导致惊群现象,也就是当listen_fd有新的accept()请求过来,操作系统会唤醒所有子进程.
Nginx通过全局互斥锁来避免惊群(accept_mutex on),每个工作进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,
并设置了一个负载均衡的算法(当某一个工作进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量.
http://nginx.org/en/docs/ngx_core_module.html#accept_mutex
Nginx解决惊群的新方法:使用内核提供的Socket ReusePort功能
NGINX 1.9.1 支持socket分片:
http://nglua.com/docs/sharding.html
http://nginx.com/blog/socket-sharding-nginx-release-1-9-1/
NGINX1.9.1支持socket的SO_REUSEPORT选项,这个选项在许多操作系统的新版本有效,包括DragonFly BSD和Linux(3.9+内核).
这个选项允许多个socket监听同一个IP地址和端口的组合.内核负载均衡这些进来的sockets连接,将这些socket有效的分片.
当SO_REUSEPORT选项没开启时,连接进来时监听socket默认会通知某个进程.
如果accept_mutex off这个指令,此时会唤醒所有的工作进程,它们将为了得到它产生竞争,这就是所谓的惊群现象.
如果使用epoll且不用锁(accept_mutex off),当监听端口有读操作时,是会产生惊群现象的.
启用SO_REUSEPORT选项后,每个进程将有个独立的监听socket.内核决定哪个是有效的socket(进程)得到这个连接.
这样做降低了延迟并提高了工作进程的性能,它也意味着工作进程在准备处理它们前被赋予了新的连接.
开启SO_REUSEPORT支持,只需将新的参数reuseport加到listen指令的后面:
listen 80 reuseport;
包含这个reuseport参数后将禁用这个监听socket的accept_mutex,因为锁变得多余了.
基准测试:NGINX 1.9.1启用reuseport完美解决惊群后,每秒处理的请求数提升了2到3倍,同时降低了延迟和stdev指标.
附:淘宝的Tengine据说很早就加入了socket分片功能.
上面说的是网络IO的异步,下面说磁盘IO的异步.
在高性能的服务器编程中,I/O模型理所当然的是重中之重,需要谨慎选型.
对于网络套接字,我们可以采用epoll的方式来轮询,尽管epoll也有一些缺陷,但总体来说还是很高效的,尤其来大量套接字的场景下.
但对于Regular File来说,是不能够用的.采用poll/epoll,即O_NOBLOCK方式对于传统文件句柄是无效的.
也就是说我们的open,read,mkdir之类的Regular File操作必定会导致阻塞.
在多线程,多进程模型中,可以选择以同步阻塞的方式来进行IO操作,任务调度由操作系统来保证公平性.
NGINX从1.7.11试验性引入线程池,特定场景性能提升9倍.
https://www.nginx.com/blog/thread-pools-boost-performance-9x/
测试服务器有2个Intel Xeon E5645处理器(共计:12核,24超线程)和10-Gbps的网络接口.
磁盘子系统是由4块西部数据WD1003FBYX磁盘组成的RAID10阵列.
操作系统是Ubuntu Server 14.04.1 LTS.
thread_pool default threads=32 max_queue=65536;
aio threads=default;
这里定义了一个名为default,包含32个线程,任务队列最多支持65536个请求的线程池.
如果任务队列过载,NGINX将输出如下错误日志并拒绝请求:
thread pool "NAME" queue overflow: N tasks waiting
有了这个线程池,NGINX有可能没有任何性能损失地卸载任何长期阻塞的操作.
许多流行的库仍然没有提供异步非阻塞接口,此前,这使得它们无法与NGINX兼容.
我们可以花大量的时间和资源,去开发我们自己的无阻塞原型库,但这么做始终都是值得的吗?
现在,有了线程池,我们可以相对容易地使用这些库,而不会影响这些模块的性能.
FreeBSD已经有足够好的异步接口来读取文件,这时候不需要使用线程池.
Linux缺乏这样的机制,所以对于一些不适合缓存在虚拟内存的文件(大文件),可以卸载读操作到AIO线程池,避免阻塞工作进程.
我们如果可以改进卸载读操作到线程池,将会非常有意义.
我们只需要知道所需的文件数据是否在内存中,只有不在内存中时,读操作才应该卸载到一个单独的线程中.
Linux内核提供的异步文件操作接口AIO,需要DirectIO,无法利用内存的Page Cache,
这种奇怪的实现可能是Oracle/IBM专门为数据库应用设计的,MySQL就用到了AIO.
AIO+DirectIO绕过了虚拟文件系统VFS高速缓存固然让大型数据库系统如Oracle,MySQL(InnoDB)非常高兴,
因为Oracle,MySQL都有自己的缓存系统,所以不需要操作系统的Page Cache缓存,DirectIO能避免数据就被缓存两次而浪费内存.
也就是说,如果你想让自己的程序通过AIO实现异步文件IO,那么你最好建立自己的内存缓存系统,而不是依赖内核.
最后,Linux上Apache 2.4系列默认的event MPM,是一个多进程,每个工作进程包含多个线程的epoll事件驱动的MPM.
Apache 2.2系列的prefork MPM是纯粹的多进程架构,没有引入多线程,也没有使用内核的epoll特性,估计主要是为了方便移植.
IBM AIX上面的IBM HTTP Server貌似就是基于Apache改造的.