php-fpm与swoole

php-fpm与swoole

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

一、php-fpm

PHP-FPM(FastCGI 进程管理器)

它的工作原理大概为:

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复用

     其实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个核心的操作:

 

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

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

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

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

3、知识拓展一下

1)协程是什么

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

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 具有很高的吞吐量。

  1. 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即套接字,是用来与另一个进程进行跨网络通信的文件,文件可读可写),换句话就是说当此socket可读时,会读取数据,并将该请求分配给worker进程,这也就解释了我们在swoole初识讲解worker进程内的回调onReceive的第三个参数$fromId的含义;当此socket可写时,会把数据发送给tcp客户端。

 

用一张图清晰的梳理下

 

 

那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进程进行管理。

 

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

 

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

 

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

 

上一篇:Mac brew 启动php


下一篇:如何搭建一对一直播PHP直播系统源码的流程