一、涉及的计算机基础知识
通信网络
全双工/单工/半双工:
1、单工:数据只在一个方向上传输,不能实现双方通信。如:闭路电视、广播。
2、半双工:允许数据在两个方向上传输,但是同一时间数据只能在一个方向上传输,其实际上是切换的单工。如:对讲机
3、全双工:允许数据在两个方向上同时传输。如:手机通话IP/TCP/UDP:
1、ip协议(网络层):IP的责任就是把数据从源传送到目的地。它不负责保证传送可靠性,流控制,包顺序和其它对于主机到主机协议来说很普通的服务。
2、tcp(传输层):是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
3、udp(传输层):一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。socket:
socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。websocket:
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,它只需要一次http握手,就可以保持一个长连接,使得服务器可以主动发送消息给客户端,大大减少了轮询机制的消耗参考文章:计算机通信网络系列文章
操作系统
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
协程:你可能看的最多的就是这样一句话“协程就是用户态的线程”,用户态的线程不是由操作系统来调度的,而是由程序员来调度的。PHP程序中,yield这个关键字就是用来产生中断, 并保存当前的上下文的, 比如说程序的一段代码是访问远程服务器,那这个时候CPU就是空闲的,就用yield让出CPU,接着执行下一段的代码,如果下一段代码还是访问除CPU以外的其它资源,还可以调用yield让出CPU. 继续往下执行,这样就可以用同步的方式写异步的代码了。
上下文:上下文切换(有时也称做进程切换或任务切换)是指CPU从一个进程或线程切换到另一个进程或线程。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
参考文章:计算机操作系统系列文章
二、涉及的PHP基础知识
五种常见运行模式
- cgi 协议模式
- fast-cgi 协议模式,一个常驻内存型的cgi。
- 模块模式,如php作为apache的模块随apache启动而启动
- php-cli模式,属于命令行模式
- 其他,如SwooleServer
fast-cgi
和cgi
都是一种协议,开启的进程是单独实现该协议的进程,php-fpm是fastcgi进程管理器。
php-fpm启动->生成n个fast-cgi协议处理进程->监听一个端口等待任务->用户请求->web服务器接收请求->请求转发给php-fpm->php-fpm交给一个空闲进程处理->进程处理完成->php-fpm返回给web服务器->web服务器接收数据->返回给用户。更详细内容可参考FastCGI相关文章
SwooleServer
则是相当于取代了php-fpm
作为管理器的位置, 由于Swoole 是运行在CLI
模式下, 所以可以常驻运行和以守护进程运行, 但也正因为如此,也需要开发者自行处理变量的销毁及各种异常和超时的处理。pcntl_fork多进程使用
- pcntl是php官方的多进程扩展,只能在linux环境使用
- 当pcntl_fork()创建子进程成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回0。失败时,在 父进程上下文返回-1,不会创建子进程,并且会引发一个PHP错误。
- 子进程会复制父进程的状态,如果在第五行执行了pcntl_fork,那么创建出的子进程,代码也是从第五行开始执行的。又子进程复制了数据,代码。
具体可参考php中pcntl_fork详解 / PHP多进程编之pcntl_forkswoole涉及的其他PHP知识
三、swoole基础
应用场景
Swoole是一个为PHP用C和C++编写的基于事件的高性能异步&协程并行网络通信引擎。使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领域。 使用 PHP + Swoole 作为网络通信框架,可以使企业 IT 研发团队的效率大大提升,更加专注于开发创新产品。
Swoole高并发高性能保证:IO复用、常驻内存型、协程、异步、进程池、耗时任务处理、高性能数据结构(hashTable)等。swoole服务运行结构图
Reactor、Worker、TaskWorker的关系
三种角色分别的职责是:
Reactor线程
- 负责维护客户端TCP连接、处理网络IO、处理协议、收发数据
- 完全是异步非阻塞的模式
- 全部为C代码,除Start/Shudown事件回调外,不执行任何PHP代码
- 将TCP客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包
- Reactor以多线程的方式运行
Worker进程
- 接受由Reactor线程投递的请求数据包和处理,包括协议解析和响应请求,并执行PHP回调函数处理数据
- 生成响应数据并发给Reactor线程,由Reactor线程发送给TCP客户端
- 可以是异步非阻塞模式,也可以是同步阻塞模式
- Worker以多进程的方式运行
- 未设置worker_num,底层会启动与CPU数量一致的Worker进程。
TaskWorker进程
- 接受由Worker进程通过swoole_server->task/taskwait方法投递的任务
- 处理任务,并将结果数据返回(使用swoole_server->finish)给Worker进程
- 完全是同步阻塞模式
- TaskWorker以多进程的方式运行
三者关系
可以理解为Reactor就是nginx,Worker就是php-fpm。Reactor线程异步并行地处理网络请求,然后再转发给Worker进程中去处理。Reactor和Worker间通过UnixSocket进行通信。swoole与nginx有很多原理相似之处,具体可参考 swoole原理和nginx原理对比。
在php-fpm的应用中,经常会将一个任务异步投递到Redis等队列中,并在后台启动一些php进程异步地处理这些任务。Swoole提供的TaskWorker是一套更完整的方案,将任务的投递、队列、php任务处理进程管理合为一体。通过底层提供的API可以非常简单地实现异步任务的处理。另外TaskWorker还可以在任务执行完成后,再返回一个结果反馈到Worker。
Swoole的Reactor、Worker、TaskWorker之间可以紧密的结合起来,提供更高级的使用方式。
一个更通俗的比喻,假设Server就是一个工厂,那Reactor就是销售,接受客户订单。而Worker是管理层及行政人员,执行小任务或派发耗时任务。当销售接到订单后,Worker就去处理一些简单的杂事,而TaskWorker可以理解工人,专门处理worker派发下来的一些耗时比较长的任务。
底层会为Worker进程、TaskWorker进程分配一个唯一的ID,不同的Worker和TaskWorker进程之间可以通过sendMessage接口进行通信
Master 主进程、Manager 进程
Master 主进程:主进程内有多个Reactor线程,基于epoll/kqueue进行网络事件轮询。收到数据后转发到Worker进程去处理
Manager 进程:对所有Worker进程进行管理,Worker进程生命周期结束或者发生异常时自动回收,并创建新的Worker进程
swoole常见功能
Swoole 使用纯 C 语言编写,提供了 PHP 语言的异步多线程服务器,异步 TCP/UDP 网络客户端,异步 MySQL,异步 Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。 Swoole内置了Http/WebSocket服务器端/客户端、Http2.0服务器端。基础入门可参考快速起步
除了异步 IO 的支持之外,Swoole 为 PHP 多进程的模式设计了多个并发数据结构和IPC(进程间通信)通信机制,可以大大简化多进程并发编程的工作。其中包括了并发原子计数器,并发 HashTable,Channel,Lock,进程间通信IPC等丰富的功能特性。
Swoole2.0 支持了类似 Go 语言的协程,可以使用完全同步的代码实现异步程序。PHP 代码无需额外增加任何关键词,底层自动进行协程调度,实现异步。常见服务类型
Swoole\Server
创建一个异步服务器程序,支持TCP、UDP、UnixSocket 3种协议,支持IPv4和IPv6,支持SSL/TLS单向双向证书的隧道加密。使用者无需关注底层实现细节,仅需要设置网络事件的回调函数即可。
请勿在使用Server创建之前调用其他异步IO的API,否则将会创建失败。可以在Server启动后onWorkerStart回调函数中使用。
Server只能用于php-cli环境,在其他环境下会抛出致命错误
Swoole\Http\Server
Http\Server继承自Server,是一个的Http服务器实现。Http\Server支持同步和异步2种模式。
无论是同步模式还是异步模式,Http\Server都可以维持大量TCP客户端连接。同步/异步仅仅体现在对请求的处理方式上。
swoole_http_server是swoole_server的子类,内置了Http的支持。当然不单单只有http,如swoole_redis_server也是swoole_server的子类,内置了Redis服务器端协议的支持。
http\Server对Http协议的支持并不完整,建议仅作为应用服务器。并且在前端增加Nginx作为代理
Swoole\WebSocket\Server
swoole内置的WebSocket服务器支持,通过几行PHP代码就可以写出一个异步非阻塞多进程的WebSocket服务器。
swoole_websocket_server是swoole_http_server的子类,内置了WebSocket的支持自定义进程Process:1.7.2版本增加了一个进程管理模块,用来替代PHP的pcntl,Swoole\Process提供了如下特性:
- 基于Unix Socket和sysvmsg消息队列的进程间通信,只需调用write/read或者push/pop即可
- 支持重定向标准输入和输出,在子进程内echo不会打印屏幕,而是写入管道,读键盘输入可以重定向为管道读取数据
- 配合Event模块,创建的PHP子进程可以异步的事件驱动模式
- 提供了exec接口,创建的进程可以执行其他程序,与原PHP父进程之间可以方便的通信
协程模式:Swoole2/4版本支持了协程,使用协程后事件回调函数将会并发地执行。协程是一种用户态线程实现,没有额外的调度消耗,仅占用内存。使用协程模式,可以理解为“每次事件回调函数都会创建一个新的线程去执行,事件回调函数执行完成后,线程退出”。
当协程执行完后,底层会恢复协程上下文,代码逻辑继续从切换点开始恢复执行。开发者整个过程不需要关心整个切换过程。可查看实现原理和Coroutine进行调用并发HashTable:swoole_table一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程/多线程数据共享和同步加锁问题。详细可参考swoole_table
- 性能强悍,单线程每秒可读写200万次
- 应用代码无需加锁,swoole_table内置行锁自旋锁,所有操作均是多线程/多进程安全。用户层完全不需要考虑数据同步问题。
- 支持多进程,swoole_table可以用于多进程之间共享数据
- 使用行锁,而不是全局锁,仅当2个进程在同一CPU时间,并发读取同一条数据才会进行发生抢锁
进程间通信IPC:此函数可以向任意Worker进程或者Task进程发送消息。在非主进程和管理进程中可调用。收到消息的进程会触发onPipeMessage事件。可查看Server->sendMessage进行调用
Swoole\Server配置参数
示例: $serv->set(array( 'reactor_num' => 2, //reactor thread num 'worker_num' => 4, //worker process num 'backlog' => 128, //listen backlog 'max_request' => 50, 'dispatch_mode' => 1, ));
最大连接:max_conn => 10000, 此参数用来设置Server最大允许维持多少个tcp连接。超过此数量后,新进入的连接将被拒绝
守护进程化:daemonize => 1,加入此参数后,执行php server.php将转入后台作为守护进程运行
reactor线程数:reactor_num => 2,通过此参数来调节Reactor线程的数量,以充分利用多核,reactor_num和默认设置为CPU核数
worker进程数:worker_num => 4,设置启动的Worker进程数量。Swoole采用固定Worker进程的模式。
全异步非阻塞服务器 worker_num配置为CPU核数的1-4倍即可。
同步阻塞服务器,worker_num配置为100或者更高,具体要看每次请求处理的耗时和操作系统负载状况
设定的Worker进程数小于reactor线程数时,会自动调低reactor线程的数量
max_request:max_request => 2000,此参数表示worker进程在处理完n次请求后结束运行。manager会重新创建一个worker进程。此选项用来防止worker进程内存溢出。
worker进程数据包分配模式:dispatch_mode = 1 //1平均分配,2按FD取模固定分配,3抢占式分配,默认为取模(dispatch=2)
更多配置可参考:Server->set配置信息Swoole\Server事件回调函数
Swoole\Server是事件驱动模式,所有的业务逻辑代码必须写在事件回调函数中。当特定的网络事件发生后,底层会主动回调指定的PHP函数。共支持13种事件。更详细的内容可参考:事件回调函数
事件执行顺序:
- 所有事件回调均在$server->start后发生
- 服务器关闭程序终止时最后一次事件是onShutdown
- 服务器启动成功后,onStart/onManagerStart/onWorkerStart会在不同的进程内并发执行
- onReceive/onConnect/onClose在Worker进程中触发
- Worker/Task进程启动/结束时会分别调用一次onWorkerStart/onWorkerStop
- onTask事件仅在task进程中发生
- onFinish事件仅在worker进程中发生
- onStart/onManagerStart/onWorkerStart 3个事件的执行顺序是不确定的
参考文章
四、easyswoole框架
easyswoole框架生命周期
具体可参考核心代码文件:vendor/easyswoole/easyswoole/src/Core.php
服务类型
- EASYSWOOLE_SERVER (对应Swoole\Server)
- EASYSWOOLE_WEB_SERVER (对应Swoole\Http\Server)
- EASYSWOOLE_WEB_SOCKET_SERVER (对应Swoole\WebSocket\Server)
easyswoole案例演示
tcp请求:
#在CliExample目录下执行 php TCPClient3.php 服务端回复: 1557998515 服务端回复: your args is:{"name":"\u4ed9\u58eb\u53ef"} #服务端内容响应: 已连接 tcp服务3 fd:3 发送消息:M{"controller":"Index","action":"index","param":{"name":"\u4ed9\u58eb\u53ef"}} tcp服务3 fd:3 发送消息:L{"controller":"Index","action":"args","param":{"name":"\u4ed9\u58eb\u53ef"}} tcp服务3 fd:3 已关闭
服务端通过addListener开启9504端口监听,客户端通过$client->connect('127.0.0.1', 9504, 0.5)发起链接后发送数据。服务端通过\EasySwoole\Socket\Dispatcher->dispatch进行路由解析,并执行具体动作后将数据返回给客户端。
http请求:
#按照demo案例添加App/HttpController/Test.php,并访问以下函数 function index() { $this->response()->write('test index'); } # 访问 http://mytest.easyswoole.com/Test/index 输出:test index
压力测试:
#硬件信息 CPU:1核 Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz 内存:1GB 操作系统: Ubuntu / 16.04 LTS amd64 (64bit) # ab长连接压测swoole页面(ab -c 100 -n 100000 -k http://127.0.0.1:9501/Test/index)返回: Concurrency Level: 100 Time taken for tests: 18.085 seconds Complete requests: 100000 Failed requests: 0 Keep-Alive requests: 100000 Total transferred: 15500000 bytes HTML transferred: 1000000 bytes Requests per second: 5529.44 [#/sec] (mean) Time per request: 18.085 [ms] (mean) Time per request: 0.181 [ms] (mean, across all concurrent requests) Transfer rate: 836.98 [Kbytes/sec] receive #ab非长连接压测swoole页面(ab -c 100 -n 100000 http://127.0.0.1:9501/Test/index)返回: Concurrency Level: 100 Time taken for tests: 46.141 seconds Complete requests: 100000 Failed requests: 0 Total transferred: 15000000 bytes HTML transferred: 1000000 bytes Requests per second: 2167.29 [#/sec] (mean) Time per request: 46.141 [ms] (mean) Time per request: 0.461 [ms] (mean, across all concurrent requests) Transfer rate: 317.47 [Kbytes/sec] received #ab长连接压测nginx页面(ab -c 100 -n 100000 -k http://127.0.0.1/test.php)返回: Concurrency Level: 100 Time taken for tests: 36.257 seconds Complete requests: 100000 Failed requests: 0 Keep-Alive requests: 0 Total transferred: 14700000 bytes HTML transferred: 1000000 bytes Requests per second: 2758.12 [#/sec] (mean) Time per request: 36.257 [ms] (mean) Time per request: 0.363 [ms] (mean, across all concurrent requests) Transfer rate: 395.94 [Kbytes/sec] received
swoole页面的长连接比非长连接qps高出一半,nginx非长连接压测的qps比swoole非长连接压测的qps要高
自定义进程
Process教程 / 多进程 / swoole_process实现多进程 / Process自定义进程demo
#按照demo部署代码,并稍微调整EasySwooleEvent.php下函数mainServerCreate代码 public static function mainServerCreate(EventRegister $register) { /** * 除了进程名,其余参数非必须 */ for($i=0;$i<3;$i++){ $myProcess = new ProcessOne("processName".$i,$i,false,2,true); ServerManager::getInstance()->getSwooleServer()->addProcess($myProcess->getProcess()); } } #稍微调整App/Process/ProcessOne.php代码 public function run($arg) { Logger::getInstance()->console($this->getProcessName().'-'.$arg." start"); while (1){ \co::sleep(4-$arg); Logger::getInstance()->console($this->getProcessName().'-'.$arg." run"); } } #查询执行的进程: root@instance-wjbdyzhe:/data/web/mytest.easyswoole.com# ps -aux|grep processName root 10288 0.0 0.0 14688 908 pts/0 S+ 20:53 0:00 grep --color=auto processName root 24261 0.0 1.2 232932 12308 pts/2 S+ 20:04 0:00 processName0 root 24262 0.0 1.2 232932 12308 pts/2 S+ 20:04 0:00 processName1 root 24263 0.0 1.2 232932 12308 pts/2 S+ 20:04 0:00 processName2 #三个进程的执行结果: [2019-04-28 08:56:37][default]processName0-0 start [2019-04-28 08:56:37][default]processName2-2 start [2019-04-28 08:56:37][default]processName1-1 start [2019-04-28 08:56:39][default]processName2-2 run [2019-04-28 08:56:40][default]processName1-1 run [2019-04-28 08:56:41][default]processName0-0 run [2019-04-28 08:56:41][default]processName2-2 run [2019-04-28 08:56:43][default]processName1-1 run [2019-04-28 08:56:43][default]processName2-2 run
可以看到以上代码及执行结果:执行程序后,创建了三个自定义进程,并且进程是并行执行的
异步任务:
#按照demo部署代码,并调整以下函数 function multiTaskConcurrency(){ // 多任务并发 $tasks[] = function () { sleep(1);return 'this is 1'; }; // 任务1 $tasks[] = function () { sleep(5);return 'this is 2'; }; // 任务2 $tasks[] = function () { sleep(3);return 'this is 3'; }; // 任务3 $results = \EasySwoole\EasySwoole\Swoole\Task\TaskManager::barrier($tasks, 6); var_dump($results); $this->response()->write('执行并发任务成功'); } # 访问 http://mytest.easyswoole.com/index/multiTaskConcurrency 输出: array(3) { [0]=> string(9) "this is 1" [2]=> string(9) "this is 3" [1]=> string(9) "this is 2" }
由代码和打印来看,虽然任务2排在第二位,但由于执行时间长,执行结果排在最后,由此可知三个任务并非串行执行,而是并行执行
连接池
Mysql协程连接池 / Redis协程连接池 / 连接池demo
#最大进程配置数8个 'SETTING' => [ 'worker_num' => 8, 'max_request' => 5000, 'task_worker_num' => 8, 'task_max_request' => 1000, ], #每个进程连接池最大连接个数5个 'REDIS' => [ 'host' => '127.0.0.1', 'port' => '6379', 'auth' => 'test', 'POOL_MAX_NUM' => '5', 'POOL_TIME_OUT' => 'utf8mb4', ], #jmeter压测配置 线程数:100;ramp-up时间:10;循环次数:30 #通过redis客户端查看链接信息: 127.0.0.1:6379> info clients # Clients connected_clients:41 client_recent_max_input_buffer:4 client_recent_max_output_buffer:0 blocked_clients:0
理论最大redis链接数 = worker_num*POOL_MAX_NUM = 8*5 = 40
并发压测客户端显示 = 41-1 = 40(多一个1实际为手动链接)
只要不终止swoole/server服务客户端连接,显示一直为41,表示链接一直处于链接可用状态。#最大进程配置数8个 'SETTING' => [ 'worker_num' => 8, 'max_request' => 5000, 'task_worker_num' => 8, 'task_max_request' => 1000, ], #匿名连接池最大配置10个链接 'MYSQL3' => [ 'host' => '127.0.0.1',//防止报错,就不切换数据库了 'port' => '3306', 'user' => 'root', 'timeout' => '5', 'charset' => 'utf8mb4', 'password' => '123456', 'database' => 'test',//防止报错,就不切换数据库了 'POOL_MAX_NUM' => '10', 'POOL_TIME_OUT' => '0.1' ], #jmeter压测配置 线程数:100;ramp-up时间:10;循环次数:30 #通过mysql查看链接个数 mysql> show processlist; …… | 306155 | root | 172.17.0.1:48704 | test | Sleep | 30 | | NULL | | 306156 | root | 172.17.0.1:48840 | test | Sleep | 36 | | NULL | | 306157 | root | 172.17.0.1:48846 | test | Sleep | 29 | | NULL | +--------+------+------------------+------+---------+------+----------+------------------+ 81 rows in set (0.00 sec)
理论最大mysql链接数 = worker_num*POOL_MAX_NUM = 8*10 = 80
并发压测客户端显示 = 81-1 = 80(多一个1实际为手动链接)
过一段再show processlist发现已无链接,可能mysql主动超时中断,有兴趣的同学可主动排查
更多参考