Timee Product Team Blog

タイミー開発者ブログ

コンテナのログがDatadogに到達するまでの険しい道のり

こんにちは、DevPFチームの菅原です。

現在、弊社のアプリケーション基盤(ECS on Fargate)では、コンテナログの収集・転送にFireLensを採用し、Datadog Logsへ集約しています。FireLensはタスク定義に数行記述するだけでログ基盤が整う非常に便利な機能です。一方で裏側では、Fluent Bitがサイドカーとして動作し、複雑なパイプラインを処理しています。

「ログなんて標準出力に出せば、あとは勝手に届くもの」

そう思われがちですが、実際にはアプリケーションコンテナからDatadog Logsのexplorerに表示されるまでには、いくつもの難所が存在します。今回は、ログがDatadogに到達するまでのアーキテクチャを紐解きながら、ログが「どこで」「なぜ」欠損するのかを解説します。

ログ転送のアーキテクチャ概要

まずは現在の構成のおさらいです。

  • 基盤: AWS ECS on Fargate(v1.4)
  • ログ収集: FireLens (実体はFluentdまたはFluent Bitサイドカー。弊社ではFluent Bitを採用)
  • 送信先: Datadog Logs

アプリケーションが出力したログは、大まかに以下の経路を辿ります。

  1. Appコンテナ: stdout/stderrへログを書き込む
  2. shim logger プラグイン: containerd-shimのロガープラグイン。shim loggerが各コンテナのstdout/stderrをパイプ経由で定期的に読み込み、サイドカーのFluent Bitに転送。
  3. Log router (Fluent Bit)コンテナ: ログを受け取り、バッファリング・加工を行う
  4. Network: Datadog APIへHTTPSで送信
  5. Datadog: Ingest(取り込み)・Indexing(インデックス化)

一見シンプルですが、この経路には大きく分けて4つの「欠損リスク」が潜んでいます。

アプリケーション 〜 Fluent Bit間のバックプレッシャー

最初の難関は、ログがサイドカーに渡る瞬間です。

仕組み

FireLensを使用する場合、Fargate (v1.4) の内部では shim-loggers-for-containerd が使用されます。アプリケーションが出力したログは、この shim logによってパイプ経由で読み取られ、Unix Socketを経由して Fluentd Forward Protocol でサイドカーのFluent Bitに転送されます。

ここで何が起きるか?

Fluent Bitで処理が詰まったりしてバッファがいっぱいになると、Fluent BitはInputを一時停止します。その間、アプリケーションが出力し続けるログはどうなるでしょうか?

Fluent BitがInputを停止している間に出力されたログは、Fargate基盤側のshim logger内のメモリバッファやその前段のパイプのバッファに溜まります。しかし、このバッファも有限です。よってFluent Bitが復帰して再開したときには、その間のログは闇に消えている可能性があります。

application-to-sidecar

対策

出来るだけ処理が詰まらないようにする、可能な範囲でバッファを大きくすることが対策となると考えます。例えば次のようなことができるでしょう。

  • 不要なログを出力しない
  • log-driver-buffer-limitオプションでのshim loggerレイヤのチューニング
  • log_routerのバッファサイズ、retryの上限設定等のチューニング

Outputの失敗と道連れ欠損

Fluent Bitがログを受け取った後、Datadogへ送信するフェーズです。

仕組み

Fluent Bitはログを1件ずつ送るのではなく、効率化のために複数のログをまとめてChunkにし、一定サイズまたは一定時間ごとに送信します。

ここで何が起きるか?

Outputの失敗は、「インフラ起因」と「ログ内容起因」の2つのパターンに分けられると考えます。それぞれの挙動とリスクを見ていきましょう。

インフラ起因:ネットワークや転送先の障害

インターネット接続の問題や、Datadog API側の一時的な障害、あるいはAPIキー等の設定ミスなどがこれに該当します。

Fluent Bitは送信に失敗すると、設定された回数(Retry_Limit設定)だけ再送を試みます。しかし、障害が長時間続いてリトライ回数を使い切ると、そのChunkは破棄されます。

欠損を防ぐためにリトライを無限に設定することも可能ですが、送信できないログがFluent Bitのバッファに滞留し続けることになります。これが解消されないとバッファが埋まったままになり、結果として前述のインプットの一時停止を引き起こし、新たなログの取り込み自体が止まってしまいます。

https://docs.fluentbit.io/manual/data-pipeline/buffering#per-input-settings

特定ログ起因: 不正なログによる巻き添え欠損

これが厄介なケースです。インフラは正常でも、アプリケーションが出力した「特定のログ」が原因で発生することがあります。

Datadog Logs APIには、「ペイロードあたり最大コンテンツサイズ(非圧縮): 5MB」という制限があります。

例えば、Base64エンコードされた画像やPDFデータを含む数MBの巨大なログが1件出力されたとします。Fluent Bitはこの巨大ログと、同じタイミングで出力された正常な小さいログをまとめて1つのChunkにしてしまいます。Fluent Bitのdatadog output pluginはChunk単位でログを送信します。このChunkを送信しようとすると、サイズ制限超過によりDatadogから 413 Payload Too Large エラーが返されたり、接続がリセットされたりします。 重要なのは、問題のある巨大ログだけでなく、同じChunkに含まれていた正常なログもまとめて拒否されてしまう点です。たった1行の行儀の悪いログのせいで、周囲の無実なログまで道連れにして消えてしまう可能性があるのです。

Fluent Bitのメモリバッファのサイズ制限を設ければいいのでは?と思われるかもしれません。しかし、該当するmem_buf_limit設定はソフトリミットとして実装されているようです。読み解いたソースコードレベルの挙動としては、「書き込み前に空き容量をチェックする」のではなく、「書き込んだ後に上限を超えていないかチェックする」という動きで、一時的に上限を超過することは許容されているようでした。

mem_buffer_limit = 5MBの時に巨大なログが来た場合の挙動

対策

方向性は大きくアプリケーション層での予防的設計と、転送層でのフィルタリング・バッファ制御が考えられます。いずれもトレードオフを伴うため、システムの特性に応じたバランスが求められます。

  • アプリケーション側で長大なログを出力しないよう制限する
  • アプリケーションまたはfluent bitレイヤで一定サイズを超えるログフィールドを自動的に切り捨てるようにする
  • 特定パターンのログを除外または置換する
  • mem_chunk_size, flush_intervalを短くして巻き添え欠損の影響範囲を小さくする

メモリバッファの消失

Fargateのようなコンテナ環境特有のリスクです。

仕組み

Fluent Bitのバッファ設定をデフォルトの memory にしている場合、ログは送信完了までメモリ上にしか存在しません。

ここで何が起きるか?

コンテナが予期せず停止した場合、メモリ上の未送信ログは消滅します。これは異常時だけでなく、通常のデプロイフローでも発生し得ます。

  1. ECSタスク停止時、コンテナにはSIGTERMが送られます。
  2. タスク定義のstopTimeout(デフォルト30秒)以内に終了しない場合、SIGKILLで強制終了されます。
  3. Fluent Bit側にもシャットダウン時の待機設定(Grace)がありますが、これらが噛み合わず、バッファをFlushしきる前にプロセスが強制停止されるとログは欠損します。

引用: 「ECS のアプリケーションを正常にシャットダウンする方法」, Amazon Web Services ブログ, https://aws.amazon.com/jp/blogs/news/graceful-shutdowns-with-ecs/

対策

  • バッファのストレージタイプをfilesystemに変更し永続化する。ただしECS Fargateの場合、現実的に使えるのがEFSくらいで永続化ストレージの利用に制限があるためトレードオフの検討が必要です。
  • stopTimeoutGraceの値を適切に調整し、シャットダウン時にFlush完了までの十分な猶予時間を確保する。
  • flush_intervalを短く設定し、バッファに溜まるログの量を減らすことで、シャットダウン時の欠損リスクを最小化する。ただし、ネットワーク負荷とのバランスを考慮する必要があります。

Datadog側の制限

無事にインターネットを越えてDatadogに届いても、最後の難所があります。

ここで何が起きるか?

Datadog側にも厳格な受入ルールがあり、これに違反すると取り込み後に削除、または切り捨てられます。

  • 日次クォータ超過: コスト管理のために設定したインデックスの上限を超えた場合、インデックスには保存されません(アーカイブ設定があればS3には残ります)。
  • タイムスタンプ: ログの日時が「18時間以上過去」の場合、取り込み対象外となり削除されます。
  • サイズ制限:
    • 単一ログが1MBを超える場合、Datadogによって切り捨て(Truncate)が行われます。
    • 前述の通り、APIへの送信ペイロード全体(非圧縮)が5MBを超えると、そもそも受け入れられずエラーとなります。

対策

  • 日次クォータの監視アラートを設定する
  • 前述の通り、アプリケーション側とFluent Bit側で対策を行い、Datadog側の制限に引っかからないようにする。
  • retry間隔がタイムスタンプの制限を超えないように再送の設計をする

まとめ

ログ基盤の信頼性を高めるためには、これらの欠損ポイントを理解した上での設計・運用が必要です。

  • Input: Fluent Bitが詰まるとその前段でログが溢れ欠損しうる。
  • Output: Chunk単位で処理されるため、異常なログが正常なログを巻き込むことがある。送信リトライはトレードオフがある。
  • Buffer: メモリバッファは揮発しうる。
  • Ingest: Datadog側のクォータやサイズ制限も意識する。

ログが欠損しているという事象に直面したとき、アプリケーションコードだけでなく、この険しい道のりのどこで詰まっているのかを想像できると、調査の解像度がぐっと上がるはずです。

参考資料