以下请在本公众号里操作
回复「微信群」
加入PHP技术交流群,一起学习交流
回复「商务合作」
即可获取合作方式与客服联系方式。
除了传统的LAMP/LNMP同步开发模式,swoole的异步开发模式是怎么样的呢?
传统web开发模式
PHP web开发采用的方式是LAMP/LNMP架构,即Linux、Nginx,Mysql和PHP。这里以nginx来举例,大致结构为:
当请求进入时,web server
将请求转交给PHP-FPM
,PHP-FPM
是一个进程池架构的FastCGI
服务,内置PHP解释器。
FPM
负责解释执行PHP文件生成响应,最终返回给web server
,展现至前端。PHP文件中实现了许多业务逻辑,包括Mysql
和Nosql
的访问,调用第三方应用等等。
这样的结构php-fpm
和nginx
的配合已经运行得足够好,但是由于php-fpm
本身是同步阻塞进程模型,在请求结束后释放所有的资源(包括框架初始化创建的一系列对象),导致PHP进程“空转”(创建<-->销毁<-->创建)消耗大量的CPU资源,从而导致单机的吞吐能力有限。
每次请求处理的过程都意味着一次PHP文件解析,环境设置等不必要的耗时操作PHP进程处理完即销毁,无法在PHP程序中使用连接池等技术实现性能优化。
Swoole运行模式
针对传统架构的问题,swoole从PHP扩展出发,解决了上述问题,相比于传统架构,Swoole进程模型最大的特点在于其多线程Reactor模式处理网络请求,使得其能轻松应对大量连接。
除此之外的优点还包括:
-
全异步非阻塞,占用资源开销小,程序执行效率高
-
程序运行只解析加载一次PHP文件,避免每次请求的重复加载
使用swoole和传统php开发的缺点
1、更难上手。这要求开发人员对于多进程的运行模式有更清晰的认识
2、更容易内存泄露。在处理全局变量,静态变量的时候一定要小心,这种不会被GC清理的变量会存在整个生命周期中,如果没有正确的处理,很容易消耗完所有的内存。在php-fpm下,php代码执行完内存就会被完全释放。
swoole当中使用容器更有意义
传统的php框架没有常驻内存,因此每次请求进来都需要把用到的类都实例化一次,每次实例化都需要申请内存,当请求处理完之后又需要释放,具体请参看第一点,所以我们可以在server启动的时候就把类实例化预先放到内存中,减入对象的创建时间。
一个简单的bean容器
class BeanFactory{
private static $container=[];
public static function set(string $name,callable $func){
self::$container[$name]=$func;
}
public static function get(string $name){
if(isset(self::$container[$name])){
return (self::$container[$name])();
}
return null;
}
}
Swoole进程结构
Swoole的高效不仅仅于底层使用c编写,他的进程结构模型也使其可以高效的处理业务,我们想要深入学习,并且在实际的场景当中使用必须了解,下面我们先看一下结构图
首先先介绍下swoole的这几种进程分别是干什么的
从这些层级的名字,我们先大概说一下,下面这些层级分别是干什么的,做一个详细的说明。
1、Master
进程:主进程
2、Manger
进程:管理进程
3、Worker
进程:工作进程
4、Task
进程:异步任务工作进程
Master进程
第一层,Master
进程,这个是swoole
的主进程,这个进程是用于处理swoole
的核心事件驱动的,那么在这个进程当中可以看到它拥有一个MainReactor
[线程]以及若干个Reactor
[线程],swoole
所有对于事件的监听都会在这些线程中实现,比如来自客户端的连接,信号处理等。
每一个线程都有自己的用途,下面多每个线程有一个了解
MainReactor(主线程)
主线程会负责监听server socket
,如果有新的连接accept
,主线程会评估每个Reactor
线程的连接数量。将此连接分配给连接数最少的reactor
线程,做一个负载均衡。
Reactor线程组Reactor
线程负责维护客户端机器的TCP连接、处理网络IO、收发数据完全是异步非阻塞的模式。
swoole
的主线程在Accept
新的连接后,会将这个连接分配给一个固定的Reactor
线程,在socket
可读时读取数据,并进行协议解析,将请求投递到Worker
进程。在socket
可写时将数据发送给TCP客户端。
心跳包检测线程(HeartbeatCheck)
Swoole配置了心跳检测之后,心跳包检测线程会在固定时间内对所有之前在线的连接
-
发送检测数据包
-
UDP收包线程(UdpRecv)
-
接收并且处理客户端udp数据包
管理进程Manager
Swoole想要实现最好的性能必须创建出多个工作进程帮助处理任务,但Worker
进程就必须fork
操作,但是fork
操作是不安全的,如果没有管理会出现很多的僵尸进程,进而影响服务器性能,同时worker
进程被误杀或者由于程序的原因会异常退出,为了保证服务的稳定性,需要重新创建worker进程。
Swoole在运行中会创建一个单独的管理进程,所有的worker
进程和task
进程都是从管理进程Fork
出来的。管理进程会监视所有子进程的退出事件,当worker
进程发生致命错误或者运行生命周期结束时,管理进程会回收此进程,并创建新的进程。换句话也就是说,对于worker
、task
进程的创建、回收等操作全权有“保姆”Manager
进程进行管理。
再来一张图梳理下Manager
进程和Worker/Task
进程的关系。
Worker进程worker
进程属于swoole
的主逻辑进程,用户处理客户端的一系列请求,接受由Reactor
线程投递的请求数据包,并执行PHP回调函数处理数据生成响应数据并发给Reactor
线程,由Reactor
线程发送给TCP
客户端可以是异步非阻塞模式,也可以是同步阻塞模式
Task进程taskWorker
进程这一进城是swoole
提供的异步工作进程,这些进程主要用于处理一些耗时较长的同步任务,在worker
进程当中投递过来。
client跟server的交互:
1、client
请求到达 Main Reactor
,Client
实际上是与Master
进程中的某个Reactor
线程发生了连接。
2、Main Reactor
根据Reactor
的情况,将请求注册给对应的Reactor
3、客户端有变化时Reactor
将数据交给worker
来处理
4、worker
处理完毕,通过进程间通信(比如管道、共享内存、消息队列)发给对应的reactor
。
5、reactor
将响应结果发给相应的连接请求处理完成
示意图:
一个更通俗的比喻,假设Server
就是一个工厂,那Reactor
就是销售,接受客户订单。而Worker
就是工人,当销售接到订单后,Worker
去工作生产出客户要的东西。而Task_Worker
可以理解为行政人员,可以帮助Worker
干些杂事,让Worker
专心工作。
进程的绑定事件
Master进程内的回调函数
-
onStart
Server启动在主进程的主线程回调此函数 -
onShutdown
此事件在Server正常结束时发生
Manager进程内的回调函数
-
onManagerStart
当管理进程启动时调用它 -
onManagerStop
当管理进程结束时调用它 -
onWorkerError
当worker/task_worker
进程发生异常后会在Manager
进程内回调此函数
Worker进程内的回调函数
-
onWorkerStart
此事件在Worker
进程/Task
进程启动时发生 -
onWorkerStop
此事件在worker
进程终止时发生。 -
onConnect
有新的连接进入时,在worker
进程中回调 -
onClose
TCP
客户端连接关闭后,在worker
进程中回调此函数 -
onReceive
接收到数据时回调此函数,发生在worker
进程中 -
onRequest
有新的连接进入时,在worker
进程中回调 -
onPacket
接收到UDP
数据包时回调此函数,发生在worker
进程中 -
onFinish
当worker
进程投递的任务在task_worker
中完成时,task
进程会通过finish()
方法将任务处理的结果发送给worker
进程。 -
onWorkerExit
仅在开启reload_async
特性后有效。异步重启特性 -
onPipeMessage
当工作进程收到由sendMessage
发送的管道消息时会触发事件
Task进程内的回调函数
-
onTask
在task_worker
进程内被调用。worker
进程可以使用swoole_server_task
函数向task_worker
进程投递新的任务 -
onWorkerStart
此事件在Worker
进程/Task
进程启动时发生 -
onPipeMessage
当工作进程收到由sendMessage
发送的管道消息时会触发事件
swoole运行模式及热重启
Swoole之所以性能卓越,是因为Swoole减少了每一次请求加载PHP文件以及初始化的开销。
但是这种优势也导致开发者无法像过去一样,修改PHP文件,重新请求,就能获取到新代码的运行结果。
如果需要新代码开始执行,往往需要先关闭服务器然后重启,这样才能使得新文件被加载进内存运行,这样很明显不能满足开发者的需求。幸运的是,Swoole提供了这样的功能。
在swoole中,我们可以向主进程发送各种不同的信号,主进程根据接收到的信号类型做出不同的处理。
比如下面这几个
1、kill -SIGTERM master_pid
终止Swoole
程序,一种优雅的终止信号,会待进程执行完当前程序之后中断,而不是直接干掉进程
2、kill -USR1 master_pid
重启所有的Worker
进程
3、kill -USR2|-12 master_pid
重启所有的Task Worker
进程
当USR1
信号被发送给Master
进程后,Master
进程会将同样的信号通过Manager
进程转发Worker
进程,收到此信号的Worker
进程会在处理完正在执行的逻辑之后,释放进程内存,关闭自己,然后由Manager
进程重启一个新的Worker
进程。新的Worker
进程会占用新的内存空间,重新加载文件。
具体场景:
如果是上线的项目,一台繁忙的后端服务器随时都在处理请求,如果管理员通过kill进程方式来终止/重启服务器程序,可能导致刚好代码执行到一半终止。
这种情况下会产生数据的不一致。如交易系统中,支付逻辑的下一段是发货,假设在支付逻辑之后进程被终止了。会导致用户支付了货币,但并没有发货,后果非常严重。
如何解决?
这个时候我们需要考虑如何平滑重启server的问题了。所谓的平滑重启,也叫“热重启”,就是在不影响用户的情况下重启服务,更新内存中已经加载的php程序代码,从而达到对业务逻辑的更新。
swoole为我们提供了平滑重启机制,我们只需要向swoole_server的主进程发送特定的信号,即可完成对server的重启。
注意事项:
1、更新仅仅只是针对worker进程,也就是写在master
进程跟manger
进程当中更新代码并不生效,也就是说只有在onWorkerStart
回调之后加载的文件,重启才有意义。
在Worker进程启动之前就已经加载到内存中的文件,如果想让它重新生效,只能关闭server再重启。
2、直接写在worker代码当中的逻辑是不会生效的,就算发送了信号也不会,需要通过include方式引入相关的业务逻辑代码才会生效
原文链接:https://www.jb51.net/article/213610.htm
点个赞
再走吧
文章评论