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

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

Avatar for sji sji
March 24, 2023

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

Avatar for sji

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 にするとメモリ消費量はちょっと減る