💾

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行書いたとき、実際は次のように流れます。

  1. コンテナの stdout に文字列が書き込まれる
  2. containerd-shim が FIFO に保持したストリームとして受け取る
  3. dockerd が FIFO を読み出し、ロギングドライバへ渡す
  4. fluentd ドライバが TCP で forward へ送る
  5. 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