Workerman 原理分析: Master-Workers 模型

编辑于 2016-12-29

* 移动设备下, 可左滑手指以查看较宽代码

master-workers

高并发框架 Workerman, 其所用的 Master-Workers模型被广泛用于 Nginx 等高性能服务器.

又有了些积累, 加上有些时间没写博客了, 来一波.

介绍

这是一个可跨协议, 跨底层套接字接口的框架. 之前用过, 但对原理很模糊. 虽然是纯 PHP 写的, 但其中的技术主要来源于操作系统.

有很多服务器模型, 例如:

  • 单进程阻塞 accept 模型

    系统受到数据后返回给服务进程, 然后阻塞. 每次只能处理一个连接, 不能利用多处理器, 性能极其低下.

  • 单进程 select/epoll... 模型

    多路复用, 即每次可处理多个连接或请求, 然后阻塞在 select/epoll... 系统调用. 亦不能利用多处理器.

  • 多进程 Master-Workers 模型

    Master 作为监控进程, Workers 处理网络 I/O 和计算. 充分利用处理器, 支持高并发. 有惊群效应.

惊群效应, 就是当多个 Workers 进程监听一个连接, 当连接过来, 系统会唤醒所有休眠的 Workers 进程, 然后其中只有一个能成功接收 (accept) 连接, 并重新休眠其他进程, 造成不必要的性能损失. 有一种解决办法是, 在 Workers 进程接收连接之前先加锁互斥, 以确保只有一个空闲进程能接受此连接. 当然加锁后要尽快释放锁, 以免其他进程等待.

在 Linux 2.6 之后, 惊群效应得到了缓解.

Workerman 并没有处理惊群效应, 因为它造成的性能损失被认为很小.

如何使用

先看下怎么基于它构建高性能的 TCP 服务器:

use Workerman\Worker;

$tcp_worker = new Worker("tcp://0.0.0.0:1234");

// 4 个子进程
$tcp_worker->count = 4;

// 连接到来时
$tcp_worker->onConnect = function($connection) {};

// 收到数据时
$tcp_worker->onMessage = function($connection, $data) {};

// 连接关闭时
$tcp_worker->onClose = function($connection) {};

Worker::runAll();

框架的目录结构也很简单:

.
├── Connection
├── Events
├── Lib
└── Protocols
│   └── Http
├── WebServer.php
└── Worker.php

Events 目录下放的是底层的网络事件接口, 如select或者libevent (一种跨平台的网络库).

Protocols 目录下放的框架支持的协议. 你也可以自定义协议.

WebServer.php 是 Web 服务器的实现, 基于 Worker.php 中的 Worker 类.

Worker.php 声明 Worker 类, 通过它调用以上的代码文件.

启动过程

Worker 类实现了几乎所有的操作, 首先在其构造方法里面初始化了一些变量如套接字名 (socket_name), 子进程信息表等. 然后使用了stream_context_create()来创建流(stream)的上下文环境, 即可以为它设置一些选项(例如可为网络流设置超时时间, 为 HTTP 流设置 POST 参数等).

public function __construct($socket_name = '', $context_option = array())
{
    ...
    $this->_context = stream_context_create($context_option);
    ...
}

流是对「连续字节流」的一种抽象, 其概念源于 UNIX 中管道 (pipe) 的概念. 在 UNIX 中, 管道是一条不间断的字节流, 用来实现程序或进程间的通信, 或读写外围设备, 文件, 数据包等. 根据流的方向又可以分为输入流和输出流, 同时可以在其外围再套上其它流, 比如缓冲流, 这样就可以得到更多流处理方法.

PHP 里的流和 Java 里的流实际上是同一个概念, 只是简单了一点. 如果有 Java 基础, 对于 PHP 里的流就更容易理解了. 其实 PHP 里的许多高级特性, 比如 SPL, 异常, 过滤器等都参考了 Java 的实现, 在理念和原理上同出一辙.

上面解释了应用层执行 new Worker() 时所执行的构造方法. 接着, 应用层进行一些设置, 最后执行 runAll(). 看看这里面有什么:

// 省略了不重要的
public static function runAll()
{
    // 初始化工作进程 id 表等
    self::init();
    // 后台化进程
    self::daemonize();
    // 为 Worker 进程指定名称等, 并监听 socket
    self::initWorkers();
    // 注册信号处理器, 以接收 stop 等指令
    self::installSignal();
    // 克隆工作进程, 继承 socket 监听并处理客户端请求
    self::forkWorkers();
    // 监控 worker 进程退出信号
    self::monitorWorkers();
}

这个方法很是酸爽, 依次看即可:

  • daemonize

protected static function daemonize()
{
    ...
    $pid = pcntl_fork();
    if (-1 === $pid) {
        throw new Exception('fork fail');
    } elseif ($pid > 0) {
        exit(0);
    }
    ...
}

作为守护进程运行时, Master 进程直接克隆 (fork) 出子进程, 然后退出父进程以后台化.

  • initWorkers

protected static function initWorkers()
{
    ...
    $worker->listen();
    ...
}

public function listen()
{
    ...
    // Get the application layer communication protocol and listening address.
    $this->protocol = '\\Protocols\\' . $scheme;
    ...    
    $local_socket = $this->transport . ":" . $address;
    ...
    // Create an Internet or Unix domain server socket.
    $this->_mainSocket = 
    stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context);

    // Non blocking.
    stream_set_blocking($this->_mainSocket, 0);
    ...
}

initWorkers 调用了 listen 方法, 它先从 protocol 目录下定位应用层使用的协议, 达到兼容多种协议的目的. 当然可以自己定义通信协议. 然后创建了一个网络套接字, $this->_context 是构造方法中创建的流上下文. 接着, 框架把此网络套接字流设置为非阻塞 (Non-blocking)的. 即受到套接字流请求后不阻塞进程, 从而进行其他操作, 可提高 CPU 利用率.

  • installSignal

protected static function installSignal()
{
    // stop
    pcntl_signal(SIGINT, array('\Workerman\Worker', 'signalHandler'), false);
    // reload
    pcntl_signal(SIGUSR1, array('\Workerman\Worker', 'signalHandler'), false);
    // status
    pcntl_signal(SIGUSR2, array('\Workerman\Worker', 'signalHandler'), false);
    // ignore
    pcntl_signal(SIGPIPE, SIG_IGN, false);
}

注册 UNIX 信号处理器, 可接受进程主动发出的控制信号如 stop, start 等, 也可接收进程意外的退出信号, 用于后面的子进程监控.

  • forkWorkers

在这个方法里克隆 (fork)出所有子进程, 父进程把子进程的 id 放入 pid 列表, 子进程则自动继承 Master 进程创建的监听 socket, 使得 Worker 子进程能够独立的接受并处理客户端的连接, 然后子进程调用 run() 方法. run() 方法从所有支持的 Events 里(即框架的 Events 文件夹内)寻找底层网络接口, 优先使用 libevent, 如果没有此 PHP 扩展则使用 select 等系统调用. 然后使用 add() 方法把套接字和回调方法传入 Events 层, 最后开始事件循环, 处理连接.

# Events 层收到数据后调用:
call_user_func($this->onMessage, $connection, $recv_buffer);

# 于是在应用层可以这样接受数据:
$tcp_worker->onMessage = function($connection, $data) {};

可见用户层的回调函数运行在子进程.

  • monitorWorkers

while (1) {
    pcntl_signal_dispatch();
    // Suspends execution of the current process
    // until a child has exited, or until a signal is delivered
    $pid = pcntl_wait($status, WUNTRACED);
    // Calls signal handlers for pending signals again.
    pcntl_signal_dispatch();
    ...
}

Master 进程进入监听信号的逻辑中, 监听 Worker 进程退出信号( Worker 进程退出后, 系统会自动向 master 进程发送一个 SIGHCLD 信号, mater 进程会重新创建子进程, 将缺失的子进程补上), Master 进程还会监听停止信号(SIGINT) 和平滑重启服务信号(SIGHUP). 当没有信号时, Master 进程会被阻塞直到信号到达.

Windows 平台下没有 fork 机制, 信号的使用也不同, 此框架和基于 Master-Workers 模型的软件(如 Nginx)没有在 Windows 下得到完整支持, 便是此原因.

此外还有Leader-Followers模型,其区别在于:一个 Leader 监听网络数据,Followers 成为新的 Leader,旧 Leader 继续处理网络事件. 其优点在于:没有 Workers 之间的互斥争用,没有把 IO 和 Workers 的计算给分离开,增加 CPU 缓存命中率.