在传统的nginx+php-fpm模式中,每次请求结束后资源都会被释放,下次有新的请求会重新加载文件,所以只要更新了代码即可马上生效,但是在cli命令行模式开发中,开启的php进程服务一般都是守护进程,代码只在开启时进行加载,就算代码有更新也不会重新加载,直到进程结束都还是最开始加载的代码,导致每次更新代码都要重启php服务,这样的体验是非常不好的,我们可以借用swoole+Inotify来解决这个问题
Inotify介绍
inotify是Linux内核提供的一组系统调用,它可以监控文件系统操作,比如文件或者目录的创建、读取、写入、权限修改和删除等。
inotify使用也很简单,使用inotify_init创建一个句柄,然后通过inotify_add_watch/inotify_rm_watch增加/删除对文件和目录的监听。
PHP中提供了inotify扩展,支持了inotify系统调用。inotify本身也是一个文件描述符,可以加入到事件循环中,配合使用swoole扩展,就可以异步非阻塞地实时监听文件/目录变化
Inotif安装
1、可以使用pecl install inotify
2、(https://pecl.php.net/get/inotify-2.0.0.tgz)编译安装步骤跟之前一样,自行安装
在swoole中,我们可以向主进程发送各种不同的信号,主进程根据接收到的信号类型做出不同的处理。比如下面这几个
1、kill -SIGTERM|-15 master_pid 终止Swoole程序,一种优雅的终止信号,会待进程执行完当前程序之后中断,而不是直接干掉进程
2、kill -USR1|-10 master_pid 重启所有的Worker进程
3、kill -USR2|-12 master_pid 重启所有的Task Worker进程
当USR1信号被发送给Master进程后,Master进程会将同样的信号通过Manager进程转发Worker进程,收到此信号的Worker进程会在处理完正在执行的逻辑之后,释放进程内存,关闭自己,然后由Manager进程重启一个新的Worker进程。新的Worker进程会占用新的内存空间。
具体场景:
如果是上线的项目,一台繁忙的后端服务器随时都在处理请求,如果管理员通过kill进程方式来终止/重启服务器程序,可能导致刚好代码执行到一半终止。
这种情况下会产生数据的不一致。如交易系统中,支付逻辑的下一段是发货,假设在支付逻辑之后进程被终止了。会导致用户支付了货币,但并没有发货,后果非常严重。
/*
*热重启
*/
class Worker{
//监听socket
protected $socket = NULL;
//连接事件回调
public $onConnect = NULL;
public $reusePort=1;
//接收消息事件回调
public $onMessage = NULL;
public $workerNum=3; //子进程个数
public $allSocket; //存放所有socket
public $addr;
protected $worker_pid; //子进程pid
protected $master_pid;//主进程id
public function __construct($socket_address) {
//监听地址+端口
$this->addr=$socket_address;
$this->master_pid=posix_getpid();
}
public function start() {
//获取配置文件
$this->watch();
$this->fork($this->workerNum);
$this->monitorWorkers(); //监视程序,捕获信号,监视worker进程
}
/**
* 文件监视,自动重启
*/
protected function watch(){
$init=inotify_init(); //初始化
$files=get_included_files();
foreach ($files as $file){
inotify_add_watch($init,$file,IN_MODIFY); //监视相关的文件
}
//监听
swoole_event_add($init,function ($fd){
$events=inotify_read($fd);
if(!empty($events)){
posix_kill($this->master_pid,SIGUSR1);
}
});
}
/**
* 捕获信号
* 监视worker进程.拉起进程
*/
public function monitorWorkers(){
//注册信号事件回调,是不会自动执行的
// reload
pcntl_signal(SIGUSR1, array($this, 'signalHandler'),false); //重启woker进程信号
//ctrl+c
$status=0;
while (1){
// 当发现信号队列,一旦发现有信号就会触发进程绑定事件回调
pcntl_signal_dispatch();
$pid = pcntl_wait($status); //当信号到达之后就会被中断
//如果进程不是正常情况下的退出,重启子进程,我想要维持子进程个数
// if($pid>1 && $pid != $this->master_pid && !pcntl_wifexited($status)){
// $index=array_search($pid,$this->worker_pid);
// $this->fork(1);
// var_dump('拉起子进程');
// unset($this->worker_pid[$index]);
// }
pcntl_signal_dispatch();
//进程重启的过程当中会有新的信号过来,如果没有调用pcntl_signal_dispatch,信号不会被处理
}
}
public function signalHandler($sigo){
switch ($sigo){
case SIGUSR1:
$this->reload();
echo "收到重启信号";
break;
}
}
public function fork($worker_num){
for ($i=0;$i<$worker_num;$i++){
$test=include 'index.php';
var_dump($test);
$pid=pcntl_fork(); //创建成功会返回子进程id
if($pid<0){
exit('创建失败');
}else if($pid>0){
//父进程空间,返回子进程id
$this->worker_pid[]=$pid;
}else{ //返回为0子进程空间
$this->accept();//子进程负责接收客户端请求
exit;
}
}
//放在父进程空间,结束的子进程信息,阻塞状态
}
public function accept(){
$opts = array(
'socket' => array(
'backlog' =>10240, //成功建立socket连接的等待个数
),
);
$context = stream_context_create($opts);
//开启多端口监听,并且实现负载均衡
stream_context_set_option($context,'socket','so_reuseport',1);
stream_context_set_option($context,'socket','so_reuseaddr',1);
$this->socket=stream_socket_server($this->addr,$errno,$errstr,STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,$context);
//第一个需要监听的事件(服务端socket的事件),一旦监听到可读事件之后会触发
swoole_event_add($this->socket,function ($fd){
$clientSocket=stream_socket_accept($fd);
//触发事件的连接的回调
if(!empty($clientSocket) && is_callable($this->onConnect)){
call_user_func($this->onConnect,$clientSocket);
}
//监听客户端可读
swoole_event_add($clientSocket,function ($fd){
//从连接当中读取客户端的内容
$buffer=fread($fd,1024);
//如果数据为空,或者为false,不是资源类型
if(empty($buffer)){
if(!is_resource($fd) || feof($fd) ){
//触发关闭事件
fclose($fd);
}
}
//正常读取到数据,触发消息接收事件,响应内容
if(!empty($buffer) && is_callable($this->onMessage)){
call_user_func($this->onMessage,$fd,$buffer);
}
});
});
}
/**
* 重启worker进程
*/
public function reload(){
foreach ($this->worker_pid as $index=>$pid){
posix_kill($pid,SIGKILL); //结束进程
var_dump("杀掉的子进程",$pid);
unset($this->worker_pid[$index]);
$this->fork(1); //重新拉起worker
}
}
//捕获信号之后重启worker进程
}
//ps -ef | grep php | grep -v grep | awk '{print $2}' | xargs kill -s 9
$worker = new Worker('tcp://0.0.0.0:9800');
//开启多进程的端口监听
$worker->reusePort = true;
//连接事件
$worker->onConnect = function ($fd) {
//echo '连接事件触发',(int)$fd,PHP_EOL;
};
$worker->onTask = function ($fd) {
//echo '连接事件触发',(int)$fd,PHP_EOL;
};
//消息接收
$worker->onMessage = function ($conn, $message) {
//事件回调当中写业务逻辑
// $a=include 'index.php';
// var_dump($a);
//var_dump($conn,$message);
$content="我是peter";
$http_resonse = "HTTP/1.1 200 OK\r\n";
$http_resonse .= "Content-Type: text/html;charset=UTF-8\r\n";
$http_resonse .= "Connection: keep-alive\r\n"; //连接保持
$http_resonse .= "Server: php socket server\r\n";
$http_resonse .= "Content-length: ".strlen($content)."\r\n\r\n";
$http_resonse .= $content;
fwrite($conn, $http_resonse);
};
$worker->start(); //启动