问题:
如何让php-fpm能优雅地重启?
办法:
- 修改php-fpm listen的方式,开启so_reuseport
- 重启php-fpm时,先启动新进程,再停止旧进程
问题:
- 新进程和旧进程有不同的tcp backlog队列
- 系统会将新建立的连接平均分配到每个backlog队列中,新旧进程只能消费自己的backlog队列
- 新进程的所有子进程共享一个新的backlog队列,就进程的所有子进程共享一个旧的backlog队列
- 旧进程如何才能将backlog中的连接消费完?
- 旧进程如何阻止系统不要再分配连接到自己的backlog队列?
- 旧进程如何判断自己的backlog队列已空?
- 如果php-fpm进程通过exec、system、popen、proc_open等方式执行了外部的程序,则外部程序会继承所有的文件描述符(如果不做有意或无意的特殊处理的话,也会包括listen 9000 的文件描述符);问题在于,这个外部的程序虽然listen了9000端口,但是它绝对不会帮你处理9000端口的请求的,然后,系统分配过来的请求就会被阻塞
kill -SIGQUIT 可以解除子进程对9000端口的listen,却不能解除master进程对9000端口的listen
子进程的listen是继承父进程的
子进程接到SIGQUIT 信号后,close掉listen的文件描述符,然后创建了一个新的socket,如下:
(为什么要创建一个新的socket?)
同一个文件描述符被fork到多个子进程后,每个进程的关闭只会在本进程内关闭(让该资源和本进程脱离关系),然后减少该资源的引用计数,直到最后一个引用被解除,该资源才真正销毁
关于上述的问题7:
- 无意中对listen 9000的覆盖
- 当使用popen(“sleep” , “w”)的方式,就是以“写”的方式打开外部程序,则外部程序继承过来的标准输入0(原本listen 9000的文件描述符)会被覆盖
- 这时候就不会出现问题。 不过,你很少会如此打开一个外部程序的,常见的方式是以“读”的方式打开外部程序的,这时候,外部程序要覆盖掉的是文件描述符1,而0不会受影响
- 能否刻意覆盖掉文件描述符0呢? 比如: popen(“sleep”, “rw”) 读写方式打开?答案是:不可以; popen只允许单向打开(原因未深究,参考文档: http://php.net/manual/en/function.popen.php )
- 那么通过proc_open 实现呢?答案是: 可以; 如下:
123456789<?php$descriptorspec = array(0 => array("pipe", "r"), // stdin is a pipe that the child will read from1 => array("pipe", "w"), // stdout is a pipe that the child will write to2 => array("file", "/tmp/error-output.txt", "a") // stderr is a file to write to);$process = proc_open('sleep 10000',$descriptorspec, $pipe);print_r($pipe);sleep(10000);
则sleep进程的文件描述符如下:
这样就对了; 而且,proc_close() 可以获取外部进程的退出码
- 如果觉得proc_open 用起来比较麻烦,那么,或许可以给启动的外部程序来一个自己的shell,该shell负责关闭文件描述符0, 如: a.sh
123#!/bin/bashexec 0<&-$@
注意: 这个shell会存在一些问题,如下:
改进如下:12345#!/bin/bashexec 0<&-cmd=$1shift$cmd "$@"
到现在为止,我们有能力不让外部的子进程来影响fpm请求的处理了;但是,还有一个listen 9000 但是不能处理请求的进程,那就是: php-fpm master进程
我们的处理方法可以如下:
- kill -SIGQUIT pid-of-php-fpm-master && sleep 1 && kill -9 pid-of-php-fpm-master (里面有 -9 ,好不和谐)
最后:
我们还没有解决如何让旧进程消费完旧进程的backlog中剩余的连接的问题,稍后研究。
分析:
socket中有一个shutdown的方法,可以关闭socket的读或(和)写,会触发关闭已建立的连接的方法,(对于listen的socket没有对端,想必不能用shutdown方法),测试发现:
- shutdown可以停止socket的listen,而不关闭文件描述符,确实可以实现关闭进程的listen,但是同时也会丢弃backlog中的连接,不管shutdown的是读还是写
- 测试脚本:
12345<?php$s = socket_create_listen (8882, 1);fgets(STDIN);socket_shutdown($s);fgets(STDIN);
(如果能通过单向关闭的方式通知内核不要再往该backlog分配连接不是很好吗?)
一种其它的实现方式:
在so_reuseport出现之前(包括现在)有一些程序是可以实现优雅重启的,原理就是,新旧进程时间继承lisen的文件描述符; 其实,继承文件描述符需要不小的工作量的。
如果能有一种标准的接口来处理继承文件描述符的事情,那么完全可以开发一个和应用无关的小程序,用来创建socket端口,然后遗传给真正要干活的应用程序,只需要遗传给应用程序的master进程就算OK了,如果需要重启程序,则只需要fork新的应用程序的master进程,然后发信号让旧的空闲的应用进程退出就可以了,这样就不会丢弃已创建的连接。
参考:
https://my.oschina.net/miffa/blog/390931