🚚

自分なりに考えたバッチ処理のポイント

に公開

この記事は、Stockmark Advent Calendar 2024 23日目の記事となります

概要

  • バッチ処理を開発運用する中で、検討したこと気づいたこと考えたことを自分なりに(できる限り参考にした文献にはない観点で)まとめてみた
    • 技術記事としてはふんわりした内容になってしまったかもだけど、ある意味普遍的な内容にはなったかも
  • いろいろ書いたけど、バッチ化する対象の性質を見極めることが結局重要だと感じた
  • あとエンジニアリング観点では、ビジネスロジックが分散しないように気をつける、StepFunctions などの良いジョブスケジューラーを採用する、ログに気をつけるなどのいくつかポイントがあった

前提

私の所属するストックマーク株式会社では、Anews というサービス(SaaS)を提供しています。Anews ではクローリングしてきたニュース記事(構造化されていない自然文)を自然言語処理で構造化(企業名の抽出やキーワードの抽出などを行う)した後、顧客の趣向に合わせた推薦結果をニュースフィードとして配信しています。Anews では他にも、事前に設定をしたキーワードベースのニュースフィードや、社内文章に対するRAGなど様々な機能を提供しています。今回は、バッチ開発一般に関する内容をお話しするものの、モデルケースとして、趣向に合わせニュースフィードを作成するバッチの開発運用に関して検討したことをお話したいと思います。以降では、ニュースフィード = 趣向に合わせて推薦したニュースフィードとします。まとめると、

記事の構造化をする自然言語処理バッチ → ニュースフィード作成バッチ(ここについて話す) → 顧客ごとに趣向ニュース配信

という位置付けの部分です。ニュースフィード作成の In、Out は以下です。

In:構造化された記事データ(大量) → バッチ処理 → Out:顧客(大量)ごとにニュース記事情報

また、(趣向)ニュースフィードの配信は1日2回朝昼に行われており、フィードの配信方法はWebサービス上に配信される他、朝の配信についてはメール配信も行なっています。2回の配信では、それぞれ推薦対象となる記事母集団は異なっています(朝は朝配信用の時間帯にある記事群から推薦し、昼は昼で同様)。

参考文献と今回考察すること

すでにバッチ処理に関してはいろんな方が良い記事を書いてくださっています。特に以下の2つの記事に関しては、自分も業務を進めるにあたってすごく参考にさせてもらいました。

[1]バッチ処理の採用と設計を考えてみよう
非常に簡潔にまとまっているのに、汎用的で普遍的なことが書かれているのですごく参考になりました
[2]バッチ処理について考える
ジョブスケジューラ、オンラインバッチ、マイクロバッチなど、各概念の解説があって参考になりました

上記の記事で既に述べられていることについて、それ以上のことを私が述べることはできないので、今回は上記では触れられていない部分で、自分が検討したことをまとめたいと思います(それが価値を提供できるかはわかりませんが...まぁ自分の思考の整理のためにこの記事を書いています)。
また、バッチ一般に関する記事なので、推薦のアルゴリズムや、構造化バッチの自然言語処理などはそれはそれで面白いテーマなのですが今回は触れません(興味ある方はリンクを貼っている弊社のテックブログをご覧ください)。また、バッチといえば大規模データを効率よく処理するという側面も感心が強い分野ですが、大規模処理のパフォーマンスについても今回の記事では特に触れません。

検討したこと

オンラインではなくバッチでフィードを作成する理由:バッチだと結果の全域性を担保できる

趣向ニュースをつくるだけならば必ずしもバッチを選択する必要はありません(ただし、現状のニュースフィード配信は1日のうち決まったタイミングのみで行われているため、リアルタイム性が必要なものでもありません)。実は、前提にあるメール配信というのがかなり重要でして、リアルタイム処理だと実際にアクセスをした人しかデータが作られない(しかもメール配信時間までに)という問題があり、そこを解決するためにバッチが必要となりました[1]。バッチでフィードのような形で全員分を 作り置き しておけば、結果の 全域性 を担保できます。これによって、メール配信時にすべてのユーザにニュースフィードを送ることができます[2]。一方でオンライン処理だとアクセスのあったユーザのフィードしか作成ができません。「バッチだと結果の全域性を担保することができる」この事実は言語化してみると(自分的には)面白いと感じました。
また、全域性を担保できるという利点は他にも良い副作用がありまして、推薦アルゴリズムに何か手を加えた場合において、前後の再現率(Recall)などを評価[3]する際に、結果取得のための仕組みを別途つくらなくてもバッチを動かすだけで全員分の結果を簡単に取得できるというものです。オンラインだと全員分の結果を明示的に取得する仕組みを作らなければならなかったので、これは想定していないメリットでした。また、これはある意味で、大規模なE2Eテスト(結果を得られるためには、まず正しく動かなければならない)がオンライン処理系よりも手軽にできる...というより強制されるということだなとも気づきました。この、「あらゆるパターンに関して成功が要求される」ことがある意味バッチの難しさの1つだと考えています。

バッチ対象の性質を見極め、ブレイクダウンしていく

バッチを採用することに決めたら、次に、(バッチ処理をする)対象の性質を見極めることが重要です。特に、「バッチがどうなったら失敗なのか」、「失敗をしたらエンジニアが何をしなければならないか」を事前に見極めておく必要があります。
例えば、不要となったデータを削除していくような削除バッチを考えると、それは期限内までにデータを削除できれば何回か失敗しても許される(日次で動く場合、何日間か連続で失敗してもサービスとして影響がない)というシナリオもありえます。一方で、今回モデルとしたニュースフィードの場合には、期限時間までにお客様のもとにとどけることが must になってきます。
また、削除の場合にはエンジニアが即時対処する必要はないと言える一方、フィード作成が失敗した場合にはエンジニアが即時対処する必要があります。このように処理対象の性質を見極めて、 SLO・SLI、そして、SLO・SLIから監視する項目と通知の方法(要即時対応としての通知なのか、単に改善情報としての通知なのか)を定義していくことが重要です。ここをしっかり見極められると、最終的にバッチをどのようなワークフローとして組めばよいのか(何が失敗したらバッチ処理全体を止めて、何が失敗しても処理を継続して良いのか等)を定義することができます。

バッチ同士の連携:イベント駆動VS時間駆動

前提にも記載したように、今回のテーマであるフィード作成のバッチにおいては、「前段の記事構造化バッチが完了している」必要があります。このように、1つのサービスや機能を提供するために、幾つもバッチが有機的に連なっているのはよくあることだと思います。フィード作成バッチは前段の構造化バッチ完了から自動トリガーされるべきでしょうか?一見すると、イベント駆動[4]のほうがシンプルで、時間的な空きも生まれないので良いように思えます。しかし、フィード作成バッチは時間起動する仕組みを選択しました。
前提に書いた様に、ニュースフィードは朝昼で処理対象を変える必要がありました。そのため、フィード作成バッチは実行時対象とする記事募集団を決定するために、どの配信回として動くべきかの情報を与える必要があります[5]。一方で、ニュースの構造化にはフィードのようなタイミングを気にする必要はありません。そのため、実はフィード作成する前に構造化のバッチが数回動くことを許しています。よって、構造化バッチは、フィード作成バッチを必ずしもトリガーできるわけではなく、また、仮にトリガーしてよいと判断できても、どの配信回として起動するべきなのか判断ができません。よって、フィード作成バッチは時間起動(Amazon EventBridge Schedulerを用いている)にしています。また、フィード作成バッチが失敗し再実行する場合に、どの配信回のバッチなのか情報が損失しないように、配信回に関する情報を(正常時にも)バッチに注入してから開始するようにしています(この仕組みを実現するのに、また再実行時のわかりやすさからも、後述する StepFunctions が向いていました)。
このように、バッチ同士の連携には必ずしもイベント駆動がベストというわけではなく、検討が必要な場合がありました。

ビジネスロジックがオンライン処理系とバッチで分散しないようにする

リアルタイムな処理をしているオンラインサーバと、バッチとで、ビジネスに関する知識が二重管理されないように気をつけましょう。両方とも同じリポジトリ内にある場合はまだ起きにくいのですが、リポジトリが分かれると二重管理のリスクも高くなります。基本的にデプロイのライフサイクルがオンライン処理系とバッチ処理系とでは異なるので、リポジトリもわけてしまうことが多いかもしれません。重要なビジネスルールが二重管理されだすと、保守コストがあがってしまいますので、共通のビジネスロジックを含む部分はシンボリックリンクなどで共有する、別サービスとして切り出すなどを検討していくと良いと思います。

運用(失敗時のこと)を考えよう

バッチは自動リトライの仕組みを実装しておくことが重要です。ただし、自動的なリトライだけではカバーできない状況はどうしても発生します。そのため、人の手によるリカバリーが発生することを覚悟し事前に用意しておくことも重要です。
これはバッチに限ったものではないですが、Slackでの失敗通知を整備し、事前に運用手順書を作成し、調査方法(例:XXXのログをまず確認する)や代表的なリカバリー方法(例:発生確率の問題で自動対応を見送ったが、クラウドプロバイダ側の問題でXXが起きる場合はYYをするとうまくいくなど)を記載しておくとよいです。再実行方法に関しては、良いジョブスケジューラー([2]の記事を参考)を選択していれば、かなり単純なものになると思います。例えば、StepFunctions では失敗したジョブについて実行引数を含めて参照再実行ができるので、その一言だけを記載すればよいかもしれません。

必要十分なログを出す

運用のことを考えると、エンジニア誰でもログから調査ができるようにしておく必要があります。当たり前ですが、日時やどのジョブのログなのかといった情報は追えるようにしておきましょう。
バッチ処理でありがちなのが、過剰にログが出ているため調査の障壁になってしまうケースです。バッチでは大規模な処理をすることが多いためです。そのため、必要十分なログになっている(余計なものは出さない、必要なものだけ出す)かは慎重に確認しましょう。また、ログ量が増えがちなことから、ログローテーションや、ログのライフサイクルにも配慮しておくと良いでしょう。

ジョブスケジューラーには StepFunctions が便利だった

これは具体的なサービスなので、数年後も同じことが言えるかはわからないですが、ジョブスケジューラーとして、StepFunctionがすごく使い勝手が良く感じています。良い点を挙げると、

  • マネージドサービスなのでサーバやメンテナンスが不要
  • マネージドサービスなので、操作が抽象化されてて使いやすい
    • 並列処理の記述がしやすい
    • エラーハンドリングとリトライの処理も記述しやすい
  • バッチジョブ間の依存関係が可視化されてわかりやすい
    • バッチの失敗も可視化されてわかりやすい
  • 実行履歴の参照ができる
    • 再実行も履歴から参照できるので楽
  • EventBridge と組み合わせれば時間駆動実行も簡単に記述できる
  • 多重起動防止なども簡単に実現できる
  • 実行時のパラメータ渡し、渡したパラメータの参照も手軽にできる

など、他にも書ききれないくらいに良い点があります。

まとめ

記事を見返すと、今回まとめたことの7割くらいは、(バッチ)処理対象の内容、性質を深く検討すれば見えてくる、演繹的に決められることだと思いました。そこをしっかり押さえられれば、StepFunctions の様なジョブスケジューラーで良いワークフローを組め、ジョブが失敗した場合に適切な通知、通知が来た場合の正しい運用が組めるのかなと思います。ただし、意外と処理対象の内容や性質を見極めるということは難しいのですよね〜基本的なことにも関わらず。
あとは、ビジネスロジックをDRYにする、ログに気をつけるなどのエンジニアリング観点の要素も気をつければ、それなりのバッチ処理を組めるのではないでしょうか。

脚注
  1. 実はここの開発のストーリーは不正確で、そもそもオンラインである必要性がないためバッチで作っていたという実情があり、その後にオンライン化の検討が入ったのですが、そこでもメールがあるからバッチは(少なくとも部分的には)必要だねという結論になったというのが正確です。わかりやすさのためその辺りのストーリーは省略しました ↩︎

  2. メール配信時にリアルタイム処理を呼び出すみたいなアーキテクチャも考えられますが、パフォーマンスや責務を考えると、メールのコンテンツ内容は配信を行うバッチで作成しないほうがよい(前段で作っておくべき)という判断がありました ↩︎

  3. 余談ですが、評価は user_id全体でfeed_news_idsの再現率がどのくらいか?といった方法で行い、当然個人の情報に立ち入らない(idだけで評価)様にしています ↩︎

  4. 厳密には、時間駆動も1つのイベント駆動だと捉えられると思いますので、ここでイベント駆動と述べてる部分は、前段のバッチが完了したことをトリガーに後段のバッチをトリガーするという意味で捉えてください ↩︎

  5. (失敗分も含め)不足しているフィードを自動判定してそれらすべてを作成するような仕組みも考えられますが、作成工数の費用対効果や、アーキテクチャの制約などで、その仕組みをすぐに実現できない事情がありました ↩︎

Discussion