Apache mpm_prefork と PHP-FPM の効率の違い

こんにちは。福岡です。

Apache mpm_prefork + mod_php と mpm_event + PHP-FPM を使ったときの、同時アクセス数が増えたときの効率を調査しましたので紹介します。

WebClass はWebサーバにApacheと、スクリプトに PHP を用いています。同時多数のアクセスを扱うためのチューニングは常に問題です。同時多数のアクセスに立ち向かうため、Apache は今では mpm_event をデフォルトとしていますし、PHP も PHP-FPM を使うことで mpm_event と組み合わせることができます。PHP-FPM も正式機能になって10年経つので、すでに利用されている環境もかなりあると思います。

ネットの記事などを読んで MPM_EventとPHP-FPM が仕組みとして mpm_prefork より効率が良さそうだというのはわかっていたのですが、定量的にどこがどれくらい良いのか、いまいちしっくりくる説明を見たことがありませんでした。なので、今更かもしれませんが、測ってみました。

ここでは具体的に、以下について調査しました。

  • 同じ同時アクセスを受けた時、mpm_prefork とくらべて、PHP-FPM はどれくらい効率的に、もしくは安定してリクエストを処理できるか
  • プロセスの立ち上げ速度に差はあるか

結果は以下でした。

  • 用意したPHPのプロセス数よりも多くの同時アクセスを受けている時には PHP-FPM のほうがレスポンスタイムを1/2倍にするくらい効率が良いことが分かりました。
  • プロセスを立ち上げる速度は Kernel や CPU 等の影響が大きく差がありませんでしたが、一気に同時アクセスを受けてからプロセスを立ち上げ始める初動では PHP-FPM のほうが良い動きでした。

ベンチマークに使う PHP プログラム

ベンチマークコマンドは以下のようにして単一URLに ab コマンドで同時アクセスをかけ、レスポンス時間を確認しました。

ab -c 300 -n 1000 http://10.0.0.104/webclass/wait1.php

アクセス先PHPプログラム wait1.php は、1±0.25 [s] だけ待って 'OK' の文字を返します。想定として、プログラムファイルはキャッシュメモリ上に乗っていてIO負荷が殆どかからない、PHPのスクリプトとしても処理はDBサーバからの結果を待つだけでほとんどやることがない状況を想定しています。

<?php
function msleep($milliseconds) {
    return usleep($milliseconds * 1000); 
}
$interval = 1000;
$var = 500; 
msleep(1000 + (rand(0, $var)-$var/2));

echo 'OK';

したがって、スクリプトの処理時間はCPUの負荷状況に影響を受けにくく、CPUが空いていても1秒より早く応答は返さないし、CPUに多少負荷がかかっていてもスクリプトの処理自体はあまり時間が延びません。

このプログラムに対するアクセスのレスポンス時間を計測し、どれだけ1sから離れるかを比較することで、Apache と PHP-FPM の効率だけを比較・評価できると考えました。

計測環境

AWS EC2 インスタンス(m4.large) でDebian 10 + WebClass 11.11.3 を構築しました。

  • CPU Intel® Xeon CPU E5-2686 v4 @ 2.30GHz x2
  • Mem 8G + Swap File 3G
  • Debian 10.9
    • Apache 2.4.38 (Debian)
    • PHP 7.3.29-1~deb10u1

同一VPC内に m4.large のDebian10インスタンスを構築し、http プロトコルで ab コマンドによるベンチマークを計測しました。

  • CPU Intel® Xeon CPU E5-2686 v4 @ 2.30GHz x2
  • Mem 8G
  • Debian 10.9
  • Ab 2.3 Rev.1843412

計測中にはどちらのサーバでも Swap と Hardware Interrupt(Steal)が発生しないのを確認してあります。

計測環境チェック

wait1.php はほぼ待つだけなので、同時アクセスが多数発生してもCPUはあまり消費せず、ApacheとPHPの同時アクセス・複数プロセスのマネジメントの効率が良いほど応答時間は1sに近づくはず、という前提を確認します。

この前提条件の確認のため、mpm_prefork とphp7.3-fpm のそれぞれについて、300 プロセス用意した状態で100同時アクセスをかけて応答時間が1s程度になることをチェックしておきました。

ab -c 100 -n  10000 http://10.0.0.104/webclass/wait1.php

MPM_Prefork + Mod_php 7.3 (MaxRequestWorkers=300 ) の結果

Mpm_prefork のケースでは、Apache のMPM設定は以下のようにしました。

StartServers   300
MinSpareServers 300
MaxSpareServers 300
MaxRequestWorkers 300
ServerLimit    300

abコマンドの結果は以下です。

Concurrency Level:      100
Time taken for tests:   101.782 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2260000 bytes
HTML transferred:       20000 bytes
Requests per second:    98.25 [#/sec] (mean)
Time per request:       1017.816 [ms] (mean)
Time per request:       10.178 [ms] (mean, across all concurrent requests)
Transfer rate:          21.68 [Kbytes/sec] received

応答時間が1sで、CPU負荷もほとんどかからなかったため、問題なさそうです。

MPM_event + php7.3-fpm (max_children=300 ) の結果

Mpm_event のケースでは、Apache のMPM設定は以下のようにしました。スレッドを100ずつ増やしていき、上限も2000にしてあるので、プロセスの起動とスレッド上限があまり影響しないようにしています。

StartServers      2
MinSpareServers 100
MaxSpareServers 100
ThreadLimit        300
ThreadsPerChild  100
MaxRequestWorkers 2000

PHP-FPM のプロセスモードは以下の設定です。

pm = dynamic
pm.max_children = 300
pm.start_servers = 50
pm.min_spare_servers = 20
pm.max_spare_serves = 50

なお、この設定は Prefork に揃えるには static にすればよかったと後で気づきました。

abコマンドの結果は以下です。

Concurrency Level:      100
Time taken for tests:   103.935 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2300000 bytes
HTML transferred:       20000 bytes
Requests per second:    96.21 [#/sec] (mean)
Time per request:       1039.351 [ms] (mean)
Time per request:       10.394 [ms] (mean, across all concurrent requests)
Transfer rate:          21.61 [Kbytes/sec] received

dynamic/static の点は気になりますが、一応は応答時間が1sで、CPU負荷もほとんどかからなかったため、問題なさそうです。

レスポンス時間の計測結果

Prefork と PHP-FPM のワーカ数上限と、同時アクセス数を変えながら ab コマンドでレスポンス時間をプロットしたのが以下です。

グラフの凡例の名前「Prefork」もしくは「PHP-FPM」の横に書いた数字が、それぞれに設定したワーカーの上限です。

wait1.php は1±0.25s 待つだけなので、同時アクセスが多数発生しても CPU に掛かる負荷はわずかでした。ですので、MPM と PHP-FPM の効率がレスポンスタイム1sよりどれくらい上に離れてくか、に反映されると考えられます。1sでフラットなグラフになるほど、同時アクセスが増えても効率が落ちないことを意味します。

MPM_Prefork は、用意したプロセス未満の同時アクセスに対しては非常に効率が良いです。しかし、プロセス数を超える同時アクセスに対しては、著しく効率が悪化し、ab はエラーを計上することもありました。MaxRequestWorkers 900 で同時アクセス1500のケースでもCPUの利用時間は大して増えませんが、Load Average は 9.3 ほどになりました。エラーを計上したところより上の同時アクセス数はデータを取っていません。

PHP-FPM はプロセス数未満の同時アクセスに対して効率が良いのはMPM_Preforkと変わりません。一方、同時アクセス数がプロセス数よりも増えた時は、MPM_Prefork よりもレスポンス時間を1/2 程度に保てました。Load Averageは1いくか行かないか程度で、CPU使用率は低いままです。

プロセス数が足りない方に寄った計測をしているため、どちらの方式でもプロセス数を増やすことによる効率の悪化の様子は観察できませんでした。ただし、実際の運用ではプロセス数が増えるとスクリプトによるCPU利用量も増えるため、プロセス管理の効率の問題以前にCPUがボトルネックになることも考えられます。

プロセスの立ち上がり速度の計測

MPM_Prefork も PHP-FPM もワーカとしてプロセスを立ち上げます。一般にプロセスのフォークは重い処理になります。また、待機しているワーカ数よりも多くの同時アクセスがワッと来た時の反応にも影響します。WebClassですと、10時半から2限では試験をします!というとき、授業の開始時刻で一斉に学生が操作し始めるので侮れないパラメータになります。

2つの環境のそれぞれに、ワーカの最大値を500に、初期待機ワーカ数を50にした状態で待機させ、ab コマンドで同時500アクセスをかけました。そこからプロセス数を1秒単位で数えていって、500に達したところまでをグラフにしています。

グラフの凡例の名前「PHP-FPM」の横に書いた数字は min_spaire_servers の値です。最初は10にして計測したところ著しくプロセス起動が遅かったので、50の結果を加えてPreforkと比べられるようにしています。なお、これ以上値を増やしても、もしくは同時アクセス数を増やしても、速度は変わりませんでした。

プロセスの立ち上げ速度(グラフの傾き)は MPM_PreforkとPHP-FPM50で差がありません。これは Kernel や CPU などのもっと下のレイヤーの性能によるものと思います。ですが、同時アクセスが急にきてからプロセスを増やし始める反応はPHP−FPMのほうが良く、500プロセス揃うまでの時間に3sの差が出ました。その結果、平均応答時間は初動の速いPHP-FPM のほうが短くなりました。

CPUへの負荷を見ると、MPM_Prefork はプロセス上限に至るまでは CPU も Load Averageも低いままですが、PHP-FPM のほうはCPU使用率と Load Average が Prefork よりも高くなりました。もしPHPにそれなりの処理が有る時、PHP-FPMのプロセスの立ち上げにどれくらい影響するかちょっと気になります。

まとめ

まずは基本的なところの違いを知りたいと思って、できるだけプロセス管理効率に的を絞った計測をしてみました。

MPM_Event については、PHP-FPMの計測でほぼ無視できるほどサクサクと1500同時リクエストなどを抱えてくれていて、数字では示せませんでしたが流石だなと思いました。そのおかげで、PHP-FPM はワーカ数を絞っていても効率的に処理できるようになっていると思います。PHPのプロセス数を絞ることができれば、接続先のDBのワーカ数も減らすことができます。コア数とメモリが限られる環境での安定性に繋がります。また、この安定性を保ったまま、CPU とメモリを増やした分だけワーカ数を増やしていければ、1台のプロセッサでカバーできる同時アクセス数も増やしやすいかもしれません。このあたり、今後詰めていきたいところです。

Debain 11 と unattended-upgrades

Debian 11 が8月にリリースされました。

PHP 7.4 + PostgreSQL 13 になっています。WebClass の対応状況は検証中ですが、構成はあまり変わっておらず、PHPもPostgreSQLも下位互換性のない変更はあまり無さそうです。

Debian 10 からですが、Install CD からインストールすときにパッケージの自動更新を有効にするかどうか聞かれます。このプログラムは unattended-upgrades といい、セキュリティアップデートなどを定期的にチェックして更新を適用します。

https://wiki.debian.org/UnattendedUpgrades

デスクトップ用途では便利かもしれませんが、サーバ用途ではサービスの再起動などがかかると夜に仕込んでいたバッチ処理に影響が出るなどするので、OFF にしています。

Install CDからインストールするとき確認画面で No を選択しているとパッケージもインストールされませんが、AWS 上のイメージを使ったときには最初から有効になっていたケースがありました。以下のコマンドで設定状況を確認して、もしあれば purge しています。

systemctl status unattended-upgrades
systemctl stop unattended-upgrades
systemctl disable unattended-upgrades
apt purge -y unattended-upgrades

Debian のaptコマンドは、インストールするときはプロセスを自動的に立ち上げてくれますが、remove/purge するときはプロセスを殺してくれません。なので、予めサービスを止めてしまいます。

Chrome 93 以降は 4週ごとにアップデートリリースになります

こんにちは。福岡です

Chrome browser のリリースサイクルが早くなるようです。これまでは6週サイクルでしたが、今日にリリース予定の 93 がリリースされた後は4週サイクルに変わります。

https://developer.chrome.com/blog/faster-release-cycle/

Microsoft Edge もいまは chromium ベースで、リリースサイクルは Chrome に合わせています。そのため、Edge の 4週サイクルになるそうです。

https://docs.microsoft.com/en-us/deployedge/microsoft-edge-release-schedule

でたばかりの頃のChrome は結構攻めた変更を入れてきた印象ですが、今は流石にシェアもあってわきまえている印象ですので、それで普段使う操作があれこれ変わるということはないと思います。それよりもセキュリティ対応を素早く提供してもらえる恩恵があると思います。

Chrome は新しい機能の実装もどんどん進められています。新しい機能は開発者向けに Origin Trails という、有効化しないと動かない隔離された実装として提供されています。それでフィードバックを集めたりしてこなれたものが標準機能に降りてくるのだと思います。

https://developer.chrome.com/blog/origin-trials/

https://developer.chrome.com/origintrials/

Firefox 91

Firefox Desktop Browser 91 が8月5日にリリースされました。

Release notes

Firefox 91 release notes

Firefox 91 release notes for developers

Pickup for WebClass

webclass に影響のある変更はありませんでした。

今後の Firefox のリリースについて

次回のリリースは2021年9月7日に予定されています。

Firefox Release Calendar

その他ブラウザのリリースはこちらにまとめています。

docker コンテナを使ってPHP のバージョンへの対応チェック

こんにちは。福岡です

ご無沙汰になってしまいましたが、最近に調整していた開発環境の話を紹介します。いろんなPHPのバージョンで動くか検査する方法です。

PHPバージョンの対応状況

WebClass は Debian と RHEL をサポートしています。具体的には、両者について OS の側でサポートされている期間の間、WebClass も動くように維持するというポリシーです。したがって、サポートする PHP バージョンもこれらOSが現在サポートしている範囲になります。

正直なところ、特に RHEL はメジャーバージョン単位のサポート期間が長くてしんどいです。RHEL 6 の PHP 5.3 はようやく去年にサポート・メンテナンス2が終了しました( REF:Red Hat Enterprise Linux 6 (RHEL6) のサポート終了と移行について)。これで 5.3 が消えると思いきや、RHEL 7 は PHP 5.4 !! 。PHP 5.4 の最初のリリースは 2012年 3月でして、これももう10年になろうとしています(REF: Version 5.4 changelog)。なお、Debian 11 が先週にリリース(REF)されたため、これから PHP 7.4 の対応等を進めていくことになります。

どうやって PHP 5.3 - PHP 7.4 で動くように確認するのか? これが問題です。

昔は、それこそ Debian 8 と Debian 9 と CentOS6 と、という感じで複数のサーバを構築していました。ですが、機能も増えてきている今の WebClass を1つ1つ手でチェックしていくというのは無理です。ベテランプログラマーはバージョン依存が出にくい PHP のコーディングスタイルを身につけていますが、新しく加わった人がそれを身につけるものしんどいし、レビューでもチェックしきれません。実際のところ、バージョン依存の問題をポロポロと出してしまっています。

そこで、検査と開発者へのフィードバックの機械化を進めています。

PHP Unit による自動テスト

WebClass ではアジャイルな開発スタイルを採用しています。開発作業の流れとしては、社内の GitLab を使って Issue => Merge Request => Merge を繰り返しています。そして、このイテレーションでは GitLab の継続的インテグレーション機能を使って、テストとパッケージングを機械化し、素早く完全な変更を繰り返していくことを意識しています。自動テストの一部は PHP Unit を使っており、継続的インテグレーション機能(Pipelie)は次のような感じです。

GitLab のPipeline

PHP Unit の実行環境として、それこそ少し前までは Debian のサーバを構築していました。その後にテスト用のサーバの維持などの問題で CI での自動テストの運用が止まってしまいましたが、そうするとやっぱり開発者が自分でテストをするというのは、どうしてもおろそかになります。そこで、CI-Runner が使う Php Unit 実行用の docker イメージを作り、メンテナンスが後手に回ったテストを直し、今では Merge Request を発行するたびに自動的に Php Unit の結果がフィードバックされるようになっています。

GitLab の MergeRequest で Pipeline の結果部分

PHP のバージョンごとにコンテナイメージを追加することで、複数のPHPバージョンで並行して自動テストを行うことができます。今は PHP5.6 と PHP 7.3 の環境で自動テストが走っています。

一方、Php Unit さえあればいいかと言うとそうでもありません。自動テストが実装されていないところはテストされないのが何より問題です。WebClass は2000年から開発スタートして、今まで完全な作り直しはありません。ですので、フレームワークのない状態から今まで拡張の傍らで必死にリファクタリングを進めて構造化し、後から自動テストスイートを加えて、そこからテスト可能な実装にリファクタリングしてテスト実装して、と走ってきました。まだテスタブルでないコードも、自動テストが実装されていないコードも残っています。

CI に PHP Linter 追加

Php Unit は自動テストが実装されないとコードを検査できませんが、PHP のコードの文法レベルであればすぐにスキャンできます。文法レベルの問題としては、例えば以下があります。

// PHP 5.4 から使える。それ以前のバージョンでは文法エラー
$array = [ 1, 2, 3];
// この書き方はどのバージョンでも使える
$array = array( 1, 2, 3 );

この程度と思いきや、文法エラーは PHP をロードした時点でエラーになって落ちるので、初期の require() などで1つでも混入すると機能が全く動かなくなります。ですので、検査する価値はあります。

PhpLinter のライブラリもいくつか公開されていますが、中を除くと基本は PHP に実装されている Lint 機能を使っているようです。この機能はコマンドラインで php -l test.php と実行して検査できます。ということは、 PhpUnit を動かせるコンテナであれば、composer とか phing を使うまでもなく以下のコマンドでチェックできます。

find $SRCDIR  \( -type f -name "*.php" -o  -name "*.inc" \)  -exec php -l {} \; | grep -v 'No syntax errors detected' 

ということで、準備しました!

PHP 7.4 で早速 PhpUnit が落ちています。また、UnitTest のJob に 7.0 などがいないのは、まだ調整中のためテストJob を入れたり外したりしているからです。

自動テストに失敗していれば、Merge Request 画面でもその結果が表示されて、マージできなくなります。

PhpUnit 失敗

テストターゲットのチューニング

Lint を実行するコマンドは、実は PHP のバージョンごとに用意してあります。

これは、大学に提供しているカスタム機能などがその大学での実行環境に依存していることが有るためです。このように、もともと特定の環境で動くことを想定しているコードがどうしてもあるのと、新しいバージョンに対応させていく優先順位はありますので、全てを一気にとは行きません。このタイムラグを解決するための苦肉の策です。

composer の overtrue/phplint などのツールもあり、phplint ツールのコンフィグファイルを書き分けるても有るのですが、それはやめました。理由は2つ。1つは、PHP バージョンごとに実行環境を分けているので、PHP バージョンが古いと composer の使いたいライブラリやバージョンを使えなかったりして返ってややこしい。1つは、php -l は Deprecated などの情報もバリバリ書き出してくれるので、将来のバージョン対応を見据えて Error ではない情報もチェックするときに便利なため。こういうときには良くも悪くも Bash は単純で、除外リストなど開発者がそのまま読んだり調整しやすいと思ったからです。

まとめと今後

PHP のバージョンごとのテスト用 Docker がひとまず揃いました。CI でも動かしますが、Docker なので開発者の手元でもすぐに動かせます。まずはこれで PHP の文法レベルの問題は未然に防げるようになると期待できます。

さらにテストの内容を充実させるために PHPUnit のテストはどんどん書き足していきます。そのため、Merge Request のレビューではテスト可能な設計になっているか、テストの内容は十分か、なども引き続き突っ込んでいきます。

また、レビューにおける脆弱性の検査もなかなか難しいため、GitLab の SAST 機能あたりに興味を持っています。よく見ると PHPについては PHP-CS の拡張パターンのようですが。

https://git.lan.datapacific.co.jp/help/user/application_security/sast/index

小さなチームでレガシーコードも抱えていますが、うまくやりくりして良くしていきます。