Slide 1

Slide 1 text

時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる 五十嵐 進士 / sji / sj-i / @sji_ch

Slide 2

Slide 2 text

自己紹介 @sji_ch SNS 上のアイコンは GitHub が自動生成した奴

Slide 3

Slide 3 text

生まれも育ちも仙台

Slide 4

Slide 4 text

PHP カンファレンス仙台とかやった けどもう 4 年も前

Slide 5

Slide 5 text

ふつうのサラリーマン 株式会社インフィニットループ仙台支社所属 スマホゲーのサーバサイドプログラマ

Slide 6

Slide 6 text

三歳児の父 かわいい 絵本好き だいぶ人間らしくなってきた

Slide 7

Slide 7 text

WEB+DB PRESS の現 PHP 連載担当 2021 年 6 月から WEB+DB PRESS の PHP 連載 だいたいわりと真面目な話をしてる 今回のトークが気になるような人には vol.128 、 129 、131 あたりオススメ

Slide 8

Slide 8 text

Agenda ISUCON について PHP でやってみた記録 得られた知見まとめ

Slide 9

Slide 9 text

ISUCON について

Slide 10

Slide 10 text

ISUCON とは Iikanjini Speed Up Contest の略 Web アプリケーションの性能改善大会 各種の言語で同内容の処理をする参考実装が与えられる ベンチマーカが負荷をかけ、処理性能でスコアを出す 3 人までのチームで参加し、サーバを数台与えられる レギュレーション内で性能改善 コード修正からミドルウェア・OS 変更までわりと何でもアリ 「ISUCON 」は、LINE 株式会社の商標または登録商標です。

Slide 11

Slide 11 text

ISUCON12 本選 ISU CONQUEST という放置ゲーのサーバ 時間で椅子を生産 椅子を売ってコインを得る コインでガチャを回す ガチャで装備を強化(生産力アップ)

Slide 12

Slide 12 text

ISUCON12 本選 仕事だ!舞台設定が完全に仕事(スマホゲー)だ! でも PHP の本選進出者がいない! 30 組のうち 26 組が Go 他は Node 、Ruby 、Rust 、Perl で 1 組ずつ PHP の参考実装は用意されている @okashoi さんの仕事 やるしかない

Slide 13

Slide 13 text

今回の挑戦方針について ISUCON12 本選の優勝者は NaruseJun というチーム Go 利用 341,258 点 本選と同等の環境で優勝者と同等のスコアを目指す 優勝者たちのミドルウェア設定や取り組みをパクリまくる ただし PHP で!

Slide 14

Slide 14 text

挑戦しない部分 時間は気にしない 本選は 10:00-18:00 の 8h ある程度時間のかかる取り組みでもやる 再起動試験対策は気にしない 細かい安定性や正確性も気にしない ベンチマーカーのスコアだけ追う PHP の限界へ近づくのに大事でない部分は割り切る

Slide 15

Slide 15 text

実行環境 本選 AMD EPYC 7763 ? アプリ: CPU 2 コア メモリ 4 GB * 5 ベンチマーカー: CPU 4 コア メモリ 8 GB * 1 今回 AWS EC2 c6a (AMD EPYC 7R13) c6a.large * 5 c6a.xlarge * 1 ストレージは EBS gp2 優勝リポジトリをデプロイすると 343,449 とか 349,547 とか 少し高いが本選スコア 341,258 とだいぶ似た数字

Slide 16

Slide 16 text

PHP でやってみた記録

Slide 17

Slide 17 text

俺のプロファイラ 自作の PHP プロファイラで随時計測しながら進める 慣れてるので楽 重いリクエストも重いクエリも見れる いざという時 VM 命令単体まで絞り込める https://github.com/reliforp/reli-prof

Slide 18

Slide 18 text

初期スコア: 644 他言語を試してもどれも大体同等の初期スコア xdebug 無効化でもほぼ変わらない ボトルネックがほぼ完全に DB 側 mysql や nginx 設定は優勝リポジトリを初手でパクった状態 この時点で本来の本選初期状態よりは改善されてる筈 デプロイの構成もパクり、各ホスト名を s1 〜 s5 に変更 基本的に優勝リポジトリのコミットログを真似して進める

Slide 19

Slide 19 text

DB のインデックス追加: 24439 user_present_all_received_history のインデックス追加 これだけでめっちゃ伸びる DB 設定パクって開始した影響が大きそう

Slide 20

Slide 20 text

プロファイルをとってみる generateID がボトルネック ユニーク ID の生成処理 DB でわけのわからんことをして ID を生成して いる UPDATE id_generator SET id=LAST_INSERT_ID(id+1) 成功するまでリトライ

Slide 21

Slide 21 text

Snowflake ID 導入: 30858 ユニーク ID の生成は DB 通さないほうがよい 衝突しづらい DB に優しい値を Web 側で生成 ULID や Snowflake など composer から Snowflake ID の生成器をインストール Go の優勝チーム記録(24498) より順調に伸びて幸先が良い 計測・ログ系をつけてないのが大きそう $ composer require godruoyi/php-snowflake

Slide 22

Slide 22 text

receivePresent N+1 修正: 33922 ↓延々コレ系 愚直にコード書き換え 生の PDO でやるのプレースホルダ的にめん どい ヘルパを作るとよい Go のコードは sqlx なので簡単そう 配列渡したらプレースホルダ作ってくれ る foreach ($list as $item) { $sql = 'SELECT * FROM tbl WHERE id=?'; $stmt = $this->db->prepare($sql); $stmt->bindValue(1, $item->id, PDO::PARAM_INT); $stmt->execute(); } $placeholders = implode( ',', array_fill(0, count($list), '?') ); $sql = "SELECT * FROM tbl WHERE id IN ({$placeholder $stmt = $this->db->prepare($sql); $pos = 1; foreach ($list as $item) { $stmt->bindValue($pos++, $item->id, PDO::PARAM_I } $stmt->execute();

Slide 23

Slide 23 text

DB ホスト分離: 45687 fpm のプール設定で clear_env=no env から環境変数で DB ホストを渡す DB アクセスがネットワーク越しになりレイテンシが増える 優勝チームはこの時点で 40,478 点

Slide 24

Slide 24 text

(おまけ) PDO::ATTR_EMULATE_PREPARES を切る: 36897 優勝チームは Go なので interpolateParams を指定 PDO では PDO::ATTR_EMULATE_PREPARES がデフォルト on で対策不要 一応切ってみるとわりと効果があるのがわかる プロファイルをとるとやはり prepare + execute の時間が大きい

Slide 25

Slide 25 text

インデックス修正: 48371 user_one_time_tokens のインデックス修正 遅めのクエリではあった 本当はこの段階でアイテム獲得処理が一番重い インデックス付け楽なのでシュッとやってた? 優勝チームは 3 人なので分業の結果っぽい

Slide 26

Slide 26 text

バルクアップデート化: 52373 user_presents のバルクアップデート化 優勝チームはこの時点で 49,602 点 この直後に計測系・ログ系を外して 53,037 点、特別賞 こちらは初手でログを外してるのでほぼ横並び

Slide 27

Slide 27 text

インデックス修正: 61256 user_presents と item_masters, user_sessions の index 追加 変わらずアイテム獲得処理がボトルネック

Slide 28

Slide 28 text

アイテム獲得処理から coin 切り出し: 65194 バルクアップデート化でクエリ削減

Slide 29

Slide 29 text

アイテム獲得処理から cards 切り出し: 66019 バルクアップデート化でクエリ削減

Slide 30

Slide 30 text

draw Gacha の N+1 修正: 66884

Slide 31

Slide 31 text

デッキ初期化の N+1 をなくす: 67607

Slide 32

Slide 32 text

アイテム獲得処理から残りの切り出し: 75336 一番重かったアイテム獲得処理が改善 ボトルネックが変わる PDO 接続や Ban / Session 、マスタ取得 この時点で優勝リポジトリは 89000 点ほど 処理系側へ負荷が移ってきたため

Slide 33

Slide 33 text

マスタ参照でのキャッシュ利用: 80022 symfony/cache で対応 PhpFilesAdapter で opcache のキャッシュ利用 参考: PHPerKaigi 2021 でPHP の不変配列が高速かつ省メモリだという話をしました 大粒のボトルネックが消えてきた そろそろシャーディングを入れる段階 優勝チームはマスタのキャッシュ利用とほぼ同時期に入れてる https://hnw.hatenablog.com/entry/2021/03/29/011242

Slide 34

Slide 34 text

シャーディング DB 2 台: 74640 !? のびねえ! ユーザ ID の剰余で接続先を振り分ける単純な実装

Slide 35

Slide 35 text

シャーディング DB 4 台: 全然変わらず 優勝チームはこの時点で 21 万点出してる この時点の構成は以下 s1 に nginx + fpm s2 〜 s5 に mysql JIT 有効にしても特に伸びず プロファイルを見てみる

Slide 36

Slide 36 text

PDO の生成コスト PDO の new がめちゃくちゃ嵩んでる 当初 persistent が効いてないのか疑う が、外すとちゃんとスコアが 5 万点台へ落ち る persistent 有効でも PDO の生成が遅い リクエストごとの生成を避けるには?

Slide 37

Slide 37 text

RoadRunner: 131353 SpiralScout の AltFPM Go 製の HTTP サーバがリクエストを受ける 通信待ちで無限ループする PHP CLI のワーカと パイプ通信 リクエスト間で情報を持ち越せる PDO インスタンスを使い回せる フレームワークの起動コストも消せる 調べつつハマりつつ 3h くらいで移行 Slim からの移行を素振りすると良さそう

Slide 38

Slide 38 text

Ban キャッシュ: 152704 php-di で持つ singleton オブジェクトのプロパティに配列を突っ込むだけ RoadRunner 導入によりワーカプロセス内で生きるキャッシュになる

Slide 39

Slide 39 text

JIT 再チャレンジ: 166629 Go 側とのスコア差から処理系性能の差が出てきたと判断 実際スコアが伸びた

Slide 40

Slide 40 text

checkViewer キャッシュ + ワーカ数調整: 180356 ユーザの端末 ID をオンメモリキャッシュ RoadRunner のワーカ数を 20 に 増やしたり減らしたり試した結果 あまり I/O バウンドでない状況を示してる

Slide 41

Slide 41 text

シャードキーの決定方法修正: 185120 Snowflake ID の下一桁はプロセスごとのカウンタ 複数プロセスが同タイミングに同じ剰余値を発行しやすい 同じDB への更新が重なりやすくなるため算出方法を変更

Slide 42

Slide 42 text

Ban とセッションの redis 利用: 183684 RoadRunner のワーカは別個に起動されるただの CLI プロセス opcache の SHM が共有されない プロセス間で共有できるメモリキャッシュを置きたい Redis を igbinary 付きで導入 接続は Unix domain socket DB アクセスを削れるがスコアが伸びない なお RoadRunner の kv plugin も試したが遅くなる

Slide 43

Slide 43 text

CPU がサチった vmstat はアイドル(id) 時間の消滅を示す user(us) と system(sy) 両方で食ってる Redis に回す CPU が余ってないので伸びない 激しいコンテキストスイッチ(cs) と割り込み(in) ---system-- ---cpu--- in cs us sy id 42871 39012 60 38 2 42980 41089 65 32 3 43729 40602 65 33 2

Slide 44

Slide 44 text

絶望的な perf stat の内訳 perf stat を見るとコンテキストスイッチと処理 系自体が重そう finish_task_switch.isra.0 __lock_text_start __softirqentry_text_start _emalloc zend_hash_find zend_hash_find_known_hash zend_array_destroy execute_ex _efree

Slide 45

Slide 45 text

どうする?わりと困った 一面では RoadRunner の実行モデル起因の限界 Go サーバとのパイプ通信に時間食ってそう それでもパイプは IPC の中では軽い…… 一面では PHP 処理系の限界 プロファイルでもわりと上のほうに VM 命令 単体が出てくる状況 利用元もスクリプトの特定行というより全 体に散在するものが多い 処理系は内部処理で PHP の配列と同じデータ 構造をよく使う 内部の配列操作自体をスクリプトから高速化 する手段はない

Slide 46

Slide 46 text

一応 Swoole も試した: 147015 一応は試した、が、ダメ PHP 処理の部分がボトルネックという前提が変わらない HTTP サーバ部分がワーカプロセスとパイプ通信する形態も同じ I/O をより効率的に行うために機構が複雑? CPU 効率ではむしろオーバヘッドが大きそう CPU 余ってると多分 Swoole のがいい? 何か下手をうって同期 I/O が混ざった可能性はある が、計測を見る限りたぶん改善しても大きくは伸びない

Slide 47

Slide 47 text

状況整理 もう余ってる CPU リソースがない 優勝チームの構成をなぞって Web 1 台 DB 4 台、なら DB サーバのリソースは余ってる 余ってる(DB サーバの) CPU で Web を回せばいいのでは? ここまで shared nothing な構成をあまり崩してないので可能 同一ワーカ内のリクエスト間でキャッシュを使ってる程度

Slide 48

Slide 48 text

立っているインスタンスは DB サーバでも使え!

Slide 49

Slide 49 text

RoadRunner 分散構成: 310198 s1 に nginx + rr + redis s2 〜 s5 に rr + mysql そもそも CPU 処理でネイティブコードの言語に 勝てないのは自然 PHP なんだから横に並べてスケールさせるでい いでしょ これで本選 2 位スコア(242,653 )を超える CPU 資源に余裕ができた

Slide 50

Slide 50 text

あとはプロファイル見ながら地道な改善

Slide 51

Slide 51 text

Redis に master_version 移行: 317157

Slide 52

Slide 52 text

obtainLoginBonus の N+1 対策: 335023

Slide 53

Slide 53 text

one_time_token の Redis 化: 346461

Slide 54

Slide 54 text

viewer ID check の Redis 化: 352674 やっと本選優勝スコアに勝利

Slide 55

Slide 55 text

負荷割合の変更 + PGO + PHP 8.2: 370253 おまけで PGO (Profile Guided Optimization) も試してみた が、負荷割合変更のほうが大きそう PHP 8.2 にするとメモリ消費量はちょっと減る

Slide 56

Slide 56 text

ここで打ち止め もっと時間を使えばもう少し伸ばせそう 予算と可処分時間が尽きてきたので一旦ここで打ち止め

Slide 57

Slide 57 text

知見まとめ

Slide 58

Slide 58 text

DB ネックの間は PHP でも Go でもほぼ変わらない

Slide 59

Slide 59 text

ボトルネックが言語・処理系側へうつっていくと (当然ながら)言語・処理系固有の知識が必要に

Slide 60

Slide 60 text

PDO での N+1 改善は多少煩雑 ヘルパを作るほうがよい

Slide 61

Slide 61 text

PDO の new は persistent でも遅くなる

Slide 62

Slide 62 text

RoadRunner や Swoole などの AltFPM は どうせ必要になるので 素振りして移行を手早く確実に

Slide 63

Slide 63 text

完全な I/O ネックでない負荷性質なら Swoole より RoadRunner の方が 性能が出る可能性がある

Slide 64

Slide 64 text

PGO は目に見えては効かないが JIT は I/O のボトルネックを解消していけば効く

Slide 65

Slide 65 text

PHP 1 台では意外とあっさり CPU がサチる

Slide 66

Slide 66 text

全マシンをフルに使えば Go でフルに使えてない状態よりは PHP の方が速くできる可能性がある

Slide 67

Slide 67 text

しかし同様に Go でも全マシンをフルに使えば 処理系性能では単に大きく負けてしまう

Slide 68

Slide 68 text

NaruseJun 組の改善速度は化け物

Slide 69

Slide 69 text

お前が信じる PHP を信じろ

Slide 70

Slide 70 text

おしまい

Slide 71

Slide 71 text

No content