跳转到内容
123xiao | 无名键客

《从 0 理解Docker 容器日志治理实战:从 json-file 到集中采集的性能、容量与排障优化:原理、流程与实战》

字数: 0 阅读时长: 1 分钟

背景与问题

很多团队刚开始用 Docker 时,对日志的处理往往只有一句话:先让它跑起来。容器 stdout/stderr 默认被 Docker 接住,落到 json-file 日志驱动里,短期看非常省事;但只要业务量一上来,问题就会接连出现:

  • 宿主机磁盘被日志打满
  • docker logs 变慢,甚至拖累容器
  • 日志采集链路重复读取、丢日志、乱序
  • 排障时只看得到局部,看不到整条调用链
  • 清理日志时手一抖,影响线上容器稳定性

我自己第一次踩这个坑,是某台节点磁盘 95% 告警,排查半天发现不是镜像,不是 volume,而是 /var/lib/docker/containers/*/*-json.log 暴涨。更麻烦的是,业务同学还在用 docker logs -f 现场排障,结果节点 IO 更高了。

所以这篇文章不只讲“怎么配日志驱动”,而是从一个更实战的角度带你走一遍:

  1. Docker 默认日志到底怎么落盘
  2. json-file 为什么容易变成容量和性能隐患
  3. 怎么从本地日志过渡到集中采集
  4. 怎么验证配置真的生效
  5. 出问题时怎么快速定位和止血

如果你已经在用 Docker,但日志治理还停留在“加个 logrotate”阶段,这篇会比较适合你。


前置知识与环境准备

你需要知道什么

建议你至少熟悉下面几件事:

  • Docker 容器基本操作:runlogsinspect
  • Linux 文件系统与磁盘查看:dfduls
  • 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[集中日志平台]

这里有两个关键点:

  1. docker logs 依赖日志驱动支持
    不是所有日志驱动都天然适合本地排障。

  2. 采集器如果直接读 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-sizemax-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 忙

优化方向

  1. 限制无价值日志
  2. 降低采集器扫描频率
  3. 合理设置缓冲区
  4. 尽量结构化日志,减少后处理成本
  5. 高噪声服务单独策略处理

例如 Fluent Bit 可适当调整:

Refresh_Interval  5
Mem_Buf_Limit     20MB
Skip_Long_Lines   On

安全/性能最佳实践

这一部分我尽量给“能落地的建议”,而不是泛泛而谈。

1. 先做容量预算,而不是出了事再清理

一个简单估算公式:

单节点日志容量 ≈ 容器数 × 单容器 max-size × max-file

例如:

  • 50 个容器
  • max-size=10m
  • max-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 个轮转文件
  • 采集器缓冲多久
  • 超出缓冲后是阻塞、丢弃还是降级
  • 告警阈值在哪里

这其实是个取舍问题:

  • 要“绝不丢日志”,通常意味着更大缓存、更高成本
  • 要“绝不影响业务”,通常意味着必要时允许部分日志丢失

生产环境里,业务可用性往往比日志完整性优先级更高。这个边界要提前和团队对齐。


一个推荐落地方案

如果你现在要在生产上做一次稳妥改造,我建议从下面这个组合开始:

  1. Docker 默认使用 json-file
  2. 强制配置 max-sizemax-file
  3. 宿主机部署 Fluent Bit / Filebeat 采集
  4. 采集状态持久化,避免重启重复读
  5. 接入集中日志平台做检索和告警
  6. 应用逐步改成结构化日志
  7. 对超高日志量服务单独限流和分级

可以把它理解成一个分层设计:

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 日志治理,表面看只是“把日志收上来”,本质上其实是三个问题:

  1. 容量是否可控:别让日志把磁盘打满
  2. 性能是否稳定:别让日志链路拖累节点和业务
  3. 排障是否高效:本地能看,集中也能查

如果你只记住几个最重要的动作,我建议是这几条:

  • 马上给 json-filemax-sizemax-file
  • 不要用外部 logrotate 粗暴处理 Docker json 日志
  • 采集器要持久化状态,避免重复采集
  • 优先使用结构化日志,减少后续解析成本
  • 为日志链路设置资源边界和故障预案

最后给一个很务实的边界判断:

  • 规模不大、排障以本地为主:json-file + 轮转就够用
  • 已有统一观测平台:json-file + Fluent Bit/Filebeat 是高性价比方案
  • 对磁盘极度敏感、平台很成熟:再考虑日志驱动直发

别一上来就追求“最先进方案”。
先把默认方案治理好,通常就能解决 80% 的线上问题。


分享到:

上一篇
《Java Web开发实战:基于Spring Boot与Redis实现高并发登录鉴权与会话管理优化》
下一篇
《区块链节点数据同步与状态管理实战:从全量同步到快照加速的工程优化路径》