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

_

| |

_ __ __ _ _ __ _ _| |_ ___

| '_ \ / _` | '__| | | | __/ _ \

| | | | (_| | | | |_| | || (_) |

|_| |_|\__,_|_| \__,_|\__\___/ .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)来简单的总结和描述这个多进程实现的过程:

multi-process.png

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,结果喜出望外:

$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 通信的方式。

2cceb4874e4973c78cddf3fbc203cf27.png

master 接收信号

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

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

如下图(1-3)所示,

ca6de8f6f0dd8b9664d4ee37823c542e.png

其他

接着我们只要实现不同信号下 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 信号处理方式

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

最后

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

THX~

ece652bd0841379c6d974362fbcab705.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值