swoole 理解
面向生产环境的 PHP 异步网络通信引擎。 — By Swoole 官网
前置
了解swoole之前,我们需要先知道几个基础概念。
进程
进程是资源分配的最小单元,每个进程都有独立的代码空间和数据空间(进程上下文)。进程间的切换会有较大的开销。
线程
线程属于轻量级进程,是程序的执行者,也就是cpu的最小调度和分派单位,有时候我们给线程也称为进程。一个进程可以有多个线程,同一个进程内的线程,可以并发执行。
区别
- 调度:线程是作为调度和分配的最小单元,进程是资源分配的最小单元。
- 并发性:进程之间可以并发执行,进程中线程之间也可以并发执行,这块需要看是否是多核cpu,因为一个cpu在同一时间只能运行一个线程。
- 资源:进程是拥有资源的一个独立单元,线程不拥有系统的资源,它依赖的是系统分配给进程的资源。
- 系统开销:在创建和销毁进程的时候,系统都需要分配和回收资源,开销明显大于线程。
关系
- 一个线程只能属于一个进程,一个进程至少有n(n>=1)个线程。
- 资源分配给进程,同一进程的所有线程共享该进程内的所有资源。
- cpu上真正运行的是线程。
- 线程执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
进程间通信方式
- 管道(pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。
- 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
- 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。
- 套接字(socket):这是一种更为一般的进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
多线程的缺点
如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。更多的线程需要更多的内存空间。
线程的中止需要考虑其对程序运行的影响。
多线程与多进程
- 多进程是指操作系统能同时运行多个应用(程序)。
- 多线程是指在同一程序中有多个顺序流在并发执行。
- 子进程和父进程有不同的代码和数据空间,
- 而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.
主线程
程序启动时,一个线程立刻运行,该线程通常称为程序的主线程。
主线程的重要性体现在两个方面:
- 它是产生其他子线程的线程。
- 通常它必须最后完成执行,因为它执行各种关闭动作。
协程
首先协程的最简定义是用户态线程,它不由操作系统而是由用户创建,跑在单个线程(核心)上,比进程或是线程都更加轻量化,通常创建它只有内存消耗。
协程比较抽象 它是程序内部的一定调度机制;是轻量级线程, 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的。是属于线程,协程是在线程里执行的。它的调度是用户手动切换的,所以又叫用户空间线程。
协程的调度策略是:协作式调度。
协程与线程区别
Swoole的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的。这与线程不同,多个线程会被操作系统调度到多个CPU并行执行。
协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核cpu的(swoole的协程没法利用多核,但是go的goroutine可以利用多核cpu,所以说swoole的协程是伪协程:) ),想利用多核 cpu 需要依赖 Swoole 的多进程模型。
一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞IO操作时会挂起,底层调度器会进入事件循环。当有IO完成事件时,底层调度器恢复事件对应的协程的执行。
事件驱动
所谓事件驱动,简单地说就是你点什么按钮(即产生什么事件),电脑执行什么操作(即调用什么函数)。当然事件也不仅限于用户的操作. 事件驱动的核心自然是事件。从事件角度说,事件驱动程序的基本结构是由一个事件收集器、一个事件发送器和一个事件处理器组成。事件收集器专门负责收集所有事件,包括来自用户的(如鼠标、键盘事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。事件发送器负责将收集器收集到的事件分发到目标对象中。事件处理器做具体的事件响应工作,它往往要到实现阶段才完全确定。对于框架的使用者来说,他们唯一能够看到的是事件处理器。这也是他们所关心的内容。
事件驱动编程通常只是用一个执行过程,CPU之间不是并发的,在处理多任务的时候,事件驱动编程是使用协作式处理任务,而不是多线程的抢占式。事件驱动简洁易用,只需要注册感兴趣的事件,在回调中设计逻辑就可以了。在调用的过程中,事件循环器(Event Loop)在等待事件的发生,跟着调用处理器。事件处理器不是抢占式的,处理器一般只有很短的生命周期。
事件驱动编程的代码核心就是事件循环器,在Linux下推荐使用epoll实现,在其它没有epoll 的系统上可以使用kqueue/ports/poll/select实现。事件循环器(Event Loop)是一个程序结构,用于等待和发送消息和事件。
swoole 架构模型
首先swoole是一个c和c++编写的一个php扩展程序。是基于事件的高性能异步和协程并行的通信引擎。
swoole是一个多进程模型的框架,当启动一个swoole进程时,一共会创建2+n+m个进程。n为worker进程数,m为tashWorker进程数,一个master进程和一个manager进程。
Master进程
当我们运行启动 Swoole 的 PHP 脚本时,首先会创建该进程(它是整个应用的 root 进程),然后由该进程 fork 出 Reactor 线程和 Manager 进程。
Rector线程
Reactor 是包含在 Master 进程中的多线程程序,用来处理 TCP 连接和数据收发(异步非阻塞方式)。Reactor 主线程在 Accept 新的连接后,会将这个连接分配给一个固定的 Reactor 线程,并由这个线程负责监听此 socket。在 socket 可读时读取数据,并进行协议解析,将请求投递到 Worker 进程;在 socket 可写时将数据发送给 TCP 客户端。
Manager进程
Manager 进程负责 fork 并维护多个 Worker 子进程。当有 Worker 子进程中止时,Manager 负责回收并创建新的 Worker 子进程,以便保持 Worker 进程总数不变;当服务器关闭时,Manager 将发送信号给所有 Worker 子进程,通知其关闭服务。
Work进程
以多进程方式运行,每个子进程负责接受由 Reactor 线程投递的请求数据包,并执行 PHP 回调函数处理数据,然后生成响应数据并发给 Reactor 线程,由 Reactor 线程发送给 TCP 客户端。所有请求的处理逻辑都是在 Worker 子进程中完成,这是我们编写业务代码时真正要关心的部分。
TaskWorker 进程
功能和 Worker 进程类似,同样以多进程方式运行,但仅用于任务分发,当 Worker 进程将任务异步分发到任务队列时,Task Worker 负责从队列中消费这些任务(同步阻塞方式处理),处理完成后将结果返回给 Worker 进程。
Swoole 官方对 Reactor、Worker、Task Worker有一个形象的比喻,如果把基于 Swoole 的 Web 服务器比作一个工厂,那么 Reactor 就是这个工厂的销售员,Worker 是负责生产的工人,销售员负责接订单,然后交给工人生产,而 Task Worker 可以理解为行政人员,负责提工人处理生产以外的杂事,比如订盒饭、收快递,让工人可以安心生产。
再给一个详细的进程关系图:
swoole 运行机制
用代码示例:
初始化:
1 |
|
服务启动:
1 |
|
至此Swoole 完全的接管了php的运行,并且监听相应端口并当发生事件时去执行开发者自定义的事件回调。
swoole是基于事件驱动的,每有一个事件时,都会有一个事件回调,也就是函数回调:
- 当 Master 主进程启动或关闭时会触发下面这两个回调函数:
- onStart
- onShutdown
- 而 Manager 管理进程启动或关闭时会触发下面这两个回调函数:
- onManagerStart
- onManagerStop
- Worker 进程的生命周期中,有多个回调函数:
- onWorkerStart:Worker 进程启动时
- onWorkerStop: Worker 进程关闭时
- onConnect:连接建立时
- onClose:连接关闭时
- onReceive:收到请求数据时
- onFinish:投递的任务处理完成时
- Task Worker 进程也有两个回调函数,分别在
- onTask:由新任务投递过来时
- onWorkerStart:Task Worker 进程启动时也会触发
swoole 生命周期
php-fmp中的生命周期
php-fmp是一种多进程模型,主要由master进程以及work进程组成,所有的cgi请求都会交由work进程处理,Master进程主要维护worker进程。而worker进程的工作方式是抢占/竞争的方式,当一个accept请求过来的时候,谁先拿到算谁的,拿到后转化为FastCGIRquest,交由脚本处理。
master process
加载fpm配置,并创建和维护work进程的数量。
worker pool
真正的处理请求的进程,由work process创建,同时会创建许多work线程,。监听对应的端口并Accept请求。拿到请求后会去执行对应的php脚本,然后返回相应的相应。
php-fmp生命周期如下:
php-fpm收到请求后会分配一个work进程去处理这条请求,而work会去读取并执行.php文件(在通常情基于框架的开发中,这个.php文件可能是index.php)。也就是说在传统模式中,每个请求都是独立在自己的进程中执行的,因为进程是隔离的而php-fpm又是同步阻塞的,所以我们可以很好的清楚和了解是谁在什么时候创建了变量、修改了变量、销毁了变量。
swoole 对象的生命周期
开发swoole程序与普通LAMP下编程有本质区别。在传统的Web编程中,PHP程序员只需要关注request到达,request结束即可。而在swoole程序中程序员可以操控更大范围,变量/对象可以有四种生存周期。
程序全局期
在swoole_server->start之前就创建好的对象,我们称之为程序全局生命周期。这些变量在程序启动后就会一直存在,直到整个程序结束运行才会销毁。
有一些服务器程序可能会连续运行数月甚至数年才会关闭/重启,那么程序全局期的对象在这段时间持续驻留在内存中的。程序全局对象所占用的内存是Worker进程间共享的,不会额外占用内存。
这部分内存会在写时分离(COW),在Worker进程内对这些对象进行写操作时,会自动从共享内存中分离,变为进程全局对象。
程序全局期include/require的代码,必须在整个程序shutdown时才会释放,reload无效
进程全局期
swoole拥有进程生命周期控制的机制,一个Worker子进程处理的请求数超过max_request配置后,就会自动销毁。Worker进程启动后创建的对象(onWorkerStart中创建的对象),在这个子进程存活周期之内,是常驻内存的。onConnect/onReceive/onClose 中都可以去访问它。
进程全局对象所占用的内存是在当前子进程内存堆的,并非共享内存。对此对象的修改仅在当前Worker进程中有效
进程期include/require的文件,在reload后就会重新加载
会话期
onConnect到onClose是一次TCP的会话周期,http keep-alive时,一个连接可能会有多个request。 http是无状态的,一个用户可能也不止一个连接,可以通过创建一个session来关联同一个用户的不同请求。
请求期
请求期就是指一个完整的请求发来,也就是onReceive收到请求开始处理,直到返回结果发送response。这个周期所创建的对象,会在请求完成后销毁。
swoole中请求期对象与普通PHP程序中的对象就是一样的。请求到来时创建,请求结束后销毁。
总结
在Swoole中,一个work进程处理完请求后并不会销毁(甚至可能同时处理多个请求),所以务必要明确你创建的变量的生命周期,以防止出现逻辑上的问题。
Swoole Vs PHP-FPM
PHP-FPM 位于 SAPI 层,PHP 底层在接收到来自 Nginx 转发过来的 PHP 请求时,会将其交给某个空闲的 PHP-FPM 进程来处理,PHP-FPM 进程会在启动阶段设置 HTTP 环境变量,然后通过 PHP 核心代码初始化所有已经启用的 PHP 模块(即扩展),并对此次请求上下文进行初始化,完成,这些操作后再调用 Zend 引擎来编译并执行业务逻辑代码,具体的代码执行流程如下:
Zend 引擎会检查 OpCode 缓存,如果代码片段已经缓存,则从缓存中读取并执行,否则还要编译成 OpCode 并缓存后才能执行。
代码执行完成后,会将处理结果打印或着发送 HTTP 响应给客户端,然后 PHP 底层代码会执行请求关闭及模块关闭函数进行后续清理工作,最后再回到 SAPI 层,调用 PHP-FPM 对应的关闭函数,从而完成此次请求的所有流程。
这个过程周而复始,每次用户有新请求过来都会从头执行一遍,所有的环境初始化、模块初始化、请求初始化以及 php 应用的启动过程,乃至后续请求关闭、模块关闭、PHP-FPM 关闭,如果 Redis、MySQL 之类的网络请求没有连接池,那么每次新请求过来,所有的连接操作也要重新建立,所以传统模式下的 PHP 应用性能表现一直为人所诟病,尽管 Nginx + PHP-FPM 模式已经大大优于基于 Apache 运行的 PHP 应用了。
那我们能不能优化这个请求处理流程呢?比如把环境初始化、模块初始化、请求初始化、PHP 应用的启动过程只执行一次,然后后面过来的请求复用上一次初始化的 PHP 环境?此外,对于 Redis、MySQL 这些耗时的网络连接以连接池的方式管理起来?事实上,基于 Swoole 就可以完成这些优化,并且我们还可以基于其提供的协程功能实现并发编程,使得在 PHP 中也可以轻松实现异步并发编程,不过关于 PHP 动态语言执行时的性能优化(边解释边执行)这一点需要 PHP 底层开发组去优化,毕竟动态语言有利有弊,不可能又要性能,又要编码灵活性。