2956 字
约 9 分钟
17
S14-A14 · 日志的两种读者:运维看现象,审计看事实

出事那天,运维要在日志里看出"哪个接口先慢、哪个依赖先错、时间顺序对不对";审计要问的是"这条删除是谁做的、删了哪些行、依据是什么"。两个问题翻向同一个日志文件,结果两头落空:运维嫌它字段不够、看不清串行关系;审计发现关键那一行被采样掉了。

日志之所以经常同时被赋予这两种职责,是因为它是最便宜的"什么都写"的地方。但两种读者的判据方向相反:运维要时序与关联,审计要完整与不可改。一个要求"多写一行不亏",另一个要求"少写一行就废"。


1. 两个读者,两套判据

比较项 运维读者 审计读者
判据 能否还原时间顺序与因果链 能否独立复述"谁、在何时、对什么、做了什么、依据什么"
时间尺度 事件前后几分钟到几小时 数月到数年
详细程度 越高越好,含上下文与堆栈 只需关键字段齐全,不需要上下文
对"多写一行"的态度 欢迎(可能有用) 谨慎(写错一行即误导)
对"少写一行"的态度 可接受(还能从别处拼) 致命(缺口无法事后补)
对采样 必需(量太大) 毁灭性(被采掉的可能就是证据)
保留期诉求 短而细 长而稳
可变性 无所谓,能查就行 必须不可篡改、可追溯写入者

两者的冲突不在格式,而在**"哪一行可以不要"**:运维可以容忍任何单行缺失,只要整体现象还在;审计不能容忍任何单行缺失,因为它无法证明"缺的那行不是被删掉的那行"。

共用一份日志时,被牺牲的几乎总是审计。原因不是有人偏心,而是两边失效的时间尺度不同:运维的缺口当天就会被人抱怨,于是当天就有人补字段、加检索;审计的缺口要等到下一次外部质询才暴露,而那时改动日志结构已经来不及,只能补一份"情况说明"。这份说明由当事人写,恰恰是审计最不认的那类材料。

2. 级别、采样、字段:三处相反的态度

<!-- 运维视角的采样配置:为了控量,采样率随流量自动下调 -->
<appender name="APP" class="...AsyncAppender">
  <filter class="...ThresholdFilter"><level>INFO</level></filter>
</appender>
<!-- ─── 同一份 appender 一旦承载审计行,采样就等于对证据做抽检 -->
维度 运维要的 审计要的 同一份日志里的结果
级别 DEBUG 在需要时打开 只保留操作事实,级别高低无关 调级别时审计行跟着被关掉
采样 必须,否则存储爆 禁止,任何比例都不可接受 采样率按运维定,审计行被连带采掉
保留 7–14 天足够定位 按合规期,典型以年计 长保留的那份被高详细度拖贵,最后被砍
字段 traceId、耗时、状态、异常 操作者、目标、前后值、依据 两套字段都写则行宽翻倍,只写一套则一边失效
// 运维要的行:能串起来
2026-11-09T10:21:03Z INFO  [trace=9f2a] order-pay cost=412ms status=200 downstream=inv
// 审计要的行:能定责
2026-11-09T10:21:03Z actor=u_8821 action=order.delete target=order:77120
                       before={status:PAID} after={deleted:true} reason=ticket:4412
// ─── 两行合成一行,通常两边都不合格:运维嫌冗余,审计嫌字段被截断

"审计字段平时没人看"正是它被砍掉的直接原因:它在正常日子里只增加行宽与存储,不产生任何可感知的价值;而它唯一的价值场景,是出事后你希望它还在。

保留期是这笔账里最容易被算错的一项,因为两个读者对"每天多少 GB"的敏感度不同:

// 同一个日志流,两种保留诉求的存储账(数量级按自己的日均量核对)
日均 50 GB × 14 天(运维)        = 700 GB
日均 50 GB × 730 天(审计两年)    = 36 TB
// ─── 决策通常落成"保留 30 天":运维嫌短但能忍,审计从头到尾没被问过
// ─── 正确的拆法:审计行只占总量的一小部分,单独按长保留存

3. 一份日志承担两种职责:三个具体失效

失效 机制 谁被牺牲
审计缺行 异步队列满、采样、级别过滤、日志轮转覆盖 审计
运维被淹没 审计行数量与业务写入同量级,翻页即疲劳 运维
保留期两头不讨好 按运维配 14 天(审计不够),按审计配 3 年(成本被砍到 30 天) 两者同时

第一条的机制值得具体说:审计行往往在"写库成功之后"才打,而日志管道是异步的——队列满时丢弃策略是 discardOldest 或 discard,丢弃的恰好是峰值期那批,而峰值期也恰好是事故期。日志系统在压力下的行为,决定了审计在最需要它的时候最不可靠。

第二条的镜像同样真实:把 before/after 全量写进业务日志,单条日志从几百字节涨到几 KB,运维的检索与翻页成本随之翻倍,最后没人再用它排查——于是"有日志"和"能排查"变成两件事。

审计读者的另一条要求是写入后不可改,这一条与"运维随时调整日志级别"直接对立:

// 追加式写入 + 链式校验:任何删改都会让后续记录失配
AuditRecord r = AuditRecord.of(actor, action, target, before, after, reason, at);
r.prevHash = lastHash;                 // 与上一行绑定
r.selfHash = sha256(r.canonicalForm());
appendOnlyStore.append(r);             // 只追加,不提供 update/delete 接口
lastHash = r.selfHash;
// ─── 校验失败不代表"没人动过",它代表"无法证明没被动过"

4. 分离的两种做法与各自成本

// 做法 A:同采集、双输出。一次写入,两个 sink,字段集不同
─── inputs: [app_stdout]
─── outputs:
───   - ops_index:     字段全量,保留 14 天,允许采样
───   - audit_index:   只留审计行(action 白名单),不采样,保留按合规期
───   relabel: 审计行带固定标记 audit=true,由写入侧打,不由采集侧猜
// 做法 B:双通道。审计不走日志框架,走独立的追加式写入
auditLog.append(new AuditRecord(actor, action, target, before, after, reason, at));
// ─── 与业务日志共用一套管道 = 共用同一套丢弃策略与同一套采样开关
做法 成本 失效风险 适用
同采集双输出 低,改动在采集侧 写入侧一旦没打标记,审计行整批漏 已有成熟日志管道
双通道(独立存储) 高,多一套写入与保留 需要保证两条通道的写入顺序与失败处理 有对外承诺或受监管
单份日志加完整字段 最低 采样与保留期仍在,只是把缺口藏得更深 内部工具、操作人极少

无论哪种做法,有一条硬约束:关联键必须在写入时生成。traceId、操作序号、请求入口时间,事后无法补——日志里没有的东西,不可能通过分析得到。这也是"日志证明过程、账目证明结果"的分界:账目能告诉你金额少了,只有日志能告诉你少在哪一步;反过来,日志能告诉你谁点了按钮,账目才能证明钱去了哪。

5. 排错:五种现场

现象 真实原因 处理方向
审计要求的那一行查不到 采样、级别过滤或异步丢弃 审计行走独立输出且不采样
同一次操作在日志里有三条时间 多实例时钟未同步,或写入时刻与业务时刻混用 统一时间源,记录"发生时刻"而非"打印时刻"
排查时无法串起调用链 traceId 未跨线程/跨服务传递,或写入时才生成 在入口生成并透传,禁止下游另生成
保留期总被缩短 高详细度与长保留同时要,成本无人认领 拆两份:详细短保留、审计长保留
日志量与业务量同比例增长 审计行与业务行同粒度 审计只记状态变更,不记读取

度量四个量:审计字段完整率(关键字段非空比例)、采样误伤率(审计行被采掉的比例)、平均排查时延(从告警到定位根因)、保留成本(按 GB·天 折算)。前两个衡量审计读者,第三个衡量运维读者,第四个是两个读者共同的预算约束。

完整率可以直接用一次统计得出,不必等外部质询:

// 审计行的字段完整率:空值即缺口,缺口即下次质询的答案
$ zcat audit-2026-11-09.log.gz | awk -F'\t' '
    $0 ~ /audit=true/ { total++;
      if ($4 == "" || $5 == "" || $7 == "") miss++ }
    END { printf "total=%d missing=%d rate=%.2f%%\n", total, miss, miss*100/total }'

6. 边界:什么时候可以只用一份日志

场景 结论 理由
内部工具、无对外承诺 一份带完整字段的日志即可 审计读者不存在外部质询,缺口可由人证补
操作人只有几个、动作可追溯 不加独立审计通道 责任人集合小,交叉验证成本低
状态变更已由业务表留痕(含操作者与时间) 日志不必重复记录 结果侧已有事实,日志只补过程
事件量极小、检索靠人眼 不做双输出 双输出的维护成本高于缺口风险
无监管要求且有明确的数据删除承诺 短保留是正确选择 长保留与删除承诺直接冲突

一句话选择:**运维读者买"能查",审计读者买"不缺",两者的成本必须分开列账。**合并成一份时,被牺牲的几乎总是审计——因为它的失效只在出事那天显形,而那天没人有余力改日志结构。


自测

  1. 运维读者与审计读者对"少写一行"的态度,为什么必然相反?
  2. 异步日志队列在峰值期的丢弃策略,对审计意味着什么?
  3. 为什么"审计字段平时没人看"会导致它被优先砍掉?
  4. 采样对运维是必需、对审计是毁灭,根本差别在哪个判据上?
  5. traceId 为什么必须写入时生成,事后补不上?
  6. 什么场景下一份带完整字段的日志就足够,不必分双读者?
S14-A14 · 日志的两种读者:运维看现象,审计看事实
https://www.clxhxhhr.top/posts/6359/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。