webman 是一款基于 workerman 开发的 http 服务框架,用于开发 web 站点或者 http 接口。支持路由、中间件、自动注入、多应用、自定义进程、无需更改直接兼容现有 composer 项目组件等诸多特性。具有学习成本低、简单易用、超高性能、超高稳定性等特点。 https://www.workerman.net/doc...
简单来说,webman
是基于 workerman
的一款常驻内存的 应用
服务框架,运行模式为多进程阻塞模式,IO模型肯定是多路复用
,至于是select/poll
还是 epoll
应该同 workerman
的场景一致,看是否安装了 event
扩展了(建议安装,高并发下 epoll
模型更具优势)。
虽然不像当前许多基于 swoole
的协程
或 类似 node/reactPHP
等 eventLoop
的异步非阻塞模式的框架,但基于 epoll
模型时,开 cpu
个 worker
单机 C10K
也没什太大鸭梨。
小课堂
单进程模式
一个服务进程,来一个请求就阻塞,处理期间拒绝响应其他请求。
1、开始等待当前请求网络IO
完成。
2、紧接着处理代码业务(期间可能也会伴随着各种网络IO
,你的业务网代码总不能只是 "hello world"
吧,数据库IO
、文件IO
、调用其他微服务的网络IO
,都会发生阻塞)。
3、发送响应完毕。可以继续接收处理下一个请求。
缺点:无法承载高并发,你将会收到各种 502
响应。
多线程/协程模式
一个主服务进程,来一个请求就创建一个线程去专用处理,线程专一处理负责的请求。相比单进程模式,可以承载较高的请求并发量,但创建和切换线程的开销也是很大的,还有死锁的问题(现在又有了协程
,用户态线程,更加轻量级,还可以)。
IO多路复用模式
IO 多路复用模式下,worker
进程在接收一个请求后,如果该请求还未就绪(内核还未完成 socket
数据的读取及未 copy
至用户态),那么 worker
是可以继续去接收其他请求的,当某请求的 socket
数据读取完成后,worker
便开始执行业务处理(注意:此阶段 worker
是被业务处理独占的,期间无法处理其他请求)。业务处理完成,worker
被释放,恢复最初的状态流程。select/poll
和 epoll
都是 IO多路复用
,不同之处在于 epoll
采用更友好的通知机制,select/poll
要主动的忙轮训来监测是否有已就绪的请求socket
,epoll
则是等待内核的主动通知。
EventLoop 模式
node
,reactPHP
是比较典型的代表。workerman
也有内置的 eventLoopFactory
,借用 reactPHP
生态的异步客户端就可以实现高性能的 eventLoop
模式,性能优异,但不太适用复杂的业务处理,异步风格的 callback hell
大家应该都有了解。事件队列维护请求的上下文,请求 IO
就绪时会事件通知 worker
来继续下面的操作,如果发生了 IO
就入队事件队列,等待 IO
完成了再召唤 worker
,所以 worker
始终在执行流程控制的业务代码,一旦发生了 IO 阻塞
,就会把请求上下文放入事件队列,去处理其他请求的事件。
网上比较形象的例子,幼儿园老师分糖吃。比如我们有100个位置,A 来了,老师说坐下,老师并不会盯着A去入座,这时候 A 还未坐好,不能给糖吃(内核还未完成请求socket的读取)。B、C 来了,老师说坐下、坐下。A说坐好了要吃糖,老师走过去把糖给A,A开始吃糖(数据库IO,网络IO),老师并不会杵在那里看A吃糖(这里可能不太形象,你就想着吃糖要人喂,但不是老师做,是另外的cpu时间片)。C说坐好了要吃糖,老师把糖给C。D来了,老师说坐下。B说坐好了要吃糖,老师把糖给B。A说吃完了,老师让A回去(响应请求),把糖纸回收(清理回收资源)。这样老师就能照顾很多个孩子一起吃糖。
虽然没有协程加持,没有 eventLoop
,但 多路IO复用
下的 epoll
模式依然能让 webman
承载高并发请求(只要你业务代码不坨,请求的网络IO阻塞可以凭借 epoll
模型实现维护 c10k
个,谁准备好了再去处理业务代码这种运行模式)。
压测
配置
i5-7360U CPU @ 2.30GHz 2 Core 4 Thread
8G RAM
开了 4 个 worker 进程
Workerman[start.php] start in DEBUG mode
------------------------------------------- WORKERMAN --------------------------------------------
Workerman version:4.0.18 PHP version:7.4.2
-------------------------------------------- WORKERS ---------------------------------------------
proto user worker listen processes status
tcp sqrtcat webman http://0.0.0.0:8787 4 [OK]
tcp sqrtcat monitor none 1 [OK]
tcp sqrtcat websocket websocket://0.0.0.0:8888 10 [OK]
--------------------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.
现在查看 mysql
的 processlist
并不会有 webman
的 worker
建立的链接,因为链接会在 worker
初次对数据库访问时建立,后续就保持长链接啦。
mysql> show processlist;
+----+-----------------+-----------+------+---------+--------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+------+---------+--------+------------------------+------------------+
| 4 | event_scheduler | localhost | NULL | Daemon | 115720 | Waiting on empty queue | NULL |
| 13 | root | localhost | NULL | Query | 0 | starting | show processlist |
+----+-----------------+-----------+------+---------+--------+------------------------+------------------+
2 rows in set (0.01 sec)
为什么 webman
没有数据库连接池呢?
因为 webman
的 worker
工作模式为 IO多路复用
,每个 worker
都可以在同请求建立链接后,请求传输数据期间
可以 不阻塞
的去处理其他请求,待当前请求的数据IO
就绪后,worker
会一口气执行 业务代码
直至 完成
,执行期间 worker
是被完全占用
的,与 worker
绑定的 dbConnect
也是被当前 业务上下文
持有的。所以执行 业务代码
期间 worker
并不能 转出
再去连接池取一个 新的dbConnect
去执行别的请求的业务(即协程或者异步的模式,可以在业务阻塞时转出,执行其他请求的业务代码),连接池也就没有存在的意义了。
我先小跑一下把 db链接
跑出来更直观大家理解,可以看到每个 worker
建立了一个链接(在某些方面来说这也是个简单的连接池,防止数据库被请求打崩掉是完全可控的了)。
mysql> show processlist;
+----+-----------------+-----------------+--------+---------+--------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------------+--------+---------+--------+------------------------+------------------+
| 4 | event_scheduler | localhost | NULL | Daemon | 116593 | Waiting on empty queue | NULL |
| 13 | root | localhost | NULL | Query | 0 | starting | show processlist |
| 14 | root | localhost:50426 | webman | Sleep | 4 | | NULL |
| 15 | root | localhost:50436 | webman | Sleep | 4 | | NULL |
| 16 | root | localhost:50438 | webman | Sleep | 4 | | NULL |
| 17 | root | localhost:50437 | webman | Sleep | 4 | | NULL |
+----+-----------------+-----------------+--------+---------+--------+------------------------+------------------+
6 rows in set (0.00 sec)
压测代码
控制器 app/controller/Index.php
/**
* 数据IO业务模拟演示
* @return Response
*/
public function db()
{
$nameList = ['james', 'lucy', 'jack', 'lilei', 'lily'];
$hobbyList = ['football', 'basketball', 'swimming'];
$name = $nameList[array_rand($nameList)];
$hobby = $hobbyList[array_rand($hobbyList)];
if (mt_rand(0, 5) >= 2) {// 0-1读 2-5写
$insertId = Db::table('test')->insertGetId([
'name' => $name,
'age' => rand(20, 100),
'sex' => ['m', 'f'][array_rand(['m', 'f'])],
'hobby' => $hobby,
]);
$data = ['id' => $insertId];
} else {
$data = Db::table('test')->where('hobby', $hobby)->first();
}
return json(['msg' => 'success', 'data' => $data]);
}
压测示例
5w请求 200并发
ab -c 200 -n 50000 -k http://0.0.0.0:8787/index/db
Server Software: workerman
Server Hostname: 0.0.0.0
Server Port: 8787
Document Path: /index/db
Document Length: 87 bytes
Concurrency Level: 200
Time taken for tests: 15.025 seconds
Complete requests: 50000
Failed requests: 0
Keep-Alive requests: 50000
Total transferred: 8413864 bytes
HTML transferred: 2713864 bytes
Requests per second: 3327.84 [#/sec] (mean)
Time per request: 60.099 [ms] (mean)
Time per request: 0.300 [ms] (mean, across all concurrent requests)
Transfer rate: 546.88 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.5 0 13
Processing: 2 60 9.9 58 183
Waiting: 2 60 9.9 58 183
Total: 2 60 9.8 58 183
Percentage of the requests served within a certain time (ms)
50% 58
66% 61
75% 64
80% 66
90% 70
95% 73
98% 84
99% 102
100% 183 (longest request)
5w请求 500并发
ab -c 500 -n 50000 -k http://0.0.0.0:8787/index/db
Server Software: workerman
Server Hostname: 0.0.0.0
Server Port: 8787
Document Path: /index/db
Document Length: 86 bytes
Concurrency Level: 500
Time taken for tests: 14.833 seconds
Complete requests: 50000
Failed requests: 0
Keep-Alive requests: 50000
Total transferred: 8404497 bytes
HTML transferred: 2704497 bytes
Requests per second: 3370.91 [#/sec] (mean)
Time per request: 148.328 [ms] (mean)
Time per request: 0.297 [ms] (mean, across all concurrent requests)
Transfer rate: 553.34 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 2.3 0 35
Processing: 5 147 16.7 146 311
Waiting: 1 147 16.7 146 311
Total: 6 147 15.9 146 311
Percentage of the requests served within a certain time (ms)
50% 146
66% 152
75% 155
80% 157
90% 162
95% 169
98% 179
99% 206
100% 311 (longest request)
5w请求 798并发
ab -c 798 -n 50000 -k http://0.0.0.0:8787/index/db
Server Software: workerman
Server Hostname: 0.0.0.0
Server Port: 8787
Document Path: /index/db
Document Length: 38 bytes
Concurrency Level: 798
Time taken for tests: 14.412 seconds
Complete requests: 50000
Failed requests: 0
Keep-Alive requests: 50000
Total transferred: 8404559 bytes
HTML transferred: 2704559 bytes
Requests per second: 3469.37 [#/sec] (mean)
Time per request: 230.013 [ms] (mean)
Time per request: 0.288 [ms] (mean, across all concurrent requests)
Transfer rate: 569.50 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 4.9 0 57
Processing: 9 227 32.6 232 365
Waiting: 2 227 32.6 232 365
Total: 10 227 31.3 232 368
Percentage of the requests served within a certain time (ms)
50% 232
66% 244
75% 249
80% 251
90% 258
95% 265
98% 280
99% 300
100% 368 (longest request)
可以看到 qps
稳定在 3500
左右,2Core
下的日常 db
操作这个 qps
我觉得很棒了。