読者です 読者をやめる 読者になる 読者になる

PHPとバッチ処理の微妙な関係

アプリ事業部でリードエンジニアをしております「永遠の中二病です。キワモノガジェットが大好きな凝り性エンジニアです。

簡単に自己紹介させていただきますと、これまでアフィリエイトCGMソーシャルゲームなどのウェブアプリケーション開発に携わってきました。これらの現場で得てきた技術やノウハウを発信できればと思います。

PHPバッチ処理の微妙な関係

さて、今回はPHPを利用したソーシャルゲーム開発現場におけるバッチ処理についての話題を取り上げたいと思います。折角「中二病」を名乗っていることもありますので、あえて日の当たらないバッチ処理にスポットを当てることにしました。

とはいえ、結論から言ってしまえば、大規模サービスにおけるバッチ処理は必要十分なものに止めたいということになってしまいます。やはり日は当たらないのかもしれません…。

バッチ処理の増加と破綻への道

ソーシャルゲームの処理の中で、一般的にバッチ化しそうなものを考えてみると、

  • ランキング集計
  • ギルドバトルのマッチング
  • 報酬の一括配布
  • 不要になったデータのパージ

この辺りがよくありそうな事例でしょうか。この程度であれば適当に実装しておいても問題無さそうに見えます。しかし、バッチ処理はサービスの運用期間が長くなればなるほど様々な問題を引き起こします。

サービスの運用期間が長くなると、まずゲームの機能が増えることでバッチ処理もその数自体が増えてきます。また、ユーザ数の増加によりそれぞれのバッチ処理時間も長くなってくるというダブルパンチを食らいます。memory_limit限界突破はネタのような話ですが、ダブルパンチで困り始めた頃にお約束のようにバッチ処理がエラーで落ちるようになった時は、大抵これが原因だったりします。

おそらくPHPバッチ処理を実装する上で最も注意すべき所はmemory_limit周りではないかと思います。PHPガベージコレクションを意識しないとバッチのループ箇所などですぐにメモリリークを引き起こしてしまうので、適当にコーディングすると確かにメモリを食いまくります。この時、その場しのぎの策としてひとまずmemory_limitの上限を上げることは必要になるでしょう。具体的には、バッチ処理スクリプトファイル内に次のように記述します。

ini_set('memory_limit', '512M');

同様の設定はphp.iniからも可能ですが、それではサービス全体に影響してしまいますので、一時的に上限を上げる場合はこちらの方法がお勧めです。いずれにせよmemoly_limitの上限を上げるのはその場しのぎにしかならないので、どこかで根本的な対策が必要になります。

根本対策としては、ループを小刻みにしたり子プロセスに分けたりと、バッチ処理を落ちないようにする手法はもちろんあります。ただ、その頃にはサービスも育ってきていて、そもそもバッチ処理を闇雲に増やすこと自体が難しいほど定時バッチ処理の予約が埋まるという事態に直面している可能性がありますので、今回は別のアプローチを紹介したいと思います。

バッチ地獄の回避策

バッチ処理が多くなりすぎた場合の対策としては、(※2013/10/08追記)コスト等を柔軟に考慮して良ければ並列分散処理などのアプローチがあります。ですが今回は、アプリケーションレイヤーの実装手法改善で手が打てるものとして、バッチでないと難しい処理以外をリアルタイムに置き換える方法を考えます。例えば、ランキング集計やマッチングはリアルタイム処理の難易度は高そう(ランキングならRedis等で実現できそうではある)ですが、報酬の配布やデータのパージはバッチでなくても比較的容易に実現できそうです。リアルタイム化が可能か否かの判断基準は、

「バッチで行っていた処理をユーザアクションと紐付けて細切れにできないか」

これに尽きるでしょう。 例えば報酬の一括配布などは、受け取り画面を作成すれば同様のことが画面側のリアルタイム処理で実現できますし、不要になったデータのパージであれば、同じテーブルを操作する際に、ついでに一部のレコードをパージすれば一度の負荷を減らせそうです。バッチ処理をリアルタイム処理に置き換えるイメージは次の図1、2のようになります。

f:id:amirtta:20160113220737p:plain 図1:バッチ処理の例  

f:id:amirtta:20160113220802p:plain 図2:リアルタイム処理の例

リアルタイム処理では、ユーザアクション毎にそのユーザ向けの処理のみを実行するため、一気に実行するバッチ処理と比較すれば一度の処理は圧倒的に低負荷になります。図2のようなリアルタイム処理を行うためには一般的に画面を増やす必要があるため、工数的には若干不利ですが、一方でその後のサービス規模拡大などによる負荷増大のリスクを考える必要が格段に下がります。

まとめ

このようにユーザアクションと紐付けて細切れにできる処理をリアルタイムに置き換えていくと、意外にバッチ処理でなければ実現できない処理というのが少ないことがおわかりいただけるのではないかと思います。もちろん置換するコストや、新しく作成するにしてもバッチ処理の方が画面が必要無い分低コストな場合があるため、そこはサービスの規模やその将来性、コスト感などとのにらめこになってくるとは思われます。とはいえ、いざサービスが大規模化してバッチ処理では実現が難しいことが発生してきたときに慌てないためにも、このような代替策を事前に想定しておくことは迅速なリスク対応という観点から重要と考えます。

こうしたサービス規模拡大によるアプリケーションの実装手法改善は、ウェブサービスの世界では一般的に行われています。アプリケーションだけで完結するものもあれば、ミドルウェアやサーバまで絡んでくることもしばしばです。今回は私の最初の投稿ということでアプリケーションで完結する事例を挙げましたが、機会があればより難易度の高い事例についても触れていければと思います。

2013/10/08 追記

一部表現に誤解を招く箇所がありましたので補足します。 バッチ処理を行っていたものをそのまま全てリアルタイム処理に置き換えた場合、負荷の総量は実際には増加してしまいます。また、置き換えたときに一処理の負荷が大きければ現実的ではありません。しかしながら、ユーザトリガーによるリアルタイム処理に置き換えられて、かつ一処理あたりの負荷が大きくない時には有効になります。この場合、アクティブユーザ分しか処理は実行されなくなりますので、結果的に負荷や無駄なデータを抑制することが可能となってきます。