日志驱动与采集管道

系统梳理 Docker 日志的两条路径:日志驱动直发与落盘后采集。逐个对比 json-file、local、journald、fluentd 等驱动的适用场景与配置参数,剖析 max-size/max-file 轮转、非阻塞模式与背压丢日志的机制,并给出采集管道、多行日志处理与容器日志排障的完整实践。

容器日志的坑大多来自一个认知错位:docker logs 看到的输出,并不等于应用真正写入日志系统的内容。前者由日志驱动决定,后者由采集管道决定,两者是彼此独立的两段链路。本篇拆开这两段,讲清七种日志驱动的取舍、轮转参数的实际含义、采集管道如何避免丢日志,以及出问题时该从哪里下手。

1. 容器日志的两条路径

Docker 把容器内进程写到 stdout/stderr 的字节流交给日志驱动处理。驱动有两种工作模式:

路径 A(落盘后采集):
  应用 → stdout/stderr → 日志驱动(json-file/local) → /var/lib/docker/containers/<id>/*.log
                                                       ↓
                                                  采集器(Promtail/Fluent Bit) → 日志后端

路径 B(驱动直发):
  应用 → stdout/stderr → 日志驱动(fluentd/gelf/syslog/awslogs) → 日志后端

选择依据很直接:

场景推荐路径原因
需要 docker logs 可用A(json-file)只有落盘驱动支持本地读取
高吞吐、低延迟投递B(fluentd)省掉一次落盘与再读
已有 journald 体系A(journald)与宿主日志统一
无守护进程的采集方案A(local + 采集器)驱动只管落盘,采集器负责解析
云托管日志服务B(awslogs/gcplogs)免运维

一个常见误区是「驱动直发就不需要落盘」。实际上多数直发驱动仍有内存缓冲,缓冲满且后端不可用时会丢日志,而落盘路径至少能在磁盘上保留一段时间。

2. 七种日志驱动对比

docker info --format '{{.LoggingDriver}}'
# json-file(默认)
docker info --format '{{json .Plugins.Log}}'
驱动目标支持 docker logs轮转参数适用
json-file本地文件是max-size/max-file默认、开发、需本地查看
local本地二进制文件是max-size/max-file生产默认替代,压缩存储
journaldsystemd journal是(需配置)由 journald 控制RHEL/CentOS 体系
fluentdFluentd/Fluent Bit否fluentd-* 系列主流集中采集
gelfGraylog否gelf-* 系列Graylog 用户
syslogsyslog 服务否syslog-* 系列传统 rsyslog 体系
awslogsCloudWatch Logs否awslogs-* 系列AWS ECS/EC2

2.1 全局与容器级配置

日志驱动可以在三个层面设置,优先级从低到高:

// /etc/docker/daemon.json —— 全局默认
{
  "log-driver": "local",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5",
    "compress": "true"
  }
}
# 单容器覆盖
docker run -d --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 nginx
# Compose 中覆盖
# services.web.logging.options.max-size: "10m"

注意两点:全局配置只对新建容器生效,已有容器必须重建;local 驱动不支持所有 json-file 的选项,例如 labels 之外的多数字段会被忽略。

2.2 local 与 json-file 的关键差异

local 驱动是 Docker 18.09 引入的、面向生产的默认推荐:

  • 使用二进制格式存储,同样的日志量磁盘占用约为 json-file 的 1/3~1/2。
  • 默认就带轮转(max-size 20m、max-file 5),而 json-file 默认不轮转,这是容器把宿主机磁盘写满的头号原因。
  • 元数据(容器名、镜像、标签)与日志一起存储,无需外部标签补全。
  • 代价是文件不是纯文本,无法 tail -f 直接读,必须用 docker logs 或采集器 API。
# json-file 的日志文件(可直接读)
ls -lh /var/lib/docker/containers/<id>/<id>-json.log
# local 的日志文件(二进制,需 docker logs 读)
docker logs --since 10m web
docker logs --tail 100 -f web

关于容器日志与指标如何统一接入监控体系,可参考 容器监控与日志实践 。

3. 轮转参数的实际语义

轮转参数只有三个,但每个都有容易误解的地方。

{
  "log-opts": {
    "max-size": "50m",
    "max-file": "5",
    "compress": "true"
  }
}
  • max-size:单个日志文件达到该大小后切分,支持 k/m/g 后缀。不是总量上限。
  • max-file:保留的历史文件数量。总磁盘上限 = max-size × max-file。
  • compress:只对 local 与 json-file 的部分版本生效,压缩历史轮转文件。

因此 max-size=50m + max-file=5 意味着单个容器最多占用 250MB。一个 200 容器的节点,理论上限就是 50GB——这是容量规划必须算的一笔账。

# 统计各容器日志占用,找出大头
du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -10

3.1 已运行容器的日志文件不会自动缩容

轮转只在写入时触发。如果一个容器已经写了 40GB 的日志且没有配置轮转,加上配置后必须重建容器才会生效,docker restart 不够。紧急情况下可以截断文件(会丢日志):

: > /var/lib/docker/containers/<id>/<id>-json.log

截断而非删除:直接 rm 会让 Docker 继续往已被删除的 inode 写,磁盘空间不释放,直到容器重启。

3.2 journald 驱动的特殊之处

用 journald 驱动时,日志进入 systemd journal,轮转由 journald 的 SystemMaxUse、MaxRetentionSec 控制,Docker 的 max-size 参数无效:

# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=2G
SystemKeepFree=4G
MaxRetentionSec=2week
journalctl -u docker -n 50
journalctl CONTAINER_NAME=web --since "10 min ago"
journalctl --disk-usage

docker logs 在 journald 驱动下仍可用,但要求 journald 的 ForwardToSyslog 等配置未被禁用。若日志中丢失了 CONTAINER_NAME 字段,检查驱动是否被容器级 --log-opt tag 覆盖。

4. 采集管道:驱动直发与文件采集

4.1 驱动直发(fluentd)

docker run -d \
  --log-driver=fluentd \
  --log-opt fluentd-address=127.0.0.1:24224 \
  --log-opt fluentd-async=true \
  --log-opt tag="docker.{{.Name}}" \
  --log-opt fluentd-buffer-limit=8MB \
  nginx

关键参数:

参数作用建议
fluentd-address采集端地址本机用 127.0.0.1,避免走外网
fluentd-async异步连接生产必须 true,否则启动阻塞
fluentd-buffer-limit内存缓冲上限按日志速率设置,默认 1MB 偏小
fluentd-retry-wait重试间隔默认 1s
fluentd-max-retries最大重试次数超过后丢弃
tag记录标签用 {{.Name}} 注入容器名

fluentd-async=false(默认)时,若采集端不可达,容器启动会直接失败或阻塞,这是生产事故的常见来源。开启 async 后,采集端不可用时会丢日志而不是卡住应用。

4.2 文件采集(Promtail / Fluent Bit / Vector)

文件采集的通用形态是:驱动用 local 或 json-file 落盘,采集器以 DaemonSet/宿主机 agent 形式读取 /var/lib/docker/containers/*/*-json.log。

# docker-compose 中部署 Vector 采集 docker 日志
services:
  vector:
    image: timberio/vector:0.40.0-debian
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./vector.toml:/etc/vector/vector.toml:ro
# vector.toml 关键片段
[sources.docker_logs]
type = "docker_logs"

[transforms.parse]
type = "remap"
inputs = ["docker_logs"]
source = '''
. |= parse_json!(string!(.message))
'''

[sinks.loki]
type = "loki"
inputs = ["parse"]
endpoint = "http://loki:3100"
labels.container = "{{ container_name }}"

相比驱动直发,文件采集的优势是:采集器崩溃不影响容器、可以重放历史文件、解析逻辑集中在采集器而非容器内。代价是多一次磁盘写读,且需要处理文件轮转与 inode 变化。日志后端的存储与查询优化可参考 Loki 优化实践 与 OpenTelemetry Collector 深入 。

5. 非阻塞模式与背压

日志链路的背压问题只有一个根源:应用写日志的速度超过下游消费速度。Docker 的处理方式由 mode 参数决定:

docker run -d \
  --log-opt max-buffer-size=4m \
  --log-opt mode=non-blocking \
  nginx
模式行为后果
blocking(默认)缓冲满时阻塞写入应用被拖慢甚至卡死
non-blocking缓冲满时丢弃新日志并计数应用不受影响,但丢日志

max-buffer-size 默认 1MB,对高吞吐服务偏小。开启 non-blocking 后,丢弃的日志量可通过以下方式观测:

docker info --format '{{json .LoggingDriver}}'
# 部分驱动会记录丢日志计数;也可从 daemon 指标获取
curl -s --unix-socket /var/run/docker.sock http://localhost/info | jq '.LoggingDriver'

生产建议:优先用 non-blocking + 合理缓冲,宁可丢日志也不要让业务进程被日志拖垮;同时用采集侧的丢弃指标做告警,因为丢日志本身是需要处理的事件。

# Compose 中配置
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"
        mode: "non-blocking"
        max-buffer-size: "8m"

6. 多行日志的处理

Java 堆栈、Python traceback、Go panic 都是多行输出,但 Docker 只保证按行交付。多行聚合必须由采集器完成:

# Vector 中按行首时间戳聚合 Java 堆栈
[transforms.multiline]
type = "multiline"
inputs = ["docker_logs"]
mode = "continue_through"
start_pattern = '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
condition_pattern = '^\s+at |^\s+\.\.\. |^Caused by:'
# Fluent Bit 中等价配置
[PARSER]
    Name        java_multiline
    Format      regex
    Regex       ^(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})
    Time_Key    time

[FILTER]
    Name          multiline
    Match         docker.*
    Multiline.Key_Content log
    Multiline.Parser java_multiline

更彻底的做法是让应用直接输出 JSON 单行日志(结构化日志),把堆栈转义进一个字段,从根本上绕开多行问题:

{"ts":"2026-10-07T19:23:01Z","level":"error","msg":"db timeout","stack":"goroutine 1 [running]:\nmain.main()..."}

结构化日志还能让采集器免去正则解析,直接按字段建索引,是日志量增长后最值得投入的一项改造。

7. 排障清单

日志链路的问题几乎都能归到下面几类:

现象排查点
docker logs 无输出驱动是否为 fluentd/syslog 等直发驱动;应用是否写到了文件而非 stdout
日志文件占满磁盘是否用了 json-file 且未设 max-size;用 du 定位大头容器
容器启动卡住fluentd 驱动 fluentd-async=false 且采集端不可达
日志时间错乱容器时区与宿主不一致;检查 TZ 与驱动是否覆盖时间戳
日志重复同时配置了驱动直发与文件采集,两条链路都在投递
采集器读不到日志挂载路径只读权限不足;容器日志文件轮转后 inode 变化
# 确认某容器实际使用的驱动与选项
docker inspect -f '{{json .HostConfig.LogConfig}}' web | jq
# 确认容器 stdout 是否真的在产出
docker logs --tail 5 web
# 确认宿主日志文件是否在增长
watch -n 2 'ls -l /var/lib/docker/containers/$(docker inspect -f "{{.Id}}" web)/*-json.log'

最后一条经验:日志驱动属于创建时确定的容器属性,无法热切换。任何驱动层面的调整都意味着重建容器,因此日志策略必须在服务首次上线前定好,而不是等到磁盘告警时再补。daemon 层面的日志与配置调优可参考 Docker Daemon 运维 。

8. 小结

把 Docker 日志拆成「驱动」与「采集」两段之后,选型就变成两个独立决策:

  • 驱动层面,生产环境优先 local(自带轮转、存储紧凑),需要统一宿主日志时选 journald,需要极低延迟投递时选 fluentd 并开启 async。
  • 采集层面,优先文件采集以获得可重放与集中解析的能力,把多行聚合与结构化解析放在采集器,而不是塞进应用。
  • 无论选哪条链路,都要显式配置轮转上限与非阻塞模式,并为「丢日志」建立独立告警。

这三条落到位,容器日志就从「出了事才去看」变成「随时可查、可追溯」的基础设施。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「docker」更多文章

  1. 容器网络排障实战
  2. DinD/DooD 与临时 CI Runner
  3. 本地开发运行时:OrbStack 与 Colima