容器日志的坑大多来自一个认知错位: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 | 生产默认替代,压缩存储 |
| journald | systemd journal | 是(需配置) | 由 journald 控制 | RHEL/CentOS 体系 |
| fluentd | Fluentd/Fluent Bit | 否 | fluentd-* 系列 | 主流集中采集 |
| gelf | Graylog | 否 | gelf-* 系列 | Graylog 用户 |
| syslog | syslog 服务 | 否 | syslog-* 系列 | 传统 rsyslog 体系 |
| awslogs | CloudWatch 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。 - 采集层面,优先文件采集以获得可重放与集中解析的能力,把多行聚合与结构化解析放在采集器,而不是塞进应用。
- 无论选哪条链路,都要显式配置轮转上限与非阻塞模式,并为「丢日志」建立独立告警。
这三条落到位,容器日志就从「出了事才去看」变成「随时可查、可追溯」的基础设施。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。