5.3 日志分析与问题定位


5.3 日志分析与问题定位

本节摘要:日志是排查的最后裁判。本节从日志的源头讲起——容器 stdout 与 stderr 如何被 json-file 驱动落盘,再到 docker compose logs 的 -f、--tail、按服务过滤三种用法,然后逐行解读连接被拒、找不到文件、权限拒绝、容器反复重启四类高频报错,最后用一个跨容器请求的完整时间线示例,演示如何把零散日志拼成破案链条。读完本节,你能用证据而不是猜测定根因。

本节导航

  1. 能说明容器日志的产生路径与默认存储位置
  2. 能用 docker compose logs 的 -f、--tail、服务过滤组合提取目标日志
  3. 能区分 DEBUG、INFO、WARNING、ERROR、FATAL 五级日志并确定关注顺序
  4. 能解读连接被拒、找不到文件、权限拒绝、反复重启四类报错行
  5. 能跨容器按时间戳串联日志,定位请求失败的首个异常点
  6. 能为不同规模的应用选择合适的日志收集方案

一、日志从哪里来:先弄清源头

排查之前先弄清日志是怎么产生的。Docker 默认日志驱动是 json-file,它把容器内进程写到 stdout 和 stderr 的内容逐行包装成 JSON 记录,存放在宿主机的 /var/lib/docker/containers/容器ID/ 目录下,每个容器一个以 -json.log 结尾的文件。docker logs 命令的本质,就是读取这个文件。

两个推论值得记住。第一,容器删除后日志文件随之删除,想保留现场证据,要么提前导出,要么配置集中收集。第二,磁盘会被日志撑爆——应用写日志不设上限,json-file 文件无限增长,所以生产环境基本都要配滚动策略。

给一份带日志配置的完整 compose 示例:

services: web: image: nginx:1.27-alpine ports: - "8080:80" logging: driver: json-file options: max-size: "10m" max-file: "3" healthcheck: test: ["CMD-SHELL", "wget -q -O - http://localhost || exit 1"] interval: 30s timeout: 10s retries: 3 db: image: mysql:8.4 environment: MYSQL_ROOT_PASSWORD: example volumes: - dbdata:/var/lib/mysql volumes: dbdata:

关键行解读:logging 段的 max-size 表示单个日志文件超过 10MB 就滚动,max-file 表示保留 3 个滚动文件,两者配合把日志占用锁死在 30MB 以内;健康检查用 wget 是因为 alpine 镜像没有 curl,这和第 5.2 节提到的坑对应。规模再大的应用,可以把 driver 换成 syslog、fluentd,把日志送往集中平台。

二、docker compose logs 三种用法

docker compose logs 是看日志最直接的入口,它把项目内所有容器的日志合并输出,每行带服务名前缀。三种用法按需求组合:

  • 跟踪:-f 参数持续输出新日志,适合一边复现问题一边观察。
  • 限量:--tail 100 只看最后 100 行,避免被历史日志淹没。容器崩溃后排查,几乎总是先用这个。
  • 过滤服务:命令末尾跟服务名,只显示指定服务的日志,docker compose logs web,多个服务就写多个名字。

组合示例:docker compose logs -f --tail 50 web,只跟踪 web 服务最近的 50 行。还有两个实用参数:--since 30m 只看最近 30 分钟的日志,--timestamps 显示完整时间戳,跨容器比对时间线时必开。

参数 作用 示例
-f 持续跟踪新日志 docker compose logs -f
--tail 行数 只看最后 N 行 docker compose logs --tail 200
服务名 只显示指定服务 docker compose logs db
--since 时长 按时间范围过滤 docker compose logs --since 30m
--timestamps 显示完整时间戳 docker compose logs --timestamps

输出前缀有个版本差异:新版 compose 的服务前缀是 web-1,老版本是 web_1,下划线变连字符,看日志时别认错。

三、日志级别:先看重的,再看上下文

日志级别代表信息的重要程度,级别划分直接决定你从哪行开始读:

级别 含义 处理建议
DEBUG 调试细节,定位问题的最细粒度 开发环境用,生产关闭
INFO 常规运行信息,如启动、请求处理 保留,作为正常基线
WARNING 潜在隐患,不影响当前功能 关注但不立即处理
ERROR 发生错误,功能可能受损 立即排查
FATAL 致命错误,应用无法继续 最高优先,先处理它

分析顺序有讲究:先找 ERROR 和 FATAL,锁定问题发生的时刻;再往回看同时间的 WARNING 和 INFO,补全上下文——错误本身只告诉你"坏了",上下文才告诉你"怎么坏的"。生产环境日志级别通常设在 INFO 或 WARNING,DEBUG 全开会导致日志量暴涨,反而淹没有效信息。

四、常见报错行解读:四类高频文本

日志里出现频率最高的四类报错行,逐个拆开看:

连接被拒。典型文本:web_1 | 2023-10-27 10:00:00 ERROR: Could not connect to database: Connection refused。含义是 TCP 连接被目标拒绝,目标端口没有服务在监听。排查顺序:目标服务是否在运行、目标端口是否监听、网络是否可达。如果目标是数据库容器,先 docker compose ps 看它是否还活着。

找不到文件。报错通常是 No such file or directory,带具体路径。先分清路径在容器内还是宿主机:容器内路径报错,查工作目录和挂载是否生效;挂载路径报错,回到第 5.2 节的卷排查流程,docker inspect 看 Mounts 段核实。

权限拒绝。Permission denied 几乎总是 UID 不匹配:容器内进程用户和挂载目录属主不一致。解法是把宿主机目录属主改成容器内用户 UID,chown 之后重启容器。

容器反复重启。日志里看不到应用报错,容器却在疯狂重启,先看退出码:137 是内存被杀死,127 是启动命令不存在,1 是应用自身异常。配合 docker inspect 看 OOMKilled 字段,能直接区分内存问题和代码问题。

⚠️ 常见坑:容器反复重启时,compose logs 输出的是容器最后一次退出的日志,不一定代表根因。先 docker inspect 看 RestartCount 和 OOMKilled 两个字段,再回来看日志,能少走一半弯路。

五、跨容器追踪:一次请求的时间线

单看一个容器的日志容易误判,把相关容器的日志按时间戳排在一起,链条就出来了。以"用户访问页面返回 502"为例,nginx、web、数据库三个容器各留一段日志:

10:00:00.100 nginx | 10.0.0.5 GET /api/orders 502 10:00:00.105 web | ERROR: connect ECONNREFUSED 172.20.0.3:3306 10:00:00.102 db | 无任何日志输出

把三行按时间排序后,结论非常清晰:web 在 10:00:00.105 报 3306 端口连接被拒,而数据库容器在这个时刻没有任何日志——请求根本没到达数据库进程。问题不在 SQL 或数据,而在网络层或数据库监听。如果数据库侧有日志,则要把排查重心移到数据库自身。

再对比一次正常请求的时间线:

10:00:01.000 nginx | GET /api/orders 200 10:00:01.008 db | 3 rows returned in 8ms 10:00:01.010 web | INFO: Request processed in 1200ms

数据库 8 毫秒就返回了,web 却花了 1200 毫秒处理——慢在应用逻辑或连接池,不在数据库。日志时间线不但能定位"断在哪",还能定位"慢在哪"。请求量大的系统,日志里加请求 ID,跨容器用请求 ID 对齐日志,比靠时间戳猜精确得多。

💡 关键直觉:日志"没出现"也是一种信息。某个容器在时间线上静默,说明请求根本没到它这一层——这常常直接锁定了断点位置,比看到报错更省事。

这张时序图就是"日志时间线"的可视化版本。正常路径和异常路径一目了然:异常时第一个报错的是 web 到数据库这一段,取证时从数据库容器日志开始核对,能最快收口。

需要提醒的是,跨容器时间线依赖各容器时钟一致。单机部署时所有容器共享宿主机时钟,时间戳天然对齐;跨主机部署时如果各节点时间不同步,先校准时钟再比对,否则时间线会把排查方向带偏。

六、过滤工具与集中收集

日志量大之后,靠肉眼扫不现实。三个命令行工具是基本盘:

  • grep:docker compose logs | grep error,全项目日志里搜关键字,先看行数判断问题是不是个例。
  • awk:docker compose logs | awk '/ERROR/{print $0}',把 ERROR 行整体提取出来,配合前面的时间戳列可以做统计。
  • sed:做替换和格式化,比如把错误关键字统一高亮,方便截图沟通。

一条实战管道把三个工具串起来:docker compose logs --timestamps web | grep ERROR | awk '{print $1, $2, $0}'。输出里第一列是时间戳,第二列是服务前缀,后面是完整报错。错误行超过几十条时,把 grep 的关键字换成 Connection refused 或 No such file,范围立刻缩小到真正需要看的内容。

结构化日志是时间线分析的加速器。一条 JSON 格式的日志长这样:

{"ts":"2023-10-27T10:00:00Z","level":"ERROR","service":"web","request_id":"a1b2c3","msg":"connect failed","target":"db:3306"}

request_id 字段就是跨容器对齐的关键:同一请求在每个容器日志里带同一个 ID,grep 一下 request_id 就能把整条链路从 nginx 到数据库全部拉出来,比靠时间戳猜精确一个量级。这也是生产环境强烈建议结构化日志的原因。

单机部署,docker compose logs 完全够用。多机或微服务规模,就要上集中收集:ELK Stack 三件套分工明确,Logstash 负责收集处理,Elasticsearch 负责存储索引,Kibana 负责可视化搜索;Graylog 自带 Web 界面和告警;Fluentd 以插件多著称,适合异构环境;轻量方案选 Promtail 加 Loki,和 Prometheus 监控生态集成顺滑。如果团队已经在用 Prometheus 做监控,Promtail 加 Loki 能复用同一套标签体系,少维护一套独立部署。选型的参考线很简单:一台机器用命令,三台机器上 Loki,规模再大上 ELK。

日志分析流程示意图

日志分析流程示意图

这张泳道图把日志分析拆成四个阶段:先收集齐证据,再过滤缩小范围,然后跨容器关联时间线,最后对照错误清单定位根因。前两步处理的是"量",后两步处理的是"序"——时间顺序对了,根因自己会浮出来。

七、让日志更好用的六个习惯

排查效率的一半来自平时的日志质量,六个习惯直接见效:

  1. 用结构化日志,JSON 格式比自由文本好解析,awk 和集中平台都省力。
  2. 加上下文信息,请求 ID、用户 ID 进日志,跨容器追踪从"猜"变成"查"。
  3. 全项目统一日志格式,时间戳、级别、来源字段顺序一致,工具才写得出通用脚本。
  4. 日志保持简洁,必要信息才打,DEBUG 别在生产常开。
  5. 定期清理,靠 logging 的 max-size、max-file 滚动,或者集中平台的保留策略。
  6. 留正常基线,记录请求处理耗时的 INFO 日志,性能劣化时才有对比对象。

温故知新

  • 日志源头要清楚:json-file 驱动把 stdout 和 stderr 写到宿主机容器目录,容器删除日志即失。
  • 三个参数打天下:-f 跟踪、--tail 限量、服务名过滤,组合起来覆盖绝大多数场景。
  • 级别决定阅读顺序:先 ERROR 和 FATAL 锁定时刻,再回看 WARNING 和 INFO 补上下文。
  • 报错行要先归因:连接被拒查监听,找不到文件查挂载,权限拒绝查 UID,反复重启查退出码。
  • 时间线是核心武器:跨容器按时间戳排序,首个异常点就是最可疑的位置。
  • 请求 ID 是进阶方案:流量大时靠时间戳对齐不够精确,请求 ID 才是硬标准。
  • 日志质量决定排查速度:结构化、带上下文、统一格式,省下的是出故障时的时间。

到这里,本章的排查链条就完整了:5.1 的工具、5.2 的清单、5.3 的证据,三件套齐活。下次再遇到"容器又挂了"的消息,先分类,再对表,最后用日志时间线收口——这套流程走熟了,故障处理就不再是熬人的苦差事。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U