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
了。