承接上一篇博客,由于时间紧迫和条件所限,公司项目需要使用多进程/线程cli脚本,由于go目前没有成熟的cli框架,而我们有没有足够的时间和人力去造go的轮子,于是选择了用C/C++开发的Swoole框架。我之前的一篇博客介绍了我开发给项目用的基于yaf的cli脚本程序,使用这个轻量级的框架结合swoole成了目前看来最快的开发方式。
之前搭建socket服务器的时候有在workman和swoole之间犹豫过,尽管作为一个对技术怀有热诚的开发人员我更喜欢swoole,但是作为架构师出于职业素养我得优先考虑稳定性和可实现性,workman的简单实用和稳定更符合我们团队的现状,于是我独自开发了结合workman和yaf的mvc结构socket服务给团队使用。
我在多进程和多线程之间也作了一些考虑,但是由于我们的linux服务器和并行业务的相关性并不高,我们系统对稳定性的要求高于硬件成本的节约(公司处于盈利状态,硬件成本只是总成本中很小的一部分),绝大多数场景下多进程会优于多线程,而且swoole新版本已经类似go支持协程,结合多进程和协程,我们实际几乎可以不必使用多线程。所以我也没有试图去冒险将我们的生产环境php重新编译为非线程安全的,我并不觉得我们团队目前有能力在公司线上不遭受损失的前提下处理由此带来的意外问题和bug。
下面介绍使用方法
官方文档:https://wiki.swoole.com/wiki/page/7.html
一.安装
这里使用的是php7和swoole 4.3,linux环境是centos7,具体环境依赖看官方文档。
安装swoole:
pecl install swoole
编译安装成功后,修改php.ini
加入
extension=swoole.so
通过php -m
或phpinfo()
来查看是否成功加载了swoole.so
如果出现错误找不到swoole.so,进入/etc/php.d/
vim sockets.ini
###加入如下内容###
extension=swoole.so
二.多进程使用
在swoole.php中写入:
<?php
//开启多进程
for($i=0;$i<5;$i++){
$process = new swoole_process(function(swoole_process $worker)use($i){
while(true){
echo $i.'--'.date('Y-m-d H:i:s').PHP_EOL;
sleep(3);
}
});
$pid = $process ->start(); //会返回pid
}
/*
* 回收子进程
* 注意,这里是必须的否则可能出现僵尸子进程
*/
while($ret = swoole_process::wait()){
echo 'exit pid:'.$ret['pid'].PHP_EOL;
}
在linux命令窗口中运行swoole.php文件,
发现会每隔3秒同时输出5行"$i--当前时间"格式的字符串,因为同时开启了5个进程,相互独立,所以它们同时开始,同时输出,同时进入sleep,相互独立运行代码中无限循环。示例:
2--2019-06-14 17:09:04
3--2019-06-14 17:09:04
1--2019-06-14 17:09:04
4--2019-06-14 17:09:04
0--2019-06-14 17:09:04
2--2019-06-14 17:09:07
1--2019-06-14 17:09:07
3--2019-06-14 17:09:07
4--2019-06-14 17:09:07
0--2019-06-14 17:09:07
2--2019-06-14 17:09:10
3--2019-06-14 17:09:10
1--2019-06-14 17:09:10
4--2019-06-14 17:09:10
0--2019-06-14 17:09:10
另起一个窗口在命令行中使用
ps -ef | grep swoole.php
会显示如下信息示例:
root 1149 15549 0 17:11 pts/3 00:00:00 php swoole.php
root 1150 1149 0 17:11 pts/3 00:00:00 php swoole.php
root 1151 1149 0 17:11 pts/3 00:00:00 php swoole.php
root 1152 1149 0 17:11 pts/3 00:00:00 php swoole.php
root 1153 1149 0 17:11 pts/3 00:00:00 php swoole.php
root 1154 1149 0 17:11 pts/3 00:00:00 php swoole.php
root 1243 15323 0 17:12 pts/1 00:00:00 grep --color=auto swoole.php
发现总共6个swoole进程,其中一个是主进程1149,其它5个是1149的子进程,我们杀死其中一个子进程1150
kill 1150
发现运行命令行的窗口多了一行
exit pid:1150
这是因为swoole_process::wait()会回收退出的子进程,默认是阻塞回收,它会一直等待子进程退出,直至所有子进程退出,我在回收子进程后输出了pid。
此时查看进程 ps -ef | grep swoole.php ,发现只剩下4个子进程,杀死所有子进程后主进程也会退出。
三.多进程开发
1.在
while($ret = swoole_process::wait()){}
中重启退出的子进程的话,可以实现进程守护
2.可以在子进程中使用
$worker->name('name--'.$i)
可以给子进程重命名,这样便于查看和管理,也方便用 pkill -f 'proces name'批量杀死子进程。
3.启动子进程的时候将第二个参数设置为true,重定向子进程的标准输入和输出。启用此选项后,在子进程内输出内容将不是打印屏幕,而是写入到主进程管道。读取键盘输入将变为从管道中读取数据。
<?php
//开启多进程
$forks = array();
for($i=0;$i<5;$i++){
$process = new swoole_process(function(swoole_process $worker)use($i){
$worker->write('用write写入内容');
while(true){
echo $i.'--'.date('Y-m-d H:i:s').PHP_EOL;
sleep(3);
}
},true);
$pid = $process ->start(); //会返回pid
$forks[$pid] = $process;
}
/*
* 回收子进程
* 注意,这里是必须的否则可能出现僵尸子进程
*/
while($ret = swoole_process::wait()){
$process = $forks[$ret['pid']];
echo 'exit pid:'.$process->read().PHP_EOL;
}
在swoole_process::wait() 中用 $process->read() 可以读取管道内容,这里注意有个好习惯就是在子进程程序执行前使用$worker->write()先县写入一段内容,否则你的程序产生意外退出而没有任何输出,会导致$process->read()阻塞主进程!!!
4.子进程使用
$worke->exec(string $execfile, array $args)
可以执行一个外部程序,此函数是exec系统调用的封装。这意味着你可以去守护和监听其他php脚本和其他语言的脚本!!
执行成功后,当前进程的代码段将会被新程序替换。子进程蜕变成另外一套程序。父进程与当前进程仍然是父子进程关系。
父进程与新进程之间可以通过可以通过标准输入输出进行通信,必须启用标准输入输出重定向。即上面一条提到的第二个参数设置为true。
注意,执行此方法后,$worker->name()设置的名称会失效。
使用方法也要注意:$execfile
必须使用绝对路径,否则会报文件不存在错误
由于exec
系统调用会使用指定的程序覆盖当前程序,所以原程序会失效,子进程中调用此方法后的代码也不会再执行。
$process = new \Swoole\Process(function (\Swoole\Process $childProcess) {
// 不支持这种写法
// $childProcess->exec('/usr/local/bin/php /var/www/project/yii-best-practice/cli/yii
t/index -m=123 abc xyz');
// 封装 exec 系统调用
// 绝对路径
// 参数必须分开放到数组中
$childProcess->exec('/usr/local/bin/php', ['/var/www/project/yii-best-practice/cli/yii',
't/index', '-m=123', 'abc', 'xyz']); // exec 系统调用
});
$process->start(); // 启动子进程
父进程与exec
子进程使用管道进行通信:
// exec - 与exec进程进行管道通信
use Swoole\Process;
$process = new Process(function (Process $worker) {
$worker->exec('/bin/echo', ['hello']);
$worker->write('hello');
}, true); // 需要启用标准输入输出重定向
$process->start();
echo "from exec: ". $process->read(). "\n";
5.使用
swoole_process::kill($pid, $signo = SIGTERM)
可以杀死指定pid的进程,
- 默认的信号为
SIGTERM
,表示终止进程 $signo=0
,可以检测进程是否存在,不会发送信号
当主进程守护多个持续运行的子进程时,可以为主进程启动一个监测子进程,当主进程意外退出时,监测子进程使用swoole_process::kill($main_pid, 0)此方法监测出主进程退出,然后swoole_process::kill($fork_pid)将子进程全部杀死之后,自己调用$worker->exit()退出,从而防止僵尸子进程。
6.子进程退出
$worker->exit(int $status=0);
可以传状态码,会被
$ret = swoole_process::wait()
回收至$ret['code']中,默认为0,不调用$worker->exit()正常退出为0,子进程可以控制此参数的值来让主进程处理不同的错误情况。
四.基本概念及注意事项
1.swoole是用C/C++编写的php拓展,所以它的进程协程开启和底层处理是快于大多数开发语言的。
2.子进程可以共享主进程的常量,全局变量,静态变量,函数,类继承,类方法,类属性,但是相对于主进程来说,这些是只读的,即在子进程中修改变量和类属性只会在子进程中生效,值的改变不会映射到主进程和其他子进程!!
3.第2条中提到的子进程的共享规律,意味着子进程可以共享主进程的数据库连接变量,但不要这么做!!,较多的子进程对一个数据库连接并发请求,mysql会拒绝请求,redis会在临界点出现意外结果,超出临界点会拒绝请求。swoole官方有提供mysql和redis的Coroutine拓展,能解决这个问题,但是谨慎使用,当数据库都变得不可靠时,你的程序稳定性就无从可谈了,尤其是生产环境,除非你的团队足够强大且不乏C/C++高手。稳定的解决办法就是每个子进程都建立自己的数据库连接。
4.尽量不要在子进程中使用$worke->exec()同时使用sleep()或usleep(),连swoole提供的Timer方法也不行,这样使用在某些时候会产生意外的结果,直接导致主进程挂掉!! 如果没有使用$worke->exec(),使用以上方法则没有问题,具体原因我不明,可能是一个bug。知道原因的可以留言。
5.子进程回收参数设置为false,swoole_process::wait(false),则是非阻塞的,如果没有子进程退出则立即返回0,可以结合
while(true){
while($ret = swoole_process::wait(false)){
//回收处理
}
sleep(3);
}
每隔三秒回收所有退出的子进程