
はじめに
SREチームの森原(@daichi_morihara)です。今日はバッチの監視周りの取り組みについて共有していこうと思います。
これまではバッチのモニタリングに関して、エラーログの検知・対応のみの最低限の監視を行っている状態でした。そこでバッチ関連のオブザーバビリティの強化およびトイル削減のために以下の3つに取り組みました。
- バッチ失敗時の自動再実行の設定
- バッチ毎の実行時間や実行結果・インフラレイヤーのメトリクスをまとめたDatadogのダッシュボード作成
- DatadogのAPM導入
それぞれの実装において得た学びや注意点を紹介していきたいと思います。
バッチのインフラ構成図
ニーリーでは以下の構成図の通りEventBridgeでStep Functionsを発火し、そのStep Functions内でECSタスクを起動させバッチを実行するという構成をとっています。Step Functionsを間に挟む理由は後に説明します。

またコード側ではバッチを実行する関数にバッチ用のデコレーターを付与しています。このデコレーターで排他制御を行い、同じバッチが同時に実行されないように制限しています。
バッチの自動再実行とインフラレイヤーでのバッチ監視
Step Functions内からバッチのECSタスクを起動する理由は以下の通りです。
- インフラ起因でのバッチの実行失敗を監視できる
- 一部のインフラ起因の失敗に対してStep Functionsの自動リトライで対応できる
Step Functionsの実行結果を監視することでインフラ起因のバッチ失敗に気づくことができます。ECSタスクの起動失敗でそもそもバッチが実行されていないというケースなどが当てはまります。
そして今年の春頃からAWSのキャパシティオーバーによってECSタスクが起動失敗するECS.ServerExceptionというエラーが1日1回くらいの頻度で発生していました。
手動でバッチを再実行するのはめんどくさいので可能な限り避けたいものです。そこでECS.ServerExceptionが起きた場合は時間を開けてリトライするようにStep Functionsで設定しました。これによりECS.ServerException起因でStep Functionsの実行が失敗するということはなくなり、手動で再実行する手間の削減につながりました。
非常に便利な機能なのでバッチ実行にStep Functionsを間に挟むのはおすすめです!
バッチ毎のインフラメトリクス取得
ECSタスク内でdatadog-agentをside carとして起動させるとDatadogにECSタスクのメトリクスを転送することができます。またdatadog-agentの環境変数にDD_TAGSを追加すると任意のタグをメトリクスに付与することができます。ECSタスク起動時にDD_TAGSを書き換えることで、バッチ毎にそれぞれ固有のタグを付与することができます。
バッチのインフラメトリクスを取得したことにより、CPUやメモリなどのリソース消費量がバッチによってかなりばらつきがあることが判明しました。今後は取得したメトリクスを監視しながらバッチ毎にリソース最適化を行い、コスト削減やスペック不足による失敗の未然防止を実施していこうと思います。
バッチの実行時間・結果の可視化
前述したバッチ用のデコレーターにバッチの実行時間と実行結果を示すログも出力させています。コードはかなり簡略化していますが、大まかな流れとしては排他制御の確認後バッチを実行するというものであり、関数の実行前後の時間を取得することで実行時間を算出しています。
def periodic_batch(func): @wraps(func) def wrapper(*args, **kwargs): try: if not has_lock: has_error = True _logger.error(f'Periodic Batch {func.__name__} --- Exclusive Lock Error') return # バッチ処理実行 start_time = time.time() func(*args, **kwargs) end_time = time.time() except Exception: finally: proc_time_ms = round((end_time - start_time) * 1000) _logger.info(f'Periodic Batch {func.__name__} --- Normall End, proc_time_ms={proc_time_ms})') return wrapper
このログをDatadogに転送し、DatadogのLog PipelinsとGenerate Metricsを使用してバッチ毎の実行回数・時間・結果を抽出します。
具体的にはまずLogPipelineのGrok Parserで以下のようにparsing ruleを設定し、batch_statusやproc_time_ms(実行時間)などをattributeとして抽出します。ruleは対象ログによって異なるのでご参考までに。
NormalRule apps.core.decorators wrapper Periodic Batch %{data:batch_job} --- %{data:batch_status} proc_time_ms=%{integer:proc_time_ms}\)
次にGenerate Metricsでログをメトリクス化します。例えば以下の設定でバッチの実行回数のメトリクスが作成されます。
name: batch.job.count, type: count, filter: service:backend-batch @batch_status: *, dimensions:batch_job, batch_status, env
これによりdimensionsで設定したバッチ名や、環境、ステータス(成功・失敗)などでメトリクスを分類することができます。
これらをインフラメトリクスと合わせて以下のような表にまとめました。またグラフでデータの変化・傾向を確認することができます。

APM導入
バッチにDatadogのAPMを導入する際はサンプリングレートの設定に注意する必要があります。なぜならDatadogのデフォルトのAPMサンプリングレートは10trace/sなので、それぞれ別のECSタスクを起動させるバッチにおいて、デフォルトのままでは100%のサンプリングレートとなってしまうからです。サンプリングレートは環境変数にDD_TRACE_SAMPLING_RULESを追加することで設定できます。バッチの実行頻度に応じて、ECSタスク起動時にDD_TRACE_SAMPLING_RULESを書き換えることでバッチ毎に適当なサンプリングレートを設定しました。
NAT gatewayのコスト増加
APM導入までで一通り完了したと思いきや、ある問題に気づきました。それはNAT gateway のコストが急増していることです。
このコスト増加の原因はdatadog-agentのイメージをECR Public Repositoryから取得していたのと、バッチのECSタスクの起動・停止回数が非常に多いことにありました。 Public Repositoryからイメージを取得すると、イメージレイヤーも含めて全てのデータがNAT gateway経由となるためかなりのデータ通信量になりコスト増加につながります。
(ECSタスクがプライベートサブネット内にあることを想定しています)
よってECR Pull Through Cacheという機能を使用してdatadog-agentのイメージをECR Private Repositoryにキャッシュすることで対応しました。イメージがPrivate Repositoryにあればイメージレイヤーは無料であるS3用のVPC Endpoint経由で取得することができます。これはECRがイメージレイヤーを同一リージョンのS3で保存しているためです。イメージレイヤーがおよそ99.9%のデータ量となるため、この対応によりNAT Gatewayのコスト増加は収まりました。

またこれを機にweb/workerのECSタスクもPrivate Repositoryにあるdatadog-agentを使用することでNAT gatewayの通信コスト削減を行いました。もしECSタスクがPublic Repositoryからイメージを取得してる場合はECR Pull Through Cacheを使用することをおすすめします。
最後に
以上が最近のバッチ監視周りの取り組みでした。監視強化できたもののAPMやNAT gatewayが起因で一時的なコスト増加に繋がるといった失敗・学びもありました。本記事が今後同様の取り組みをされる方のお役に立ちますと幸いです。