从 0 到 1 优雅的实现 PHP 多进程管理 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
TIGERB
V2EX    程序员

从 0 到 1 优雅的实现 PHP 多进程管理

  •  2
     
  •   TIGERB
    TIGERB 2017-12-01 10:23:52 +08:00 5319 次点击
    这是一个创建于 2879 天前的主题,其中的信息可能已经有所发展或是发生改变。
     _ | | _ __ __ _ _ __ _ _| |_ ___ | '_ \ / _` | '__| | | | __/ _ \ | | | | (_| | | | |_| | || (_) | |_| |_|\__,_|_| \__,_|\__\___/ .TIGERB.cn An object-oriented multi process manager for PHP Version: 0.1.0 

    业务场景

    在我们实际的业务场景中(PHP 技术栈),我们可能需要定时或者近乎实时的执行一些业务逻辑,简单的我们可以使用 unix 系统自带的 crontab 实现定时任务,但是对于一些实时性要求比较高的业务就不适用了,所以我们就需要一个常驻内存的任务管理工具,为了保证实时性,一方面我们让它一直执行任务(适当的睡眠,保证 cpu 不被 100%占用),另一方面我们实现多进程保证并发的执行任务。

    目的

    综上所述,我的目标就是:实现基于 php-cli 模式实现的 master-worker 多进程管理工具。其次,“我有这样一个目标,我是怎样一步步去分析、规划和实现的”,这是本文的宗旨。

    备注:下文中,父进程统称为 master,子进程统称为 worker。

    分析

    我们把这一个大目标拆成多个小目标去逐个实现,如下:

    • 多进程
      • 目的:一个 master fork 多个 worker
      • 现象:所有 worker 的 ppid 父进程 ID 为当前 master 的 pid
    • master 控制 worker
      • 目的:master 通知 worker,worker 接收来自 master 的消息
    • master 接收信号
      • 目的:master 接收并自定义处理来自终端的信号

    多进程

    PHP fork 进程的方法 pcntl_fork, 这个大家应该有所了解,如果不知道的简单 google/bing 一下应该很容易找到这个函数。接着 FTM, 我们看看pcntl_fork这个函数的使用方式大致如下:

    $pid = pcntl_fork(); // pcntl_fork 的返回值是一个 int 值 // 如果$pid=-1 fork 进程失败 // 如果$pid=0 当前的上下文环境为 worker // 如果$pid>0 当前的上下文环境为 master,这个 pid 就是 fork 的 worker 的 pid 

    接着看代码:

    $pid = pcntl_fork(); switch ($pid) { case -1: // fatal error 致命错误 所有进程 crash 掉 break; case 0: // worker context exit; // 这里 exit 掉,避免 worker 继续执行下面的代码而造成一些问题 break; default: // master context pcntl_wait($status); // pcntl_wait 会阻塞,例如直到一个子进程 exit // 或者 pcntl_waitpid($pid, $status, WNOHANG); // WNOHANG:即使没有子进程 exit,也会立即返回 break; } 

    我们看到 master 有调用pcntl_wait或者pcntl_waitpid函数,为什么呢?首先我们在这里得提到两个概念,如下:

    • 孤儿进程:父进程挂了,子进程被 pid=1 的 init 进程接管(wait/waitpid),直到子进程自身生命周期结束被系统回收资源和父进程 采取相关的回收操作
    • 僵尸进程:子进程 exit 退出,父进程没有通过 wait/waitpid 获取子进程状态,子进程占用的进程号等描述资源符还存在,产生危害:例如进程号是有限的,无法释放进程号导致未来可能无进程号可用

    所以,pcntl_wait或者pcntl_waitpid的目的就是防止 worker 成为僵尸进程(zombie process)。

    除此之外我们还需要把我们的 master 挂起和 worker 挂起,我使用的的是 while 循环,然后usleep(200000)防止 CPU 被 100%占用。

    最后我们通过下图(1-1)来简单的总结和描述这个多进程实现的过程:

    master 控制 worker

    上面实现了多进程和多进程的常驻内存,那 master 如何去管理 worker 呢?答案:多进程通信。话不多说 google/bing 一下,以下我列举几种方式:

    • 命名管道: 感兴趣
    • 队列: 个人感觉和业务中使用 redis 做消息队列思路应该一致
    • 共享内存: 违背“不要通过共享内存来通信,要通过通信来实现共享”原则
    • 信号: 承载信息量少
    • 套接字: 不熟悉

    所以我选择了“命名管道”的方式。我设计的通信流程大致如下:

    • step 1: 创建 worker 管道
    • step 2: master 写消息到 worker 管道
    • step 3: worker 读消息从 worker 管道

    接着还是逐个击破,当然话不多说还是 google/bing 一下。posix_mkfifo创建命名管道、fopen打开文件(管道以文件形式存在)、fread读取管道、fclose关闭管道就呼啸而出,哈哈,这样我们就能很容易的实现我们上面的思路的了。接着说说我在这里遇到的问题:fopen阻塞了,导致业务代码无法循环执行,一想不对啊,平常fopen普通文件不存在阻塞行为,这时候二话不说 FTM 搜fopen,crtl+f 页面搜“ block ”,重点来了:

    fopen() will block if the file to be opened is a fifo. This is true whether it's opened in "r" or "w" mode. (See man 7 fifo: this is the correct, default behaviour; although Linux supports non-blocking fopen() of a fifo, PHP doesn't).

    翻译下,大概意思就是“当使用 fopen 的 r 或者 w 模式打开一个 fifo 的文件,就会一直阻塞;尽管 linux 支持非阻塞的打开 fifo,但是 php 不支持。”,得不到解决方案,不支持,感觉要放弃,一想这种场景应该不会不支持吧,再去看看posix_mkfifo,结果喜出望外:

    <?php $fh=fopen($fifo, "r+"); // ensures at least one writer (us) so will be non-blocking stream_set_blocking($fh, false); // prevent fread / fwrite blocking ?> The "r+" allows fopen to return immediately regardless of external writer channel. 

    结论使用“ r+”,同时我们又知道了使用stream_set_blocking防止紧接着的fread阻塞。接着我们用下图(1-2)来简单的总结和描述这个 master-worker 通信的方式。

    master 接收信号

    最后我们需要解决的问题就是 master 怎么接受来自 client 的信号,google/bing 结论:

    master 接收信号 -> pcntl_signal 注册对应信号的 handler 方法 -> pcntl_signal_dispatch() 派发信号到 handler 

    如下图(1-3)所示,

    其他

    接着我们只要实现不同信号下 master&worker 的策略,例如 worker 的重启等。这里需要注意的就是,当 master 接受到重启的信号后,worker 不要立即 exit,而是等到 worker 的业务逻辑执行完成了之后 exit。具体的方式就是:

    master 接收 reload 信号 -> master 把 reload 信号写 worker 管道 -> worker 读取到 reload 信号 -> worker 添加重启标志位 -> worker 执行完业务逻辑后且检测到重启的标志位后 exit 

    建模

    上面梳理完我们的实现方式后,接着我们就开始码代码了。码代码之前进行简单的建模,如下:

    进程管理类 Manager

    - attributes + master: master 对象 + workers: worker 进程对象池 + waitSignalProcessPool: 等待信号的 worker 池 + startNum: 启动进程数量 + userPasswd: linux 用户密码 + pipeDir: 管道存放路径 + signalSupport: 支持的信号 + hangupLoopMicrotime: 挂起间隔睡眠时间 - method + welcome: 欢迎于 + configure: 初始化配置 + fork: forkworker 方法 + execFork: 执行 forkworker 方法 + defineSigHandler: 定义信号 handler + registerSigHandler: 注册信号 handler + hangup: 挂起主进程 

    进程抽象类 Process

    - attributes + type: 进程类型 master/worker + pid: 进程 ID + pipeName: 管道名称 + pipeMode: 管道模式 + pipeDir: 管道存放路径 + pipeNamePrefix: 管道名称前缀 + pipePath: 管道生成路径 + readPipeType: 读取管道数据的字节数 + workerExitFlag: 进程退出标志位 + signal: 当前接受到的信号 + hangupLoopMicrotime: 挂起间隔睡眠时间 - method + hangup: 挂起进程(抽象方法) + pipeMake: 创建管道 + pipeWrite: 写管道 + pipeRead: 读管道 + clearPipe: 清理管道文件 + stop: 进程 exit 

    master 实体类 MasterProcess

    - attributes + - method + hangup: 挂起进程 

    worker 实体类 MasterProcess

    - attributes + - method + dispatchSig: 定义 worker 信号处理方式 

    最后我们需要做的就是优雅的填充我们的代码了。

    最后

    项目地址 https://github.com/TIGERB/naruto

    个人知识还有很多不足,如果有写的不对的地方,希望大家及时指正。

    THX~

    23 条回复    2019-07-12 12:02:16 +08:00
    whahuzhihao
        1
    whahuzhihao  
       2017-12-01 10:30:59 +08:00
    所以为什么不用 swoole 呢
    explon
        2
    explon  
       2017-12-01 10:35:17 +08:00 via iPhone
    所以为什么不用 resque 呢?
    owenliang
        3
    owenliang  
       2017-12-01 10:38:52 +08:00
    不错,但是我更加建议用 C 做 UNIX 开发的学习语言。
    kof21411
        4
    kof21411  
       2017-12-01 11:07:50 +08:00
    为什么多进程不用 python 呢?
    TIGERB
        5
    TIGERB  
    OP
       2017-12-01 11:23:34 +08:00
    @whahuzhihao 就是想用 PHP 自己实现下,哈哈
    TIGERB
        6
    TIGERB  
    OP
       2017-12-01 11:24:19 +08:00
    @explon @owenliang @kof21411 就是想用 PHP 自己实现下,哈哈
    extreme
        7
    extreme  
       2017-12-01 11:27:13 +08:00
    不错不错。

    大概看了下代码,不知道对楼主的代码理解有没有误:
    可以用 pcntl_sigwaitinfo()去等待子进程的信号,再调用 pcntl_signal_dispatch(),另外子进程 TERMINATED 了,父进程会收到 SIGCHLD,注册这个信号处理器,处理器里面循环 wait()获取结束的进程一一处理,直到 wait()返回错误。这样就不需要在 Master 那循环+usleep()监控子进程的状态了。

    不久前我也弄过一个多任务的,抽象了 Pthread 和 Pcntl: https://github.com/yzsme/Alone-Multitask
    不过弄来自用的,不算很精致,当时也还不需要用到 IPC,就没实现 IPC。
    zn
        8
    zn  
       2017-12-01 12:43:12 +08:00
    TigerB,老虎 B ?这名字不错。
    fuxkcsdn
        9
    fuxkcsdn  
       2017-12-01 12:52:46 +08:00 via iPhone
    之前实现过类似的,基于 ipc 实现,可实现自动重启被 kill 的进程
    代码没整理过…
    https://github.com/consatan/pdaemon
    fuxkcsdn
        10
    fuxkcsdn  
       2017-12-01 12:55:35 +08:00 via iPhone
    上面说错,应该是基于 php 的 System V IPC
    zn
        11
    zn  
       2017-12-01 13:01:21 +08:00
    @TIGERB @extreme 说的子进程回收处理方法才是最正确的,不需要 usleep + 死循环。

    @extreme 不过你这搞得太复杂了,依赖了好几个外部项目,一下子就麻烦起来了。貌似还依赖 Pthread ? Pthread 这玩意儿不好装。
    slince
        12
    slince  
       2017-12-01 13:04:55 +08:00   1
    写过一个 php 多进程 https://github.com/slince/process
    extreme
        13
    extreme  
       2017-12-01 14:01:05 +08:00
    @zn 也没好几个那么夸张,其实两边几乎没耦合,当时是作为一个子模块来弄的。
    extreme
        14
    extreme  
       2017-12-01 14:04:21 +08:00
    @zn 另外没强制依赖 Pthread,不用的话就不需要安装,Pthread 要求 PHP Zend Thread Safety 版本。
    顺便吐槽下 PHP 的 Pthread 模块实现得很反人类,基本上无法投入到我的生产环境中……
    yougeren
        15
    yougeren  
       2017-12-01 16:22:21 +08:00
    你可以考虑下 swoole_process,挺友好的
    zh10086
        16
    zh10086  
       2017-12-02 00:41:16 +08:00
    @TIGERB 我也是学 php 的,但是对进程这块一点不懂,非科班,应该看什么书能对原理理解的透彻些,望各位前辈指点一下
    TIGERB
        17
    TIGERB  
    OP
       2017-12-02 00:47:05 +08:00
    @extreme 学习下~
    TIGERB
        18
    TIGERB  
    OP
       2017-12-02 00:47:45 +08:00
    @fuxkcsdn 学习下~
    TIGERB
        19
    TIGERB  
    OP
       2017-12-02 00:47:54 +08:00
    @zn 哈哈~
    TIGERB
        20
    TIGERB  
    OP
       2017-12-02 00:48:16 +08:00
    @slince 学习下~
    TIGERB
        21
    TIGERB  
    OP
       2017-12-02 00:50:04 +08:00
    @zh10086 我也是,我现在理解的也不是很透彻,可以了解下 php-fpm 或者 nginx 的实现
    huigeer
        22
    huigeer  
       2017-12-02 07:03:30 +08:00 via iPhone
    @TIGERB,可以看看 workman,
    awanganddong
        23
    awanganddong  
       2019-07-12 12:02:16 +08:00
    关于 php 学习多进程的,首先自己先实现 php spl 函数,诸如 php-pcntl 相关函数,以及进程之间怎么通讯。
    workman 是完全基于 php 函数写的,可以看下他的源码。swoole 是基于 C 扩展。如果没有 c 基础,不建议看。

    这里推荐一本经典书,apue。可以大概了解了解。
    然后就自己动手撸吧,先从简单的实现。
    想想存在的问题,
    然后迭代。

    友情建议,github 上有很多简单粗暴流的,初期可以看看,因为最起码可以明白原理。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2567 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 07:50 PVG 15:50 LAX 00:50 JFK 03:50
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86