webman 體驗及性能壓測

webman 是一款基於 workerman 開發的 http 服務框架,用於開發 web 站點或者 http 接口。支持路由、中間件、自動注入、多應用、自定義進程、無需更改直接兼容現有 composer 項目組件等諸多特性。具有學習成本低、簡單易用、超高性能、超高穩定性等特點。

簡單來說,webman 是基於 workerman 的一款常駐內存的 應用 服務框架,運行模式爲多進程阻塞模式,IO模型肯定是多路複用,至於是 select/poll 還是 epoll 應該同 workerman 的場景一致,看是否安裝了 event 擴展了(建議安裝,高併發下 epoll 模型更具優勢)。

雖然不像當前許多基於 swoole協程 或 類似 node/reactPHPeventLoop 的異步非阻塞模式的框架,但基於 epoll 模型時,開 cpuworker 單機 C10K 也沒什太大鴨梨。

小課堂

單進程模式

一個服務進程,來一個請求就阻塞,處理期間拒絕響應其他請求。 1、開始等待當前請求 網絡IO 完成。 2、緊接着處理代碼業務(期間可能也會伴隨着各種 網絡IO,你的業務網代碼總不能只是 "hello world" 吧,數據庫IO文件IO、調用其他微服務的網絡IO,都會發生阻塞)。 3、發送響應完畢。可以繼續接收處理下一個請求。 缺點:無法承載高併發,你將會收到各種 502 響應。

多線程/協程模式

一個主服務進程,來一個請求就創建一個線程去專用處理,線程專一處理負責的請求。相比單進程模式,可以承載較高的請求併發量,但創建和切換線程的開銷也是很大的,還有死鎖的問題(現在又有了協程,用戶態線程,更加輕量級,還可以)。

IO多路複用模式

IO 多路複用模式下,worker 進程在接收一個請求後,如果該請求還未就緒(內核還未完成 socket 數據的讀取及未 copy 至用戶態),那麼 worker 是可以繼續去接收其他請求的,當某請求的 socket 數據讀取完成後,worker 便開始執行業務處理(注意:此階段 worker 是被業務處理獨佔的,期間無法處理其他請求)。業務處理完成,worker 被釋放,恢復最初的狀態流程。select/pollepoll 都是 IO多路複用,不同之處在於 epoll 採用更友好的通知機制,select/poll 要主動的忙輪訓來監測是否有已就緒的請求socketepoll 則是等待內核的主動通知。

EventLoop 模式

nodereactPHP 是比較典型的代表。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.

現在查看 mysqlprocesslist 並不會有 webmanworker 建立的鏈接,因爲鏈接會在 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 沒有數據庫連接池呢? 因爲 webmanworker 工作模式爲 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 了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章