Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

バッチ処理の採用と設計を考えてみよう

こんにちは。メルペイで、決済・振込申請のバックエンドソフトウェアエンジニアをしている id:koemu です。

今日は、バッチ処理を行う理由について、考察を深めて設計に活かしていく話をしたいと思います。

はじめに

バッチ処理とは、ある決まったタイミングで1つのプログラムが複数のデータを 一括処理 することを指します。この反対の言葉として、オンライン処理があります。オンライン処理とは、お客様の操作を初めとしたイベントをもとに 逐次処理 されるものです。OLTP(Online Transaction Processing)とも言います。

本エントリでは、バッチ処理を採用するにあたり、どういったユースケースが適切なのかを整理して、今後のソフトウェアの設計の指針にできることを目指しています。今回は、「バッチ処理を採用するとき」と「バッチ処理の設計」の2つについて取り上げます。

バッチ処理を採用するとき

バッチ処理の定義

改めて、バッチ処理について定義を確認しましょう。

Dave*1によると、バッチ処理は以下の2つのカテゴリに分かれるとあります。

  • Functional Jobs
  • Technical Jobs

Functional Jobs は、業務要件をもとにした処理です。たとえば、メルカリ(以下、当社)では振込申請という機能があり、その結果をお客様に返すに当たり、金融機関の締めの時間になると送ることができる条件があります。その業務に沿って、まとめて処理を行うものが該当します。このような処理は、「締め」という言葉が出てくる業務でよく使われます。

Technical Jobs は、技術要件をもとにした処理です。たとえば、キャンペーンで一度に多数のお客様へPush通知を送る際、管理画面(当社ではP-Toolと呼んでいます)のプログラムが直接処理すると、タイムアウトしてしまうものがあります。これを、バッチで処理することで長時間実行可能にし、非同期かつ他のオンライン業務に影響を与えないよう処理する際に用います。

業務システムですと、オンラインバッチとオフラインバッチの違いもあります。しかし、当社はインターネットサービスを提供しており、24時間オンラインということから、今回はこれらの違いについての説明を省き、オンラインバッチのみに限定して述べます。

オンライン処理を諦めるとき

バッチ処理を採用する別の理由として、オンライン処理を諦めるとき、があります。たとえば、1つの処理がオンラインで行うには処理に時間がかかり、計算機への負荷が高過ぎて、その結果お客様の体験を悪化させている場合の対処法の一つとして行うものです。たとえば、出品情報を検索インデックスに登録するのは、当社では同期的には行っておりません。

その場合は、ワーカーのような逐次発行できる非同期の処理で問題を解決できる場合もあります。バッチ処理の特性、ワーカーの特性、それぞれの良さを加味しながら、技術を選択します。なお、ここではワーカーについての説明は省略します。

バッチ処理の設計

バッチプログラムを作るにあたって、私は以下の5点を考慮しながら開発にあたっています。

  • 処理件数
  • 処理時間
  • 回復可能か
  • 多重起動可能か
  • バッチの突き抜け対策

処理件数

まず初めに確認しなければならないのが、バッチ処理が1回起動するごとに対象となる処理件数の見積です。

業務要件を読み解くことが重要となります。既にオンラインで行っている処理をバッチ処理に移行する場合は、そこから計測して求めます。これから始める業務の場合は、企画担当者が考えている利用数の想定曲線から求めます。1ヶ月後、3ヶ月後、半年後、そして1年後までを、大まかに低利用率・中利用率・高利用率の別で求められると良いでしょう。

処理時間

バッチ処理を設計するにあたり、まず取りかからねばならないのが時間のことです。3つの観点で考えていきます。

  • いつ起動するか
  • いつまでに終えなければならないか
  • どのくらい処理時間がかかりそうか

まず、いつ起動するかです。多くの場合は業務要件が決まっており、それに従うことになります。実行時刻は、毎時・日次・週次・月次など周期的に決まることがほとんどです。システム移行や復旧など、1回きりのものもあるかもしれません。

次に、いつまでに終えなければならないかです。こちらは業務要件が決まっている場合と、明確な定義がない場合があります。前者はお金の締め処理といった、実行した日に必ず終えなければならない背景があるものです。問題は後者で、業務要件を練っている企画担当者の中で、どのくらいのサービスレベルを求めているのかを設計の段階で明確にしていく必要があります。そうしないと、処理量が増えたときに、企画担当者の想定するサービスレベルが担保できない可能性が出るためです。

最後に、どのくらい処理時間がかかりそうかです。既にオンラインで行っている処理が存在する場合、New Relic, Datadogなどのモニタリングサービスなどの分析結果から、その処理の所要時間が見えます。そこにまずは処理件数を掛け算することで、大まかな単位あたりの処理時間が見えてくるはずです。これから始める業務の場合は、類似の処理から想定される単位あたりの処理時間を求めます。

上記3点を設計上の処理時間としてまとめたとき、当初の要求仕様と照らし合わせてそれを満たせるかを確認します。満たせない場合は、要求仕様自体を見直すか、後述する「バッチの突き抜け対策」をもとに技術の側面で対策します。

回復可能か

バッチ処理は、不幸にも何らかの理由で途中で異常終了する可能性があります。このときに、その事態を回復可能にできるよう設計しておく必要があります。以下の優先順位で事前に対策を取ります。

  • 処理が完了した次のデータから処理が再開できる
  • 再度同じ処理が開始されるが、結果が冪等になる(二重に処理されない)
  • 失敗したIDを控えることができ、再実行できるコマンドオプションを用意する

ベストなのは、処理が完了した次のデータから処理が再開できることです。これですと、開発担当者が休日等で席を外している場合などで、開発した担当者以外がバッチを再実行する場合でも、安心して対応することが可能です。ただし、この方法は「どこまで処理したか」を管理する必要があります。とはいえ、締め処理など、ステータスが遷移するデータを処理するバッチだと実装が容易です。

次に、結果が冪等になるよう作ることです。もう一度最初から実行されるため、再実行の時間は長くなることが予想されますが、先ほどと同様に開発した担当者以外がバッチを再実行する場合でも安心して再実行できます。

問題は、再実行可能するレコードを指定して行う方法です。これは、失敗したIDを控える操作や、それをコマンドに渡す操作などをミスなく行う必要があり、再実行のときに細心の注意を要します。あまり推奨されません。

一番の問題は、全く回復する方法が用意されていないことです。そうなりますと、正常に処理できたデータまでも再度処理が行えるようにデータを修正する必要があり、新たな障害を発生させかねません。

多重起動可能か

多重起動可能かどうかは、業務の性質によります。多重起動により処理するデータを破壊しかねない場合は、実行をロックできる機構を設けましょう。

最も危ないのが、たとえば毎時実行のバッチ処理が突き抜けて(突き抜けは後述します)60分以上かかり、次の定時に実行されるバッチで二重・三重に処理されてしまうケースです。実行周期が早いバッチ処理は、突き抜けによる事故の予防処置としてロックを適切に施すことを推奨します。

バッチの突き抜け対策

処理件数と処理時間を見積もった際、例えば1年後に高利用率だった場合に、設計上の処理時間を越えてしまう、いわゆる「バッチの突き抜け」が想定されるとしましょう。

そのときに対処できる方法として、次の3つの事柄があります。

  • 並列実行
  • インフラの増強・チューニング
  • 処理の分割

まず、並列実行です。これは、一部の処理をマルチスレッド化したり、ワーカーに処理を委譲することで、全体のスループットを上げる方法です。プログラム単体で解決可能ですが、異常が起きた際にデバッグの手間が少々増える可能性があります。

次に、インフラの増強です。データベースへのI/Oなどで律速できる可能性があったり、計算機の処理性能をスケールアップすると解決できる可能性がある場合、対策を施すというものです。データベースのテーブルへIndexを張るなどのチューニングも検討の価値があります。

最後に、処理を分割する方法があります。たとえば、クリーンナップなどを同時に行っているのであれば、その処理を分割することでメインの処理を行っているバッチの処理時間を結果的に短くすることができます。このメリットは、メインの処理の品質を担保しつつ、時間制限がゆるい後処理も適切に終わらせることができるのです。

これらの対策は、処理時間が伸びてきたことを確認できた段階で、計画的かつ段階的に施すこともできます。当初は最小限で開発することで、すばやくリリースすることも検討します。

まとめ

ここまで、「バッチ処理を採用するとき」と「バッチ処理の設計」について取り上げてきました。バッチは実行時間が決まっていること、そして失敗を想定して回復可能な設計にしておくことが大切であることを述べてきました。

今後、もし時間があれば、「テストケースの作成」「監視(起動・正常終了・異常終了)」そして「当社の振込申請の事例」について取り上げてみたいと思っています。

それでは皆様、ごきげんよう。

*1:Dave Ingram, Design – Build – Run: Applied Practices and Principles for Production-Ready Software Development, Wrox, 2009