はじめに
ECS FargateでFireLens(Fluent Bit)をサイドカーとして導入したところ、Fluent Bitが原因不明で落ちてサービスが不安定になる事象が発生しました。Fluent Bitは essential: true で定義していたため、Fluent Bitが死ぬとアプリコンテナも道連れになり、サービス全体が停止します。
日本語のトラブルシュート情報を探しながら手探りで試行錯誤していたのですが、最終的に AWS公式ブログと amazon-ecs-firelens-examples の OOMKill Prevention ガイドを読んだら、合理的な設計指針がちゃんと書かれていて解決しました。
この記事では、実際にOOMを再現して exit code まで追いかけた検証結果と、公式ドキュメントの内容を照らし合わせて、なぜ公式の指針が妥当なのかを整理します。同じようにハマった方の参考になれば幸いです。
環境
- ECS Fargate(プラットフォームバージョン 1.4.0)
- アプリ: PHP 8.4 + Apache(prefork MPM)+ WordPress系CMS
- ログルーター: FireLens(aws-for-fluent-bit:stable, v1.9.10)
- タスクサイズ: 0.25 vCPU / 512 MB
症状
- Fluent Bitサイドカーが起動後しばらくすると落ちる
essential: trueなのでタスク全体が停止する- ECSが再起動 → またFluent Bitが落ちる → 無限ループ
- CloudWatch LogsのFluent Bitシステムログを見ると、ストリームは大量に作られているのにログが0件
当時のタスク定義(問題のある構成)
{ "cpu": "256", "memory": "512", "containerDefinitions": [ { "name": "app", "image": "php:8.4-apache ベースのアプリイメージ", "essential": true, "memory": 512, "logConfiguration": { "logDriver": "awsfirelens", "options": { "Name": "cloudwatch_logs", "region": "ap-northeast-1", "log_group_name": "/ecs/firelens-demo/app", "log_stream_prefix": "ecs/", "auto_create_group": "true" } } }, { "name": "fluentbit", "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable", "essential": true, "firelensConfiguration": { "type": "fluentbit" } } ] }
一見問題なさそうに見えますが、致命的な問題があります。
FireLens でのログ転送の仕組み
原因の説明に入る前に、FireLens(Fluent Bit)がどのようにログを転送しているかを整理します。

アプリコンテナの stdout/stderr に出力されたログは、Docker のログドライバ経由で同一タスク内の Fluent Bit コンテナに転送されます。Fluent Bit はログを受信し、内部のバッファに一時的に溜めてから、CloudWatch Logs などの送信先に送ります。
ここで重要なのは、Fluent Bit のメモリ消費の大半はこのバッファだということです。送信が順調なときはバッファはほとんど溜まりませんが、送信先がダウンしたりログが急増すると、バッファが膨らみます。
■ 送信先が正常な時
受信 → バッファ(少量) → 送信 → CWLogs ※バッファ溜まらない
■ 送信先がダウンした時(バッファ制御なし)
受信 → バッファ(膨張し続ける💥) → 送信失敗 ※メモリ食い尽くす → OOM
■ 送信先がダウンした時(Mem_Buf_Limit あり)
受信 → バッファ(上限で停止⛔) → 送信失敗 ※新規ログ取り込み一時停止
「これ以上溜めない」 タスクは生き続ける
この「バッファをどう制御するか」が、FireLens を安定運用するための核心です。
原因
構造的な問題: Fluent Bitにメモリの保証がない
タスク全体: 512MB ├── app: memory=512MB(ハードリミット)← タスク全体と同じ └── fluentbit: memory=指定なし ← メモリの保証がない
Fargateではタスクレベルのメモリがコンテナ間で共有されます。アプリに memory: 512(ハードリミット)を設定していますが、これはアプリが最大512MBまで使えるという意味です。Fluent Bitには memory も memoryReservation も設定されていないため、メモリの保証が一切ありません。
アプリのメモリ消費が少ないうちはFluent Bitも動けますが、アプリがメモリを使い切ると、Fluent Bitに残るメモリがなくなります。
誘発原因: PHP + Apache(prefork)のメモリ消費
PHP + Apache + WordPress系CMSの構成では、preforkモードの子プロセスが1リクエストあたり40〜80MBのメモリを消費します。子プロセスが8個起動するだけで320〜640MB。512MBのタスクでは、アクセスが増えた時点でタスク全体のメモリが枯渇し、メモリの保証がないFluent Bitが先に殺されます。
再現検証
実際にこの問題を再現してみました。なお、本検証は aws-for-fluent-bit:stable(検証時のバージョン: v1.9.10)での結果です。新しいバージョンでは挙動が異なる可能性があります。
再現用のDockerfile
WordPress風の重いPHPアプリを模擬するイメージを作成しました。
FROM php:8.4-apache WORKDIR /var/www/html # PHP拡張モジュール(WordPress環境で一般的なもの) RUN apt-get update && apt-get install -y \ libonig-dev libfreetype6-dev libjpeg62-turbo-dev libpng-dev \ libzip-dev zlib1g-dev libicu-dev && \ docker-php-ext-configure gd --with-freetype --with-jpeg && \ docker-php-ext-install gd mysqli pdo_mysql mbstring zip intl opcache bcmath exif && \ a2enmod rewrite && \ a2dismod mpm_event && a2enmod mpm_prefork && \ apt-get clean && rm -rf /var/lib/apt/lists/* # php.ini RUN echo "memory_limit = 256M" > /usr/local/etc/php/conf.d/wordpress.ini # prefork設定(子プロセスを多めに起動) RUN echo "StartServers 8\nMinSpareServers 5\nMaxSpareServers 20\nMaxRequestWorkers 50\nMaxConnectionsPerChild 0" \ > /etc/apache2/mods-enabled/mpm_prefork.conf COPY index.php /var/www/html/index.php EXPOSE 80
再現用のindex.php(1リクエストあたり約48MB消費)
<?php // WordPress風のメモリ消費を模擬 $plugins = []; for ($i = 0; $i < 30; $i++) { $plugins[] = str_repeat(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 40000); } if (function_exists('imagecreatetruecolor')) { $img = imagecreatetruecolor(2000, 2000); imagedestroy($img); } $cache = []; for ($i = 0; $i < 20; $i++) { $cache["query_$i"] = array_fill(0, 1000, str_repeat("data", 100)); } echo "<p>Memory used: " . round(memory_get_usage(true) / 1024 / 1024) . " MB</p>";
❌ 問題のあるタスク定義で実行
タスクサイズ 0.25 vCPU / 512 MB、アプリに memory: 512、Fluent Bitにメモリ指定なしで起動し、ab コマンドで負荷をかけました。
ab -n 200 -c 50 http://<タスクIP>/index.php
ab の結果
Complete requests: 185 Failed requests: 35 (Connect: 0, Receive: 0, Length: 35, Exceptions: 0)
200リクエスト中35がFailed(Length mismatch)。途中でタスクがOOMで停止し、処理中だったリクエストの接続が切れています。
タスクの停止状態
{ "stoppedReason": "Essential container in task exited", "stopCode": "EssentialContainerExited", "containers": [ { "name": "app", "exitCode": 137, "reason": null }, { "name": "fluentbit", "exitCode": 139, "reason": "OutOfMemoryError: container killed due to memory usage" } ] }
Fluent Bitのシステムログ(最後の行)
[2026/04/21 13:09:21] [engine] caught signal (SIGSEGV)
Fluent Bitが OutOfMemoryError で停止し、essential: true のためタスク全体が停止しました。
なお、exit code 139 は 128 + 11(SIGSEGV)です。通常、cgroup OOMKillerはプロセスに SIGKILL(exit code 137)を送りますが、今回は SIGSEGV になっています。これはFluent Bit内部でメモリ確保に失敗した際に、不正なメモリアクセスが発生してクラッシュしたものと考えられます。ECSの reason フィールドには OutOfMemoryError と記録されており、メモリ枯渇が根本原因であることは間違いありません。
CloudWatch Logsにアプリログが17件しか残っておらず、約90%のログが欠損しました。
公式ドキュメントを読んで分かったこと
この時点で「どうやって直せばいいんだろう」と試行錯誤していたのですが、AWS公式の情報源に答えが書かれていました。
AWS公式ブログ「詳解 FireLens」
詳解 FireLens には、以下の記述があります。
memory フィールドによるメモリ使用量のハードリミットは設定しないことをお勧めします。Fluent Bit と Fluentd のメモリ使用量は急増する可能性があり、その場合にコンテナが OOMKill される原因となるからです。
つまり、Fluent Bit の memory(ハードリミット)は設定しないのが公式推奨です。
AWS公式GitHubリポジトリ(OOMKill Prevention ガイド)
amazon-ecs-firelens-examples/oomkill-prevention には、より詳しい解説があります。
In normal/happy case conditions, the memory usage of Fluent Bit will often stay below 100 MB. But if there is a spike in log output from your application that Fluent Bit cannot handle or your log destination goes down, then Fluent Bit will have to buffer lots of logs. (...) Fluent Bit's memory usage can often spike to up to a Gigabyte or even more.
Fluent Bitの通常時のメモリ使用量は100MB以下だが、ログ送信先のダウンやスパイク時にバッファが膨らみ、GB単位まで膨らむことがある、とのこと。
だから個別コンテナレベルでハードリミットを設定すると、スパイク時に即死するわけです。
公式が示すメモリ制御の設計
公式の指針を整理するとこうなります。
- Fluent Bit には
memoryReservation(ソフトリミット)だけを設定する: スパイクに備えて、タスクメモリの空きを使えるようにしておく Mem_Buf_Limitを設定してバッファを制限する: メモリ暴走の主因はバッファなので、そこで制御するRetry_Limitを設定する: 送信失敗時の無限リトライを防ぎ、バッファの肥大化を防ぐ- タスクメモリに十分な余裕を持たせる: アプリ消費 + Fluent Bit分(最低100MB+α)を見込む
メモリ見積もりの計算式も提供されています。
Total Max Memory Usage <= 2 × SUM(各inputのMem_Buf_Limit)
Mem_Buf_Limit 50MB なら最大 100MB 程度で収まる、という見積もりが立てられます。つまりバッファ層で制御すれば、個別コンテナのハードリミットがなくてもメモリ消費は予測可能になります。
ECS Developer Guide(タスク定義例)
FireLens のタスク定義サンプル にも、Fluent Bit コンテナは memoryReservation: 51 のみで、memory は設定されていません。
{ "name": "my_service_log_router", "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:3", "cpu": 0, "memoryReservation": 51, "essential": true, "firelensConfiguration": { "type": "fluentbit" } }
サンプルコードに埋め込まれた設計思想が、公式ブログと OOMKill Prevention ガイドで明文化されているわけです。
修正版タスク定義で再検証
公式推奨に沿って修正しました。ポイントは memoryReservation のみではなく、Mem_Buf_Limit + Retry_Limit とセットで適用することです。
タスク定義
{ "cpu": "512", "memory": "1024", "containerDefinitions": [ { "name": "app", "memoryReservation": 768, "logConfiguration": { "logDriver": "awsfirelens", "options": { "Name": "cloudwatch_logs", "region": "ap-northeast-1", "log_group_name": "/ecs/firelens-demo/app", "log_stream_prefix": "ecs/", "auto_create_group": "true", "retry_limit": "3" } } }, { "name": "fluentbit", "memoryReservation": 100, "firelensConfiguration": { "type": "fluentbit", "options": { "config-file-type": "file", "config-file-value": "/fluent-bit/etc/extra.conf" } } } ] }
Fluent Bit カスタム設定ファイルの適用
Mem_Buf_Limit はタスク定義の logConfiguration.options では設定できないため、カスタム設定ファイルで指定します。タスク定義の firelensConfiguration.options に config-file-type / config-file-value を指定し(上記タスク定義参照)、設定ファイルはカスタムイメージに含めます。
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:stable COPY extra.conf /fluent-bit/etc/extra.conf
バージョンを固定したい場合は :stable の代わりに :2 や具体的なバージョンタグを指定してください。
Fluent Bit 設定ファイル(extra.conf)
Retry_Limit はタスク定義の logConfiguration.options に記載しているため、extra.conf には [SERVICE] セクションのみ記載します。
[SERVICE]
Flush 1
Grace 30
Mem_Buf_Limit 50MB
修正のポイント:
- タスク全体のメモリを1024MBに増量: アプリ + Fluent Bitが共存できるサイズにする
- アプリは
memoryReservation(ソフトリミット)に変更: ハードリミットでタスク全メモリを予約しない。Fargateではタスクメモリが事実上のハード上限として機能する - Fluent Bitに
memoryReservation: 100を設定: メモリの最低保証を与え、ハードリミット(memory)は公式推奨に従い設定しない retry_limit: "3"(タスク定義のlogConfiguration.optionsに記載): 送信失敗時の無限リトライを防ぎ、バッファの肥大化を抑制するMem_Buf_Limit 50MB(カスタム設定ファイルに記載): バッファの上限を設定し、メモリ消費を予測可能にする
この組み合わせにより、
- 通常時: Fluent Bit は100MB程度で動作(
memoryReservationで保証) - スパイク時: タスクメモリの空きを使って膨らめるが、
Mem_Buf_Limitでバッファ上限があるので暴走しない - 送信先ダウン時:
Retry_Limitでリトライが打ち切られ、バッファが無限に溜まらない
という挙動になります。
同じ負荷テストの結果
ab -n 200 -c 50 http://<タスクIP>/index.php
Complete requests: 200 Failed requests: 3 (Connect: 0, Receive: 0, Length: 3, Exceptions: 0)
{ "containers": [ { "name": "app", "status": "RUNNING" }, { "name": "fluentbit", "status": "RUNNING" } ] }
Fluent Bitのログにはエラーなし(infoレベルのみ)。アプリログは190件記録されました。
Failed 3(Length mismatch)は Fluent Bit の問題ではなく、同時50並列リクエストに対してタスクメモリ(1024MB)が不足したことによる Apache 子プロセスレベルのエラーです。Apache 親プロセスと Fluent Bit は正常に動作し続けており、ログの欠損は発生していません。タスクメモリの増量で解消できます。修正前の Failed 35(Fluent Bit の OOMKill によるタスク停止・ログ90%欠損)とは質的に全く異なります。
ビフォーアフターまとめ
| ❌ 修正前 | ✅ 修正後 | |
|---|---|---|
| タスクサイズ | 256cpu / 512MB | 512cpu / 1024MB |
| アプリのメモリ設定 | memory: 512(ハードリミット) |
memoryReservation: 768(ソフトリミット) |
| Fluent Bitのメモリ設定 | 指定なし | memoryReservation: 100(ハードリミットなし) |
| Fluent Bit 設定 | なし | Mem_Buf_Limit 50MB(extra.conf)+ retry_limit: "3"(タスク定義) |
| ab Failed | 35(タスク停止による接続断) | 3 |
| Fluent Bit | OutOfMemoryError (SIGSEGV) | エラーなし |
| アプリログ | 17件(約90%欠損) | 190件 |
| タスク | 停止(EssentialContainerExited) | 安定稼働 |
検証で分かった重要ポイント: memoryReservation だけでは不十分
検証の過程で、memoryReservation: 100 のみ(Mem_Buf_Limit / Retry_Limit なし)でも同じ負荷テストを実行しました。結果は以下の通りです。
Complete requests: 200 Failed requests: 82 (Connect: 0, Receive: 0, Length: 82, Exceptions: 0)
タスクは落ちませんでしたが、Failed が82件に増加しました。Mem_Buf_Limit がないとFluent Bitのバッファが制御されず、タスクメモリの空きをFluent Bitが消費してアプリ側が圧迫されたためと考えられます。
公式推奨は memoryReservation だけではなく、Mem_Buf_Limit + Retry_Limit とセットで初めて機能する、というのが本検証で得られた重要な知見です。
なぜ「Fluent Bit にハードリミットを使わない」が合理的なのか
最初、直感的には「Fluent Bit にもハードリミット(memory)を設定して、暴走を防ぐべきでは?」と考えていました。しかし公式ドキュメントの指針は逆で、その理由を整理すると以下のようになります。
Fluent Bit のメモリ消費はバッファが支配的
Fluent Bit のメモリ消費の大半は未送信ログのバッファです。CPU処理や内部状態で使うメモリは限定的で、バッファサイズが実質的なメモリ使用量を決めます。
バッファ量は外部要因で大きく変わる
- 通常時: 送信が追いつくのでバッファは溜まらない(100MB以下)
- 送信先がダウンした時: 送れないログがどんどんバッファに積まれる(GB超えも)
- ログスパイク時: 一時的にバッファに積まれる
この変動は外部要因(送信先の状態、アプリのログ量)で決まるため、"安全な固定値" を事前に決めるのが難しい。
個別コンテナのハードリミットの問題点
memory: 128 のようなハードリミットをかけると、スパイクで 128MB を超えた瞬間に OOMKill されます。essential: true なのでタスクごと死にます。ログ送信先が一時的にダウンしているだけの状況でも、タスク全体が落ちてしまう。
「バッファ層で制御する」のが筋の良い設計
バッファそのものに Mem_Buf_Limit で上限をかければ、スパイク時は新しいログの取り込みを一時停止する(バックプレッシャー)という挙動になります。これは「タスクが落ちる」よりもずっとマイルドな失敗モード。
- タスクメモリ = 物理的な最終上限
memoryReservation= 最低保証Mem_Buf_Limit= 主要な制御手段(メモリ消費の原因側で制御)Retry_Limit= バッファ肥大化の抑制
「原因側(バッファ)で制御する」のが公式の設計思想で、これが納得感のある結論でした。
対策まとめ
1. タスクメモリを十分に確保する
アプリのメモリ消費 + Fluent Bitのメモリ(最低100MB+α)を合計して、余裕を持ったタスクサイズを設定してください。
2. memoryReservation(ソフトリミット)を基本に、ハードリミット(memory)は極力使わない
- アプリ:
memoryReservationを使う。ハードリミットでタスク全メモリを予約しない。Fargateではタスクメモリが事実上のハード上限として機能するため、アプリの暴走はタスクメモリで抑えられる - Fluent Bit:
memoryReservationのみを設定する。memory(ハードリミット)は設定しない(AWS公式推奨)
3. Fluent Bitの Mem_Buf_Limit を設定する(最重要)
メモリ暴走の主因はバッファです。ここを制御すれば、ハードリミットがなくてもメモリ消費は予測可能になります。Mem_Buf_Limit はタスク定義の logConfiguration.options では設定できないため、カスタム設定ファイル(extra.conf)に記載します。
# extra.conf
[SERVICE]
Mem_Buf_Limit 50MB
値の目安は公式ガイドの計算式(Total Max Memory Usage <= 2 × SUM(Mem_Buf_Limit))を参考にしてください。memoryReservation だけ設定して Mem_Buf_Limit を設定しないと、バッファが制御されずアプリ側を圧迫する可能性があります(本検証で確認済み)。
4. Retry_Limit を適切に設定する
無制限リトライ(Retry_Limit False)はバッファが膨らむ原因になります。タスク定義の logConfiguration.options に記載できます。
"options": { ... "retry_limit": "3" }
あるいは、カスタム設定ファイル(extra.conf)の [OUTPUT] セクションに Retry_Limit 3 と記載することもできます。
おわりに
正直に言うと、トラブル発生時は日本語情報を探しながら手探りで試行錯誤していました。「Fluent Bit にもハードリミットをかければ暴走を抑えられるのでは」と遠回りな対処もしました。
しかし最終的に AWS公式ブログ「詳解 FireLens」と amazon-ecs-firelens-examples の OOMKill Prevention ガイドを読んだら、必要な情報が一通り書かれていました。
memory(ハードリミット)は設定しないmemoryReservationで最低保証を与えるMem_Buf_Limitでバッファを制御するRetry_Limitでリトライ蓄積を防ぐ
この4点セットで、Fluent Bitのメモリ暴走は合理的に制御できる設計になっていました。特に memoryReservation だけでは不十分で、Mem_Buf_Limit とセットで初めて機能するという点は、実際に検証して初めて実感できたことです。
似たような構成でFireLensがOOMで落ちて困っている方へ: まずAWS公式ドキュメントを読むのが一番の近道です(自戒)。
参考リンク
- 詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る | AWS ブログ
- amazon-ecs-firelens-examples/oomkill-prevention | GitHub
- Configuring Amazon ECS logs for high throughput | AWS Docs
- Fluent Bit Buffering and storage | Fluent Bit Official Manual
- aws/aws-for-fluent-bit Issue #382 - fluentbit sidecar obviously dies due to OOM