今年胃カメラ初挑戦して泣きそうになった片居木です。いつの間にかエンジニアになって10年以上経ってしまいました。思い返すとプレイドに来る前は主にバッチ処理による集計を書いてました。プレイドに入ってからはもっぱらフロントエンドやリアルタイム集計などのストリーム処理を作る日々です。せっかく両方を経験しているので、それぞれの処理の特徴についてまとめてみたいと思います。
なお、ここでのバッチ処理は、1時間に1度や1日に1度、その時間帯に発生したデータ全部を対象に処理するものをさします。また、ストリーム処理は発生した1データごとに逐次処理するものをさします。また、今回は主に、データの集計処理をするシステムを想定しています。
「ストリーム処理って、バッチ処理の実行間隔を最小まで短くしたものでしょ?」と言われることも多いです。1データごとにバッチ処理を回せば一緒でしょ、ということですね。処理にかかる「コスト」が0の理想的な世界ならばそれは正しいです。ただ、現実世界ではその「コスト」が大きいため、バッチ処理とストリーム処理では全く別のアーキテクチャが使われます。
ここでは、処理のロジックではなく、バッチ処理やストリーム処理を運用面で気をつけるべき「気にするポイント」の違いについて考えてみます。
バッチ処理は1回の処理に時間がかかる
パイプラインの管理する
バッチ処理はまとめて多くのデータを処理するため、1回の実行時間が長くなりやすいです。複数のバッチ処理でパイプラインを組んでいる場合、1つの処理の実行遅れが他のバッチ処理にも波及してしまいます。そのため、その影響をなるべく早く把握するために、バッチ処理の実行時間アラートを丁寧に設計したり、ジョブの実行状況の可視化をしたりします。そのため、パイプラインの管理を行うOSSのフレームワークも登場しています。
https://raw.githubusercontent.com/spotify/luigi/master/doc/user_recs.png
多くの実行ログを出す
正常な場合でも、入力データがスパイクした場合(セールを行ったとか)、実行時間が急に増えることがあります。この場合、「正常に動いているが長い時間かかっている」のか「途中で失敗しているが、エラー終了していない」のか、見分けがつかなくなります。また、いつ終わるのか見積もれないため、パイプライン上での影響が見積もれなくなってしまいます。そのため、どこまで実行されたかを監視できることが重要になります。私の場合、1000件に1回程度の頻度でプログレスを表示するためのログ(全件○件/現在○件処理済みたいな形)を出力していました。
print "$line_count / $line_total" if $line % 1000 == 0;
また、1回の処理時間が長いため、処理が途中で失敗してしまった時に、再度実行させてみるかどうかの判断が難しいです。外的要因で処理できなかったのか(同時に別の処理が走っていてロックを取得できなかったとか)、データ起因で処理ができなかったのか(データのフォーマットが壊れていたなど)。データが原因の場合は再度実行したところで、同じ場所で処理が失敗するだけです。エラーが起きた状況を再現するにも長い処理時間がかかるので、試しに再度実行するというのは、あまり現実的ではありません。そのため、なるべく実行時のログは大目に出すようにして、エラーが発生した瞬間がどうなっていたのかをなるべく詳細に残すようにしていました。
事前に見積もる
バッチ処理の場合、事前に入力データ量がわかっていることが多いです。そのため、実行前にある程度のパフォーマンス計画ができます。時間あたりに必要な処理量がわかるため、そこから必要なインスタンス数が計算できます。事前に十分なシステムリソースを準備をしてから処理実行開始することができます。
ストリーム処理は待っていられない
流量制御する
ストリーム処理の場合、入力データ量がスパイクした時の変化の幅が格段に大きくなります。スパイクした時のデータ量でも耐えられるシステムにできれば良いのですが、現実的にはスパイク時も耐えられるほどのシステムは、通常時にはオーバースペックとなり、コスト増になります。そのため、多くの場合はシステム内に流れるデータ量を均等化する、という方法が取られます。
例えば、ストリーム処理のパイプラインの間にジョブキューを入れることで、流量が増えた場合でも一時的にジョブキューに入力データを貯めることで、ストリーム処理自体のデータ処理量はほぼ一定を保つことができます。
ログはエラーになってから
再実行することに時間がかからないため、実行ログは「エラーが発生してから入れる」という方法も比較的現実的です。特にストリーム処理の場合、1処理の実行時間は短いため、実行ログを出力しすぎると処理パフォーマンスに大きく影響します。そのため、開発時には運用のための実行ログはあまり設計せずに、運用中にエラーが発生したところを中心に実行ログを増やす、とした方が適切なことも多いです。
if (IS_DEBUG) {
console.log("user_id = " + user_id);
}
また、ストリーム処理の場合、前述のジョブキューを使っているため、集計が止まるとジョブキューが溢れてしまいます。そのため、たとえ1データがエラーになったとしても、基本的には停止はしないようにしています。
動的に計画する
ストリーム処理は常に動かし続けるため、処理の開始時に将来のパフォーマンスの見積もりをすることが難しいです。そのため、見積もりできることより、動的にシステム拡張できるようにすることが優先されます。
まとめ
実際にはKARTEでは、バッチ処理とストリーム処理の両方が組み合わされて使われています。いろんな集計システムを組み合わせて最適なものを作りたい、という人はぜひプレイドへ!