Dockerログの道のり:containerd-shimからFluentdまで(前半)
ある日、docker コマンドがハングし、根本原因にfluentdが関連していそうだったのがきっかけで、ログの流れをちゃんと調べることにしました。
この記事では Docker のロギングドライバと Fluentd がどう繋がっているのか を、実際の構成例から順に説明していきます。
構成は2章立てで、前半が「仕組みの基礎」、後半が「エラーを再現しながら挙動を理解する」です。
この記事でわかること
- Docker のロギングドライバが stdout/stderr をどこへ流すのか
- Fluentd が どのプロトコルでログを受けるのか
- 「配送先が落ちると上流が止まる」理由の背景
具体例(Compose構成)
最初に「本番でありがちな構成」を置きます。以降の説明はこの例を参照します。
例: API と Worker のログを Fluentd に集約する
API / Worker (stdout)
↓
Docker logging driver (fluentd)
↓
Fluentd (forward input)
↓
ログ保管先(例: Elasticsearch / S3 / ログ基盤)
docker compose(例)
services:
fluentd:
image: fluent/fluentd:v1.16-1
ports:
- "24224:24224"
volumes:
- ./fluentd/fluent.conf:/fluentd/etc/fluent.conf:ro
- ./fluentd/log:/fluentd/log
api:
image: myorg/api:1.2.3
logging:
driver: fluentd
options:
fluentd-address: 127.0.0.1:24224
tag: prod.api
worker:
image: myorg/worker:1.2.3
logging:
driver: fluentd
options:
fluentd-address: 127.0.0.1:24224
tag: prod.worker
Fluentd 側(例)
<source>
@type forward
port 24224
</source>
# 本番では Elasticsearch / S3 などへ出す想定。
# ここでは動きを理解するため file 出力にしています。
<match prod.**>
@type file
path /fluentd/log/prod.log
append true
</match>
補足: fluentd-address は コンテナ内ではなく dockerd(ホスト)から解決されます。
そのため 127.0.0.1:24224 を使う場合は、Fluentd 側でホストにポート公開されている必要があります。
この「api / worker → fluentd → ログ保管先」という流れを、以降の章で分解して説明します。
Docker のロギングドライバとは
Docker はコンテナの stdout/stderr を以下のように受け取ります。
container stdout/stderr
↓
containerd-shim (FIFO)
↓
dockerd
↓
logging driver
↓
ログ配送先
ここで logging driver が配送の主役です。fluentdの他には:
-
json-file(デフォルト、ホスト上に JSON で保存) -
local(ローテーション効率が良い) -
journald(systemd の journal へ) -
syslog(syslog へ) -
gelf/awslogs/splunkなど
ドライバを変えると「ログの保存先」と「配送の挙動」が変わるのがポイントです。
また、ドライバはコンテナごとに指定できるため、サービス単位で使い分けることもできます。
(Docker デーモンのデフォルト設定を変えつつ、特定のサービスだけ上書きする運用も可能です)
用語の前提(forward / containerd-shim / dockerd)
forward プロトコル
Fluentd/Fluent Bit がログを送受信するためのプロトコルです。
Fluentd 側は @type forward で待ち受け、Docker の fluentd ロギングドライバは TCP で forward に接続してログを送る仕組みです。
containerd-shim
コンテナごとに起動する小さな仲介プロセスです。
コンテナの親プロセスとして stdout/stderr の FIFO を持ち続ける役割があり、containerd が再起動してもコンテナが生き続けられるようにします。
ここでいう FIFO は「名前付きパイプ」のことで、読み手が詰まると書き手も止まる性質があります。
余談: “shim” は「すき間を埋める薄い仲介層」という意味で、containerd とコンテナの間に挟まる役割からこの名前が付いています。
dockerd
Docker Engine の本体デーモンです。
docker run / docker stop などの API を受け付け、containerd を介してコンテナを管理します。
ロギングドライバも dockerd 内で動作し、stdout/stderr を Fluentd へ転送します。
この3つをつなげると、ログの流れは次のようになります。
アプリの stdout/stderr
→ containerd-shim(FIFO保持)
→ dockerd(ロギングドライバ)
→ Fluentd(forward入力)
Fluentd ロギングドライバの動き
1. コンテナがログを書く
例の api / worker が stdout にログを書きます。
2. dockerd が Fluentd に送る
Fluentd ロギングドライバが fluentd-address=127.0.0.1:24224 へ接続し、ログを転送します。
3. Fluentd が受信する
Fluentd 側は @type forward で受け取り、prod.api / prod.worker のタグで処理します。
4. Fluentd が出力する
受け取ったログを file / Elasticsearch / S3 などへ出力します(例では file)。
もう少し具体的に説明すると、
例の api が stdout に1行書いたとき、実際は次のように流れます。
- コンテナの stdout に文字列が書き込まれる
- containerd-shim が FIFO に保持したストリームとして受け取る
- dockerd が FIFO を読み出し、ロギングドライバへ渡す
- fluentd ドライバが TCP で forward へ送る
- Fluentd 側でタグ(例: prod.api)に応じて振り分ける
この「1行がどこを通るのか」を知っておくと、後半で解説する「なぜdockerコマンドがハングするのか」が理解しやすくなります。
タグ(tag)の役割
例の compose では prod.api / prod.worker のようにタグを付けています。
Fluentd はこのタグで ルーティングやフィルタ条件を分けられるので、実運用ではほぼ必須です。
<match prod.**>
@type file
path /fluentd/log/prod.log
</match>
この例だと prod.* のログが同じファイルに集約されます。
タグを分けると「API と Worker のログを別系統で扱う」こともできます。
配送モード(blocking / non-blocking)
Docker には配送モードがあり、ドライバによっては non-blocking にできます。
-
blocking: 配送成功まで待つ(安全だが止まりやすい)
- Fluend ドライバのデフォルト
- non-blocking: バッファに積んで先に戻る(落ちても止まりにくい)
Fluentd ドライバは fluentd-async や mode=non-blocking で非同期化できますが、
配送先が落ちると完全に止まる問題は残ることがあるため、過信は禁物です。
エラーを再現しながら挙動を理解する(予告)
ここまでが「仕組みの基礎」です。
後半では実際に Fluentd を止めて docker compose stop/down が固まる様子を再現しながら、
「どこが詰まるのか」「なぜ復帰するのか」を追います。
まとめ
- Docker のログは「containerd-shim → dockerd → logging driver → Fluentd」という 同期パイプライン で流れている
- fluentd ロギングドライバは stdout/stderr を TCP forward でそのまま配送するため、配送先障害で Docker 操作まで止まるという性質がある
- このパイプラインは FIFO / ストリームで繋がっているため、
- Fluentd 停止・詰まり
- = containerd-shim の読み取り停止
- = docker 操作のブロック になる
基本的な仕組みがわかったところで、後半の記事では Fluentd 停止時に Docker がハングする問題を再現し、原因を深掘りしまする予定です。
Discussion