Upgrade to Pro — share decks privately, control downloads, hide ads and more …

時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる

sji
March 24, 2023

時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる

sji

March 24, 2023
Tweet

More Decks by sji

Other Decks in Programming

Transcript

  1. WEB+DB PRESS の現 PHP 連載担当 2021 年 6 月から WEB+DB

    PRESS の PHP 連載 だいたいわりと真面目な話をしてる 今回のトークが気になるような人には vol.128 、 129 、131 あたりオススメ
  2. ISUCON とは Iikanjini Speed Up Contest の略 Web アプリケーションの性能改善大会 各種の言語で同内容の処理をする参考実装が与えられる

    ベンチマーカが負荷をかけ、処理性能でスコアを出す 3 人までのチームで参加し、サーバを数台与えられる レギュレーション内で性能改善 コード修正からミドルウェア・OS 変更までわりと何でもアリ 「ISUCON 」は、LINE 株式会社の商標または登録商標です。
  3. ISUCON12 本選 仕事だ!舞台設定が完全に仕事(スマホゲー)だ! でも PHP の本選進出者がいない! 30 組のうち 26 組が

    Go 他は Node 、Ruby 、Rust 、Perl で 1 組ずつ PHP の参考実装は用意されている @okashoi さんの仕事 やるしかない
  4. 実行環境 本選 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 とだいぶ似た数字
  5. 初期スコア: 644 他言語を試してもどれも大体同等の初期スコア xdebug 無効化でもほぼ変わらない ボトルネックがほぼ完全に DB 側 mysql や

    nginx 設定は優勝リポジトリを初手でパクった状態 この時点で本来の本選初期状態よりは改善されてる筈 デプロイの構成もパクり、各ホスト名を s1 〜 s5 に変更 基本的に優勝リポジトリのコミットログを真似して進める
  6. Snowflake ID 導入: 30858 ユニーク ID の生成は DB 通さないほうがよい 衝突しづらい

    DB に優しい値を Web 側で生成 ULID や Snowflake など composer から Snowflake ID の生成器をインストール Go の優勝チーム記録(24498) より順調に伸びて幸先が良い 計測・ログ系をつけてないのが大きそう $ composer require godruoyi/php-snowflake
  7. 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();
  8. DB ホスト分離: 45687 fpm のプール設定で clear_env=no env から環境変数で DB ホストを渡す

    DB アクセスがネットワーク越しになりレイテンシが増える 優勝チームはこの時点で 40,478 点
  9. (おまけ) PDO::ATTR_EMULATE_PREPARES を切る: 36897 優勝チームは Go なので interpolateParams を指定 PDO

    では PDO::ATTR_EMULATE_PREPARES がデフォルト on で対策不要 一応切ってみるとわりと効果があるのがわかる プロファイルをとるとやはり prepare + execute の時間が大きい
  10. マスタ参照でのキャッシュ利用: 80022 symfony/cache で対応 PhpFilesAdapter で opcache のキャッシュ利用 参考: PHPerKaigi

    2021 でPHP の不変配列が高速かつ省メモリだという話をしました 大粒のボトルネックが消えてきた そろそろシャーディングを入れる段階 優勝チームはマスタのキャッシュ利用とほぼ同時期に入れてる https://hnw.hatenablog.com/entry/2021/03/29/011242
  11. シャーディング DB 4 台: 全然変わらず 優勝チームはこの時点で 21 万点出してる この時点の構成は以下 s1

    に nginx + fpm s2 〜 s5 に mysql JIT 有効にしても特に伸びず プロファイルを見てみる
  12. PDO の生成コスト PDO の new がめちゃくちゃ嵩んでる 当初 persistent が効いてないのか疑う が、外すとちゃんとスコアが

    5 万点台へ落ち る persistent 有効でも PDO の生成が遅い リクエストごとの生成を避けるには?
  13. RoadRunner: 131353 SpiralScout の AltFPM Go 製の HTTP サーバがリクエストを受ける 通信待ちで無限ループする

    PHP CLI のワーカと パイプ通信 リクエスト間で情報を持ち越せる PDO インスタンスを使い回せる フレームワークの起動コストも消せる 調べつつハマりつつ 3h くらいで移行 Slim からの移行を素振りすると良さそう
  14. checkViewer キャッシュ + ワーカ数調整: 180356 ユーザの端末 ID をオンメモリキャッシュ RoadRunner のワーカ数を

    20 に 増やしたり減らしたり試した結果 あまり I/O バウンドでない状況を示してる
  15. Ban とセッションの redis 利用: 183684 RoadRunner のワーカは別個に起動されるただの CLI プロセス opcache

    の SHM が共有されない プロセス間で共有できるメモリキャッシュを置きたい Redis を igbinary 付きで導入 接続は Unix domain socket DB アクセスを削れるがスコアが伸びない なお RoadRunner の kv plugin も試したが遅くなる
  16. 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
  17. 絶望的な 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
  18. どうする?わりと困った 一面では RoadRunner の実行モデル起因の限界 Go サーバとのパイプ通信に時間食ってそう それでもパイプは IPC の中では軽い…… 一面では

    PHP 処理系の限界 プロファイルでもわりと上のほうに VM 命令 単体が出てくる状況 利用元もスクリプトの特定行というより全 体に散在するものが多い 処理系は内部処理で PHP の配列と同じデータ 構造をよく使う 内部の配列操作自体をスクリプトから高速化 する手段はない
  19. 一応 Swoole も試した: 147015 一応は試した、が、ダメ PHP 処理の部分がボトルネックという前提が変わらない HTTP サーバ部分がワーカプロセスとパイプ通信する形態も同じ I/O

    をより効率的に行うために機構が複雑? CPU 効率ではむしろオーバヘッドが大きそう CPU 余ってると多分 Swoole のがいい? 何か下手をうって同期 I/O が混ざった可能性はある が、計測を見る限りたぶん改善しても大きくは伸びない
  20. 状況整理 もう余ってる CPU リソースがない 優勝チームの構成をなぞって Web 1 台 DB 4

    台、なら DB サーバのリソースは余ってる 余ってる(DB サーバの) CPU で Web を回せばいいのでは? ここまで shared nothing な構成をあまり崩してないので可能 同一ワーカ内のリクエスト間でキャッシュを使ってる程度
  21. RoadRunner 分散構成: 310198 s1 に nginx + rr + redis

    s2 〜 s5 に rr + mysql そもそも CPU 処理でネイティブコードの言語に 勝てないのは自然 PHP なんだから横に並べてスケールさせるでい いでしょ これで本選 2 位スコア(242,653 )を超える CPU 資源に余裕ができた
  22. 負荷割合の変更 + PGO + PHP 8.2: 370253 おまけで PGO (Profile

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