stefafafan の fa は3つです

"すてにゃん" こと id:stefafafan のブログです

ISUCONの素振りとして private-isu をPHP実装で解いてみた (約50万点まで)

先日はISUCON14に参加しましたが*1、その直前まで素振りがてら catatsuy/private-isuPHPで解いていました。普段ISUCONはGo言語で参加していますが、他言語での挑戦にも興味がでてきたため、PHPでやってみることにしました。

実際のコードと作業ログは以下のリポジトリにまとめてありますので、これから試してみたいという方の参考になればとても嬉しいです。


前提

private-isu の執筆時点のREADMEに記載されている「推奨インスタンスタイプ」のインスタンスを競技用とベンチマーカー用で1台ずつ建てています ( c7a.largec7a.xlarge )。

github.com

以前Go実装で挑戦したことがありますが、この時のインスタンスタイプは c6i.largec6i.xlarge だったようなので、点数について単純比較はできなさそうです。

blog.stenyan.jp

デプロイ

今回は rsyncwebapp/php ディレクトリを丸ごとデプロイするという方式を取りました*2

GitHubにあげているコードは .gitignorevendor 含めているので、GitHubからpullしてアプリケーションを動かしたい場合は毎回 composer install が必要になります。 rsync でデプロイする場合は手元の環境で既に composer install が済んでいればあとは丸ごとデプロイするだけなのが今回の利点でした。

Makefileに以下のコードを記載することで、 make deploy-app を実行するだけでアプリケーションをデプロイできるようにしました。

deploy-app:
	rsync -av webapp/php/ isu01:~/private_isu/webapp/php/

プロファイリング

今回は tkuchiki/alppt-query-digest で遅いエンドポイントや遅いクエリを分析しました。

その上でアプリケーションのボトルネックをさらに分析するために、Go言語でISUCONに挑戦する際に活用してきた pprof による Flamegraph を利用したアプリケーションの負荷の可視化と同じようなものが使いたくなり、調査しました。

参考にした資料一覧:

はじめは xhprof を使う方式を検討して少し試していましたが、最終的に Reli を使うことにしました。

Reliの利点はアプリケーションコードに手を入れずに使えることです。Go言語のpprofを使う場合は毎回アプリケーションコードに手を入れていたことを考えると、Reliはあまりにも便利で驚きました。おすすめします。

private-isu で Reli を使う

簡単に private-isu で Reli を使って FlameGraph を出力する方法を紹介します。

まずはインスタンスにReliを入れます。

composer create-project reliforp/reli-prof
cd reli-prof

動いているプロセスの名前を確認しておきます。

$ ps auxfw | grep fpm
root         594  0.0  0.6 209068 23900 ?        Ss   11:31   0:00 php-fpm: master process (/etc/php/8.3/fpm/php-fpm.conf)
isucon      2071  0.1  0.4 209632 19228 ?        S    11:49   0:00  \_ php-fpm: pool www
isucon      2150  0.0  0.4 209632 19332 ?        S    11:49   0:00  \_ php-fpm: pool www
isucon      2181  0.0  0.4 209632 18792 ?        S    11:50   0:00  \_ php-fpm: pool www
isucon      2214  0.0  0.0   7076  2176 pts/0    S+   11:51   0:00              \_ grep fpm

i:daemon というサブコマンドを利用して、ベンチ実行中にトレースを取得します。結果をファイルに保存しておきます。

sudo ./reli i:daemon -P "php-fpm" > traces.log

ベンチマークの実行が終わったら c:flamegraph サブコマンドを利用してFlameGraph形式の結果をsvgファイルに書き出します。

./reli c:flamegraph < traces.log > traces.svg

最後にローカル環境にsvgファイルをダウンロードしてブラウザで開いてみると、FlameGraphが表示されます。

rsync isu01:~/reli-prof/traces.svg ./
FlameGraphの様子

設定ファイルの確認・アプリケーションエラーログの出力

private-isu のマニュアルでは nginx と php8.3-fpm.service のログを確認することが案内されています。

php-fpmの設定については、/etc/php/8.3/fpm/以下にあります。

エラーなどの出力については、

$ sudo journalctl -f -u php8.3-fpm
$ sudo tail -f /var/log/nginx/error.log

などで見ることができます。

private-isu/manual.md at 6de65501fa86f0cca01ac70ac3be18ce65703bd3 · catatsuy/private-isu · GitHub

実際触っていると php8.3-fpm.service をみていても正直ほしい情報はそんなに流れてこなくて、基本 nginx のエラーログをみて判断する感じになります。

php.ini にエラーログの設定を追加することによって、アプリケーションのエラーログを書き出すことができるようになります。
php.ini の場所はアプリケーション中(例えばトップページを表示するロジックの中で) phpinfo(); を一時的に追加しブラウザから確認しました。

private-isuの場合、PHP 8.3 向けの FPM の設定ファイルは /etc/php/8.3/fpm/php.ini にありました。設定ファイル内に error_logコメントアウトされていました。デフォルトではエラーログの出力先は空なので、お好きなパスを指定してあげます。

; Log errors to specified file. PHP's default behavior is to leave this value
; empty.
; https://php.net/error-log
; Example:
- ;error_log = php_errors.log
+ error_log = /var/log/php_errors.log

ファイルを一応作っておきます。

sudo touch /var/log/php_errors.log
sudo chmod 664 /var/log/php_errors.log
sudo chown isucon:isucon /var/log/php_errors.log

systemctlの再起動をして反映します。

sudo systemctl daemon-reload
sudo systemctl restart php8.3-fpm.service

これでエラーが起きた際に指定したファイルにアプリケーションのログが出力されるようになります。

sudo tail -f /var/log/php_errors.log

PDO::ATTR_PERSISTENT や composer autoloader optimize の設定

PHP (PDO) でデータベースの接続をプールして使い回すには PDO::ATTR_PERSISTENT を設定する必要があります。

    return new PDO(
        "mysql:dbname={$config['db']['database']};host={$config['db']['host']};port={$config['db']['port']};charset=utf8mb4",
        $config['db']['username'],
-        $config['db']['password']
+        $config['db']['password'],
+        array(PDO::ATTR_PERSISTENT => true)
    );

また、 composer の autoloader に対しても最適化をするための設定があるようです。 以下のように "optimize-autoloader": true,composer.json に追加しておくと良いでしょう。

{
+   "optimize-autoloader": true,
    "require": {
        "slim/slim": "^4.7",
        ...
    }
}

composerのサイトを見に行くとトレードオフは基本的になく、本番環境では有効化しておくことをおすすめされているのでISUCONでもとりあえずやっておくのは良いと思います。

There are no real trade-offs with this method. It should always be enabled in production.

Autoloader optimization - Composer

JITの有効化

PHP 8.0 からはJITコンパイラを有効化することでパフォーマンス向上を見込めます。

phpinfo() の出力結果をみると、 JITの設定は opcache の中にあるようで、 opcache は別途設定ファイルがあるようです。

PHP 8.3の場合は /etc/php/8.3/mods-available/opcache.ini に設定ファイルがあったので、以下のような設定を追加しJITを有効化しました。

; configuration for php opcache module
; priority=10
zend_extension=opcache.so
+ opcache.enable_cli=1
+ opcache.jit=on
+ opcache.jit_buffer_size=100M

詳しくはこちら:

www.php.net

PHP 8.4へのバージョンアップ

PHP-FPMを扱ったままPHPのバージョンをアップグレードすることもできました。

参考: PHP 8.4 Installation and Upgrade guide for Ubuntu and Debian • PHP.Watch

バージョンの上げ方としては、各種パッケージをインストールし、

sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install php8.4-cli php8.4-fpm php8.4-mysql

php8.3-fpm を停止、 php8.4-fpm を有効化する、

sudo systemctl stop php8.3-fpm.service
sudo systemctl disable php8.3-fpm.service
sudo systemctl start php8.4-fpm.service
sudo systemctl enable php8.4-fpm.service

FPMのプールの設定が /etc/php/8.3/fpm/pool.d/www.conf にあったので 8.3 と 8.4 の差分を確認して揃えます。

- user = www-data
- group = www-data
+ user = isucon
+ group = isucon
...
- listen = /run/php/php8.4-fpm.sock
+ listen = 127.0.0.1:9000

再起動します。

sudo systemctl restart php8.4-fpm.service
sudo systemctl restart nginx.service

nginxの設定がlocalhostの9000番に向いているままであれば設定をいじらなくてもそのままPHP 8.4の状態でつながります。

Unix domain socket を使うように変更したい場合はこの www.conf の設定と nginx の設定を合わせて変更する必要がありました。

github.com

PHP-FPMを捨ててRoadRunnerへ (移行失敗)

徐々にPHP-FPMがボトルネックになっていることに気づき、以下のスライドを参考にRoadRunnerへの移行を試みました。

結論としては結構大変で諦めてしまいました。その代わり、RoadRunnerへ移行するために必要な手順を理解することができました。

  • RoadRunnerの設定ファイル (rr.yaml) の追加
  • RoadRunner向けworkerファイルの追加 (worker.php)
  • workerファイルから既存のDI ContainerやRoutesの設定をハンドリングできるようにコードを調整
  • Ubuntu上にRoadRunnerをインストールし、systemd向けのユニットファイルを定義 (デーモン化)
  • PHP-FPMデーモンの停止・無効化。RoadRunnerの起動・有効化
  • nginxの設定で既存のFastCGIからRoadRunnerの設定を参照するように変更

ブラウザ上で "なんとなくprivate-isuが動く" 状態にできましたが、よくよく見てみると画像アップロード周りで壊れていたり、複数workerに対応できなかったりしてベンチマーカーがエラーを返してしまいました。以下のPull Requestが試していた様子となります。


点数遷移

細かいFlameGraphの様子などは以下のドキュメントにまとめていますが、ここでもざっとやった改善と点数の推移を記載します。

github.com

以下みていくと、ISUCON本*3でも紹介されている改善を愚直にやると点数がどんどんあがっていって、PHPっぽい改善で効果を感じたのは「PDO::ATTR_PERSISTENTの設定」くらいでした。PHPバージョンアップやJIT有効化を実施してみたものの、他の部分がボトルネックになっていたのであまり効果は感じられなかったのかもしれません(RoadRunner移行が達成できれば更なる点数の上昇は期待できたと思います)。

実施内容 点数
初回ベンチマーク (Ruby実装) 1026
PHP実装に切り替え 3288
各種計測ログや計測を仕込んだ後 2802
commentsテーブルへindex追加 27353
ユーザの画像をnginxから配信 57835
トップページから呼んでいるクエリの全件取得をやめる + ORDER BY狙いINDEX追加 93054
他のmake_posts周辺クエリも同様に改善する 112481
make_posts内のpostのuserを引いてくるクエリをなくす 124309
make_posts内のcomment_countのN+1改善 141431
MySQLのbinlog無効化 153314
make_posts内のcommentsのN+1解消 188721
make_posts内の最後のN+1解消 226115
外部コマンド呼び出しをやめる 257402
comments.user_id へのINDEX追加 272468
INDEXの追加とFORCE INDEXの指定 326587
MySQLの設定チューニング 320129
静的ファイルをnginxから配信 323388
nginxのworker_connections増やしたりgzipの設定を追加したり 322611
ベンチマーカーインスタンスのファイルディスクリプタを増やす 327821
opcache.jit 有効化 329357
PHP 8.4 へバージョンアップ (&& reliプロファイラ実行をやめる) 438270
PHP 8.4でもJIT有効化を試みる 433228
PHP 8.3に戻してプロファイラ実行せずにベンチ回してみる 431496
PHP 8.3に戻してプロファイラを再び実行する 344578
PDO::ATTR_PERSISTENT を設定する 371402
composer autoloader optimize 設定する 370456
Unix domain socket を使うようにする 377267
各種パラメータ調整、PHP8.4に戻す、ログを無効化 533117
MySQL 8.0 → 8.4 へアップグレード 502465
MySQL 8.4 → 9.1 へアップグレード 498680

感想

素振りを進める中で思ったこととしては、結局プログラミング言語として何を選んだとしても大きな影響はなさそうということです。昨今ISUCONにGo言語で挑戦する人が多いですが(私含め)、ボトルネックはデータベースやアプリケーションロジックがメインなので、手に馴染んだ言語で挑戦するのが良さそうだなと改めて気付かされました。

与えられた時間が無制限にある場合Go言語は最終的にテンプレートの生成がボトルネックになっていくのをprivate-isuに前回挑んだ時に感じましたが、PHPの場合はPDOのコンストラクタを作るところに行き着く(なのでPHP-FPMをやめよう)という部分に言語特有の違いが感じられて面白かったです。ただ、ISUCON当日にここまでたどり着く時間的余裕はなかなかなさそうなのでそんなに「PHP-FPMからの移行の素振り」とかは気にしなくてもいいかもと思ったりしました。

アプリケーションの負荷やエラーログの確認方法はエンバグしたときのデバッグに不可欠な要素なので、その点だけしっかりと見直しておけると良いと思います。