はじめに
注・最初っからそうしとけよ、という話です。
研修で使うようなゲームっぽいプロダクト(ゲームマスター:プレーヤー=1:n)で、双方が行った操作(データ変更)をお互いにある程度リアルタイムで検知するために、ポーリングを用いて定期的にチェックすることで実現していました。
ポーリングを用いたのは下記の理由によります。
- push型が理想だがミドルウェア構成をシンプルにしたかった
- 仕組みも簡単だし確実
- DBの問い合わせも軽そうなので負荷もそれほどではないだろう(甘かった)
現実に全力で殴られる
さていざ実戦投入してみると、数十クライアント程度でCPUがパンパンになって、ロードアベレージが100近くまで行きました。
DBを外に出したり、ポーリング間隔をあけたりしましたが、劇的な効果は得られませんでした。
(php-fpmがアイドリングでもなぜかCPUを食いっぱなしになるという現象もあったのでそれも一因かもしれません。これはstraceで真面目に調べようと思ったら突如再現しなくなりました。)
反攻に出る
これは根本的に対策せねばなるまいということで、どうするか検討しました。
ポーリングで返しているデータは、ゲームの状態を入れたjsonで、アクセスが来るたびにDBからデータを取ってきてPHPでデータを加工して返していました。
実はこのプロダクトのゲーム状態の変化はせいぜい分速ひと桁程度です。
そしてゲーム状態に変化がなければ当然jsonデータにも変化はなく、アクセスが来るたびにDBにアクセスして作る必要はありません。
ですので変化が起きるタイミングでスタティックなjsonファイルに書き出して、ポーリングはそのファイルにアクセスさせることにしました。これならポーリングのアクセスはnginxの階層のみで動くので、軽くなるはずです。
余談
jsonファイルのreadとwriteがバッティングするとまずそうなので、書き込み方法は下記のようにしています。
完璧ではないかも知れませんが、少なくとも同じファイルに読み書きが同時に起こることはないので概ね大丈夫ではないかと思います。
- tmpファイルにjsonデータを書き出す
- 現jsonファイルを別の名前にrenameする
- tmpファイルを正規の名前にrenameする
さらなる反攻
スタティックなファイルにしてすっきり軽くなりましたが、今度は状態に変化がなくても毎回同じものが転送されるのが無駄に感じます。
ですので更新チェックを入れて、前回のアクセスと変化がない場合は304(Not Modified)を返させてbodyの転送を節約するようにしました。
これは、前回アクセス時のレスポンスのEtag
ヘッダの値をリクエストのIf-None-Match
ヘッダの値として付けることで実現しました。
まとめ
- ポーリングが来た時に毎回DBにアクセスしていたら大したアクセス量じゃないのに重すぎた
- 状態が変化する頻度が低いのでファイルに書き出すことにした
- さらに更新チェックを入れて無駄なデータ転送を減らした