Docker 基础体系 · 第 16/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker 日志与监控:Logging Driver、Metrics、事件和容量告警

Docker 生产运维中的“可观测性”不是单一功能,而是三类数据共同组成的系统:

  • 日志(Logs):回答“程序输出了什么”。
  • 指标(Metrics):回答“资源和服务状态如何随时间变化”。
  • 事件(Events):回答“Docker 在什么时候发生了什么状态变化”。

容量告警贯穿这三类数据:日志会消耗磁盘,镜像和构建缓存会增长,容器可能因 OOM 被杀死,监控系统本身也可能因磁盘或 inode 耗尽而失效。

本文以现代 Docker Engine、BuildKit 和 Compose 规范为基础,重点讨论 Linux 主机上的 Linux 容器。Windows 容器的日志路径、资源统计和底层实现存在差异,不能直接套用本文所有结论。


一、先建立整体模型:数据从哪里产生,经过哪些组件

一个容器的标准输出不会直接“自动进入监控系统”。典型数据流如下:

flowchart LR
    APP[容器内应用] --> STDOUT[stdout/stderr]
    STDOUT --> SHIM[容器运行时与 Docker]
    SHIM --> LD[Logging Driver]
    LD --> LOCAL[本地日志文件]
    LD --> REMOTE[远程日志系统]

    DOCKERD[dockerd] --> API[Docker Engine API]
    API --> STATS[容器 stats]
    API --> EVENTS[Docker events]
    API --> DMETRICS[Daemon Prometheus Metrics]

    CGROUP[cgroups] --> STATS
    KERNEL[Linux 内核] --> CGROUP
    KERNEL --> OOM[OOM Killer]
    OOM --> EVENTS

    HOSTFS[主机文件系统] --> CAP[磁盘与 inode 容量检查]
    LOCAL --> HOSTFS
    IMAGE[镜像与层] --> HOSTFS
    BUILD[BuildKit 缓存] --> HOSTFS
    VOLUME[Volumes] --> HOSTFS

    STATS --> COLLECTOR[指标采集器]
    EVENTS --> COLLECTOR
    DMETRICS --> COLLECTOR
    CAP --> COLLECTOR
    REMOTE --> LOGSYS[日志平台]
    COLLECTOR --> ALERT[告警规则]

这里有几个必须区分的事实:

  1. Logging Driver 负责日志路由和存储,不是指标采集器。
  2. docker stats 主要读取容器的资源统计,不是应用级 QPS、延迟或错误率。
  3. docker events 是状态变化流,不是可替代日志的审计数据库。
  4. 磁盘使用量不只来自容器日志,镜像层、BuildKit 缓存、匿名卷、命名卷和容器可写层都可能占用空间。
  5. Docker Daemon 的 Prometheus metrics 与容器资源 metrics 是不同数据源,不能只抓其中一个。

二、容器日志的起点:stdout、stderr 和 Docker 日志模型

2.1 Docker 默认关注容器的标准输出

容器内的应用通常向两个文件描述符写入:

  • stdout:标准输出;
  • stderr:标准错误。

例如:

printf 'request accepted\n'
printf 'database timeout\n' >&2

如果这两个输出没有被应用重定向到容器内部文件,Docker 就可以接收它们,并交给配置的 logging driver

这意味着下面两种日志不等价:

应用写 stdout/stderr
应用写 /var/log/app.log

前者属于 Docker 日志链路,可以通过 docker logs 或 logging driver 处理;后者只是容器文件系统中的普通文件,Docker 不会因为它位于 /var/log 就自动采集。

因此,容器化应用通常应将结构化日志写到 stdout/stderr,而不是依赖容器内的日志轮转服务。若应用必须写文件,则需要额外的日志代理、共享卷或应用自身的日志收集方案。

2.2 docker logs 不是“读取所有日志”

docker logs 请求 Docker 返回容器的日志流:

docker run --name demo alpine \
  sh -c 'echo "normal"; echo "error" >&2'

docker logs demo

预期输出类似:

normal
error

实时跟踪:

docker logs -f --since 10m demo

常用参数的语义是:

  • -f:持续跟随新日志;
  • --since:只读取某个时间点之后的日志;
  • --until:读取到某个时间点;
  • --tail N:只返回最后 N 行;
  • -t:显示日志时间戳。

docker logs 能否工作取决于 logging driver。对于某些远程 driver,Docker 只负责发送日志,不保留本地可读副本,此时可能出现:

configured logging driver does not support reading

或无法返回历史日志的情况。因此,“已经配置远程日志”不等于“docker logs 一定可用”。


三、Logging Driver:日志如何存储、传输和失败

3.1 Logging Driver 的职责

Logging driver 决定 Docker 如何处理容器的 stdout/stderr。常见 driver 包括:

  • json-file
  • local
  • journald
  • syslog
  • fluentd
  • gelf
  • splunk
  • awslogs
  • gcplogs
  • etwlogs

可查看当前 Docker Daemon 的默认 driver:

docker info --format '{{.LoggingDriver}}'

也可以查看某个容器实际使用的 driver:

docker inspect -f '{{json .HostConfig.LogConfig}}' demo

一个容器在创建时会确定其日志配置。修改 /etc/docker/daemon.json 的默认值,通常不会自动改变已经存在的容器;要让已有服务采用新配置,通常需要重新创建容器,而不是只重启进程。

3.2 json-file:兼容性好,但必须配置轮转

json-file 将每条日志写成 JSON 记录。典型记录包含日志内容、时间戳和流类型。

例如使用:

{
  "log": "normal\n",
  "stream": "stdout",
  "time": "2025-01-01T00:00:00.000000000Z"
}

默认情况下,json-file 可能持续增长。生产环境应配置轮转,例如 /etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}

含义是:

  • 单个日志文件达到 50m 后轮转;
  • 保留最多 5 个文件;
  • 近似上限为 50m × 5,但实际磁盘占用还会受到文件元数据、其他 Docker 数据以及轮转时短暂并存的影响。

修改后验证配置:

sudo systemctl restart docker
docker info --format '{{.LoggingDriver}}'

注意:

  1. Docker Daemon 重启会影响主机上的容器管理操作,生产环境应安排维护窗口。
  2. 新默认配置主要影响之后创建的容器。
  3. max-sizemax-file 只是日志文件轮转,不会限制应用写入速度。
  4. 轮转并不等价于集中式日志保留策略;旧日志仍可能在本机占用空间。

3.3 local:本地存储效率更好,但不要直接解析内部文件

local driver 是 Docker 提供的本地日志 driver,设计目标包括减少磁盘占用、支持轮转,并使用更适合本地读取的格式。它通常比无限增长的 json-file 更适合作为本机日志存储。

可以设置:

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5",
    "compress": "true"
  }
}

local driver 的内部格式和目录布局不应被当作稳定公共 API。应用或运维脚本应使用:

docker logs <container>

而不是直接解析 /var/lib/docker/containers 下的内部文件。

3.4 远程 Logging Driver:集中化与可用性的交换

远程 driver 可以把日志发送到 syslog、Fluentd、GELF、Splunk 或云日志服务。其优点是:

  • 日志离开 Docker 主机;
  • 便于统一检索、权限控制和长期保留;
  • 主机磁盘压力可能降低。

但它引入了新的失败路径:

应用写日志
  -> Docker 接收
  -> Logging Driver 建立网络连接
  -> 远端日志系统接收

远端不可用时,行为取决于 driver 和模式。常见风险包括:

  • 应用写日志被阻塞;
  • 日志在本地短暂缓存后丢失;
  • Docker 启动或容器启动依赖远端日志服务;
  • 日志发送产生网络流量和 CPU 开销;
  • 远程服务恢复后出现突发重传或积压。

不要简单地认为“日志绝不能丢,所以必须同步发送”。同步发送可以提高交付保证,但也可能把日志系统故障传递给业务请求线程。反过来,异步发送提高业务隔离性,却需要接受缓存满后的丢弃策略或本地积压。

3.5 blocking 与 non-blocking:日志可靠性和业务可用性的边界

Docker 支持某些 logging driver 使用非阻塞模式。例如:

{
  "log-driver": "syslog",
  "log-opts": {
    "mode": "non-blocking",
    "max-buffer-size": "4m"
  }
}

两种模式可以抽象为:

blocking:
  应用 -> 日志管道 -> 远端发送
                    |
                    +-- 远端慢时,写日志可能阻塞应用

non-blocking:
  应用 -> 内存缓冲区 -> 远端发送
                         |
                         +-- 缓冲区满时,可能丢弃日志

因此:

  • blocking 更偏向日志交付,但可能扩大故障影响面;
  • non-blocking 更偏向业务可用性,但必须监控丢日志风险和缓冲区行为。

具体 driver 是否支持这些选项、选项名称以及失败行为,应以对应 Docker Engine 版本和 driver 文档为准,不能把某一个 driver 的参数套用到所有 driver。


四、Compose 中配置日志

Compose 可以在服务级别覆盖 Docker Daemon 的默认 logging driver:

services:
  api:
    image: example/api:1.2
    logging:
      driver: local
      options:
        max-size: "20m"
        max-file: "5"

  worker:
    image: example/worker:1.2
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

启动后验证:

docker compose up -d
docker compose ps
docker inspect -f '{{json .HostConfig.LogConfig}}' "$(docker compose ps -q api)"

配置作用边界:

  • logging 配置属于容器运行时配置;
  • 修改 Compose 文件后,必须让 Compose 重新创建容器,配置才会真正进入容器;
  • 已经写入旧容器的日志不会因为新配置而自动迁移;
  • 若使用远程 driver,还必须验证远程端点、认证、网络和失败策略。

可以用以下方式确认实际状态:

docker compose config
docker inspect "$(docker compose ps -q api)" \
  --format '{{json .HostConfig.LogConfig}}'

docker compose config 检查 Compose 展开后的配置,docker inspect 检查 Docker Engine 最终保存的容器配置。两者都通过,但远程日志仍可能发送失败,因此还需要在日志平台验证实际到达情况。


五、日志磁盘占用:为什么删除容器日志仍可能不够

Docker 的数据根目录可以查看:

docker info --format '{{.DockerRootDir}}'

Linux 上常见位置是 /var/lib/docker,但不能硬编码,因为可以通过 Daemon 配置改变。

检查主机文件系统:

df -h
df -i

两者分别关注:

  • df -h:块空间是否耗尽;
  • df -i:inode 是否耗尽。

即使磁盘还有很多 GB,inode 用完也会导致无法创建新文件。

5.1 Docker 可能占用空间的来源

常见来源包括:

  1. 容器日志;
  2. 镜像层;
  3. 容器可写层;
  4. BuildKit 构建缓存;
  5. 未使用的镜像、网络和容器;
  6. 命名卷和匿名卷;
  7. Docker Desktop 或其他运行时的虚拟磁盘文件。

查看 Docker 对象占用:

docker system df
docker system df -v

查看 BuildKit 缓存:

docker buildx du

不同版本的 buildx 输出字段可能不同,但核心目标是确认构建缓存的大小与可回收对象。

5.2 不要直接删除 /var/lib/docker/containers 中的文件

直接删除日志文件可能导致:

  • Docker 内部状态与文件状态不一致;
  • 文件句柄仍被进程持有,空间不会立即释放;
  • 后续日志写入异常;
  • 破坏 Docker 管理的数据。

当日志失控时,应优先:

  1. 停止产生异常日志的应用;
  2. 配置正确的轮转;
  3. 重新创建使用新日志配置的容器;
  4. 将日志发送到集中式系统;
  5. 对无用 Docker 对象进行经过确认的清理。

对于已被删除但仍被进程打开的文件,可以使用:

sudo lsof +L1

它用于发现“目录项已删除但文件描述符仍打开”的文件。若确认是 Docker 或某个进程持有,通常需要重启相应容器或服务才能释放空间;不能通过继续删除目录项解决。


六、Metrics:指标与 docker stats 的关系

6.1 指标的基本结构

指标通常由以下部分组成:

metric_name{label1="value1",label2="value2"} value timestamp

例如:

container_cpu_usage_seconds_total{
  container="api"
} 12345

这是一个随时间单调增加的计数器。监控系统通常通过两个采样点计算变化率:

rate=C(t2)C(t1)t2t1rate = \frac{C(t_2)-C(t_1)}{t_2-t_1}

其中:

  • C(t)C(t) 是时刻 tt 的累计 CPU 时间;
  • t2t1t_2-t_1 是采样间隔;
  • rate 表示单位时间内消耗的 CPU 时间。

如果 CPU 使用率用百分比表示,还需要明确分母:是一个 CPU、容器限制、还是整台主机的所有 CPU。不同工具的展示口径可能不同,不能只看数字而忽略定义。

6.2 docker stats 提供什么

最直接的容器资源观察命令是:

docker stats --no-stream

典型输出包含:

CONTAINER ID   NAME   CPU %   MEM USAGE / LIMIT   MEM %   NET I/O     BLOCK I/O   PIDS

这些字段大致表示:

  • CPU %:一段采样区间内的 CPU 使用率;
  • MEM USAGE / LIMIT:内存使用量与可用上限;
  • MEM %:内存使用量相对 limit 的比例;
  • NET I/O:网络收发累计字节;
  • BLOCK I/O:块设备读写累计字节;
  • PIDS:进程或线程相关的 PID 计数,具体语义受平台和实现影响。

只查看某个容器:

docker stats --no-stream api

机器可读输出:

docker stats --no-stream \
  --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.PIDs}}'

docker stats 适合现场诊断,不适合直接作为完整生产监控系统,因为它是按需查询,并不能天然提供:

  • 长期时间序列;
  • 统一标签;
  • 告警历史;
  • 采集失败监控;
  • 应用 QPS、延迟和业务错误率。

6.3 CPU 指标的计算直觉

Docker 的 CPU 百分比基于两个时间差:

  1. 容器在本次采样期间消耗的 CPU 时间;
  2. 系统在同一期间经过的 CPU 时间。

可以抽象为:

CPU%=Δcontainer_cpuΔsystem_cpu×N×100CPU\% = \frac{\Delta container\_cpu}{\Delta system\_cpu} \times N \times 100

其中 NN 是可用 CPU 数量。不同 Engine 版本、cgroups 版本和平台可能影响具体实现,但重要的是:100% 不一定表示整台多核主机已经全部耗尽,也可能表示一个 CPU 核被充分使用。

如果容器设置了 CPU 限制,还应同时观察:

  • 容器 CPU 使用量;
  • CPU quota 是否被频繁消耗;
  • throttling 时间;
  • 主机总体 CPU 竞争。

只看 docker stats 中的 CPU 百分比,可能漏掉“容器没有达到很高百分比,但已经持续被 quota 限流”的情况。CPU 配额属于 cgroups 资源治理范畴,告警应结合限制和 throttling 指标分析。

6.4 内存指标与 OOM 的关系

Linux 上,容器内存限制最终依赖 cgroups。若容器达到限制,可能发生:

  1. 内核尝试回收可回收内存;
  2. 分配仍无法满足;
  3. cgroup 内存压力增加;
  4. 内核 OOM Killer 选择进程;
  5. 进程被杀死,容器可能退出或被重启策略拉起。

可以观察容器的限制:

docker inspect api \
  --format 'memory={{.HostConfig.Memory}} memorySwap={{.HostConfig.MemorySwap}}'

值为 0 通常表示未设置对应限制,但具体 swap 语义还受主机 cgroups 配置和 Docker 版本影响。

查看容器退出状态:

docker inspect api \
  --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'

这里的 OOMKilled=true 是重要证据,但不能把它当作所有内存问题的唯一证据。还应检查:

docker events --since 30m \
  --filter type=container \
  --filter container=api

以及主机内核日志:

sudo journalctl -k --since "30 min ago" | grep -i -E 'oom|out of memory|killed process'

在 cgroups v2 和不同 Linux 发行版上,内核日志格式、Docker 暴露的字段以及 memory event 文件可能不同。诊断时要同时确认:

  • 是容器 cgroup 内 OOM;
  • 还是整台主机内存耗尽;
  • 是否存在 swap;
  • 是否有其他容器争抢内存;
  • 重启策略是否掩盖了真实退出。

6.5 PID、I/O 与网络指标的边界

如果容器设置了 PID 限制:

docker run --pids-limit 256 ...

需要关注两个不同问题:

  • 内存是否充足;
  • 可创建的进程或线程数量是否达到 PID 限制。

PID 达到限制时,应用可能报:

fork: retry: Resource temporarily unavailable

这不是典型的 OOM,不能只检查内存。

BLOCK I/O 也有边界:它反映 Docker 可见的块设备 I/O 统计,不等于应用真正完成的业务写入延迟,也不一定准确表达 page cache、网络文件系统或存储后端的全部行为。若要解释数据库延迟,还需要结合主机磁盘延迟、文件系统、数据库自身指标和应用请求延迟。


七、生产指标采集:Docker Daemon、cgroups 与容器监控不是一回事

7.1 Docker Daemon Prometheus metrics

Docker Daemon 可以暴露 Prometheus 格式的 Daemon 指标,配置示例:

{
  "metrics-addr": "127.0.0.1:9323"
}

重启 Docker 后,可在本机检查:

curl http://127.0.0.1:9323/metrics

这里有两个安全边界:

  1. 将 metrics 监听在 0.0.0.0 会暴露给网络上的其他主机;
  2. Docker API socket 与 metrics endpoint 不是同一个东西,不能因为 metrics 需要远程抓取就开放 Docker socket。

Daemon metrics 主要反映 Docker Engine 自身的运行状态,例如 API 请求、镜像操作或引擎内部活动。它不自动等价于每个容器的完整 CPU、内存、网络和文件系统时间序列。

7.2 常见的生产采集分层

一个更完整的 Linux 监控拓扑通常包含:

主机层:
  node_exporter -> CPU、内存、磁盘、inode、文件系统、网络

容器层:
  cAdvisor 或其他 cgroups 采集器 -> 容器 CPU、内存、网络、I/O、PIDs

Docker 层:
  dockerd /metrics -> Engine 活动和错误

应用层:
  应用自身 metrics endpoint -> QPS、延迟、业务错误率、队列长度

事件层:
  docker events -> 容器创建、停止、OOM、健康检查、镜像操作

每层解决的问题不同:

  • 主机指标判断“主机是否还有资源”;
  • 容器指标判断“哪个容器消耗资源”;
  • Docker 指标判断“Engine 是否异常”;
  • 应用指标判断“用户请求是否正常”;
  • 事件判断“什么时候发生了状态转移”。

如果只监控容器 CPU 和内存,却不监控应用延迟,可能出现“资源看起来正常但服务已经超时”的盲区。

7.3 Prometheus 拉取容器指标的风险

采集器通常需要访问 Docker API 或宿主机上的 cgroups、procfs。Docker socket 通常是:

/var/run/docker.sock

将此 socket 以高权限方式挂载给普通容器,通常等价于给予该容器很强的宿主机控制能力。原因是拥有 Docker API 控制权的进程可能创建特权容器、挂载宿主机文件系统或改变容器配置。

因此,指标采集应优先:

  • 限制 socket 访问范围;
  • 使用只读代理或受限 API;
  • 将采集器与普通业务容器隔离;
  • 结合 Unix socket 权限、TLS、网络策略和主机访问控制。

read_only: true 只限制容器内挂载点的写入,并不自动把 Docker API 变成安全的只读 API。


八、Docker Events:事件流、状态变化和审计边界

8.1 事件是什么

docker events 输出 Docker Engine 产生的实时事件:

docker events

只看容器事件:

docker events \
  --filter type=container \
  --since 30m

只看某个容器:

docker events \
  --filter type=container \
  --filter container=api \
  --since 1h

常见事件包括:

  • create
  • start
  • stop
  • die
  • kill
  • oom
  • restart
  • health_status
  • destroy

还可以按对象类型过滤:

docker events --filter type=image
docker events --filter type=network
docker events --filter type=volume

事件适合回答:

容器何时停止?
谁触发了重启链路中的某一步?
是否发生了 OOM?
健康检查何时变成 unhealthy?
某镜像何时被拉取或删除?

8.2 事件不是持久化审计日志

docker events 是事件流接口。直接执行命令时,它不会自动为你建立可靠的长期历史数据库。监控系统重启、连接中断或 Docker Daemon 重启后,不能假设所有事件都能被完整补回。

生产环境若依赖事件告警,应建立一个事件消费者:

docker events/API
       |
       v
长期运行的消费者
       |
       +--> 消息队列或日志平台
       +--> 事件存储
       +--> 告警规则

消费者必须处理:

  • 断线重连;
  • 事件重复;
  • 时间窗口重叠;
  • Docker Daemon 重启;
  • 下游存储不可用;
  • 消费延迟和积压。

不能把一条 die 事件直接当成“业务故障”。例如:

容器 die
  -> restart policy 生效
  -> 新容器进程 start

这可能是短暂崩溃,也可能是持续崩溃重启。需要将事件与退出码、重启次数、日志和应用健康检查结合起来。

8.3 dieoom、健康检查的诊断顺序

发现服务异常时,可以按以下顺序获取证据:

docker ps -a --filter name=api

docker inspect api \
  --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} health={{if .State.Health}}{{.State.Health.Status}}{{end}}'

docker logs --since 15m --tail 200 api

docker events --since 15m \
  --filter type=container \
  --filter container=api

解释:

  1. docker ps -a 确认容器是否已经退出;
  2. inspect 确认退出码、OOM 标记和健康状态;
  3. logs 查看应用在退出前输出了什么;
  4. events 重建状态变化顺序。

如果容器一直处于 restarting,先临时停止重启风暴:

docker update --restart=no api
docker stop api

然后手动启动或运行同一镜像进行诊断。修改 restart policy 可能影响生产服务,恢复时需要明确设置回原策略,例如:

docker update --restart unless-stopped api

实际策略应以部署配置为准,不应凭猜测恢复。


九、容量告警:不能只告警“磁盘百分比”

9.1 容量问题的形式化判断

设某文件系统容量为:

  • 总块空间:CC
  • 已使用空间:UU
  • 可用空间:F=CUF=C-U
  • 使用率:ρ=U/C\rho=U/C

仅使用率告警:

ρ>0.8\rho > 0.8

存在明显缺陷:一个 10 TB 文件系统使用率 80% 时还有 2 TB,可一个 20 GB 文件系统使用率 80% 时只剩 4 GB。

因此更稳健的告警条件应同时包含绝对剩余空间:

ρ>ρthresholdF<Fminimum\rho > \rho_{\text{threshold}} \quad \lor \quad F < F_{\text{minimum}}

还应加入增长速度。若在窗口 Δt\Delta t 内使用量从 U1U_1 增长到 U2U_2

g=U2U1Δtg=\frac{U_2-U_1}{\Delta t}

预计耗尽时间为:

Texhaust=FgT_{\text{exhaust}}=\frac{F}{g}

g>0g>0TexhaustT_{\text{exhaust}} 小于维护或恢复所需时间时,即使当前使用率还未达到阈值,也应该告警。

例如:

文件系统总容量:100 GB
已使用:82 GB
可用:18 GB
最近 6 小时增长:12 GB
增长速度:2 GB/小时

则预计约:

18/2=9 小时18 / 2 = 9\text{ 小时}

后耗尽。若夜间无人值守超过 9 小时,当前 82% 的使用率已经不是“暂时安全”。

9.2 需要监控的容量对象

至少应分别观察:

主机根文件系统
Docker Root Dir 所在文件系统
BuildKit 缓存所在文件系统
Volumes 所在文件系统
inode 使用率
容器日志目录增长速度

执行现场检查:

docker info --format \
  'root={{.DockerRootDir}} logging={{.LoggingDriver}}'

df -h
df -i

docker system df -v
docker buildx du

如果 Docker Root Dir 位于单独挂载点,主机 / 的可用空间不能代表 Docker 数据区的可用空间。

9.3 容器日志容量估算

假设:

  • 容器数为 NN
  • 每个容器日志轮转上限为 SS
  • 保留文件数为 KK

理论上的日志保留上界可粗略估算为:

LmaxN×S×KL_{\text{max}} \approx N \times S \times K

例如:

100 个容器
每个文件 50 MB
每个容器保留 5 个文件

粗略上界为:

100×50 MB×5=25 GB100 \times 50\text{ MB} \times 5 = 25\text{ GB}

这不是精确账单,因为还存在:

  • 容器数量变化;
  • 轮转时的瞬时额外空间;
  • 不同 driver 的压缩和元数据;
  • 非 Docker 日志文件;
  • 应用写入容器文件系统的其他文件。

但它能说明为什么“单个容器 50 MB 看起来不大”在大规模部署下仍然可能造成几十 GB 的固定风险。

9.4 Docker 清理命令的风险

以下命令会删除未使用对象,必须先检查输出和业务依赖:

docker system prune

更激进的镜像清理:

docker system prune -a

匿名卷清理:

docker system prune --volumes

风险区别:

  • 不带 -a 时,通常只清理悬空或未使用的对象;
  • -a 可能删除当前没有容器使用的镜像;
  • --volumes 可能删除未被容器引用的卷,卷中的数据可能不可恢复。

生产清理应遵循:

先统计
  -> 判断对象是否可重建
  -> 确认卷是否有备份
  -> 低峰期执行
  -> 记录删除对象
  -> 检查服务与构建流程

不要把 docker system prune -a --volumes -f 直接放入无保护的定时任务。它可能把构建依赖、回滚镜像或未运行但仍有价值的数据一并删除。


十、容量告警的可执行检查示例

下面的脚本检查 Docker Root Dir 所在文件系统的块空间和 inode,并列出 Docker 资源概览:

#!/usr/bin/env bash
set -euo pipefail

root_dir="$(docker info --format '{{.DockerRootDir}}')"
mount_point="$(df -P "$root_dir" | awk 'NR==2 {print $6}')"

echo "Docker Root Dir: $root_dir"
echo "Mount point:     $mount_point"
echo

df -h "$mount_point"
echo
df -i "$mount_point"
echo
docker system df

运行:

chmod +x check-docker-capacity.sh
./check-docker-capacity.sh

前置条件:

  • 当前用户能访问 Docker Engine;
  • 主机提供 dfawk
  • Docker Root Dir 对应的路径存在。

这个脚本只适合现场检查。生产告警还需要:

  • 周期性执行;
  • 保存历史样本;
  • 计算增长速度;
  • 区分 warning 和 critical;
  • 监控脚本自身失败;
  • 将结果发送到告警系统。

例如,脚本执行失败时不能静默当作“容量正常”。监控系统应把“采集失败”作为独立告警,否则 Docker socket 权限错误、Daemon 停止或文件系统异常都会伪装成没有问题。


十一、把日志、指标和事件组合成故障诊断路径

11.1 磁盘接近耗尽

建议按以下路径检查:

df -h
df -i

docker info --format '{{.DockerRootDir}}'
docker system df -v
docker buildx du

然后确认日志:

docker ps -q | while read -r id; do
  docker inspect "$id" \
    --format '{{.Name}} {{json .HostConfig.LogConfig}}'
done

若确定是日志增长:

  1. 找出高增长容器;
  2. 检查应用是否进入错误重试或异常循环;
  3. 设置日志轮转;
  4. 重新创建容器;
  5. 检查远程日志是否正常接收;
  6. 再清理确认无用的 Docker 对象。

不应直接把“删除大文件”当作恢复方案,因为删除文件不能阻止应用继续产生同样的日志。

11.2 容器频繁重启

执行:

docker ps -a
docker inspect api \
  --format '{{json .State}}'
docker logs --tail 300 api
docker events --since 30m --filter container=api

可能的事件链:

start
  -> health_status: unhealthy
  -> die exit=1
  -> restart

也可能是:

start
  -> oom
  -> die
  -> restart

两者处理方向完全不同:

  • 健康检查失败:检查依赖服务、监听地址、超时、启动顺序;
  • OOM:检查内存限制、堆大小、缓存、并发量和主机压力;
  • exit code 1:检查应用配置和初始化错误;
  • exit code 137:常见于 SIGKILL,可能与 OOM 有关,但必须结合 OOMKilled 和内核日志确认,不能仅凭数字断言。

11.3 应用没有日志,但容器显示运行中

检查:

docker inspect api \
  --format '{{json .HostConfig.LogConfig}}'

docker logs api
docker exec api sh -c 'ls -l /var/log || true'

常见原因:

  • 应用把日志写到了容器内部文件;
  • 应用以守护进程方式运行,父进程不保持 stdout/stderr;
  • logging driver 不支持读取;
  • 应用日志级别过高;
  • 应用实际运行的是另一个进程或配置;
  • 日志发送到远程 driver 但远端不可达。

这类问题不能靠单独增加 docker logs -f 解决,必须先确认日志产生位置和 driver 数据流。


十二、告警设计:指标告警、事件告警和日志告警的区别

12.1 指标告警适合持续状态

指标适合表达持续或可聚合的状态:

容器内存使用率持续超过 85%
文件系统可用空间低于 10 GB
inode 使用率超过 80%
CPU throttling 持续增加
容器重启次数在 10 分钟内超过阈值

指标告警通常需要持续时间条件,避免单个采样点造成误报:

内存使用率 > 85%
持续 10 分钟

“持续”很重要,因为短时间 GC、镜像解压或启动阶段的资源峰值不一定代表故障。

12.2 事件告警适合瞬时状态变化

事件适合:

容器发生 OOM
容器 unexpectedly die
健康检查从 healthy 变为 unhealthy
Docker Daemon 重启
镜像或卷被删除

事件本身通常是瞬时的,因此应在接收时转换为:

  • 计数器;
  • 时间序列;
  • 审计记录;
  • 告警通知。

例如,将每次 oom 事件记录为:

docker_container_oom_total{container="api"} 7

之后可以基于增长量告警,而不是依赖某个事件是否仍存在。

12.3 日志告警适合异常内容,但容易受文本变化影响

日志可以检测:

OutOfMemoryError
connection refused
too many open files
disk full

但基于文本的告警存在问题:

  • 日志格式改变会导致规则失效;
  • 同一错误可能被重复刷屏;
  • 日志可能丢失;
  • 文本出现不一定表示当前故障仍存在。

更可靠的方式是:

  • 用应用 metrics 暴露错误计数;
  • 用事件识别容器 OOM 和退出;
  • 用主机 metrics 判断磁盘和内存;
  • 用日志补充根因和上下文。

十三、几个常见但危险的误解

误解一:配置了 logging driver,就自动有日志轮转

错误。不同 driver 的默认行为不同,远程 driver 也可能在网络故障时产生积压。必须检查实际配置:

docker inspect <container> \
  --format '{{json .HostConfig.LogConfig}}'

误解二:docker logs 就是应用日志的完整历史

错误。它只代表 Docker 能从当前 logging driver 读取到的日志范围。应用写入容器文件、远程 driver 不保留本地副本或日志已经轮转后,都可能不在 docker logs 中。

误解三:docker stats 的内存百分比达到 100% 才会 OOM

错误。Linux 内存回收、swap、cgroup 版本、进程分配模式和主机竞争都会影响 OOM 时机。达到 limit 是重要信号,但不能把展示百分比当作精确的故障边界。

误解四:容器退出码 137 一定是 OOM

错误。137 通常表示进程收到 SIGKILL,但 SIGKILL 的来源可能是 OOM Killer,也可能是人为或其他控制路径。应检查:

docker inspect <container> \
  --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'

并结合内核日志与事件。

误解五:清理镜像就能解决磁盘满

错误。真正占用空间的可能是:

  • 日志;
  • BuildKit 缓存;
  • 命名卷;
  • 容器可写层;
  • inode;
  • Docker 目录之外的系统日志。

应先用 dfdocker system dfdocker buildx du 定位来源。

误解六:监控容器就不需要监控主机

错误。容器共享主机内核、CPU、内存、存储和网络。容器层指标无法替代文件系统、inode、内核 OOM、磁盘延迟和主机负载指标。


十四、一套可验证的生产基线

在 Linux Docker 主机上,可以用以下检查作为最低验证集:

# 1. Engine 与日志默认配置
docker info
docker info --format \
  'root={{.DockerRootDir}} logging={{.LoggingDriver}}'

# 2. 某个容器的日志配置
docker inspect api \
  --format '{{json .HostConfig.LogConfig}}'

# 3. 日志读取
docker logs --since 10m --tail 100 api

# 4. 实时资源
docker stats --no-stream api

# 5. 容器状态和 OOM
docker inspect api \
  --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'

# 6. Docker 事件
docker events --since 30m \
  --filter type=container \
  --filter container=api

# 7. 主机块空间与 inode
df -h
df -i

# 8. Docker 对象与构建缓存
docker system df -v
docker buildx du

验证不应只看命令能否执行,还要确认输出是否符合预期:

  • 日志是否能从正确 driver 读取;
  • 容器配置是否包含轮转参数;
  • stats 是否显示合理的 limit;
  • OOM 标记与内核日志是否一致;
  • 事件是否能被采集器接收;
  • Docker Root Dir 所在文件系统是否被纳入容量告警;
  • BuildKit 缓存是否有回收策略;
  • 采集失败时是否会触发告警。

十五、最终边界:可观测性系统也必须被监控

Docker 日志、metrics、events 共同提供了容器运行时的可见性,但它们不是彼此替代的接口:

日志   = 详细上下文
指标   = 可聚合的时间序列
事件   = 状态变化
容量   = 数据平面和监控平面的生存条件

一个可用的生产系统至少应能回答四个问题:

  1. 应用输出是否被可靠收集?
  2. 容器和主机资源是否接近限制?
  3. OOM、退出、健康检查变化是否能及时发现?
  4. 日志、镜像、缓存、卷和 inode 是否会在服务前耗尽?

真正需要避免的不是某一个容器日志文件过大,而是整个故障链没有被观测到:

应用异常重试
  -> stdout 暴增
  -> 日志文件增长
  -> Docker 数据盘接近满
  -> 新日志无法写入
  -> 构建和容器创建失败
  -> 监控采集也可能失败

因此容量告警必须独立于容器日志本身,指标采集必须监控采集失败,事件消费者必须具备重连和持久化能力,日志 driver 的选择则应明确业务更重视“发送不丢”还是“日志故障不阻塞业务”。只有把这些机制和边界同时纳入设计,Docker 的日志与监控才真正具备生产运维价值。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。