16

Swoole高效跟传统的web开发有什么区别?

 4 years ago
source link: http://www.cnblogs.com/a609251438/p/12031529.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、swoole的运行模式

Swoole高效跟传统的web开发有什么区别,除了传统的LAMP/LNMP同步开发模式,swoole的异步开发模式是怎么样的。

1.1、传统web开发模式

PHP web开发采用的方式是LAMP/LNMP架构,即Linux、Nginx,Mysql和PHP。这里以nginx来举例,大致结构为:

7zQ7baV.jpg!web

当请求进入时,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程序中使用连接池等技术实现性能优化。

1.2、Swoole运行模式

针对传统架构的问题,swoole从PHP扩展出发,解决了上述问题,对于swoole的进程模型,我们刚刚已经学过了。

相比于传统架构,Swoole进程模型最大的特点在于其多线程Reactor模式处理网络请求,使得其能轻松应对大量连接。

除此之外的优点还包括:

全异步非阻塞,占用资源开销小,程序执行效率高

程序运行只解析加载一次PHP文件,避免每次请求的重复加载

1.3、使用swoole和传统php开发的缺点

1、更难上手。这要求开发人员对于多进程的运行模式有更清晰的认识

2、更容易内存泄露。在处理全局变量,静态变量的时候一定要小心,这种不会被GC清理的变量会存在整个生命周期中,如果没有正确的处理,很容易消耗完所有的内存。在php-fpm下,php代码执行完内存就会被完全释放。

二、注解机制

一般而言,在编程届中注解是一种和注释平行的概念,在解释注解之前我们需要先定义一下 注解 与 注释 的区别:

注释:给程序员看,帮助理解代码,对代码起到解释、说明的作用。

注解:给应用程序看,注解往往充当着对代码的声明和配置的作用,为可执行代码提供机器可用的额外信息,在特定的环境下会影响程序的执行。

框架可以基于这些元信息为代码提供各种额外功能,本质上注解就是理解注解只是配置的另一种展现方式:

比如通过注解的方式实现权限的控制,就比配置文件当中配置要更加的方便

比如利用注解的方式配置路由、配置定时任务

现有的基于swoole的框架很多都是基于注解开发的,所以我们需要对注解机制有了解,接下来利用代码来实现下注解

二、容器

3.1、什么是容器?

容器 就是一个巨大的工厂,用于存放和管理 对象的生命周期,并且能够解决程序的依赖关系,实现解耦。

3.2简单的通过代码理解依赖注入

 1 /**
 2 * 耦合严重的写法
 3 **/
 4 class db {
 5     public static function get_db() {
 6         return new mysqli('127.0.0.1','user','pass','dbname',3306);
 7     }
 8 }
 9 class post {
10     private $db;
11 public function __construct($db){
12    //假设数据库驱动发生了变化呢?如果写死只能直接改动代码
13        $this->db =new mysqli('127.0.0.1','user','pass','dbname',3306);    }
14     public function get_post($id){
15         return $this->db->query('SELECT * FROM post WHERE id ='.$id);
16     }
17 }
18 $post = new post();
19 $post->get_post(12);
20 
21 /*
22 *依赖注入的方式
23 */
24 <?php
25 class db {
26     public static function get_db() {
27         return new mysqli('127.0.0.1','user','pass','dbname',3306);
28     }
29 }
30 class post {
31     private $db;
32     public function set_db(db $db){
33         $this->db = $db;
34     }
35     public function get_post($id){
36         return $this->db->query(select xx from xxx);
37     }
38 }
39 $post = new post();
40 $post->set_db( db::get_db() ); //注入post类依赖的数据库连接对象,通过类名直接调用静态方法get_db
41 $post->get_post(11);

当没有Ioc/DI容器时

当有了IoC/DI的容器后,post类不再主动去创建db类了,如下图所示:

MZv6vqe.jpg!web

依赖注入:在A类中使用了B类的实例时,B对象的构造不是在A类某个方法中初始化的,而是在A类外部初始化之后以B类的对象传入进来。这个过程就是依赖注入。所需要的类通过参数的形式传入的就是依赖注入。

y6RZZz2.jpg!webAb6ruyf.jpg!web

依赖注入:在A类中使用了B类的实例时,B对象的构造不是在A类某个方法中初始化的,而是在A类外部初始化之后以B类的对象传入进来。这个过程就是依赖注入。所需要的类通过参数的形式传入的就是依赖注入。

控制反转IoC(Inversion of Control)是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方,比如转移交给了IOC容器,它就是一个专门用来创建对象的工厂,你要什么对象,它就给你什么对象,有了 IOC容器,依赖关系就变了,原先的依赖关系就没了,它们都依赖IOC容器了,通过IOC容器来建立它们之间的关系,控制反转意思是说将依赖类的控制权交出去,由主动变为被动。

3.3、为什么说在swoole当中使用容器更有意义?

传统的php框架没有常驻内存,因此每次请求进来都需要把用到的类都实例化一次,每次实例化都需要申请内存,当请求处理完之后又需要释放,具体请参看第一点,所以我们可以在server启动的时候就把类实例化预先放到内存中,减入对象的创建时间。

一个简单的bean容器

 1 class BeanFactory{
 2     private static $container=[];
 3     
 4     public static function set(string $name,callable $func){
 5         self::$container[$name]=$func;
 6     }
 7 
 8 
 9     public static function get(string $name){
10         if(isset(self::$container[$name])){
11             return (self::$container[$name])();
12         }
13         return null;
14     }
15 }

3.4、Swoole进程结构

Swoole的高效不仅仅于底层使用c编写,他的进程结构模型也使其可以高效的处理业务,我们想要深入学习,并且在实际的场景当中使用必须了解,下面我们先看一下结构图

YzE32ee.jpg!web

首先先介绍下swoole的这几种进程分别是干什么的

从这些层级的名字,我们先大概说一下,下面这些层级分别是干什么的,做一个详细的说明。

1、Master进程:主进程

2、Manger进程:管理进程

3、Worker进程:工作进程

4、Task进程:异步任务工作进程

1、Master进程

第一层,Master进程,这个是swoole的主进程,这个进程是用于处理swoole的核心事件驱动的,那么在这个进程当中可以看到它拥有一个MainReactor[线程]以及若干个Reactor[线程],swoole所有对于事件的监听都会在这些线程中实现,比如来自客户端的连接,信号处理等。

YBjyaum.jpg!web

每一个线程都有自己的用途,下面多每个线程有一个了解

1.1、MainReactor(主线程)

主线程会负责监听server socket,如果有新的连接accept,主线程会评估每个Reactor线程的连接数量。将此连接分配给连接数最少的reactor线程,做一个负载均衡。

1.2 、Reactor线程组

Reactor线程负责维护客户端机器的TCP连接、处理网络IO、收发数据完全是异步非阻塞的模式。

swoole的主线程在Accept新的连接后,会将这个连接分配给一个固定的Reactor线程,在socket可读时读取数据,并进行协议解析,将请求投递到Worker进程。在socket可写时将数据发送给TCP客户端。

1.3、心跳包检测线程(HeartbeatCheck)

Swoole配置了心跳检测之后,心跳包线程会在固定时间内对所有之前在线的连接

发送检测数据包

1.4、UDP收包线程(UdpRecv)

接收并且处理客户端udp数据包

2、管理进程Manager

Swoole想要实现最好的性能必须创建出多个工作进程帮助处理任务,但Worker进程就必须fork操作,但是fork操作是不安全的,如果没有管理会出现很多的僵尸进程,进而影响服务器性能,同时worker进程被误杀或者由于程序的原因会异常退出,为了保证服务的稳定性,需要重新创建worker进程。

Swoole在运行中会创建一个单独的管理进程,所有的worker进程和task进程都是从管理进程Fork出来的。管理进程会监视所有子进程的退出事件,当worker进程发生致命错误或者运行生命周期结束时,管理进程会回收此进程,并创建新的进程。换句话也就是说,对于worker、task进程的创建、回收等操作全权有“保姆”Manager进程进行管理。

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

F77vai2.jpg!web

3、Worker进程

worker 进程属于swoole的主逻辑进程,用户处理客户端的一系列请求,接受由Reactor线程投递的请求数据包,并执行PHP回调函数处理数据生成响应数据并发给Reactor线程,由Reactor线程发送给TCP客户端可以是异步非阻塞模式,也可以是同步阻塞模式

4、Task进程

taskWorker进程这一进城是swoole提供的异步工作进程,这些进程主要用于处理一些耗时较长的同步任务,在worker进程当中投递过来。

5、client跟server的交互:

1、client请求到达 Main Reactor,Client实际上是与Master进程中的某个Reactor线程发生了连接。

2、Main Reactor根据Reactor的情况,将请求注册给对应的Reactor

3、客户端有变化时Reactor将数据交给worker来处理

4、worker处理完毕,通过进程间通信(比如管道、共享内存、消息队列)发给对应的reactor。

5、reactor将响应结果发给相应的连接请求处理完成

示意图:

AjiYZ3e.jpg!web

一个更通俗的比喻,假设Server就是一个工厂,那Reactor就是销售,接受客户订单。而Worker就是工人,当销售接到订单后,Worker去工作生产出客户要的东西。而Task_Worker可以理解为行政人员,可以帮助Worker干些杂事,让Worker专心工作。

6、进程的绑定事件

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 发送的管道消息时会触发事件

3.5、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方式引入相关的业务逻辑代码才会生效

四、为什么需要分布式服务

4.1、早期单体架构带来的问题

单体架构在规模比较小的情况下工作情况良好,但是随着系统规模的扩大,它暴露出来的问题也越来越多,主要有以下几点:

1.复杂性逐渐变高

比如有的项目有几十万行代码,各个模块之间区别比较模糊,逻辑比较混乱,代码越多复杂性越高,越难解决遇到的问题。

2.技术债务逐渐上升

公司的人员流动是再正常不过的事情,有的员工在离职之前,疏于代码质量的自我管束,导致留下来很多坑,由于单体项目代码量庞大的惊人,留下的坑很难被发觉,这就给新来的员工带来很大的烦恼,人员流动越大所留下的坑越多,也就是所谓的技术债务越来越多。

3.阻碍技术创新

比如以前的某个项目使用tp3.2写的,由于各个模块之间有着千丝万缕的联系,代码量大,逻辑不够清楚,如果现在想用tp5来重构这个项目将是非常困难的,付出的成本将非常大,所以更多的时候公司不得不硬着头皮继续使用老的单体架构,这就阻碍了技术的创新。

4.无法按需伸缩

比如说推荐模块是CPU密集型的模块,而订单模块是IO密集型的模块,假如我们要提升订单模块的性能,比如加大内存、增加硬盘,但是由于所有的模块都在一个架构下,因此我们在扩展订单模块的性能时不得不考虑其它模块的因素,因为我们不能因为扩展某个模块的性能而损害其它模块的性能,从而无法按需进行伸缩。

5.系统高可用性差

因为所有的功能开发最后都部署到同一个框架里,运行在同一个进程之中,一旦某一功能涉及的代码或者资源有问题,那就会影响整个框架中部署的功能。

五、什么是RPC?

RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。

比如说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,就需要通过网络来表达调用的语义和传达调用的数据,而这种方式就是rpc

5.1、为什么需要RPC?

RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。

Call(“listServices”)->info();

rpc隐藏了通讯的细节,调用远程的服务就像调用本地的代码一样,其调用协议通常包含传输协议和编码协议。

传输协议: 可以是自定义的tcp协议,可以是http、websockect

编码协议: 如基于文本编码的 xml、 json,也有二进制编码的 protobuf 、binpack 等。

5.2、使用什么协议?

RPC是一个软件结构概念,是构建分布式应用的理论基础。就好比为啥你家可以用到发电厂发出 来的电?

是因为电是可以传输的。至于用铜线还是用铁丝还是其他种类的导线,也就是用http还是用其他协议的问题了。这个要看什么场景,对性能要求怎么样。

5.3、rpc就只是接口调用?

一个完善的rpc其实还包含另一块内容,通信协议外还有“服务注册发现”,错误重试,服务限流,服务调用的负载均衡等等,rpc是不仅仅是一套设计规范,还包含了服务治理。

5.4 实际操作

传输协议: TCP协议

编码协议: json编码

我的 知乎专栏


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK