workerman如何通过reusePort解决Linux内核进程惊群问题?

惊群

惊群是什么?

  • 多个进程/线程存在等待同一个事件的行为,当该事件被触发时,所有关注等待该事件的进程/线程都被系统内核唤醒,而最终往往只有一个进程/线程获取到了该事件,其他进程/线程因为未获取到事件而重新恢复到等待挂起状态。
  • 这里通常指的是accept惊群(Linux内核已优化)。
  • 当然epoll也存在惊群(linux内核已优化)。

惊群会如何?

  • 进程/线程的切换需要系统内核进行调度,涉及到上下文切换等。
  • 惊群会让所有该事件的等待进程被唤起,从而带来大量无效的调度。
  • 这样的无效调度会浪费系统资源,导致CPU飙高等问题。

感谢 @chaz6chez

workerman的惊群

workerman使用pcntl_fork()来实现master/worker的多进程模型,每个worker进程通过使用stream_socket_server()函数来创建socket,由于fork创建的worker进程具备亲缘关系,所以不同的worker进程可以对相同的端口监听;不同worker进程监听相同的socket,在该socket存在事件时,所有监听该socketworker进程会被唤醒,所有worker进程对socket资源进行抢占式处理,但最终只有一个worker进程可以对socket进行accept;在这个过程中就存在n-1worker进程是无效调度的,仅仅只是被唤起了然后抢占失败并再次入眠。

reuseport 简介

reuseport 是什么?

SO_REUSEPORT (reuseport) 是网络的一个选项设置

  1. 它能开启内核功能:网络链接分配 内核负载均衡,该功能允许多个进程/线程 bind/listen 相同的 IP/PORT,提升了新链接的分配性能。
  2. reuseport 也是内核解决 惊群问题 的优秀方案:每个进程可以 bind/listen 相同的 IP/PORT,相当于每个进程拥有独立的 listen socket 的完全队列,避免了共享 listen socket 的资源争抢,提升了并发的吞吐。内核通过哈希算法,将新链接相对均衡地分配到各个开启了 reuseport 属性的进程,所以资源的负载均衡得到解决。

reuseport 原理

Socket options
    The socket options listed below can be set by using setsockopt(2)
    and read with getsockopt(2with the socket level set to
    SOL_SOCKET for all sockets.  Unless otherwise noted, optval is a
    pointer to an int.
...
    SO_REUSEPORT (since Linux 3.9)
                Permits multiple AF_INET or AF_INET6 sockets to be bound
                to an identical socket address.  This option must be set
                on each socket (including the first socket) prior to
                calling bind(2) on the socket.  To prevent port hijacking,
                all of the processes binding to the same address must have
                the same effective UID.  This option can be employed with
                both TCP and UDP sockets.
 
                For TCP sockets, this option allows accept(2) load
                distribution in a multi-threaded server to be improved by
                using a distinct listener socket for each thread.  This
                provides improved load distribution as compared to
                traditional techniques such using a single accept(2)ing
                thread that distributes connections, or having multiple
                threads that compete to accept(2from the same socket.
 
                For UDP sockets, the use of this option can provide better
                distribution of incoming datagrams to multiple processes
                (or threads) as compared to the traditional technique of
                having multiple processes compete to receive datagrams on
                the same socket.
  • 允许多个线程/进程绑定到相同ip:port的套接字地址;这个选项必须设置在socket上调用 bind(2)方法之前;此外,为了防止端口劫持, 绑定到同一地址的所有进程必须具有 相同的有效 UID。

  • 对于 TCP 套接字,此选项允许 accept(2) 加载 通过以下方式改进多线程服务器中的分布 为每个线程使用不同的侦听器套接字。这个 提供改进的负载分配相比传统方式更好,例如:使用单个 accept(2)ing 分配连接的线程,或具有多个 竞争从同一个socket来accept(2)的线程。

reuseport 解决了什么问题?

设置当前worker是否开启监听端口复用(socketSO_REUSEPORT选项)。开启监听端口复用后允许多个无亲缘关系的进程监听相同的端口,并且由系统内核做负载均衡,决定将socket连接交给哪个进程处理,避免了惊群效应,可以提升多进程短连接应用的性能。官方文档:https://www.workerman.net/doc/workerman/worker/reuse-port.html

服务端程序通常通过监听服务器上的某个端口号,来接收客户端的请求。在Linux中,服务器网卡 + 端口号被抽象成了一个 Socket

为了提升性能,一般的服务端程序在运行时都有多个进程(俗称 Worker)监听同一个 Socket,在没有客户端连接到来的时候,这些Worker是处于挂起状态的,不消耗CPU资源。

如果某一刻有一个客户端连接到来,Linux 内核就会同时唤醒这些 Worker,让他们竞争去处理这个连接,如图:

workerman如何通过reusePort解决Linux内核进程惊群问题?

图片来源:http://io.upyun.com/2015/07/20/Nginx-socket-sharding/

结果只有一个 Worker 可以获得处理这个连接的机会,其他Worker在竞争失败后继续回到挂起状态。唤醒 Worker 的过程是要消耗CPU资源的,Worker 数量越多,消耗的 CPU 资源就越多,造成了资源的浪费。这就是常说的 惊群效应

为什么不每次只唤醒一个Worker呢?很遗憾,Linux内核并没有这样的功能。在 Linux 3.9 及以后的版本,加入 reuseport 特性。这个特性有什么用呢?

在有 reuseport 之前,一个端口号只能被一个 Socket 监听,有了 reuseport 之后,这个限制就被打破了:一个端口号可以被多个 Socket 同时监听。

Linux 内核没法做到一次只唤醒一个 Worker,但是,内核可以做到将客户端连接均匀地发送到监听统一端口的一群 Socket 上。这样一来,服务端程序就可以这么设计:

workerman如何通过reusePort解决Linux内核进程惊群问题?

图片来源:http://io.upyun.com/2015/07/20/nginx-socket-sharding/

在上图中,有多个 listener 共同 bind/listen 相同的 IP/PORT,也就是说每个进程/线程有一个独立的 listener,相当于每个进程/线程独享一个 listener 的全链接队列,不需要多个进程/线程竞争某个公共资源,能充分利用多核,减少竞争的资源消耗,效率自然提高了。

workerman如何通过reusePort解决Linux内核进程惊群问题?

结论:如果你的 Linux 内核版本是 3.9 及以上的话,那么在使用 Workerman 时,可以将 reusePort 设置为 true 提升程序运行效率。

workerman 如何利用 reuseport

虽然你只要在 workerman 中把 reusePort 设置为 true,就能享受到 Linux 的这个高级特性。但 workerman 的源码中,并不只是开启一个内核参数那么简单。

Worker 类是 workerman 里最主要的类,其中有个 listen() 函数。listen() 函数的作用就是在当前进程创建一个InternetUnix域服务器Socket套接字并开始监听请求。

protected function listen()
{
    ...
    if (!$this->_mainSocket) {
         // SO_REUSEPORT.
         if ($this->reusePort) {
               stream_context_set_option($this->_context, 'socket''so_reuseport', 1);
         }
        // Create an Internet or Unix domain server socket.
        $this->_mainSocket = stream_socket_server($local_socket$errno$errmsg$flags$this->_context);
        ...
    }
    ...
}

[1] 当 reusePortfalse 时,主进程在创建 Worker 之前就调用了 listen() 函数:

protected function initWorkers() {
    ....
    // Listen.
    if (!$worker->reusePort) {
        $worker->listen();
    }
    ....
}

随后主进程通过 pcntl_fork() 创建 Worker。pcntl_fork() 有个特性:创建出来的子进程(Worker)中的变量都是父进程复制而来的,包括父进程创建的 $_mainSocket。所以,当 reusePortfalse 时,所有的 Worker 都复制父进程的 $_mainSocket,也即共用一个 Socket

[2] 当 reusePorttrue 时,情况就不同了。主进程在创建 Worker 前不会调用 listen(),而是在创建完 Worker 后由每个 Worker 自行发起 listen() 调用。

/**
* Fork one worker process.
* @param self $worker
*/
protected static function forkOneWorkerForLinux($worker
{
    ...
    $pid = pcntl_fork();
    if ($pid === 0) {
        ...
        $worker->run();
        ...
    }
    ...
}

/**
* Run worker instance.
* @return void
*/
public function run()
{
   $this->listen();
   ...
}

/**
* Run worker instance.
* @return void
*/
public function listen()
{
   ...
   // SO_REUSEPORT.
   if ($this->reusePort) {
         stream_context_set_option($this->_context, 'socket''so_reuseport', 1);
   }
   ...
}

实际应用

不开启

现象

有时候我们通过命令php start.php status 看到,请求被集中在特定的某些进程中处理,其它进程完全空闲。

workerman如何通过reusePort解决Linux内核进程惊群问题?

抢占机制

workerman多个进程获取连接的方式默认是抢占式的,也就是说当客户端有连接发起时,所有空闲的进程都有机会去获取这个连接,快者先得。

到底谁快,是由操作系统内核调度决定的。操作系统可能会优先选取最近一次使用的进程获得cpu使用权,因为cpu寄存器里可能还存在上个进程的上下文信息,这可以减少上下文切换开销。

所以当业务足够快的时候或者压测过程中,更容易出现连接集中被某些进程处理的情况,因为这个策略可以避免频繁的进程切换,性能往往是最优的,并不是什么问题。

不平均正常,并不是请求数平均是最优的,系统会在尽量减少进程切换开销的情况下处理请求。

开启

现象

有时候我们通过命令php start.php status 看到,所有的请求被平均的方式分配给所有进程。

workerman如何通过reusePort解决Linux内核进程惊群问题?

轮询机制

workerman可以通过设置 $worker->reusePort = true; 的方式将获取连接的方式改为轮询的方式,轮询的方式内核会将连接近似平均的方式分配给所有进程,这样所有的进程将会一起处理请求。

误区

很多开发者认为所有进程都参与请求处理性能越好,实际上不一定。当业务足够简单时,参与处理请求的进程数越趋近于cpu核心数服务器吞吐量越高。

例如4核服务器,进程数设置为4时,helloworld压测QPS一般是最高的。如果参与处理的进程数超过cpu核数太多,进程上下文开销越大,性能反而越差。而一般带数据库业务时,进程数设置为cpu核数的3倍-6倍性能可能会更好。

参考文献

  1. 请求集中在某些进程:https://www.workerman.net/doc/workerman/faq/requests-concentrated-in-certain-processes.html
  2. Workerman的reusePort属性详解:https://www.jianshu.com/p/97cc8c52d47a
  3. Workerman监听接口listen:https://www.workerman.net/doc/workerman/worker/listen.html
  4. Workerman Worker类属性 reusePort:https://www.workerman.net/doc/workerman/worker/reuse-port.html


原文始发于微信公众号(开源技术小栈):workerman如何通过reusePort解决Linux内核进程惊群问题?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247773.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!