php-fpm与swoole

php-fpm与swoole

   php本身是单进程单线程的,那么它是怎么解决并发问题的呢?这就涉及到本文将要提及的php-fpm和swoole

   一、php-fpm(FastCGI 进程管理器)

   php-fpm的生命周期如图:

   

    它的工作原理大概为:

    php-fpm启动->生成n个fast-cgi协议处理进程->监听一个端口等待任务用户请求->web服务器接收请求->请求转发给php-fpm->php-fpm交给一个空闲进程处理->进程处理完成->php-fpm返回给web服务器->web服务器接收数据->返回给用户

    nginx+php-fpm 就是用的以上的方法。

    优点:

    nginx+php-fpm场景下php的单线程并不妨碍处理并发。当并发来了fpm会启动更多的worker去处理。并没有发生阻塞。

    缺点:

   1)这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少。操作系统可以创建的进程数量是有限的。

   2)启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%可以忽略不计,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到 CPU 的百分之几十甚至 100%。

    另外有一些场景多进程模型无法解决,比如即时聊天程序(IM),一台服务器要同时维持上万甚至几十万上百万的连接(经典的C10K问题),多进程模型就力不从心了。

    还有一种场景也是多进程模型的软肋。通常Web服务器启动100个进程,如果一个请求消耗100ms,100个进程可以提供1000qps,这样的处理能力还是不错的。但是如果请求内要调用外网Http接口,像QQ、微博登录,耗时会很长,一个请求需要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到10qps,这样的处理能力就太差了。

    有没有一种技术可以在一个进程内处理所有并发IO呢?答案是有,这就是IO复用技术。

    二、IO复用

     1、IO复用是什么实现的?

     其实IO复用的历史和多进程一样长,Linux很早就提供了 select 系统调用,可以在一个进程内维持1024个连接。后来又加入了poll系统调用,poll做了一些改进,解决了 1024 限制的问题,可以维持任意数量的连接。但select/poll还有一个问题就是,它需要循环检测连接是否有事件。这样问题就来了,如果服务器有100万个连接,在某一时间只有一个连接向服务器发送了数据,select/poll需要做循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源。

    直到Linux 2.6内核提供了新的epoll系统调用,可以维持无限数量的连接,而且无需轮询,这才真正解决了 C10K 问题。现在各种高并发异步IO的服务器程序都是基于epoll实现的,比如Nginx、Node.js、Erlang、Golang。像 Node.js 这样单进程单线程的程序,都可以维持超过1百万TCP连接,全部归功于epoll技术。

    IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket句柄的事件变化

    

    Reactor有4个核心的操作:

    1)add添加socket监听到reactor,可以是listen socket也可以是客户端socket,也可以是管道、eventfd、信号等

    2)set修改事件监听,可以设置监听的类型,如可读、可写。可读很好理解,对于listen socket就是有新客户端连接到来了需要accept。对于客户端连接就是收到数据,需要recv。可写事件比较难理解一些。一个SOCKET是有缓存区的,如果要向客户端连接发送2M的数据,一次性是发不出去的,操作系统默认TCP缓存区只有256K。一次性只能发256K,缓存区满了之后send就会返回EAGAIN错误。这时候就要监听可写事件,在纯异步的编程中,必须去监听可写才能保证send操作是完全非阻塞的。

   3)del从reactor中移除,不再监听事件

   4)callback就是事件发生后对应的处理逻辑,一般在add/set时制定。C语言用函数指针实现,JS可以用匿名函数,PHP可以用匿名函数、对象方法数组、字符串函数名。

   Reactor只是一个事件发生器,实际对socket句柄的操作,如connect/accept、send/recv、close是在callback中完成的。具体编码可参考下面的伪代码:

    

    Reactor模型还可以与多进程、多线程结合起来用,既实现异步非阻塞IO,又利用到多核。目前流行的异步服务器程序都是这样的方式:如

  •     Nginx:多进程Reactor
  •     Nginx+Lua:多进程Reactor+协程
  •     Golang:单线程Reactor+多线程协程
  •     Swoole:多线程Reactor+多进程Worker

     2、知识拓展一下:Redis 为什么使用单进程单线程方式也这么快

      Redis 采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由 C 语言编写。官方提供的数据是可以达到100000+的 qps。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差。

      Redis 快的主要原因有:

  •        完全基于内存
  •        数据结构简单,对数据操作也简单
  •        使用多路 I/O 复用模型

     这里主要围绕第三点采用多路 I/O 复用技术来展开。

    多路 I/O 复用模型

    它是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了 Redis 具有很高的吞吐量。

     三、Swoole

     swoole的进程管理模式与php-fpm并没有本质上的区别。同样还是一个master多个worker进程。但是不同的是一个同步阻塞。一个异步非阻塞。

     1、swoole的生命周期

     如下图:

     

     分析:

     php-fpm在执行代码的时候遇到IO(数据库读写,缓存读写,文件读写)会同步阻塞。后面的任务必须等前面的任务执行完了之后才会接着执行。这种模式的性能是不如swoole的异步执行的。同时因为性能不足还引发了一个更重要的问题。当并发来的时候,进程数达到了最大限制,fpm就会进入等待。当一个进程释放之后才能继续执行代码。

     而swoole因为单个进程执行速度更快,就能更快的释放掉,也就比fpm更不容易等待进程。这样就能比fpm承载更大的QPS。

    2、Swoole的进程模型

    多进程的实现一般会被设计Master-Worker模式,常见的nginx默认的多进程模式也正是如此,当然swoole默认的也是多进程模型

    相比Master-Worker模式,swoole的进程模型可以用Master-Manager-Worker来形容。即在Master-Worker的基础上又增加了一层Manager进程。

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

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

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

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

    说明两点:

    1)主线程的Accept操作,socket服务端经常用accept阻塞

    

 

 

    2)信号处理,信号就相当于一条消息,比如我们经常操作的Ctrl+C其实就是给Master进程的主线程发送一个SIGINT的信号,意思就是你可以终止啦,其实除此之外,信号有很多种。

通常,主线程处理完新的连接后,会将这个连接分配给固定的Reactor线程,并且这个Reactor线程会一直负责监听此socket(socket即套接字,是用来与另一个进程进行跨网络通信的文件,文件可读可写),换句话就是说当此socket可读时,会读取数据,并将该请求分配给worker进程;当此socket可写时,会把数据发送给tcp客户端。

    用一张图清晰的梳理下

    

    3、Manager进程的作用

    1)疑问

    疑问:那swoole为什么不能像Nginx一样,是Master-Worker进程结构的呢?Manager进程的作用是什么呢?

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

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

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

    2)fork操作

    关于fork操作不安全,这里扩展一下:

    在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

    The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

    也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

    这就是多线程中fork所带来的一切问题的根源所在了。

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

    当然,Master进程和Manager进程我们是不怎么关心的,真正实现业务逻辑,是在worker/task进程内完成的。

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

    

   四、协程是什么

    协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配 CPU 时间决定。具体来说,Swoole 的每个Worker进程会存在一个协程调度器来调度协程,协程切换的时机就是遇到 I/O 操作或代码显性切换时,进程内以单线程的形式运行协程,也就意味着一个进程内同一时间只会有一个协程在运行且切换时机明确,也就无需处理像多线程编程下的各种同步锁的问题。

    协程从底层技术角度看实际上还是异步IO Reactor模型,应用层自行实现了任务调度,借助Reactor切换各个当前执行的用户态线程,但用户代码中完全感知不到Reactor的存在。

   作用:节省出进程中因为IO操作而阻塞等待的时间

   

   

   参考链接:

   http://rango.swoole.com/
   http://www.manks.top/swoole-process-model.html
   https://www.jianshu.com/p/64e52f6f4837
   https://www.easyswoole.com/

posted @ 2021-07-04 22:51  欢乐豆123  阅读(1687)  评论(0编辑  收藏  举报