swoole 第5章 进程模型 swoole 第5章 进程模型

2021-06-09

一、模型

swoole 是事件驱动的。在使用 swoole 的过程中,我们也体会到,swoole 的使用非常简单,仅仅注册相应的回调处理我们的业务逻辑即可。

但是,在继续学习 swoole 之前,我们有必要再看一看 swoole 的运行流程和进程模型。

前面两篇文章我们已经对 server 和 task 做了简单的介绍,后面再对 server 的创建以及脚本的执行,如无特殊说明均在 CLI 下执行,我就不啰嗦了。

$serv = new swoole_server("127.0.0.1", 9501);
$serv->set([
    "worker_num" => 2,
    "task_worker_num" => 1,
]);
$serv->on("Connect", function ($serv, $fd) {
});
$serv->on("Receive", function ($serv, $fd, $fromId, $data) {
});
$serv->on("Close", function ($serv, $fd) {
});
$serv->on("Task", function ($serv, $taskId, $fromId, $data) {
});
$serv->on("Finish", function ($serv, $taskId, $data) {
});

$serv->start();

注意这里我们选择了两个 worker 进程个一个 task 进程,那是不是就意味着创建这个 server 就是开启了 3 个进程呢?我们来看下

新开一个终端,我们用 ps 命令看下结果

ps aux | grep server-process
root     21843  xxx... php server-process.php
root     21844  xxx... php server-process.php
root     21846  xxx... php server-process.php
root     21847  xxx... php server-process.php
root     21848  xxx... php server-process.php
root     21854  xxx... grep --color=auto server-process

为了方便阅读,ps 的结果中部分不重要数据已经被稍加处理了。

排除最后一个结果(最后一个是我们运行的 ps 命令)我们发现,竟然有多达 5 个相似的进程在运行,按照我们理解,不应该是 3 个吗,怎么多了两个呢?

还记得我们在进程/线程一文中说过的多进程的实现吗?我们说到多进程的实现一般会被设计 Master-Worker 模式,常见的 nginx 默认的多进程模式也正是如此,当然 swoole 默认的也是多进程模型。

相比 Master-Worker 模式,swoole 的进程模型可以用 Master-Manager-Worker 来形容。即在 Master-Worker 的基础上又增加了一层 Manager 进程。这也就解答了我们开头抛出的问题为什么是 5 个进程而不是 3 个进程了。(1 个 Master 进程+1 个 Manager 进程+2 个 Worker 进程+1 个 Task 进程)

正所谓“存在即合理”,我们来看一下 Master\Manager\Worker 三种进程各自存在的原因。

二、三种进程

Master 进程是一个多线程程序。注解:按照我们之前的理解,多个线程是运行在单一进程的上下文中的,其实对于单一进程中的每一个线程,都有它自己的上下文,但是由于共同存在于同一进程,所以它们也共享这个进程,包括它的代码、数据等等。

再回来继续说 Master 进程,Master 进程就是我们的主进程,掌管生杀大权,它挂了,那底下的都得玩完。Master 进程,包括主线程,多个 Reactor 线程等。

每一个线程都有自己的用途,比如主线程用于 Accept、信号处理等操作,而 Reactor 线程是处理 tcp 连接,处理网络 IO,收发数据的线程。

A、说明两点:

  • 主线程的 Accept 操作,socket 服务端经常用 accept 阻塞,上一节介绍 socket 编程的时候有一张配图,可以看看

  • 信号处理,信号就相当于一条消息,比如我们经常操作的 Ctrl+C 其实就是给 Master 进程的主线程发送一个 SIGINT 的信号,意思就是你可以终止啦,信号有很多种,后面还有介绍

B、Reactor 线程

通常,主线程处理完新的连接后,会将这个连接分配给固定的 Reactor 线程,并且这个 Reactor 线程会一直负责监听此 socket(上文中后面对 socket 更新为 socket 即套接字,是用来与另一个进程进行跨网络通信的文件,文件可读可写),换句话就是说当此 socket 可读时,会读取数据,并将该请求分配给 worker 进程,这也就解释了我们在 swoole 初识讲解 worker 进程内的回调 onReceive 的第三个参数 $fromId 的含义;当此 socket 可写时,会把数据发送给 tcp 客户端。

用一张图清晰的梳理下

https://file.lulublog.cn/images/3/2022/07/T1lAaDqsdR23zq8DlA2YWaOh6WaJJZ.png

那 swoole 为啥不能像 Nginx 一样,是 Master-Worker 进程结构的呢?Manager 进程是干啥的?

这个我正准备说。

C、Manager进程

我们知道,在 Master-Worker 模型中,Master 只有一个,Worker 是由父进程 Master 进程复制出来的,且 Worker 进程可以有多个。

注解:在 linux 中,父进程可以通过调用 fork 函数创建一个新的子进程,子进程是父进程的一个副本,几乎但不完全相同,二者的最大区别就是都拥有自己独立的进程 ID,即 PID。

对于多线程的 Master 进程而言,想要多 Worker 进程就必须 fork 操作,但是 fork 操作是不安全的,所以,在 swoole 中,有一个专职的 Manager 进程,Manager 进程就专门负责 worker/task 进程的 fork 操作和管理。换句话也就是说,对于 worker 进程的创建、回收等操作全权有“保姆”Manager 进程进行管理。

通常,worker 进程被误杀或者由于程序的原因会异常退出,Manager 进程为了保证服务的稳定性,会重新拉起新的 worker 进程,意思就是 Worker 进程你发生意外“死”了,没关系,我自身不“死”,就可以 fork 千千万万个你。

当然,Master 进程和 Manager 进程我们是不怎么关心的,从前面两篇文章我们了解到,真正实现业务逻辑,是在 worker/task 进程内完成的。

再来一张图梳理下 Manager 进程和 Worker/Task 进程的关系。

https://file.lulublog.cn/images/3/2022/07/V36Qjw39mJHhJ9mAQFFfUbtbCczWQ9.png

D、区分进程

再回到我们开篇抛出的的 5 个进程的问题,ps 的结果简直一模一样,有没有办法能区分这 5 个进程哪个是哪个呢?

有同学要说啦,既然各个进程之间存在父子关系,那我们就可以通过 linux 的 pstree 命令查看结果。

pstree | grep server-process

 | |   \-+= 02548 manks php server-process.php

 | |     \-+- 02549 manks php server-process.php

 | |       |--- 02550 manks php server-process.php

 | |       |--- 02551 manks php server-process.php

 | |       \--- 02552 manks php server-process.php

 |     \--- 02572 manks grep server-process

注:centos 下命令可修改为 pstree -ap | grep server-process

从结果中我们可以看出,进程 id 等于 02548 的进程就是 Master 进程,因为从结构上看就它是“父”嘛,02549 是 Manager 进程,Worker 进程和 Task 进程就是 02550、02551和02552了(每个人的电脑上显示的进程 id 可能不同,但顺序是一致的,依照此模型分析即可)。

我们看到 pstree 命令也只能得到大致结果,而且在事先不知道的情况下,根本无法区分 Worker 进程和 Task 进程。

在 swoole 中,我们可以在各个进程启动和关闭的回调中去解决上面这个问题。各个进程的启动和关闭?那岂不是又要记住主进程、Manager 进程、Worker 进程,二三得六,6 个回调函数?

是的,不过这 6 个是最简单也是最好记的,你实际需要了解的可能还要更多。

Master进程:
    启动:onStart
    关闭:onShutdown
Manager进程:
    启动:onManagerStart
    关闭:onManagerStop
Worker进程:
    启动:onWorkerStart
    关闭:onWorkerStop

提醒:task_worker 也会触发 onWorkerStart 回调。

是不是很好记?那我们就在 server-process.php 中通过上面这几种回调来实现对各个进程名的修改。

$serv->on("start", function ($serv){
    swoole_set_process_name("server-process: master");
});
// 以下回调发生在Manager进程
$serv->on("ManagerStart", function ($serv){
    swoole_set_process_name("server-process: manager");
});
$serv->on("WorkerStart", function ($serv, $workerId){
    if($workerId >= $serv->setting["worker_num"]) {
        swoole_set_process_name("server-process: task");
    } else {
        swoole_set_process_name("server-process: worker");
    }
});
ps aux | grep server-process
root     27546  xxx... server-process: master
root     27547  xxx... server-process: manager
root     27549  xxx... server-process: task worker
root     27550  xxx... server-process: worker
root     27551  xxx... server-process: worker
root     27570  xxx... grep --color=auto simple

运行结果谁是谁一目了然,简直了!

有同学傻眼了,说在 workerStart 回调中写的看不明白,worker 进程和 task 进程怎么区分的?

我来解释一下:在 onWorkerStart 回调中,$workerId 表示的是一个值,这个值的范围是 0~worker_num,worker_num 是我们的对 worker 进程的配置,其中 0~worker_num 表示 worker 进程的标识,包括 0 但不包括worker_num;worker_num~worker_num+task_worker_num 是 task 进程的标识,包括 worker_num 不包括 worker_num+task_worker_num。

按照高中学的区间的知识可能更好理解,以我们案例的配置,workerId 的值的范围就是 [0,2],[0,2) 表示 worker 进程,[2,3) 就表示 task_worker 进程。

swoole 的进程模型很重要,本节掌握不好,后面的理解可能就会有些问题。

补充:我们在 onWorkerStart 的回调中,用了 serv−>setting 去获取配置的 server 信息,在 swoole 中预留了一些 swooleserver 的属性,我们可以在回调函数中访问。比如说我们可以用 serv->connections 属性获取当前 server的所有的连接,再比如我们可以通过 $serv->master_pid 属性获取当前 server 的 主进程 id 等等。

三、进程

3.1、进程创建

new swoole_process()
  • 参数1:mixed $function 子进程创建成功后执行的函数

  • 参数2:$redirect_stdin_stdout 重定向子进程的标准输入和输出。启用此选项后,在进程内 echo 将不是打印屏幕,而是写入到管道。读取键盘输入将变成从管道钟读取。默认为阻塞读取。

  • $create_pipe 是否创建管道。启用

  • $redirect_stdin_stdout 后,此选项将忽略用户参数,强制为 true,如果子进程内没有进程间通信,可以设置为 false

//进程对应的执行函数
function doProcess(swoole_process $worker){
    echo "PID",$worker->pid,"\n";
    sleep(10);
}
//创建进程
$process = new swoole_process("doProcess");
$pid = $process->start();

$process = new swoole_process("doProcess");
$pid = $process->start();

$process = new swoole_process("doProcess");
$pid = $process->start();
//等待结束
swoole_process::wait();

3.2、进程事件

swoole_event_add()
  • 参数1:

    • int $sock,int 文件描述符

    • mixed $read_callback 就是 stream_socket_client/fsockopen 创建的资源

    • sockets 资源,就是 sockets 扩展钟 socket_create 创建的资源,需要在编译时加入 ./configure --enalbe-sockets

  • 参数2:可读取回调函数

$workers = []; //进程数组
$workerNum = 3; //创建进程的数量
//创建进程
for($i=0; $istart(); //启动进程,并获取进程 ID
    $workers[$pid] = $process; //存入进程数组
}
//创建进程执行函数
function doProcess(swoole_process $process){
    $process->write("PID:$process->pid"); //子进程写入信息
    echo "写入信息:$process->pid $process->callback";
}
//添加进程事件,向每一个子进程添加需要执行的动作
foreach($workers as $process){
    swoole_event_add($process->pipe, function($pipe) use($process){
        $data = $process->read(); //读取数据
        echo "接收到:$data \n";
    });
}

3.3、进程队列通信

string swoole_process->pop(int $maxsize = 8192);
bool swoole_process->push(string $data);
array swoole_process::wait(bool $blocking = true);
$workers = []; //进程仓亏
$workerNum = 3; //最多进程数
//批量创建进程
for($i=0; $iuseQueue(); //开启队列,类似于全局函数
    $pid = $process->start();
    $workers[$pid] = $process;
}
//进程执行函数
function doProcess(swoole_process $process){
    $recv = $process->pop(); //默认 8192
    echo "从主进程获取到数据:$recv \n";
    sleep(5);
    $process->exit(0);
}
//主进程向子进程添加数据
foreach($workers as $pid => $process){
   $process->push("Hello 子进程 $pid \n");
}
//等待子进程结束,回收资源
for($i=0; $i<$workerNum; $i++){
    $ret = swoole_process::wait(); //等待执行完成
    $pid = $ret['pid'];
    unset($workers[$pid]);
    echo "子进程退出 $pid \n";
}

3.4、进程信号触发器

bool swoole_process::signal(int $signo, callable $callback);
function swoole_process::alarm(int $interval_usec, int $type = ITIMER_REAL) : bool
//触发函数,异步执行,达到 10 次停止
swoole_process::signal(SIGALRM, function (){
    static $i = 0;
    echo "$i \n";
    $i++;
    if($i>10){
        swoole_process::alarm(-1); //清除定时器
    }
});
//定时器信号
swoole_process::alarm(100 * 1000);

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开微信扫一扫,即可进行扫码打赏哦

阅读 775