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作为高性能服务器的支柱,为许多的商店和个人服务,他的进程模型相比经典,如下所示:

图片 1

领队通过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的简化版本。
图片 2

经过信号量设计

信号量是经过间通讯的一种办法,相比较简单,单功能也相比较弱,只好发送信号给进程,进程遵照信号做出不同的处理。

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("/");
}

拢共分五步:

  1. fork子进程,父进程退出。
  2. 设置子进程为会话老董,进程经理。
  3. 重复fork,父进程退出,子进程继续运行。
  4. 回复文件掩码为0
  5. 切换当前目录到根目录/

第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

此外,会话、进程组、进程的关联如下图所示,这张图有助于更好的理解。
图片 3

至今,你也得以轻松地造出一个daemon进程了。

命令设计

本身准备给这多少个类库设计6个指令,如下所示:

  1. start 启动命令
  2. restart 强制重启
  3. stop 平滑截至
  4. reload 平滑重启
  5. quit 强制截止
  6. 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哦,有怎样错误还望指出来。

参照随笔

凭记忆想到的参考作品,查了成百上千忘记了

  1. Linux多任务编程(七)—Linux守护进程及其基础实验
  2. workerman源码

相关文章