V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
TIGERB
V2EX  ›  程序员

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

  •  2
     
  •   TIGERB ·
    TIGERB · 2017-12-01 10:23:52 +08:00 · 4829 次点击
    这是一个创建于 2556 天前的主题,其中的信息可能已经有所发展或是发生改变。
                           _        
                          | |       
    _ __   __ _ _ __ _   _| |_ ___  
    | '_ \ / _` | '__| | | | __/ _ \ 
    | | | | (_| | |  | |_| | || (_) |
    |_| |_|\__,_|_|   \__,_|\__\___/ .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   ·   实用小工具   ·   1082 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 113ms · UTC 23:34 · PVG 07:34 · LAX 15:34 · JFK 18:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.