PHP(Laravel) ランタイムのベンチマークをしました
結論から述べると
RoadRunner は いいぞ。
経緯
PHP は昨今様々なランタイムで動作させることが出来ます。
最も一般的なものは nginx + php-fpm かなと思いますが、 「最近聞こえるようになってきたアレとかコレとかと比べてどうなんだろう?」 という疑問を持ちました。検索しても私の疑問を解消してくれるだけの回答は見つかりませんでした。
だったら自分でやろうということでやりました。
ついでに、 各ランタイムごとにどんな Dockerfile/docker-compose.yml を書いたらいいかのテンプレート にもなったかなと思います。
前提
測定対象
- Apache2.4 + mod_php
- nginx + php-fpm
- nginx unit
- RoadRunner(behind Laravel Octane)
環境
- Ubuntu 20.04 on WSL2 on Windows 10
- CPU: AMD Ryzen 7 3700X 8-Core
- Memory: 16 GB
本当は 1 コア 1 GB縛りで起動させたかったのですが、うまくいかなくて 16-core 判定されてます。
アプリケーション
Laravel 8.x のプロジェクトを用意しました。 /laravel
以下に置きます。
簡単ないくつかの application/json なエンドポイントを実装しています。
-
GET /
単純に Laravel のライフサイクルを通過し{"ok":true}
を返すだけ -
POST /register
指定されたユーザー名・メールアドレス・パスワードでアカウントを生成する -
POST /login
メールアドレスとパスワードを入力して、ログイントークンを生成し受け取る -
GET /user
ログインした自分の情報を取得する
ベンチマークシナリオ
今回は Locust を用いてシナリオを書くことにしました。複雑なシナリオにはしないので、ファイル一つで簡単に記述+一応ワーカーをスケール出来ることから。 k6 でも別によかったかなと思っています。
監視
多分今一番有名な Prometheus + grafana パターンを採用しました。
最近はたくさんのダッシュボードテンプレートが公開されていて、ボタン一つでダッシュボードをインポート出来るのですね。ワンクリックでかなりそれっぽいダッシュボードが生成出来てとても感動しました。
今回は Docker and system monitoringby Thibaut Mottet テンプレートを使って監視しました。
実施結果
Locust の結果グラフが最もシンプルにわかりやすいです。
- Apache, nginx unit, RoadRunner は安定して動作した。
- php-fpm は失敗がいくつかあった。
- RoadRunner(大体 400 rps) は Apache(大体 200 rps) の倍速かった。
- nginx unit は特定のリクエスト数から線形にレスポンス時間が増えた。
RoadRunner が安定性・速度(rps)共に良好でした。
Apache + mod_php
Type ,Name ,Request Count ,Failure Count ,Median Response Time ,Average Response Time ,Min Response Time ,Max Response Time ,Average Content Size ,Requests/s ,Failures/s ,50% ,66% ,75% ,80% ,90% ,95% ,98% ,99% ,99.9% ,99.99% ,100%
GET ,/ ,10514 ,0 ,6 ,1868.1894447423442 ,3.615924999849085 ,144274.3889899998 ,11.0 ,45.972785616332224 ,0.0 ,6 ,6 ,7 ,7 ,13 ,22 ,48000 ,93000 ,108000 ,144000 ,144000
POST ,/login ,10236 ,0 ,60 ,63.19641408714408 ,53.364038999916374 ,192.5710179998532 ,99.0 ,44.757222138936335 ,0.0 ,60 ,62 ,63 ,64 ,68 ,83 ,120 ,130 ,170 ,190 ,190
POST ,/register ,10241 ,0 ,67 ,467.58879590694096 ,57.37784000029933 ,141931.2456470002 ,82.0 ,44.779084791407485 ,0.0 ,67 ,70 ,73 ,74 ,82 ,97 ,130 ,140 ,110000 ,141000 ,142000
GET ,/user ,10236 ,0 ,9 ,10.539792544645117 ,6.357847000344918 ,166.13893800013102 ,18.0 ,44.757222138936335 ,0.0 ,9 ,10 ,11 ,11 ,13 ,16 ,20 ,25 ,95 ,160 ,170
,Aggregated ,41227 ,0 ,55 ,610.8978216214371 ,3.615924999849085 ,144274.3889899998 ,52.22373687146773 ,180.2663146856124 ,0.0 ,55 ,61 ,64 ,66 ,72 ,81 ,120 ,140 ,105000 ,138000 ,144000
- 失敗はない。
- 稀に極端に遅くなる。
nginx + php-fpm
Type ,Name ,Request Count ,Failure Count ,Median Response Time ,Average Response Time ,Min Response Time ,Max Response Time ,Average Content Size ,Requests/s ,Failures/s ,50% ,66% ,75% ,80% ,90% ,95% ,98% ,99% ,99.9% ,99.99% ,100%
GET ,/ ,12871 ,4362 ,1200.0 ,1956.3762488766995 ,1.012581999930262 ,60016.234768 ,7.387304793722321 ,55.899288209991994 ,18.944347383418933 ,1200 ,2800 ,3300 ,3400 ,4400 ,5500 ,8500 ,12000 ,36000 ,60000 ,60000
POST ,/login ,8347 ,342 ,2800.0 ,2406.8120073027426 ,1.0153160001209471 ,60010.91787000019 ,94.96369953276627 ,36.25136809018749 ,1.4853202212584307 ,2800 ,3300 ,3400 ,3500 ,3900 ,4700 ,5400 ,5600 ,12000 ,60000 ,60000
POST ,/register ,12704 ,4208 ,1300.0 ,1749.8805279032588 ,0.9263499998724001 ,60010.61475999995 ,54.87822732997481 ,55.17400026569329 ,18.275518979694375 ,1300 ,2900 ,3300 ,3400 ,3600 ,4500 ,5200 ,5600 ,20000 ,60000 ,60000
GET ,/user ,8189 ,520 ,2700.0 ,2319.045340666502 ,0.93426500006899 ,60008.57017099997 ,16.918182928318476 ,35.56516752013243 ,2.258381622966035 ,2700 ,3200 ,3400 ,3400 ,3700 ,4600 ,5300 ,5600 ,19000 ,60000 ,60000
,Aggregated ,42111 ,9432 ,2000.0 ,2053.889079942271 ,0.9263499998724001 ,60016.234768 ,40.926598750920185 ,182.8898240860052 ,40.96356820733777 ,2000 ,3200 ,3400 ,3400 ,3800 ,4700 ,5700 ,7700 ,22000 ,60000 ,60000
Method ,Name ,Error ,Occurrences
GET ,/user ,"ConnectionResetError(104, 'Connection reset by peer')" ,24
POST ,/login ,"ConnectionResetError(104, 'Connection reset by peer')" ,18
POST ,/register ,CatchResponseError('Invalid code: 0') ,4205
GET ,/ ,RemoteDisconnected('Remote end closed connection without response') ,3755
POST ,/login ,RemoteDisconnected('Remote end closed connection without response') ,323
POST ,/login ,HTTPError('504 Server Error: Gateway Time-out for url: http://laravel-02-fpm-nginx/login') ,1
GET ,/ ,HTTPError('502 Server Error: Bad Gateway for url: http://laravel-02-fpm-nginx/') ,2
GET ,/user ,HTTPError('504 Server Error: Gateway Time-out for url: http://laravel-02-fpm-nginx/user') ,3
GET ,/ ,HTTPError('504 Server Error: Gateway Time-out for url: http://laravel-02-fpm-nginx/') ,7
GET ,/ ,"ConnectionResetError(104, 'Connection reset by peer')" ,598
POST ,/register ,CatchResponseError('Invalid code: 504') ,3
GET ,/user ,RemoteDisconnected('Remote end closed connection without response') ,493
- いくつか失敗があった🦴
- レスポンス時間中央値が遅い。
nginx unit
Type ,Name ,Request Count ,Failure Count ,Median Response Time ,Average Response Time ,Min Response Time ,Max Response Time ,Average Content Size ,Requests/s ,Failures/s ,50% ,66% ,75% ,80% ,90% ,95% ,98% ,99% ,99.9% ,99.99% ,100%
GET ,/ ,1994 ,0 ,14000.0 ,13609.744020604317 ,6.798829999979716 ,27302.611280000063 ,11.0 ,9.715961699020356 ,0.0 ,14000 ,18000 ,21000 ,22000 ,25000 ,26000 ,27000 ,27000 ,27000 ,27000 ,27000
POST ,/login ,1492 ,0 ,14000.0 ,13531.813830516086 ,54.17121999994379 ,27313.352868000038 ,99.0 ,7.269917179006204 ,0.0 ,14000 ,18000 ,21000 ,22000 ,26000 ,26000 ,27000 ,27000 ,27000 ,27000 ,27000
POST ,/register ,1717 ,0 ,14000.0 ,13560.566945165408 ,61.25557099994694 ,27387.015026000197 ,82.0 ,8.36625187423167 ,0.0 ,14000 ,18000 ,21000 ,22000 ,25000 ,26000 ,27000 ,27000 ,27000 ,27000 ,27000
GET ,/user ,1293 ,0 ,14000.0 ,13513.131971850735 ,7.905277999952887 ,27341.067264000005 ,18.0 ,6.300270048562347 ,0.0 ,14000 ,18000 ,21000 ,22000 ,25000 ,26000 ,27000 ,27000 ,27000 ,27000 ,27000
,Aggregated ,6496 ,0 ,14000.0 ,13559.616517344053 ,6.798829999979716 ,27387.015026000197 ,51.37161330049261 ,31.652400800820576 ,0.0 ,14000 ,18000 ,21000 ,22000 ,25000 ,26000 ,27000 ,27000 ,27000 ,27000 ,27000
- 失敗はない。
- レスポンス時間中央値が遅い。
- スケールしてなさそう(CPU 制限がちゃんと効いている?)
RoadRunner
Type ,Name ,Request Count ,Failure Count ,Median Response Time ,Average Response Time ,Min Response Time ,Max Response Time ,Average Content Size ,Requests/s ,Failures/s ,50% ,66% ,75% ,80% ,90% ,95% ,98% ,99% ,99.9% ,99.99% ,100%
GET ,/ ,22661 ,0 ,620.0 ,686.0100739353505 ,1.7283189999943716 ,1820.2351189997898 ,11.0 ,98.1907207658494 ,0.0 ,620 ,1000 ,1200 ,1300 ,1400 ,1500 ,1500 ,1600 ,1700 ,1800 ,1800
POST ,/login ,22310 ,0 ,680.0 ,739.8630925711354 ,48.17444099990098 ,1857.2384980002425 ,99.0 ,96.66982835206302 ,0.0 ,680 ,1100 ,1300 ,1400 ,1500 ,1500 ,1600 ,1600 ,1800 ,1900 ,1900
POST ,/register ,22492 ,0 ,690.0 ,755.3494069234396 ,52.70074300005945 ,1869.7350940001343 ,82.0 ,97.45843923328559 ,0.0 ,690 ,1100 ,1300 ,1400 ,1500 ,1600 ,1600 ,1600 ,1800 ,1900 ,1900
GET ,/user ,22161 ,0 ,630.0 ,689.2725135808386 ,2.4813560003167368 ,1820.1287659999252 ,18.0 ,96.02420735589729 ,0.0 ,630 ,1000 ,1300 ,1400 ,1400 ,1500 ,1500 ,1600 ,1700 ,1800 ,1800
,Aggregated ,89624 ,0 ,660.0 ,717.6237047520526 ,1.7283189999943716 ,1869.7350940001343 ,52.45473310720343 ,388.3431957070953 ,0.0 ,660 ,1000 ,1300 ,1400 ,1500 ,1500 ,1600 ,1600 ,1800 ,1900 ,1900
- 失敗はない。
- 388 rps と良好な速度。
感想
まず 4 ランタイムに対してちゃんと動く Dockerfile を作るのに少しかかりました。最適化の余地はありますが、テンプレートの一つとして動作するのではないでしょうか。
Prometheus + Grafana による監視は初めてだったんですが、 prometheus は up するだけで良い感じに動くし、 Grafana のテンプレートインポート機能が優秀すぎて 「もう監視全部これでいいんじゃないか...?」 と思いました。
実行結果を見て、 Apache が安定して枯れているのは想定していましたが、未チューニングの nginx + php-fpm ではあまり負荷をさばけない所が新発見でした。
nginx unit は json で簡単に環境を用意できますが、今回の要件ではうまくリクエストをこなしてくれませんでした。 こちらの参考記事 では良い感じで動いているみたいなので、設定の問題かもしれません。
RoadRunner は安定して高速でした。プロセス間通信というかなり枯れた技術を用いて、 golang と php-cli プロセスで通信を行って処理するパターンが今回は最速になりました。
動作検証は 10 分程度しか行わなかった&Windows 10 ホスト機で動かしていたため、 AWS や GCP に上げて同じことを試してみたらまた違う結果になると思います。
ランタイム選定の参考になれば幸いです。
Discussion