PHP多进程消费队列
引言
最近开发一个小功能,用到了队列mcq,启动一个进程消费队列数据,后边发现一个进程处理不过来了,又加了一个进程,过了段时间又处理不过来了......
这种方式每次都要修改crontab,如果进程挂掉了,不会及时的启动,要等到下次crontab执行的时候才会启动。关闭(重启)进程的时候用的是kill,这可能会丢失正在处理的数据,比如下面这个例子,我们假设sleep过程就是处理逻辑,这里为了明显看出效果,将处理时间放大到10s:
<?php
$i = 1;
while (1) {
echo "开始第[{$i}]次循环\n";
sleep(10);
echo "结束第[{$i}]次循环\n";
$i++;
}
当我们运行脚本之后,等到循环开始之后,给进程发送 kill {$pid}
,默认发送的是编号为15的SIGTERM
信号。假设$i
是从队列拿到的,拿到2的时候,正在处理,我们给程序发送了kill信号,和队列数据丢失一样,问题比较大,因此我要想办法解决这些问题。
开始第[1]次循环
结束第[1]次循环
开始第[2]次循环
[1] 28372 terminated php t.php
nginx进程模型
这时候我想到了nginx,nginx作为高性能服务器的中流砥柱,为成千上万的企业和个人服务,他的进程模型比较经典,如下所示:
管理员通过master进程和nginx进行交互,从/path/to/nginx.pid
读取nginx master进程的pid,发送信号给master进程,master根据不同的信号做出不同的处理,然后反馈信息给管理员。worker是master进程fork出来的,master负责管理worker,不会去处理业务,worker才是具体业务的处理者,master可以控制worker的退出、启动,当worker意外退出,master会收到子进程退出的消息,也会重新启动新的worker进程补充上来,不让业务处理受影响。nginx还可以平滑退出,不丢失任何一个正在处理的数据,更新配置时nginx可以做到不影响线上服务来加载新的配置,这在请求量很大的时候特别有用。
进程设计
看了nginx的进模型,我们完全可以开发一个类似的类库来满足处理mcq数据的需求,做到单文件控制所有进程、可以平滑退出、可以查看子进程状态。不需要太复杂,因为我们处理队列数据接收一定的延迟,做到nginx那样不间断服务比较麻烦,费时费力,意义不是很大。设计的进程模型跟nginx类似,更像是nginx的简化版本。
进程信号量设计
信号量是进程间通讯的一种方式,比较简单,单功能也比较弱,只能发送信号给进程,进程根据信号做出不同的处理。
master进程启动的时候保存pid到文件/path/to/daeminze.pid
,管理员通过信号和master进程通讯,master进程安装3种信号,碰到不同的信号,做出不同的处理,如下所示:
SIGINT => 平滑退出,处理完正在处理的数据再退出
SIGTERM => 暴力退出,无论进程是否正在处理数据直接退出
SIGUSR1 => 查看进程状态,查看进程占用内存,运行时间等信息
master进程通过信号和worker进程通讯,worker进程安装了2个信号,如下所示:
SIGINT => 平滑退出
SIGUSR1 => 查看worker进程自身状态
为什么worker进程只安装2个信号呢,少了个SIGTERM
,因为master进程收到信号SIGTERM
之后,向worker进程发送SIGKILL
信号,默认强制关闭进程即可。
worker进程是通过master进程fork出来的,这样master进程可以通过pcntl_wait
来等待子进程退出事件,当有子进程退出的时候返回子进程pid,做处理并启动新的进程补充上来。
master进程也通过pcntl_wait
来等待接收信号,当有信号到达的时候,会返回-1
,这个地方还有些坑,在下文中会详细讲。
PHP中有2种信号触发的方式,第一种方式是declare(ticks = 1);
,这种效率不高,Zend每执行一次低级语句,都会去检查进程中是否有未处理的信号,现在已经很少使用了,PHP 5.3.0
及之前的版本可能会用到这个。
第二种是通过pcntl_signal_dispatch
来调用未处理的信号,PHP 5.4.0
及之后的版本适用,可以巧妙的将该函数放在循环中,性能上基本没什么损失,现在推荐适用。
PHP安装修信号量
PHP通过pcntl_signal
安装信号,函数声明如下所示:
bool pcntl_signal ( int $signo , [callback $handler [, bool $restart_syscalls = true ] )
第三个参数restart_syscalls
不太好理解,找了很多资料,也没太查明白,经过试验发现,这个参数对pcntl_wait
函数接收信号有影响,当设置为缺省值true
的时候,发送信号,进程用pcntl_wait
收不到,必须设置为false
才可以,看看下面这个例子:
<?php
$i = 0;
while ($i<5) {
$pid = pcntl_fork();
$random = rand(10, 50);
if ($pid == 0) {
sleep($random);
exit();
}
echo "child {$pid} sleep {$random}\n";
$i++;
}
pcntl_signal(SIGINT, function($signo) {
echo "Ctrl + C\n";
});
while (1) {
$pid = pcntl_wait($status);
var_dump($pid);
pcntl_signal_dispatch();
}
运行之后,我们对父进程发送kill -SIGINT {$pid}
信号,发现pcntl_wait没有反应,等到有子进程退出的时候,发送过的SIGINT
会一个个执行,比如下面结果:
child 29643 sleep 48
child 29644 sleep 24
child 29645 sleep 37
child 29646 sleep 20
child 29647 sleep 31
int(29643)
Ctrl + C
Ctrl + C
Ctrl + C
Ctrl + C
int(29646)
这是运行脚本之后马上给父进程发送了四次SIGINT
信号,等到一个子进程推出的时候,所有信号都会触发。
但当把安装信号的第三个参数设置为false
:
pcntl_signal(SIGINT, function($signo) {
echo "Ctrl + C\n";
}, false);
这时候给父进程发送SIGINT
信号,pcntl_wait
会马上返回-1
,信号对应的事件也会触发。
所以第三个参数大概意思就是,是否重新注册此信号,如果为false只注册一次,触发之后就返回,pcntl_wait
就能收到消息,如果为true,会重复注册,不会返回,pcntl_wait
收不到消息。
信号量和系统调用
信号量会打断系统调用,让系统调用立刻返回,比如sleep
,当进程正在sleep的时候,收到信号,sleep会马上返回剩余sleep秒数,比如:
<?php
pcntl_signal(SIGINT, function($signo) {
echo "Ctrl + C\n";
}, false);
while (true) {
pcntl_signal_dispatch();
echo "123\n";
$limit = sleep(2);
echo "limit sleep [{$limit}] s\n";
}
运行之后,按Ctrl + C
,结果如下所示:
123
^Climit sleep [1] s
Ctrl + C
123
limit sleep [0] s
123
^Climit sleep [1] s
Ctrl + C
123
^Climit sleep [2] s
daemon(守护)进程
这种进程一般设计为daemon进程,不受终端控制,不与终端交互,长时间运行在后台,而对于一个进程,我们可以通过下面几个步骤把他升级为一个标准的daemon进程:
protected function daemonize()
{
$pid = pcntl_fork();
if (-1 == $pid) {
throw new Exception("fork进程失败");
} elseif ($pid != 0) {
exit(0);
}
if (-1 == posix_setsid()) {
throw new Exception("新建立session会话失败");
}
$pid = pcntl_fork();
if (-1 == $pid) {
throw new Exception("fork进程失败");
} else if($pid != 0) {
exit(0);
}
umask(0);
chdir("/");
}
拢共分五步:
- fork子进程,父进程退出。
- 设置子进程为会话组长,进程组长。
- 再次fork,父进程退出,子进程继续运行。
- 恢复文件掩码为
0
。 - 切换当前目录到根目录
/
。
第2步是为第1步做准备,设置进程为会话组长,必要条件是进程非进程组长,因此做第一次fork,进程组长(父进程)退出,子进程通过posix_setsid()
设置为会话组长,同时也为进程组长。
第3步是为了不让进程重新控制终端,因为一个进程控制一个终端的必要条件是会话组长(pid=sid)。
第4步是为了恢复默认的文件掩码,避免之前做的操作对文件掩码做了设置,带来不必要的麻烦。关于文件掩码, linux中,文件掩码在创建文件、文件夹的时候会用到,文件的默认权限为666,文件夹为777,创建文件(夹)的时候会用默认值减去掩码的值作为创建文件(夹)的最终值,比如掩码022
下创建文件666 - 222 = 644
,创建文件夹777 - 022 = 755
:
掩码 | 新建文件权限 | 新建文件夹权限 |
---|---|---|
umask(0) | 666 (-rw-rw-rw-) | 777 (drwxrwxrwx) |
umask(022) | 644 (-rw-r--r--) | 755 (drwxr-xr-x) |
第5步是切换了当前目录到根目录/
,网上说避免起始运行他的目录不能被正确卸载,这个不是太了解。
对应5步,每一步的各种id变化信息:
操作后 | pid | ppid | pgid | sid |
---|---|---|---|---|
开始 | 17723 | 31381 | 17723 | 31381 |
第一次fork | 17723 | 1 | 17723 | 31381 |
posix_setsid() | 17740 | 1 | 17740 | 17740 |
第二次fork | 17840 | 1 | 17740 | 17740 |
另外,会话、进程组、进程的关系如下图所示,这张图有助于更好的理解。
至此,你也可以轻松地造出一个daemon进程了。
命令设计
我准备给这个类库设计6个命令,如下所示:
- start 启动命令
- restart 强制重启
- stop 平滑停止
- reload 平滑重启
- quit 强制停止
- status 查看进程状态
启动命令
启动命令就是默认的流程,按照默认流程走就是启动命令,启动命令会检测pid文件中是否已经有pid,pid对应的进程是否健康,是否需要重新启动。
强制停止命令
管理员通过入口文件结合pid给master进程发送SIGTERM
信号,master进程给所有子进程发送SIGKILL
信号,等待所有worker进程退出后,master进程也退出。
强制重启命令
强制停止命令
+ 启动命令
平滑停止命令
平滑停止命令,管理员给master进程发送SIGINT
信号,master进程给所有子进程发送SIGINT
,worker进程将自身状态标记为stoping
,当worker进程下次循环的时候会根据stoping
决定停止,不在接收新的数据,等所有worker进程退出之后,master进程也退出。
平滑重启命令
平滑停止命令
+ 启动命令
查看进程状态
查看进程状态这个借鉴了workerman的思路,管理员给master进程发送SIGUSR1
信号,告诉主进程,我要看所有进程的信息,master进程,master进程将自身的进程信息写入配置好的文件路径A中,然后发送SIGUSR1
,告诉worker进程把自己的信息也写入文件A中,由于这个过程是异步的,不知道worker进程啥时候写完,所以master进程在此处等待,等所有worker进程都写入文件之后,格式化所有的信息输出,最后输出的内容如下所示:
➜/dir /usr/local/bin/php DaemonMcn.php status
Daemon [DaemonMcn] 信息:
-------------------------------- master进程状态 --------------------------------
pid 占用内存 处理次数 开始时间 运行时间
16343 0.75M -- 2018-05-15 09:42:45 0 天 0 时 3 分
12 slaver
-------------------------------- slaver进程状态 --------------------------------
任务task-mcq:
16345 0.75M 236 2018-05-15 09:42:45 0 天 0 时 3 分
16346 0.75M 236 2018-05-15 09:42:45 0 天 0 时 3 分
--------------------------------------------------------------------------------
任务test-mcq:
16348 0.75M 49 2018-05-15 09:42:45 0 天 0 时 3 分
16350 0.75M 49 2018-05-15 09:42:45 0 天 0 时 3 分
16358 0.75M 49 2018-05-15 09:42:45 0 天 0 时 3 分
16449 0.75M 1 2018-05-15 09:46:40 0 天 0 时 0 分
--------------------------------------------------------------------------------
等待worker进程将进程信息写入文件的时候,这个地方用了个比较trick的方法,每个worker进程输出一行信息,统计文件的行数,达到worker进程的行数之后表示所有worker进程都将信息写入完毕,否则,每个1s检测一次。
其他设计
另外还加了两个比较实用的功能,一个是worker进程运行时间限制,一个是worker进程循环处理次数限制,防止长时间循环进程出现内存溢出等意外情况。时间默认是1小时,运行次数默认是10w次。
除此之外,也可以支持多任务,每个任务几个进程独立开,统一由master进程管理。
代码已经放到github中,有兴趣的可以试试,不支持windows哦,有什么错误还望指出来。
参考文章
凭记忆想到的参考文章,查了好多忘记了
PHP多进程消费队列的更多相关文章
- PHP开发者该知道的多进程消费队列
引言 最近开发一个小功能,用到了队列mcq,启动一个进程消费队列数据,后边发现一个进程处理不过来了,又加了一个进程,过了段时间又处理不过来了… 这种方式每次都要修改crontab,如果进程挂掉了,不会 ...
- python 多进程——使用进程池,多进程消费的数据)是一个队列的时候,他会自动去队列里依次取数据
我的mac 4核,因此每次执行的时候同时开启4个线程处理: # coding: utf-8 import time from multiprocessing import Pool def long_ ...
- Python 3 并发编程多进程之队列(推荐使用)
Python 3 并发编程多进程之队列(推荐使用) 进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的. 可以往 ...
- 【mq读书笔记】消息消费队列和索引文件的更新
ConsumeQueue,IndexFile需要及时更新,否则无法及时被消费,根据消息属性查找消息也会出现较大延迟. mq通过开启一个线程ReputMessageService来准时转发commitL ...
- 【并发】7、借助redis 实现多线程生产消费队列
1.这是第一个简单的初始化版本,看起来比使用fqueue似乎更好用 package queue.redisQueue; import queue.fqueue.vo.TempVo; import re ...
- kafak manager + zookeeper + kafka 消费队列快速清除
做性能测试时,kafka消息队列比较长,让程序自己消费完毕需要等待很长时间.就需要快速清理kafka队列 清理方式把 这kafak manager + zookeeper + kafka 这些应用情况 ...
- python学习笔记——multiprocessing 多进程组件-队列Queue
1 消息队列 1.1 基本语法 消息队列:multiprocessing.Queue,Queue是对进程安全的队列,可以使用Queue实现对进程之间的数据传输:还有一个重要作用是作为缓存使用. Que ...
- python中多线程,多进程,队列笔记(一)
threading简介:If you want your application to make better use of the computational resources of multi- ...
- java操作RabbitMQ添加队列、消费队列和三个交换机
假设已经在服务器上安装完RabbitMQ.我写的教程 一.发送消息到队列(生产者) 新建一个maven项目,在pom.xml文件加入以下依赖 <dependencies> <depe ...
随机推荐
- Java四种引用类型
纸上得来终觉浅,绝知此事要躬行 --陆游 问渠那得清如许,为有源头活水来 --朱熹 Java从1.2版本开始引入了4种引用,这四种引用的级别由高到低依次为:强引用>软引用>弱引用 ...
- 好看的dialog,sweet Alert Dialog 导入Android Studio
系统自带的dialog实在是丑到无法忍受.所以找到了一款比较好的第三方dialog. github 地址如下:https://github.com/pedant/sweet-alert-dialog ...
- 演练Ext JS 4.2自定义主题
本文将根据API文档中关于主题的介绍做的一次演练,以便熟悉自定义主题的过程. 练习环境: Sencha Cmd v4.0.1.45 Ruby 1.9.3-p392 firefox 26 首先,使用以下 ...
- Linux自动安装JDK的shell脚本
Linux自动安装JDK的shell脚本 A:本脚本运行的机器,Linux B:待安装JDK的机器, Linux 首先在脚本运行的机器A上确定可以ssh无密码登录到待安装jdk的机器B上,然后就可以在 ...
- 从硬件竞争到软实力PK——电视媒体竞争观察
本文观点及数据摘自中广研究<三网融合月度精粹>第26期(2013年2月版),详细参考对应在线简版(http://doc.sarft.net/index.php?f=2013/02/2013 ...
- ExtJS:Grid数据导出至excel实例
导出函数ExportExcel() var config={ store: alldataStore, title: '测试标题' }; var tab=tabPanel.getActiveTab() ...
- Stackoverflow上人气最旺的10个Java问题
1. 为什么两个(1927年)时间相减得到一个奇怪的结果? (3623个赞) 如果执行下面的程序,程序解析两个间隔1秒的日期字符串并比较: 01 public static void main(Str ...
- Dll的编写 在unity中加载
1. 在VS中新建Dll项目 2.在头文件中对函数进行声明 extern "C" int _declspec(dllexport) testunity(); 3.在源文件中写函数体 ...
- ViewPagerIndicator+viewpager指示器详解
前几天学习了ViewPager作为引导页和Tab的使用方法.后来也有根据不同的使用情况改用Fragment作为Tab的情况,以及ViewPager结合FragmentPagerAdapter的使用.今 ...
- Qt5中this application has requested the runtime to terminate it in an unusual way 无法运行问题的解决
在windows平台使用Qt5.8mingw版写出的程序,在Qt中运行正常,而以release的形式编译并且补充完必要的dll文件后,在其他电脑上运行出现了以下问题: 经过查阅许多资料和亲身实验,终于 ...