こんにちは。MA部の田島です。
弊社では開発ガイドラインというものを用いて、システムの品質を担保しています。今回私がテックリードを務めているということもあり、バッチアプリケーションを開発するためのガイドラインを作成しました。本記事では「開発ガイドライン」と「バッチ開発ガイドライン」を紹介します。
バッチアプリケーション開発に限定したTipsはまとまっているものが多くないため参考にしていただければと思います。
開発ガイドラインについての紹介
冒頭でも紹介した通り弊社では、開発ガイドラインというものを用いてシステムの品質を担保しています。バッチ開発ガイドラインを紹介する前に、まず開発ガイドラインを紹介します。
開発ガイドラインの種類
開発ガイドラインは現在、以下の種類が存在します。
- 共通
- Android
- iOS
- Frontend
- Backend
- Infra
- API
- Batch
- DB(Database)
- ML(Machine Learning)
各チームはこの開発ガイドラインに沿うように、システムを構築・改修しています。また、新規システムの構築や大きめのシステムを改修するときは、リリースフローが定められており、その過程で開発ガイドラインに沿っているかのチェックが行われます。
ガイドラインの遵守ルール
ガイドラインには項目ごとに以下のようなタグが付いており、タグによってどの程度ガイドラインを遵守すべきかが変わります。
タグ | 遵守の必要性 |
---|---|
MUST / MUST NOT | 必須 |
RECOMMENDED / NOT RECOMMENDED | 推奨 |
新規ルールが策定された際、既存システムがガイドラインに追従するのは多大な工数が生じることもあります。そのため、完全な遵守は新規システムや新規改修時にのみ適用するようにしています。ただし、既存システムにおいてもガイドラインのルールを守ることで品質の向上を図れるため、遵守することを推奨しています。
バッチ開発ガイドラインの紹介
続いては、本ブログのメインテーマであるバッチアプリケーション開発に特化したバッチ開発ガイドラインについて紹介します。
バッチ開発ガイドライン作成の背景
今回、新たにバッチアプリケーション開発のためのガイドラインを作成しました。こちらはもともと私が所属する部署である、MA部がバッチアプリケーションを大量に開発・メンテナンスしていたため部署向けに作ったものでした。しかし、内容はMA部に限定せず汎用的なものを作成したため、それを社内全体のガイドラインとすることになりました。
バッチ開発ガイドライン
以下がバッチ開発ガイドラインです。実際のガイドラインの内容をほぼそのまま掲載しました。ただし、社内向けの補足情報などが含まれているのでそれらは省略しています。その代わりに、今回各項目において「補足」という項目を追加し具体例や補足情報を追加していますので参考にしていただければと思います。クラウドサービスの利用が前提となっている項目もあるのでその点はご了承ください。
コードベース
バッチの設定をアプリケーションと同じリポジトリで管理する(RECOMMENDED)
バッチアプリケーションでは以下を同じリポジトリで管理するようにしてください。
項目 | 具体例 |
---|---|
バッチアプリケーション | バッチ処理を具体的に行うアプリケーション(Shell/Python/Java/SQLなど) |
バッチ設定 | 各バッチのスケジューリングや依存関係など |
補足
このガイドラインの前提として、Backendガイドラインにおいて以下が定められています。
アプリケーションの動作に必要な「全て」のコードをGitHubで管理する(MUST)
また、このガイドラインの意図は、バッチの設定と動作するアプリケーションを近くに置くことで、システム把握を容易にすることです。RECOMMENDEDにしている理由は、例えばRailsアプリケーションでバッチ専用のAPIを用意し、バッチアプリケーションはそのAPIを叩くだけといった構成にしたい需要もあると考えたからです。
基盤
依存関係があるバッチにはワークフローエンジンを利用する(RECOMMENDED)
バッチ同士の依存関係が複雑な処理にはワークフローエンジンを利用してください。
補足
以下にワークフローエンジンの例を掲載します。いずれも弊社で利用実績のあるワークフローエンジンです。
ツール |
---|
Apache Airflow(Cloud Composer) |
Digdag |
Argo Workflows |
Kubeflow Pipelines(Vertex AI Pipelines) |
AWS Step Functions |
GCP Workflows |
処理ごとにコンピュートリソース(CPU/メモリ/ストレージ)を選択できる(RECOMMENDED)
バッチ処理毎にコンピュートリソースを選べるようなアーキテクチャを利用してください。以下のようなワークフローエンジンでは、コンテナを利用することでコンピュートリソースを処理ごとに選択できる機構が存在します。
ツール | 機構 |
---|---|
Apache Airflow | Kubernetes Executor / airflow-aws-executors |
Digdag | ECS Command Executor / Kubernetes Command Executor |
Argo Workflows | Core Concepts 参照 |
AZの障害発生時に別のAZにてバッチを実行出来る(MUST)
AZの障害時に、別のAZにてバッチを実行出来るような構成にしてください。
ただし、バッチサーバーが多重起動し、重複してバッチが実行されないように注意してください(参照: 「バッチの2重起動を防ぐ」)。
例えば、ジョブキュー型のワークフローエンジンを利用すると、別AZにジョブワーカーを起動することでバッチ処理の途中から再開が可能となります(※キューもMultiAZ構成になっている必要あり)。
補足
ここでは例としてAZの単位での障害を想定していますが、要件によって「Region」「AZ」等の分離レベルを検討してください。
処理ワーカーが自動スケールされる(RECOMMENDED)
バッチ処理を行うのに必要なコンピュートインスタンスが、必要に応じて自動スケールするようなアーキテクチャを利用してください。
ただし、過剰にワーカーがスケールされすぎていないか、ワーカー数に上限を設けるようにしてください。
永続化ファイルは外部ストレージに配置する(RECOMMENDED)
ログなどの永続化するファイルはバッチサーバーではなく、外部ストレージに永続化してください。
ツールによってはデフォルトで外部ストレージを利用する設定があるのでそれを利用することをおすすめします。
以下がストレージの例です。
ストレージ | 対応ツール |
---|---|
S3 | Apache Airflow / Digdag / Argo Workflows |
GCS | Apache Airflow / Digdag / Argo Workflows |
補足
本ガイドラインの意図としては、バッチアプリケーションが稼働するノードがリタイアしたとしてもログ自体は永続化したいということです。そのため、ストレージでなくともAWSの CloudWatch
やGCPの CloudLogging
などでのログの永続化も選択肢の1つとなります。
ただし、それらLoggingサービスはストレージサービスよりも比較的高価なため、Storageサービスにログを保存し、それらを容易に参照できる状態になっているのが良いと考えています。
アプリケーション設計 / SRE
自動リトライをする(MUST)
特定の処理がネットワークの一時的な問題などで失敗することがあるため、処理ごとに自動リトライを行ってください。
リトライはツール・ライブラリに任せる(RECOMMENDED)
バッチごとにリトライを実装することはバグの原因になるため、ツールのリトライ機構に任せるようにしてください。
ツールのリトライ機構では足りない場合は、ライブラリを利用してください。
また前提として、バッチ処理で利用しているクライアントライブラリなどにリトライ機構が含まれている場合は適切に設定してください。
以下がツールやライブラリの例です。
ツール
ツール | 機構 |
---|---|
Apache Airflow | retry parameter |
Digdag | _retry parameter |
Argo Workflows | Retries |
Step Functions | Retry |
GCP Workflows | Retry steps |
ライブラリ
言語 | ライブラリ |
---|---|
Java | failsafe など |
Python | tenacity など |
Go | retry-go など |
バッチの2重起動を防ぐ(MUST)
重複起動されてはいけないバッチ処理が多重起動されないようにしてください。
例えば、2重起動を防ぐ仕組みがあるワークフローエンジンを使う、またはバッチの先頭でLockを取ることなどで実現可能です。
処理を冪等にする(MUST)
処理はリトライを何度行っても問題ないようにしてください。
補足
冪等性を考慮する場面として、データの操作があげられます。以前のテックブログで「BigQueryでのデータ追記処理における冪等化の取り組み」を紹介しているので、そちらもご参照ください。
現在日時に依存する処理を入れない(NOT RECOMMENDED)
上記「処理を冪等にする」を達成するために、現在日時に依存する処理を避けてください。
代わりに処理開始時刻等で代替できないか検討してください。
またリトライ時にも冪等な処理となるように、リトライ時にリトライ前の処理開始時間を利用したり、特定の時間を外部から注入したり出来るようにしてください。
補足
実装の具体例として、ワークフローエンジンを利用している場合には、特定のワークフローの開始時間を取得できます。例えばDigdagでは以下のような「SessionTime」というものが利用できます。これを利用することで、リトライが発生した場合もリトライ前、リトライ後で同様の時間を利用した処理がされます。
ひとつひとつの処理を小さくする(RECOMMENDED)
処理が失敗した時のリトライ時の復旧時間を短くするため、ひとつひとつの処理を責任毎に分割してください。
また、各処理を責任毎に小さくすることで全体の見通しが良くなります。
補足
例えば以下のようなクエリでSELECT文に時間がかかる場合、INSERTの処理で失敗するとまた時間のかかるSELECT文からやり直す必要が出てきます。
INSERT INTO `project_id.dataset_id.destination_table` (count_result, timestamp) SELECT COUNT(*) AS count_result, CURRENT_TIMESTAMP() AS timestamp FROM `project_id.dataset_id.source_table` WHERE last_purchase_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 100 DAY);
そこで処理を以下のように別々にすることで、2つ目の処理だけをリトライできます。ただし、この場合1つ目のSELECT結果を2つ目のクエリに渡してあげる必要があります。
SELECT COUNT(*) AS count_result, CURRENT_TIMESTAMP() AS timestamp FROM `project_id.dataset_id.source_table` WHERE last_purchase_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 100 DAY);
INSERT INTO `project_id.dataset_id.destination_table` VALUES (${count_result}, ${timestamp})
ワーカーを増やせば処理性能が線形に伸びるように実装する(RECOMMENDED)
大量のデータ処理など、データ量が増えた場合でも処理ワーカーを増やすことで処理性能が線形に伸びるように実装してください。
例えばデータの処理を分割し、並列化することで実現ができます。その時のチャンクサイズは後から変えられるようにしてください。
補足
例えば、以下のような毎秒1000件で合計1000件を配信するようなバッチを考えます。
その後配信量が倍に増え、2000件の配信が必要になった場合以下のようにワーカーを倍にすることで処理スピードは落とさずに倍の配信ができます。ただし、ここではリクエストされる側の負荷は気にしないものとします。
早い段階でValidationを行う(RECOMMENDED)
早い段階でデータのValidationをおこない、不正なデータが確認された場合はバッチを落としてください。
長時間のデータ処理が完了した後に不正データにより処理が失敗することによる、処理遅延を防ぐことに繋がります。
処理失敗時に通知する(MUST)
バッチ処理が失敗した場合SlackやPagerDutyで気付けるようにしてください。
補足
弊社ではSlackやPagerDutyを利用していますが、それぞれの利用ツールに合わせて通知先は変更してください。以下のようなツールでは通知の仕組みやプラグインが用意されています。
ツール | 機構 |
---|---|
Apache Airflow | slack / pagerduty |
Digdag | slack / その他参考 |
処理時間のSLAを設ける(MUST)
バッチ処理時間にSLAを設け、SLAを超えた場合はSlackやPagerDutyなどで気付けるようにしてください。
SLAの設定は、「処理時間」または「特定の日時」のどちらでも問題ありません。
補足
以下のようなツールではSLAの仕組みが用意されています。
ツール | 機構 |
---|---|
Apache Airflow | Timeouts |
Digdag | sla |
Argo Workflows | timeouts |
Step Functions | TimeoutSeconds |
バッチ処理が適切に動作を開始しているかを監視する(MUST)
定期実行バッチが適切に開始されているかを監視し、開始されていない場合は気付けるようにしてください。
処理ワーカー数やワーカープロセスの監視を行ったり、プロセスやログが定期的に動いているかを監視したりすることで実現可能です。
例えば、正常にバッチ処理が開始しないケースとしては以下が考えられます。
- アプリケーション自体の設定は正しいが、バッチ処理を実行するためのインスタンスが0台になっている
- アプリケーション自体の設定は正しく、バッチ処理を実行するためのインスタンスも起動しているが、スケジューラなどのプロセスが起動していない
依存関係を把握する(MUST)
各バッチが他の「どのような処理に依存しているのか」・「どのような処理に依存されているのか」を把握してください。
また、特定のバッチに異常があった場合に、依存関係のあるバッチを止めるなどの対応方針を事前に検討してください。
依存処理がある場合は待ち合わせ処理を実装する(MUST)
バッチが他の処理に依存している場合は、時間で待ち合わせをするのではなく確実に依存処理が完了したことを確認したうえでバッチ処理を実行するようにしてください。
実行タイミングがいつでもいいものは平日の日中に実施する(RECOMMENDED)
実行タイミングがいつでもいい処理に関しては、対応者が対応しやすい時間帯である平日の日中に行ってください。
月1回など実行頻度の少ないバッチは極力避け、原則デイリー実行にする(RECOMMENDED)
月1回だけ実行されるような実行頻度の短いバッチは、デイリー実行でも問題無いようにして原則1日1回以上動かすようにしてください。
バッチを修正等してから初回起動までに時間が空かないため、問題の発見・修正を素早くできます。
ただし、毎日実行だと料金的なコストが大幅に上がるなどあればその限りではありません。
データの更新や配信等の副作用のあるバッチはDRY RUNを行う(RECOMMENDED)
データの更新や配信等の副作用のあるバッチはDRY RUNを行い事前に動作が問題ないこと、処理内容が問題ないことを確認してください。
新規開発・修正をした直後のバッチ実行時には立ち会って実行ログや動作結果を確認する(MUST)
バッチの修正を行った直後の初回起動時は、実行時間に立ち会ってログや動作結果を確認してください。
ガイドライン導入の効果と改善点
以上バッチ開発ガイドラインを紹介しました。今回バッチ開発ガイドラインを新規に作成したと紹介しましたが、実際にはガイドラインを作成してから2年ほどが経ちました。ガイドライン作成の結果MA部では、ガイドラインに書いてある項目について気を付けて実装ができるようになったと感じています。
特に冪等性の担保という部分においては、ガイドライン作成前に比べて注意して実装ができるようになったと感じています。そのお陰でシステムのリトライを気軽かつ安全にできるようになりました。
ただし、油断しているとガイドラインに沿っていない実装がまだまだ意図せずに入ってしまっていることがあります。ガイドラインの項目も多いので全PRですべてのチェックを1個1個行うのには時間がかかります。そのため、メンバーそれぞれがガイドラインの内容を当たり前にできるようになることが大事だなと感じています。
まとめ
本ブログでは、開発ガイドライン並びにバッチ開発ガイドラインを紹介しました。バッチ開発ガイドラインについては私達が利用しているガイドラインをほぼそのまま紹介しました。ぜひ利用して頂いて、バッチアプリケーションの品質向上に役立てていただければ幸いです。
終わりに
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。