1.1 需求拆解:吞吐、低延迟、不丢

日志管道的需求常被一句话带过——「把日志收上来」。但拆开看,它是三条互相牵制的线:吞吐、低延迟、不丢。三条线方向不同:吞吐想攒大批慢慢处理,低延迟想立刻就走,不丢想每条都确认——三者天然打架。本篇把三条线拆到模块级,给后面的选型和实现定调。

三条线各自牵动什么

需求 表面含义 真正牵动的模块 不满足会怎样
吞吐 每秒处理多少条 攒批、压缩、顺序写 高峰期积压,磁盘被写爆
低延迟 日志多久能被检索到 通道缓冲、刷盘策略 排障时日志还没到,等于没有
不丢 进程崩了日志还在吗 WAL、确认机制、落盘 故障现场丢失,无从复盘

三条线里「吞吐」最容易引起注意,「低延迟」常被默认达成,「不丢」最容易被彻底忽略——很多管道直到第一次崩溃重启才发现丢了一大段。日志这种数据很特殊:写的时候不觉得珍贵,丢的时候才知道是唯一现场。

吞吐是「批发」逻辑

吞吐的核心不是「写得快」,而是攒批:单条写一次磁盘,开销全花在寻道上;攒成一批一次写,单条摊销成本能降一个数量级。所以高吞吐管道的骨架几乎都是「内存攒批 → 定时或定量刷盘」,这决定了模块里必须有攒批器和刷盘策略两个部件。

攒批维度 常见触发 取舍
按条数 攒满 N 条 吞吐稳,低谷期延迟高
按时间 每隔 T 毫秒 延迟可控,吞吐可能碎
按字节 攒满 M KB 贴合磁盘块大小
混合 条数与时间先到为准 实战主流

批越大吞吐越高,但批越大意味着崩溃时丢得越多——这就和不丢需求直接冲突,必须靠 WAL 之类的手段补,详见 4.4。

低延迟是「零售」逻辑

低延迟要求日志「尽快可见」。它和吞吐的冲突在于:批还没攒满,要不要先发?答案是把「攒满才发」改成「满了或超时就发」,用时间维度兜底延迟。此外还要注意内存队列长度:队列越长吞吐越平滑,但端到端延迟被队列长度乘以单条处理时间拉长,监控时要把队列积压当作延迟的前置指标看。

不丢是「记账」逻辑

「不丢」的本质是每一笔都有下落:进程崩溃前,已确认的日志必须已在磁盘上。常见实现是 WAL(预写日志):日志先追加写一份 WAL 再进内存批,崩溃后重放 WAL 恢复。代价是每条多一次顺序写——顺序写很快,但仍是一笔开销。

机制 保证什么 代价
无 WAL 什么都不保证 最快
内存队列 延迟平滑 崩溃全丢
WAL 已写日志不丢 每条多一次顺序写
副本双写 跨机器不丢 网络与存储翻倍

选择哪一档取决于业务:开发测试环境无 WAL 无所谓,生产排障现场必须 WAL,金融或计费日志才上副本双写。

三条线的优先级

优先级 需求 理由 典型指标
P0 不丢 日志是故障现场 崩溃后 0 丢失
P0 吞吐 高峰不能积压打爆 峰值 N 万条/秒
P1 低延迟 可检索才有价值 P95 < 2 秒

优先级定下来,资源就往 P0 倾斜:先保证 WAL 与背压,再把攒批做大冲吞吐,最后用定时刷盘收延迟。三条线不是同时要满分,而是各自有下限:不丢是底线,吞吐看峰值,延迟看 P95。这三条线的拆解方式与 Go 微服务实战 里的可观测性章节思路一致,感兴趣可对照。

量化三条线:一张需求矩阵

拆完原则,还要落到数字上,否则「三条线有下限」仍然是一句口号。建议按下面的矩阵,根据自己业务的真实画像填一遍。示例是一家中型电商的系统日志:峰值发生在秒杀和大促,平时吞吐只有峰值的十分之一,排障对延迟敏感(出问题就想看日志),日志用于故障复盘所以一条都不能丢。

指标 平时 峰值 定档依据
吞吐 2 千条/秒 5 万条/秒 大促历史峰值 × 2
延迟 P95 < 1 秒 < 3 秒 排障体验可接受范围
丢失率 0 0 日志是唯一故障现场
积压上限 不超 1 分钟 不超 5 分钟 超过即触发降级采样

矩阵里最容易被忽视的是积压上限:很多团队只定吞吐不下限,结果高峰期管道没挂但积压半小时,等于延迟塌方。把积压上限写进矩阵,监控才有一个可以报警的阈值,而不是「看起来还能跑」。填完矩阵还可以做一次压力预演:人为制造一场「峰值 × 2」的灌入,看积压曲线与丢日志告警会不会按预期出现。

三条线之间的三处隐蔽冲突

嘴上说「各有下限」,工程上还有三处隐蔽冲突,不提前调好上线一定返工。第一处是攒批大小与延迟:批越大吞吐越高,但低谷期批迟迟不满,日志就压在内存里。第二处是WAL 与吞吐:每条多一次写,高峰期 WAL 自己就可能成为瓶颈,需要 WAL 也做批量追加。第三处是背压与采集进程:下游满了上游必须慢下来,否则内存队列无边上涨最终 OOM,但慢下来又可能影响同机业务进程。

冲突 表现 调和手段
攒批 vs 延迟 低谷期日志迟迟不可见 定时刷盘兜底
WAL vs 吞吐 WAL 写入成为新瓶颈 WAL 批量追加
背压 vs 业务 队列无边上涨或抢 CPU 有界通道 + 低优先级

这三处冲突的共同点是:它们在低流量时全部隐形。本地跑一万条日志什么问题都没有,一上峰值全部冒头。所以验收一定要在「峰值 × 2」的压力下做,而不是在 comfort zone 里做。调和技术也很直白:定时刷、批量写、有界通道,三者组合几乎能覆盖大部分冲突场景,这在后续 2.3 与 2.4 节会逐个落到代码。

日志管道与业务系统的边界

还有一条边界值得提前划清:日志管道不该成为业务的关键路径。意思是管道挂了,业务服务必须还能正常跑,只是日志暂时收不上来。这听起来像废话,但很多团队做深了之后,会在管道里加各种「实时分析后反控业务」的逻辑,不知不觉把管道做成了关键路径,管道一抖业务跟着抖。正确的姿势是:管道做单向汇入,分析结果是「事后 suger」而非「实时油门」;即便要实时告警,也走旁路订阅而不是串在主链路上。同时管道自身要有降级手段:极端高峰时按业务线分级保量,核心业务日志保量、审计类日志降采样,而不是所有日志一起死扛。这条边界与 Java 订单中心 里「订单链路不挂可观测组件」的原则一样,可对照。

边界 允许 禁止
依赖关系 业务不依赖管道存活 管道挂业务挂
分析位置 旁路订阅、事后分析 串主链路反控
高峰降级 核心日志保量、审计降采样 全量死扛到积压

边界划清后,排障责任也清了:业务出问题先查业务,管道的问题只影响「日志晚几秒到」,不会模糊故障边界。再补一句:日志管道的可用性目标不要定成「永不挂」,而要定成「挂了业务无感」——前者代价极高且做不到,后者成本可控且真正有用。

踩坑提示

最典型的是把吞吐当唯一目标:团队压测做出很高的 TPS,上线高峰确实没积压,可一次崩溃重启发现丢了十分钟日志——WAL 没做,攒批里的数据全蒸发了。排障时才发现,丢的恰好是故障发生前那段最关键的现场。三条线必须分别定验收:吞吐看峰值 TPS、延迟看 P95、不丢做一次「 kill -9 后重启」的演练,少一条上线都会塌。演练时顺手把「重启后日志能否续上」也验了——能续上说明 WAL 真的生效,不能续上说明所谓不丢只是写在了 PPT 上。

小结

吞吐靠攒批与顺序写,低延迟靠定时刷盘与短队列,不丢靠 WAL 与确认机制。三条线互相牵制,设计上先保不丢底线、再定吞吐峰值、最后收延迟,管道的骨架就稳了。把这三条写成验收指标贴在 wiki 上,后面每一章的实现都有靶子。

需求矩阵里还有一个容易被忽视的细节:指标要有明确的测量点。「延迟」是从日志产生到可检索,还是从采集到落盘?口径不同数字差好几倍。把测量点写进矩阵(例如「采集进程读到该行 → CLS 可检索到该行」),验收时就不会为口径吵架,历史数据也才可比。一张好矩阵,三分在数字、七分在口径定义。

延伸阅读