並列で重い処理をブン回す

by kiridaruma at PHPer kaigi 2022

自己紹介

@kiridaruma

  • 1998/3/11生まれ

  • ピクシブ株式会社

    • WebのインフラやDB、ネットワーク周りを色々
  • 趣味でHaskell, Elm, Rust

    • 型やそれに関連した設計の話が好きです

自己紹介

@kiridaruma

  • サブカル系DJ
    • アニソン、同人音楽、声優

自己紹介

@kiridaruma

  • ミリタリー関連
    • 米軍のマニュアル読んだり
    • シミュレーターで遊んだり
    • サバゲーしたり

自己紹介ここまで

並列で重い処理をブン回す

重い処理

  • Webサーバで処理が1秒を超えることはほぼない
    • それ以上遅い場合、大抵DBやネットワーク起因
  • ただし、バッチ処理で大量のデータを扱うと
    • さすがに遅くなる
    • PHPじゃなくても遅くなる

時間がかかる原因

  • 原因はいろいろ

    • 計算量
    • ディスク・ネットワークなど
      • CPU以外とのやり取り
  • 原因によって対応方法が変わってくる

CPUが何をしているかの違い

  • 計算量が原因の場合

    • CPUは動いている
  • ディスクやネットワークなどが原因の場合

    • CPUは他の機器を待っている

CPUが動いているか待っているか

  • 基本的に計算量が大きい処理は速くできない

    • 計算量を減らすしかない
    • 処理を変える
  • CPUが待っている時間が長い処理(IOなど)

    • 無駄なので効率よく処理を進めましょう

待ってる間に他の処理を進める

→Generator

PHP8.1からはFiberも

GeneratorとFiber

  • 「どう使うのか」は調べたら沢山出てくる
    • ここでは省略

実際にあった例

  • Webページの表示速度の計測を行うバッチ

    • WebAPIを利用する
  • 計測には時間がかかる

    • まず計測するAPIを実行
    • その後、結果を取得するAPIを実行
      • 計測が完了するまでは結果取得を行い続ける

実際にあった例

  • この場合、ボトルネックはCPUの外部にある

  • 計測対象のページは複数

  • →Fiberでまとめて並行処理を行うようにした

Fiberで非同期/並行処理が簡単に

今回のタイトル

並列で重い処理をブン回す

並列で重い処理をブン回す

並行と並列

並行

タスク1→タスク2→タスク3→タスク1→タスク2→…

並列

タスク1→タスク1→タスク1→…
タスク2→タスク2→タスク2→…
タスク3→タスク3→タスク3→…

並列

  • 基本的に計算量が大きい処理は速くできない

    • 同時に進めることはできる
  • 重い処理を同時に実行して全体を早くはできる

PHPで並列処理

  • GeneratorもFiberも並行処理

    • 並列じゃないので、同時に実行はされない
  • そもそもPHPだけでは並列処理できない

    • PHPだけでは…

外部の力を借りよう

OSの力

スレッドとプロセス

OSの並列処理

  • スレッド

    • 他スレッドとメモリを共有する
    • 上手くやらないと意図しない挙動を起こす
  • プロセス

    • 他プロセスからメモリは隔離される
    • 入出力周りに気を付ければ何とかなる

PHPでスレッド

  • pthreads

    • 昔からある
    • もうサポートされていない
  • parallel

    • 新しいやつ
    • 特にこだわりがなければこっち

PHPでスレッド

  • pthreadsもparallelもPHPのモジュール
    • なので別途有効化する必要がある
    • 場合によってはPHPの再ビルドも

PHPでプロセス

  • 標準でいくつか使える関数がある

  • Linuxなら大体すぐ使える

    • Windows/Macは一部動かない

スレッドとプロセス

  • スレッドをちゃんと動かすのは大変

  • 「プロセスよりスレッドの方が早いんでしょ?」

    • そんな差はない(大抵は気が付かないレベル)
    • そもそも重い処理をしてる時点で変わらない
  • プロセスの方が扱いやすい

    • メモリが隔離されてるのでバグを生みにくい
    • プロセス間通信は少し考えないといけない

マルチスレッドは難しい

マルチプロセスの方が楽

マルチプロセスなコードの書き方

  • 基本的に2つのプロセスに分けて考える

    • 仕事をするプロセスを生成するプロセス
    • 実際に仕事をするプロセス
  • よくあるミドルウェアもだいたい同じ構成

プロセスを生成する

  • 別プロセスを実行する関数

    • exec(), passthru(), system()など
    • popen()
    • proc_open()
  • 色々ある

exec(), passthru(), system()など

  • 一番簡単に外部コマンドを実行できる
    • ただし、外部コマンドの操作等はできない
    • 標準出力だけ取れる

popen()

  • ファイルと同じように読み書きできる
    • プロセスの標準入出力を扱える
    • ただし、1方向(入力or出力)だけ

proc_open()

  • 他の関数と比べて一番詳細に設定できる
    • 標準入出力のパイプ
    • カレントディレクトリの設定
    • 環境変数の設定

何を使うべきか

  • A. やりたいことによる
    • ので、php.netを良く読むのが良いです
    • 特に入出力が大きく違う(個人的意見)
      • ログの取り回し
      • プロセスの制御

実際にあった例

  • DBで大規模なデータ移行の必要があった
    • テーブルのサイズの合計は4TB以上
    • 100億レコード以上
    • 愚直に回すと1ヶ月かかる計算

実際にあった例

  • マルチプロセスで並列で実行した

  • オンプレの専用の強いマシンを使用

    • DBも同居している
    • それでも処理に7日かかった

並列処理には強いマシンが必要

強いマシン

  • オンプレ環境は調達すれば強いマシンが使える

    • お金とスペースの許す限り強いマシンを
  • クラウドの場合は分散させることが多い

    • lambda, functions, cloud runなど

バッチの実行時間

  • 数分程度ならlambdaやfunctionsで回せる

  • 時間のかかる処理の場合はVMの方が良い場合も

    • 数時間や数日となるとコストが問題になる
    • EC2, GCEなど
    • こういうところで並列化が生きてくる

長時間のバッチでありがちなこと

  • 「あれ、良く分からないけど失敗してる」

    • 長時間かけて実行するバッチの場合は特に
  • バッチ処理はログの出力が大事

    • エラーが起きた時にすぐ気づけるように

ログの出力

  • 基本はプロセス毎にログを出力するのが楽

    • プロセスごとに詳細なログが追える
    • ファイル等に出力する場合、ロックに注意
    • 外部に送る場合はあまり考えなくてよい
  • そもそもプロセス同士の依存を減らした方が楽

    • 複雑なプロセス間通信が必要な場合は要注意

シグナル

  • シグナルハンドラは必ず設定する

    • PHPの場合はpcntl_signal()など
  • オンプレ/クラウド関わらずシグナル処理は大事

    • killした時に子プロセスもまとめて止めるなど
      • 親だけ止まって、子は動いてる状態などを防ぐ
    • インスタンス停止時にシグナルが送られるものも

終了の通知とexitコード

  • 正常終了時には0、異常終了時はそれ以外でexitする

    • 当たり前だけど重要
  • 異常終了したら通知する仕組みなどがあったり

    • 正常/異常問わず、終了に気づける

時間がかかるバッチ処理は面倒

時間を無駄にしないために

(o・∇・o) < おわりだよー