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[告警规则]
这里有几个必须区分的事实:
- Logging Driver 负责日志路由和存储,不是指标采集器。
docker stats主要读取容器的资源统计,不是应用级 QPS、延迟或错误率。docker events是状态变化流,不是可替代日志的审计数据库。- 磁盘使用量不只来自容器日志,镜像层、BuildKit 缓存、匿名卷、命名卷和容器可写层都可能占用空间。
- 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-filelocaljournaldsyslogfluentdgelfsplunkawslogsgcplogsetwlogs等
可查看当前 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}}'
注意:
- Docker Daemon 重启会影响主机上的容器管理操作,生产环境应安排维护窗口。
- 新默认配置主要影响之后创建的容器。
max-size和max-file只是日志文件轮转,不会限制应用写入速度。- 轮转并不等价于集中式日志保留策略;旧日志仍可能在本机占用空间。
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 可能占用空间的来源
常见来源包括:
- 容器日志;
- 镜像层;
- 容器可写层;
- BuildKit 构建缓存;
- 未使用的镜像、网络和容器;
- 命名卷和匿名卷;
- Docker Desktop 或其他运行时的虚拟磁盘文件。
查看 Docker 对象占用:
docker system df
docker system df -v
查看 BuildKit 缓存:
docker buildx du
不同版本的 buildx 输出字段可能不同,但核心目标是确认构建缓存的大小与可回收对象。
5.2 不要直接删除 /var/lib/docker/containers 中的文件
直接删除日志文件可能导致:
- Docker 内部状态与文件状态不一致;
- 文件句柄仍被进程持有,空间不会立即释放;
- 后续日志写入异常;
- 破坏 Docker 管理的数据。
当日志失控时,应优先:
- 停止产生异常日志的应用;
- 配置正确的轮转;
- 重新创建使用新日志配置的容器;
- 将日志发送到集中式系统;
- 对无用 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
这是一个随时间单调增加的计数器。监控系统通常通过两个采样点计算变化率:
其中:
- 是时刻 的累计 CPU 时间;
- 是采样间隔;
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 百分比基于两个时间差:
- 容器在本次采样期间消耗的 CPU 时间;
- 系统在同一期间经过的 CPU 时间。
可以抽象为:
其中 是可用 CPU 数量。不同 Engine 版本、cgroups 版本和平台可能影响具体实现,但重要的是:100% 不一定表示整台多核主机已经全部耗尽,也可能表示一个 CPU 核被充分使用。
如果容器设置了 CPU 限制,还应同时观察:
- 容器 CPU 使用量;
- CPU quota 是否被频繁消耗;
- throttling 时间;
- 主机总体 CPU 竞争。
只看 docker stats 中的 CPU 百分比,可能漏掉“容器没有达到很高百分比,但已经持续被 quota 限流”的情况。CPU 配额属于 cgroups 资源治理范畴,告警应结合限制和 throttling 指标分析。
6.4 内存指标与 OOM 的关系
Linux 上,容器内存限制最终依赖 cgroups。若容器达到限制,可能发生:
- 内核尝试回收可回收内存;
- 分配仍无法满足;
- cgroup 内存压力增加;
- 内核 OOM Killer 选择进程;
- 进程被杀死,容器可能退出或被重启策略拉起。
可以观察容器的限制:
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
这里有两个安全边界:
- 将 metrics 监听在
0.0.0.0会暴露给网络上的其他主机; - 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
常见事件包括:
createstartstopdiekilloomrestarthealth_statusdestroy
还可以按对象类型过滤:
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 die、oom、健康检查的诊断顺序
发现服务异常时,可以按以下顺序获取证据:
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
解释:
docker ps -a确认容器是否已经退出;inspect确认退出码、OOM 标记和健康状态;logs查看应用在退出前输出了什么;events重建状态变化顺序。
如果容器一直处于 restarting,先临时停止重启风暴:
docker update --restart=no api
docker stop api
然后手动启动或运行同一镜像进行诊断。修改 restart policy 可能影响生产服务,恢复时需要明确设置回原策略,例如:
docker update --restart unless-stopped api
实际策略应以部署配置为准,不应凭猜测恢复。
九、容量告警:不能只告警“磁盘百分比”
9.1 容量问题的形式化判断
设某文件系统容量为:
- 总块空间:
- 已使用空间:
- 可用空间:
- 使用率:
仅使用率告警:
存在明显缺陷:一个 10 TB 文件系统使用率 80% 时还有 2 TB,可一个 20 GB 文件系统使用率 80% 时只剩 4 GB。
因此更稳健的告警条件应同时包含绝对剩余空间:
还应加入增长速度。若在窗口 内使用量从 增长到 :
预计耗尽时间为:
当 且 小于维护或恢复所需时间时,即使当前使用率还未达到阈值,也应该告警。
例如:
文件系统总容量:100 GB
已使用:82 GB
可用:18 GB
最近 6 小时增长:12 GB
增长速度:2 GB/小时
则预计约:
后耗尽。若夜间无人值守超过 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 容器日志容量估算
假设:
- 容器数为 ;
- 每个容器日志轮转上限为 ;
- 保留文件数为 。
理论上的日志保留上界可粗略估算为:
例如:
100 个容器
每个文件 50 MB
每个容器保留 5 个文件
粗略上界为:
这不是精确账单,因为还存在:
- 容器数量变化;
- 轮转时的瞬时额外空间;
- 不同 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;
- 主机提供
df、awk; - 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
若确定是日志增长:
- 找出高增长容器;
- 检查应用是否进入错误重试或异常循环;
- 设置日志轮转;
- 重新创建容器;
- 检查远程日志是否正常接收;
- 再清理确认无用的 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 目录之外的系统日志。
应先用 df、docker system df 和 docker 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 共同提供了容器运行时的可见性,但它们不是彼此替代的接口:
日志 = 详细上下文
指标 = 可聚合的时间序列
事件 = 状态变化
容量 = 数据平面和监控平面的生存条件
一个可用的生产系统至少应能回答四个问题:
- 应用输出是否被可靠收集?
- 容器和主机资源是否接近限制?
- OOM、退出、健康检查变化是否能及时发现?
- 日志、镜像、缓存、卷和 inode 是否会在服务前耗尽?
真正需要避免的不是某一个容器日志文件过大,而是整个故障链没有被观测到:
应用异常重试
-> stdout 暴增
-> 日志文件增长
-> Docker 数据盘接近满
-> 新日志无法写入
-> 构建和容器创建失败
-> 监控采集也可能失败
因此容量告警必须独立于容器日志本身,指标采集必须监控采集失败,事件消费者必须具备重连和持久化能力,日志 driver 的选择则应明确业务更重视“发送不丢”还是“日志故障不阻塞业务”。只有把这些机制和边界同时纳入设计,Docker 的日志与监控才真正具备生产运维价值。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Registry 与镜像分发:Tag、Digest、认证、缓存和清理
- 下一篇:Docker 健康检查与优雅关闭:PID 1、Probe、Stop Signal 和超时
- 延伸:Docker 资源治理:CPU、内存、PID、I/O、cgroups 与 OOM
- 延伸:Docker 故障排查:启动失败、网络、磁盘、OOM、构建和 Daemon
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论