2308 字
约 7 分钟
18
S15-A25 · 日志采集链路的断点:从写入到能查到,中间有六段

一条错误日志在排查时"查不到"。开发确认代码里有 log.error,也确认走了同一个 logger——问题出在采集链路的第三段:容器日志驱动把行截断在 16KB,而这条日志带着完整堆栈,超出的部分被丢掉,于是在日志平台上它只剩第一行,关键字匹配也就失败。

日志从代码里的一行 log.error 到能在平台上被搜到,中间至少有六段。任何一段失败,表现都是"没有这条日志",而"没有日志"是最难排查的现象——因为你不知道自己丢的是哪一段。


1. 六段链路

段 环节
1 应用写 stderr/stdout 或文件
2 容器/进程日志驱动收集
3 采集代理读文件并解析
4 传输(缓冲、压缩、重试)
5 平台入库(索引、分片)
6 查询(时间范围、字段映射)

六段的失败模式不同:第三段失败是"整批丢",第四段失败是"延迟",第五段是"能查到一部分",第六段是"存在但搜不到"。表现不同,排查入口就不同。

2. 每段的典型断点

段 断点
1 日志级别过滤、异步队列满
2 单行长度限制、驱动配置
3 路径不匹配、多行解析失败
4 缓冲溢出、重试窗口过短
5 字段类型冲突、索引被拒
6 时间时区、字段名不一致

六段之外还有两个"跨段"的东西:时钟与关联键。各段的时间戳可能来自不同机器,采集侧写的是采集时刻、应用侧写的是产生时刻,两者差几秒到几分钟;而 trace_id 如果在某一段被丢弃,跨服务关联就断了。

第二段的"单行长度限制"是最常见的隐形杀手:带堆栈的错误日志通常 20–60KB,而多数驱动的默认单行上限远小于此。

第四段的"重试窗口过短"很典型:平台抖动两分钟,代理重试三轮后放弃,那两分钟的日志永久丢失,而系统看起来一切正常。

// 采集侧:把容易踩的默认值显式写出来
filebeat.inputs:
  - type: container
    paths: ["/var/lib/docker/containers/*/*.log"]
    max_bytes: 204800        # 200KB,默认往往只有 10KB
    multiline:
      pattern: '^\d{4}-\d{2}-\d{2}'
      negate: true
      match: after
output.logstash:
  hosts: ["logstash:5044"]
  bulk_max_size: 2048
  backoff.max: 60s           # 抖动时的重试窗口

3. 多行日志:最常见的解析断点

现象 原因
堆栈被拆成几十行 未配置多行合并
合并把多条日志粘成一条 行首正则太宽
合并后时间戳错乱 用行内时间而非采集时间
首行丢失 采集从中间开始(rotate 竞争)

第三项(时间戳错乱)的机制值得说明:多行合并后,整条日志的时间取第一行的时间,如果正则把上一条的尾部也括进来,时间就会提前一条,导致"日志序号乱了"这类难以解释的现象。

第二项与第一项是一对:正则太窄则堆栈被拆,太宽则相邻日志被粘。稳妥的做法是让应用输出 JSON 单行日志,把多行合并这件事从采集侧移到应用侧。

// 应用侧输出单行 JSON:彻底绕开多行解析
{"ts":"2026-11-10T12:00:01.123Z","level":"error","msg":"pay failed",
 "err":"...","stack":"at a.b.C(D.java:12)\n at a.b.E(F.java:34)"}
// 堆栈转义进一个字段,采集侧无需任何多行规则

4. 传输:缓冲与背压

参数 影响
缓冲大小 能扛多久的平台不可用
批量大小 吞吐与延迟的权衡
重试上限 丢失窗口的长度
磁盘缓冲 是否落盘(重启不丢)

缓冲还有个"隐性成本":缓冲越大,丢失窗口越长。平台恢复后一次性推上去的几十万条日志,会把入库侧打满,反而造成二次积压——所以缓冲要配合限速,而不是单纯加大。

第四项决定"重启会不会丢日志":内存缓冲在容器重启时全部消失,而日志丢失恰好常发生在重启前后——也就是最需要日志的时刻。

// 断点自检:三段各问一句
1. 应用侧:有没有因队列满丢弃?(看 app 的 discarded 计数)
2. 传输侧:缓冲能不能扛住 5 分钟平台不可用?(内存还是磁盘)
3. 平台侧:入库被拒的条数是多少?(看 bulk rejected)
// 三个数都能查到,链路才算可观测

5. 入库与查询

问题 表现
字段类型冲突 部分文档被拒,日志"少了一些"
索引模板不匹配 字段无法过滤
时间时区不一致 查某时段查不到
分片与保留策略 老日志被提前删除

排查这类问题有个固定动作:在平台的写入侧看 rejected 计数,而不是在查询侧看结果。查询侧只会告诉你"少了一些",写入侧才知道少了多少、为什么。

// 三处计数对齐,才能判断"丢了哪一段"
应用:logger 丢弃计数 (async queue full)
代理:read_lines vs sent_lines 的差
平台:rejected / mapping_exception 计数
// 三个数都对得上 → 链路无丢;任一处有差 → 该段是断点

第一项在动态映射的平台上很常见:同一个字段这天是数字、那天是字符串,后写入的文档被拒,而拒绝信息只在平台的写入日志里,采集侧看不到。

平台侧的保留策略也要与"什么时候还会被查"对齐:保留 7 天而复盘周期是两周,等于永远查不到上周的事故。这个数字应当由复盘习惯决定,而不是由存储预算单方面决定。

第三项是"存在但搜不到"的头号原因:应用写本地时间,平台按 UTC 索引,于是排查时按本地时间选窗口,永远差八小时。

// 验证一条日志能否被查到:端到端最小用例
logger -t probe "PROBE-$(date +%s)"
// 三步验证
kubectl logs <pod> | grep PROBE-          # 段 1–2
tail /var/log/containers/*.log | grep PROBE-   # 段 2–3
curl -s "$ES/prod-logs/_search" -H 'Content-Type: application/json' \
  -d '{"query":{"match_phrase":{"message":"PROBE-"}}}'   # 段 5–6

6. 排错:五种"日志查不到"

现象 段 处理方向
容器里有、平台没有 3/4 查代理路径与缓冲
平台有但只有第一行 2 提高单行上限
能查到但过滤不到 5/6 查字段映射与类型
某时间段整体缺失 4 查重试窗口与缓冲
重启前后的日志缺失 4 改磁盘缓冲

第四项要特别注意:"整体缺失一段时间"往往被误认为平台故障,实际是代理在抖动期间放弃重试。判定方法是看代理自身指标里的 rejected/retry 计数。

7. 边界:什么情况不必做全链路治理

场景 为什么 做法
单机小服务 段 3/4 不存在 只保证段 1
日志量极小 丢一点影响小 保留基本采集
只用指标不用日志 无链路 不做
合规要求保留原文件 平台只是辅助 以文件为准

还有一条与"自检"有关的边界:端到端探针应当每天自动跑一次。一条带唯一标记的日志从应用到查询的完整链路,能在十秒内验证六段全通——这比事故时临时排查便宜得多。

判断口径一句话:这次排查如果丢了日志,会不会无法复原事故。会,六段都要有可观测的自检点;不会,前两段做好就够。


8. 自测

  1. 六段链路里,哪一段失败表现为"延迟",哪一段表现为"存在但搜不到"?
  2. 为什么"带堆栈的错误日志"最容易被单行长度限制吃掉?对策是什么?
  3. 多行合并的正则"太窄"与"太宽"各导致什么问题?为什么推荐让应用输出单行 JSON?
  4. 为什么"内存缓冲"在最需要日志的时刻最不可靠?该改成什么?
  5. "某时间段整体缺失"最可能的段与原因是什么?怎么和平台故障区分?
  6. 判断"是否需要全链路治理"的那个问题是什么?
S15-A25 · 日志采集链路的断点:从写入到能查到,中间有六段
https://www.clxhxhhr.top/posts/6448/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。