日志采集器的难点不在采集:背压、断点与投递语义
把日志、指标、链路三类数据收进同一个进程,这类统一采集器的公开材料喜欢把重点放在吞吐数字和插件数量上。从工程实现看,决定它能不能长期跑下去的是三个不太显眼的位置:位点管理、背压与丢弃策略、投递语义。它们对应的正是出事时的三个表现——丢了能不能知道、重复了谁消化、后端不可用时会不会把业务进程一起拖下去。

厂商材料里的性能倍数不适合当结论引用:测试结果跟数据形态强相关(大行还是小行、是否压缩、后端是否同机房、有没有开启端侧处理),换到自己的日志分布上就没有可比性。下面只讲机制和可验收的判据,不涉及具体倍数。
一、成本闸门:过滤和聚合该落在链路的哪一端
观测数据的成本结构很别扭:花钱的部分(传输、存储、索引)在链路后段,而”这条有没有用”往往要等出事之后才知道。调试期打开的日志不敢关,因为下一次故障可能就靠它;于是全量进后端,账单一到,再回头砍采集范围。
采集器处在这个链条的第一道闸门,能做的动作按节省幅度分档:
| 动作 | 省的是什么 | 代价 |
|---|---|---|
| 过滤(整行/整类丢弃) | 传输、存储、索引全免 | 一旦判错,明细永久没了 |
| 字段裁剪与脱敏 | 体积,条数不变 | 后端字段模板要跟着改 |
| 结构化解析 | 后端的解析计算 | 采集端 CPU,解析失败要能兜原文 |
| 预聚合与计数 | 数量级压缩 | 明细丢失,事后无法下钻 |
| 采样(按比率或按 traceId 尾采样) | 大头都在 | 需要保住”能追回整条链路”的能力 |
靠近数据源一侧执行过滤、转换、聚合,收益很直接:跨网络传输量与后端存储量都降下来。实现上通常有两类引擎,一类是编译进采集器的原生插件,一类是管道式描述语言(读—处理—写的表达式,SPL 那类)。选哪一种不重要,重要的是认清端侧处理的边界:端侧只能做无状态或少状态的判断。采集器看到的是本机流量的一份切片,凡是要求全局视野的动作——跨服务关联、按业务优先级动态调整采样率、跨实例去重——放在端侧都是错的。
还有一条纪律值得写进规范:进采集配置的过滤规则,输入必须是”已被验证为噪声”的模式,不是”看着没用”。并且被过滤掉的数据要留一份可证明其价值的证据,例如按固定比例保留一份采样明细,或者保留规则命中的样本与统计。否则过半年没人能回答”这条过滤是不是把关键信息砍了”。
二、位点不是行号:轮转、inode 复用与容器文件名
“不丢也不重”的底座是一张位点表。这张表的主键不该是文件路径。文件系统里真正标识一个文件的是(设备号,inode)这个组合,路径只是一个可以随时被改名、删除、重建的入口。
stat -c 'dev=%d inode=%i size=%s mtime=%y' /var/log/app/app.log |
排查”位点丢了”这类问题时,先用这条命令确认两个文件到底是不是同一个文件,比看路径靠谱。下面几种现场事件,是位点表必须逐条回答的。
| 现场事件 | 只按路径存位点的行为 | 应有的做法 |
|---|---|---|
按大小轮转:app.log 改名成 app.log.1,新建空 app.log |
跟随路径跳到新文件从头读,老文件最后没读完的一段永久丢失 | 以 inode 为主键,轮转历史保留到该 inode 读到 EOF 且确认投递完成,再清理 |
人工截断:> app.log 或日志级别调低后文件被清空 |
位点大于文件大小,行为未定义,有的实现会卡住不读 | 显式定义”位点 > 大小”时的动作(复位到 0 并计数告警),不要留给运气 |
| inode 复用:老文件删除后,新文件拿到同一个 inode 号 | 把新文件当成老文件续读,前一段内容直接跳过 | 位点表额外记录文件大小与首块校验(首行或前若干字节的哈希),恢复时不一致就判定为新文件,位点归零 |
容器日志:运行时按 0.log、0.log.1 轮转,Pod 重建后整个目录路径都变 |
找不到原路径,要么重复全量重读,要么当作无日志 | 位点键用”文件 inode + 容器身份(namespace/pod/uid)”组合;采集范围要覆盖软链指向的全部轮转文件;Pod 删除时清理状态 |
| 采集器自身重启 | 取决于位点提交时机(见第四节),重启会暴露成丢失或重复 | 状态落盘位置必须在容器外的卷上,写在容器可写层等于每次重建都丢位点 |
多文件并发读取时,每个文件独立推进自己的位点,不要指望跨文件有序。另外要区分三种时间:应用写日志的事件时间、采集器读到的采集时间、后端落库的接收时间。断点续传补传时,事件时间是原始的、接收时间是恢复后的,如果看板按接收时间聚合,恢复瞬间会出现一根假尖峰,很容易被当成”业务突发”。
三、背压的四个旋钮:内存上限、磁盘缓冲、限速、丢弃
背压的本质是一句话:产出快于投递时,多出来的数据必须有地方去。四个旋钮就是在决定它去哪里。
内存队列上限。 设小了反压来得快、丢得早;设大了能抗突发,代价是进程被 OOM 判杀的概率上升,而崩在内存里的那批数据因为位点尚未提交,重启后要从更早的位置重读——重复量直接跟队列深度成正比。
磁盘缓冲。 把突发写进本地磁盘的定长文件,网络恢复后按序补传。这里有四个必须显式定义的点:容量上限固定并预分配,绝不能”看磁盘还剩多少就用多少”,采集器把节点盘写满会连带影响业务容器、镜像拉取和别的租户组件;写满后的覆盖策略(丢最老的一段并计数);读写 I/O 的优先级,别让它跟业务的数据盘抢队列;缓存卷的类型,用空目录(emptyDir 落在节点本地盘)还是独立 PersistentVolume,容量与节点驱逐行为完全不同。
限速。 限速的位置决定压谁。限在读文件之前,等于主动制造积压,把压力还给文件系统——日志还在往磁盘写,风险从内存变成磁盘空间;限在发送之前,数据已经进了内存或磁盘缓冲,风险变成进程自身资源。
丢弃策略。 一个成熟的采集器要能表达”丢什么、保什么”:按输入文件丢、按标签或服务丢、按行级别丢(错误级别保留)。丢弃必须有一级指标——“被丢弃条数/字节数”要作为可告警的计数器暴露出来,而不是写在采集器自己的日志里。事故复盘时只看到”日志断了一段”和看到”某节点某文件在拥塞窗口内丢弃了多少行”,是两种完全不同的处置效率。
后端不可用(超时、限流、返回 429)时的降级顺序应该是:指数退避重试 → 转入磁盘缓冲 → 按优先级丢弃 → 暂停采集新输入并告警。这里的难点是把下游的失败信号翻译成不同强度,而不是一律重试。持续重试会在后端刚恢复的当口把它再打死,所以退避要带上限和随机抖动;对明确限流的响应,要按后端返回的额度信号降速,而不是按自己的固定阈值。
对业务的影响半径也要单独设上限:文件型采集器和业务共享页缓存与磁盘,它的 CPU 与 IO 配额要靠 cgroup 显式钉住。没有上限时,最常见的故障表现是”业务 P99 抖动”,没人会把它跟采集器关联起来。
四、至少一次语义下,重复从哪来、由谁消化
投递语义选至少一次(At-Least-Once),配合本地磁盘缓存做断点续传,等于公开承诺了”数据不该丢,但可能重复”。重复的来源有四个:
- 进程重启或崩溃,位点未提交的那一批重发。重复量取决于位点提交策略,见下段。
- 网络重试:请求已经到达后端并落库,但响应在回程丢了,采集器判为失败后重发。这类重复最难被发现,因为两条内容完全一样。
- 两个实例同时读同一份文件:DaemonSet 与 sidecar 配了同一路径、滚动升级期新旧两个 Pod 同时存活、灰度时两套采集配置都命中同一个容器。
- 位点表损坏或丢失后的全量重采,典型诱因是状态文件写在容器可写层。
位点提交时机是这里的核心取舍。入队即提交,内存与磁盘缓冲里的数据在崩溃时真丢,那是至多一次;确认后端收到才提交,重启后必然重复。至少一次选的是后者,也就是说——**采集器选了这个语义,幂等就变成消费端的责任,不能靠采集器”尽量不重”**。
去重放在哪里,实践上有三种做法:
- 采集端做内容哈希窗口判重。要为每个输入文件维护 LRU,内存开销和计算都在热路径上,除非有很明确的重复来源,否则不值。
- 采集端为每条数据附加一个天然可复现的键:文件指纹 + 该行的起始字节偏移(或”文件 + 行号”)。这个键不需要读内容就能算,写进元数据字段而不是改写原始文本。有了它,消费端的去重成本从”内容比对”降级成”主键判重”,这是最划算的一档。
- 消费端做批内去重与主键冲突丢弃;对无法引入唯一键的场景(按条计费的存储写入、以日志条数为分子的 SLO 计算),要么接受虚高,要么在入库前加一次窗口判重。
重复的下游影响经常被低估:告警规则按条数计数会误触发,指标在重复采样下会被拉偏(偏差方向取决于聚合算法),成本按写入条数计费会直接多付。所以正确的姿态是把”重复率”当成一个显式指标去测,而不是把它当成一个理论上应该没有的问题。
五、三种部署形态与采集配置的版本化
在 K8s 里,同一份采集器可以长成三种样子,资源和风险的分布差别很大。
| 维度 | DaemonSet(节点级) | Sidecar 注入(Pod 级) | 应用内 SDK(OTLP 上报本机端点) |
|---|---|---|---|
| 资源开销 | 每节点一份,总量可控 | 随 Pod 数线性增长,节点内存压力大 | 计入业务容器,进程内缓冲占应用堆 |
| 隔离性 | 节点内共享,一个采集器影响该节点全部 Pod | 按应用隔离,可单独设限额与处理逻辑 | 完全在应用进程内 |
| 配置更新 | 由控制器统一下发,改动爆炸半径大 | 跟 Pod 模板走,随发布滚动 | 跟应用代码一起发版,节奏最慢 |
| 故障影响半径 | 该节点所有容器日志同时断 | 单个应用 | 单个应用,且可能拖慢业务线程 |
| 位点归属 | 节点本地目录,重建后仍在 | 容器可写层会随 Pod 删除而丢,需挂卷 | 由应用侧发送队列承担 |
| 能看到什么 | 节点上的文件与 stdout、节点指标 | 本容器的文件、stdout、本应用专属字段 | 进程内上下文:trace 关联、业务标签 |
| 退出时的数据完整性 | 与 Pod 生命周期无关,较稳 | 需 preStop 与优雅终止窗口等它 flush | 依赖应用自己的关闭钩子 |
混合是常态:文件与 stdout 走 DaemonSet,链路与自定义指标走 SDK 上报到本机的 OTLP 端点(常见的默认监听地址是回环地址上的 4318 端口,由同 Pod 或同节点的采集器接收后转发),Prometheus 抓取的指标再另走一路。这种并行最容易出的事故是同一条日志被两条路各采一遍——SDK 打印的同时又落盘被文件采集器读走,看起来”数据全了”,实际条数翻倍。
配置该按代码管:进同一个仓库、走同一条评审流程、能回滚。很多实现把采集管道声明成集群级自定义资源(CRD),一份配置影响整个集群的匹配对象,一条规则写错的爆炸半径非常大——解析规则写坏会让整批数据被打错标签,过滤条件写反会把真正需要的行丢掉,而且这类错误在监控上往往表现为”数据量下降”,跟业务低峰难以区分。灰度维度按集群 → 节点 → 应用逐级放;改动尽量走”先双写再切读”,新旧两套配置并行采一段,比对条数与字段后再停旧那套;回滚方案要专门写一条:配置回滚是否连带位点回滚。位点一旦回退,恢复后立刻产生大批重复,这是回滚演练里最常漏的一项。
六、洹拓视角:上线前的八项验收
自建还是选型,判断依据都落在这张清单上。我们内部把它当作采集类组件的准入门槛,一条答不上来就不能进生产:
- 端到端延迟:从应用写出到后端可查的分位延迟,正常态与补传态分开测。断网十分钟后恢复,看积压多久清空。
- 丢失的可度量办法:采集端计数与后端落库计数对账,差值要能被第 3 项拆开解释,”被丢弃计数”必须是一级可告警指标。
- 重复的可度量办法:用文件指纹 + 偏移这类天然键做抽样判重,估出重复率;每次滚动升级与重启演练后必测。
- 故障演练覆盖四个场景:短断网、长断网(触发磁盘缓冲上限)、后端限流、后端恢复瞬间的重试风暴。分别看资源曲线、积压落在哪、恢复耗时。
- 单实例资源上限:CPU、内存、磁盘 IO 的硬上限与实测值写进运维手册,并做一次日志风暴压测——把某个测试服务的级别调到 DEBUG 狂刷,看谁先出问题。
- 位点与状态的持久化位置:写在容器可写层算不合格;清理策略要能防止节点上残留已删除 Pod 的状态。
- 时间戳语义与看板口径一致:事件时间、采集时间、接收时间三选一写进规范,确认补传时展示行为不产生假尖峰。
- 配置变更流程:有无灰度维度、有无双写比对、回滚是否连带位点,三者都要能演示而不是文档里写一句”可回滚”。
结论其实很朴素:把”能不能采到”当次要问题,把”丢了知不知道、重复了怎么处理、后端挂了会不会压垮应用”当主要问题。前者是功能清单,一次演示就能验证;后者要在事故里才能验证,而事故不给你第二次机会。洹拓科技在几个交付项目里吃过这个亏——采集上线时只测了”有没有数据”,等真正出事,才发现看板上那条断口对应的是一整段无法找回的明细。