背景与问题
很多团队刚开始用 Docker 时,对日志的处理往往只有一句话:先让它跑起来。容器 stdout/stderr 默认被 Docker 接住,落到 json-file 日志驱动里,短期看非常省事;但只要业务量一上来,问题就会接连出现:
- 宿主机磁盘被日志打满
docker logs变慢,甚至拖累容器- 日志采集链路重复读取、丢日志、乱序
- 排障时只看得到局部,看不到整条调用链
- 清理日志时手一抖,影响线上容器稳定性
我自己第一次踩这个坑,是某台节点磁盘 95% 告警,排查半天发现不是镜像,不是 volume,而是 /var/lib/docker/containers/*/*-json.log 暴涨。更麻烦的是,业务同学还在用 docker logs -f 现场排障,结果节点 IO 更高了。
所以这篇文章不只讲“怎么配日志驱动”,而是从一个更实战的角度带你走一遍:
- Docker 默认日志到底怎么落盘
json-file为什么容易变成容量和性能隐患- 怎么从本地日志过渡到集中采集
- 怎么验证配置真的生效
- 出问题时怎么快速定位和止血
如果你已经在用 Docker,但日志治理还停留在“加个 logrotate”阶段,这篇会比较适合你。
前置知识与环境准备
你需要知道什么
建议你至少熟悉下面几件事:
- Docker 容器基本操作:
run、logs、inspect - Linux 文件系统与磁盘查看:
df、du、ls - YAML 基础语法
- 至少了解一种日志平台:ELK / Loki / Splunk / Kafka 均可
实验环境
本文示例尽量保持简单,可在一台 Linux 主机上复现:
- Docker Engine 20+
- Linux x86_64
- 一个可以持续输出日志的测试容器
- 可选:Fluent Bit 作为轻量级采集器
核心原理
1. Docker 日志链路到底发生了什么
大多数应用在容器里把日志写到标准输出和标准错误:
- stdout:普通日志
- stderr:错误日志
Docker 会把这两路输出交给日志驱动处理。默认常见的是 json-file,也就是把每一条日志写成一行 JSON,保存在宿主机容器目录下。
flowchart LR
A[应用进程 stdout/stderr] --> B[Docker Engine]
B --> C[日志驱动 json-file]
C --> D[/var/lib/docker/containers/<id>/<id>-json.log]
D --> E[docker logs]
D --> F[日志采集器 tail]
F --> G[集中日志平台]
这里有两个关键点:
-
docker logs依赖日志驱动支持
不是所有日志驱动都天然适合本地排障。 -
采集器如果直接读 json-file,本质是“二次消费本地文件”
本地落盘 + 再采集,意味着你要同时考虑磁盘容量、采集延迟、文件轮转一致性。
2. 为什么 json-file 是默认值,但不一定是最终解
json-file 的优点非常明显:
- 开箱即用
- 不依赖外部系统
docker logs体验好- 本地排障方便
但它的问题也很典型:
容量问题
如果不限制大小,日志文件会一直增长。容器越多、日志越密集,宿主机磁盘就越危险。
性能问题
- 高并发日志写入会带来更多磁盘 IO
- 日志采集器再去 tail 这些文件,会进一步放大 IO 压力
docker logs在大文件场景下会明显变慢
运维问题
- 轮转策略没配好,容易丢日志或撑爆磁盘
- 手工删除日志文件可能破坏 Docker 对日志文件句柄的管理
- 采集器和 Docker 同时处理轮转,边界容易出错
3. 从本地日志到集中采集的常见架构
典型演进路径一般是这样的:
flowchart TD
A[容器 stdout/stderr] --> B[Docker json-file]
B --> C[宿主机本地日志文件]
C --> D[Fluent Bit / Filebeat]
D --> E[Kafka / Loki / Elasticsearch]
E --> F[检索 / 告警 / 分析]
A2[另一种方式: Docker logging driver 直发]
A2 --> G[fluentd / gelf / syslog]
G --> E
你会看到两种思路:
方案 A:本地落盘,再采集
优点:
- 本地有兜底,平台挂了也不一定立刻丢
docker logs继续可用- 迁移成本低
缺点:
- 本地磁盘有压力
- 采集链路更长
- 要处理日志文件轮转与状态同步
方案 B:日志驱动直接发到远端
优点:
- 减少本地磁盘占用
- 架构更“流式”
缺点:
- 更依赖远端系统可用性
- 本地排障体验可能下降
- 配置和兼容性更复杂
中级团队的稳妥选择通常是:先把 json-file 管好,再用轻量采集器集中收集。
4. json-file 的核心配置项
最常见的就是这几个:
max-size:单个日志文件最大尺寸max-file:保留几个轮转文件labels/env:将标签或环境变量写入日志元信息(部分场景有用)
例如:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
这表示:
- 每个容器日志文件最多 10MB
- 最多保留 5 个文件
- 理论上单容器日志上限约 50MB(实际会略有偏差)
这就是最基础、但也最重要的容量护栏。
方案对比与取舍
在动手前,先把几种方案放在一起看,会更容易做决策。
| 方案 | 本地排障 | 磁盘压力 | 部署复杂度 | 远端依赖 | 适用场景 |
|---|---|---|---|---|---|
json-file 不限额 | 好 | 很高 | 低 | 低 | 只适合临时测试 |
json-file + 轮转 | 好 | 可控 | 低 | 低 | 多数中小规模生产 |
json-file + 采集器 | 好 | 中等 | 中 | 中 | 推荐的平衡方案 |
| logging driver 直发 | 一般 | 低 | 中高 | 高 | 平台成熟、链路稳定 |
| 应用直写文件再 sidecar 采集 | 一般 | 中高 | 高 | 中 | 需兼容旧应用时 |
我的建议很直接:
- 如果你还没有统一日志平台:先把
json-file限额配起来 - 如果你已经有日志平台:优先上“
json-file+ 采集器” - 如果你追求极低本地占用:再考虑 driver 直发,但要先评估失联和回退机制
实战代码(可运行)
下面我们从零搭一个最小可验证方案。
步骤 1:准备一个会持续打日志的容器
先启动一个测试容器,每 0.2 秒输出一条日志。
docker run -d \
--name log-demo \
alpine:3.18 \
sh -c 'i=0; while true; do echo "$(date -Iseconds) INFO message-$i"; i=$((i+1)); sleep 0.2; done'
查看日志:
docker logs --tail 5 log-demo
查看容器日志文件位置:
docker inspect --format='{{.LogPath}}' log-demo
你会得到类似输出:
/var/lib/docker/containers/xxxxxxxx/xxxxxxxx-json.log
查看文件大小增长:
watch -n 1 'du -h $(docker inspect --format="{{.LogPath}}" log-demo)'
如果你的机器上没有配置轮转,这个文件会持续变大。
步骤 2:为 Docker 配置 json-file 轮转
编辑 Docker daemon 配置文件:
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
重启 Docker:
sudo systemctl restart docker
注意:这只会影响新创建的容器,不会自动修改已经运行中的旧容器。
重新创建测试容器:
docker rm -f log-demo
docker run -d \
--name log-demo \
alpine:3.18 \
sh -c 'i=0; while true; do echo "$(date -Iseconds) INFO message-$i"; i=$((i+1)); sleep 0.01; done'
检查容器日志配置是否生效:
docker inspect log-demo --format='{{json .HostConfig.LogConfig}}'
预期输出类似:
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}
步骤 3:验证轮转效果
持续观察容器日志目录:
LOG_PATH=$(docker inspect --format='{{.LogPath}}' log-demo)
LOG_DIR=$(dirname "$LOG_PATH")
watch -n 1 "ls -lh $LOG_DIR"
一段时间后,你应该看到类似文件:
xxxxxxxx-json.log
xxxxxxxx-json.log.1
xxxxxxxx-json.log.2
这说明 Docker 自己在做日志轮转,而不是依赖系统级 logrotate。
步骤 4:接入 Fluent Bit 做集中采集
这里我用 Fluent Bit 做例子,因为它足够轻量,适合宿主机侧采集 Docker 日志文件。
先准备配置目录:
mkdir -p fluent-bit/conf
创建 fluent-bit/conf/fluent-bit.conf:
[SERVICE]
Flush 1
Daemon Off
Log_Level info
[INPUT]
Name tail
Path /var/lib/docker/containers/*/*-json.log
Parser docker
Tag docker.*
Refresh_Interval 2
Mem_Buf_Limit 50MB
Skip_Long_Lines On
DB /fluent-bit/state/flb.db
DB.Sync Normal
[FILTER]
Name modify
Match docker.*
Add collector fluent-bit
[OUTPUT]
Name stdout
Match docker.*
再创建 fluent-bit/conf/parsers.conf:
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
启动 Fluent Bit:
docker run --rm -it \
--name fluent-bit \
-v $(pwd)/fluent-bit/conf/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro \
-v $(pwd)/fluent-bit/conf/parsers.conf:/fluent-bit/etc/parsers.conf:ro \
-v /var/lib/docker/containers:/var/lib/docker/containers:ro \
-v $(pwd)/fluent-bit/state:/fluent-bit/state \
fluent/fluent-bit:2.1
如果配置正确,你会在终端看到被采集出的日志记录。
这里输出到 stdout 只是为了演示。生产环境通常会改成 Elasticsearch、Loki、Kafka、HTTP 或 Fluentd。
步骤 5:把输出改成 Loki 或 Elasticsearch
输出到 Elasticsearch 示例
[OUTPUT]
Name es
Match docker.*
Host 192.168.1.100
Port 9200
Index docker-logs
Logstash_Format On
Retry_Limit False
输出到 Loki 示例
[OUTPUT]
Name loki
Match docker.*
Host 192.168.1.101
Port 3100
Labels job=docker,collector=fluent-bit
Line_Format json
逐步验证清单
这部分很实用,我建议你每次改日志链路时都照着过一遍。
本地链路验证
-
docker inspect能看到预期日志驱动 - 日志文件路径能定位到
-
max-size、max-file对新容器生效 - 轮转后文件数量符合预期
-
docker logs仍能正常查看近期日志
采集链路验证
- 采集器能读取新日志
- 采集器重启后不会从头重复灌入
- 日志轮转后采集不中断
- 超长日志不会卡死采集器
- 远端平台能按容器名/节点名检索
容量与性能验证
- 节点磁盘占用有上限
- 高峰时 CPU/IO 没有异常抖动
- 采集器内存 buffer 可控
- 日志平台短暂不可用时,系统可承受
常见坑与排查
这部分是实战里最值钱的内容。
坑 1:改了 daemon.json,但旧容器没生效
这是最常见的误区。
现象
你明明已经配置了:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
但某些容器还是不轮转。
原因
Docker 的默认日志配置只作用于新创建容器。
排查命令
docker inspect <container_name> --format='{{json .HostConfig.LogConfig}}'
处理方法
重建容器,而不是只重启容器:
docker rm -f <container_name>
# 然后重新 run / compose up
坑 2:用 logrotate 去切 Docker 日志文件
现象
你手工或通过系统 logrotate 清理了 *-json.log,结果:
docker logs异常- 采集器重复读
- 日志丢失或断档
原因
Docker 对日志文件有自己的管理方式,外部强行 rotate/truncate,容易和 Docker 文件句柄状态冲突。
建议
优先使用 Docker 自带的 max-size + max-file。
除非你非常清楚句柄与轮转行为,否则不要让系统 logrotate 接管容器 json 日志。
坑 3:直接 rm 大日志文件,磁盘没立刻释放
这个坑我自己踩过。
现象
文件删了,但 df -h 看磁盘还是满的。
原因
进程还持有被删除文件的句柄,空间不会马上释放。
排查命令
sudo lsof | grep deleted
止血方案
- 重启对应容器
- 必要时重启 Docker
- 更稳妥的是提前配置轮转,别等磁盘打满再救火
坑 4:采集器重复采集,平台日志翻倍
常见原因
- tail 状态数据库没持久化
- 容器文件路径匹配过宽
- 轮转后 inode 识别异常
- 同一节点跑了两个采集器
Fluent Bit 关键配置
DB /fluent-bit/state/flb.db
DB.Sync Normal
如果不持久化 DB,采集器一重启,很容易从头读。
排查思路
flowchart TD
A[发现日志重复] --> B{是否只有重启后重复?}
B -- 是 --> C[检查 tail DB 是否持久化]
B -- 否 --> D{是否多个采集器读同一路径?}
D -- 是 --> E[去重部署]
D -- 否 --> F[检查路径匹配与轮转行为]
C --> G[验证 offset 是否恢复]
F --> G
坑 5:日志量太大,采集器把节点打高了
现象
- 节点 load 升高
- 采集器 CPU 高
- 容器本身没问题,但主机 IO 忙
优化方向
- 限制无价值日志
- 降低采集器扫描频率
- 合理设置缓冲区
- 尽量结构化日志,减少后处理成本
- 高噪声服务单独策略处理
例如 Fluent Bit 可适当调整:
Refresh_Interval 5
Mem_Buf_Limit 20MB
Skip_Long_Lines On
安全/性能最佳实践
这一部分我尽量给“能落地的建议”,而不是泛泛而谈。
1. 先做容量预算,而不是出了事再清理
一个简单估算公式:
单节点日志容量 ≈ 容器数 × 单容器
max-size×max-file
例如:
- 50 个容器
max-size=10mmax-file=5
则理论日志上限约:
50 × 10MB × 5 = 2500MB
也就是约 2.5GB。
这还不包括:
- Docker 元数据
- 镜像层
- volume
- 采集器缓存
所以不要把日志上限算得太贴边。至少预留 30%~50% 安全空间。
2. 优先输出结构化日志
与其让采集器用正则硬拆,不如应用直接输出 JSON。
例如应用输出:
{"time":"2023-09-08T08:00:00Z","level":"INFO","service":"order","trace_id":"abc123","msg":"create order success"}
这样做的好处:
- 检索字段更准确
- 聚合分析更稳定
- 采集配置更简单
- 错误率更低
如果你的日志平台支持字段索引,结构化日志收益会非常明显。
3. 避免把敏感数据直接打到 stdout
很多团队会无意中把这些内容写进日志:
- Access Token
- Cookie
- 手机号、身份证号
- 数据库连接串
- 内部 IP 与敏感配置
容器日志一旦被集中采集,传播范围就更大了。建议:
- 应用侧做字段脱敏
- 采集器侧增加过滤规则
- 平台侧做权限隔离与保留策略
4. 把“高频噪声日志”压在源头
如果应用每秒打印成百上千条 INFO,采集器再强也只是被动承受。
更有效的做法:
- 调整日志级别
- 合并重复日志
- 关键路径只保留必要字段
- 健康检查、轮询类日志尽量降噪
一句话:最便宜的日志,是没被打印出来的无用日志。
5. 节点侧采集器要限制资源
如果你使用 Docker 容器部署采集器,也建议给它资源边界:
docker run -d \
--name fluent-bit \
--cpus="0.5" \
--memory="256m" \
-v /var/lib/docker/containers:/var/lib/docker/containers:ro \
fluent/fluent-bit:2.1
这样即便采集器异常,也不至于把节点拖垮。
6. 远端平台不可用时,要有边界策略
集中日志平台并不总是 100% 可用,所以要提前想清楚:
- 本地是否保留最近 N 个轮转文件
- 采集器缓冲多久
- 超出缓冲后是阻塞、丢弃还是降级
- 告警阈值在哪里
这其实是个取舍问题:
- 要“绝不丢日志”,通常意味着更大缓存、更高成本
- 要“绝不影响业务”,通常意味着必要时允许部分日志丢失
生产环境里,业务可用性往往比日志完整性优先级更高。这个边界要提前和团队对齐。
一个推荐落地方案
如果你现在要在生产上做一次稳妥改造,我建议从下面这个组合开始:
- Docker 默认使用
json-file - 强制配置
max-size和max-file - 宿主机部署 Fluent Bit / Filebeat 采集
- 采集状态持久化,避免重启重复读
- 接入集中日志平台做检索和告警
- 应用逐步改成结构化日志
- 对超高日志量服务单独限流和分级
可以把它理解成一个分层设计:
sequenceDiagram
participant App as 应用容器
participant Docker as Docker日志驱动
participant Local as 本地json-file
participant Agent as Fluent Bit
participant Center as 集中日志平台
participant User as 排障人员
App->>Docker: stdout/stderr
Docker->>Local: 写入并轮转
User->>Docker: docker logs 查询近期日志
Agent->>Local: tail 增量读取
Agent->>Center: 转发结构化日志
User->>Center: 检索/聚合/告警分析
这个方案的优点是:
- 本地排障能力保留
- 风险相对可控
- 迁移成本不高
- 适合大多数 Docker 生产环境
总结
Docker 日志治理,表面看只是“把日志收上来”,本质上其实是三个问题:
- 容量是否可控:别让日志把磁盘打满
- 性能是否稳定:别让日志链路拖累节点和业务
- 排障是否高效:本地能看,集中也能查
如果你只记住几个最重要的动作,我建议是这几条:
- 马上给
json-file配max-size和max-file - 不要用外部
logrotate粗暴处理 Docker json 日志 - 采集器要持久化状态,避免重复采集
- 优先使用结构化日志,减少后续解析成本
- 为日志链路设置资源边界和故障预案
最后给一个很务实的边界判断:
- 规模不大、排障以本地为主:
json-file+ 轮转就够用 - 已有统一观测平台:
json-file+ Fluent Bit/Filebeat 是高性价比方案 - 对磁盘极度敏感、平台很成熟:再考虑日志驱动直发
别一上来就追求“最先进方案”。
先把默认方案治理好,通常就能解决 80% 的线上问题。