webman 是一款基于 workerman 开发的 http 服务框架,用于开发 web 站点或者 http 接口。支持路由、中间件、自动注入、多应用、自定义进程、无需更改直接兼容现有 composer 项目组件等诸多特性。具有学习成本低、简单易用、超高性能、超高稳定性等特点。
简单来说,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 我觉得很 ok 了。