文章

应用日志采集:数据的位置决定采集方式

同样是接一个新服务的日志,为什么有的十分钟就能配好采集,有的却要单独写一套逻辑?答案不在规范,在数据本身落在哪、写成什么格式。这篇按数据位置拆开讲,每种位置配一个具体的例子。

应用日志采集:数据的位置决定采集方式

上一篇讲了集群侧的采集架构,解决的是”数据怎么搬”。这篇往前退一步:搬运方式是被数据落在哪这一点定死的,跟规范写得好不好没关系。

一、先看全景

日志落地的位置,其实只有四种:

位置谁去读
stdoutOTel Collector(DaemonSet)
PVC 上的文件sidecar 里的 fluent-bit
中间件自己的文件中间件自带的转发能力,或专门的采集配置
不落盘,直接走网络没人读,是应用自己推过去的

四种位置分别展开讲,每种配一个具体的例子。

二、单行还是多行

不管数据落在 stdout 还是文件,底层读取方式都是按行读的,隐含前提是一行等于一条记录。这个前提能不能成立,取决于日志写成单行还是多行。

一条异常堆栈默认是多行的:

1
2
3
2026-08-18T10:00:00 ERROR order failed
	at OrderService.create(OrderService.java:42)
	at OrderController.submit(OrderController.java:18)

按行读的采集器会把这三行拆成三条毫无关系的记录,谁都拼不回去。

能自己控制格式的场景——业务代码自己打的日志——直接改成单行 JSON,堆栈整个塞进一个字段:

1
{"time":"2026-08-18T10:00:00","level":"ERROR","msg":"order failed","stack":"at OrderService...\nat OrderController..."}

一行文本,一条记录,采集端不用猜,这是首选做法。

改不了格式的场景也存在,比如某些第三方组件或者遗留系统,日志格式是它内置写死的,异常照样按多行输出。这时候只能退一步,在采集端配多行合并规则:用一个正则匹配”新记录的开头长什么样”,比如 ^\d{4}-\d{2}-\d{2}(匹配时间戳开头),凡是不匹配这个正则的行,都并入上一条。这个正则本质是在猜,堆栈里偶尔出现一行凑巧长得像时间戳,合并就会拼错,而且采集端要为等这些后续行多维护一份状态,比直接读单行慢。

所以结论很直白:能改成单行 JSON 的地方,一次性改掉,不留隐患;改不了的地方,多行合并规则只是退而求其次的兜底,不是首选方案。

三、stdout:Collector 自己记着读到哪

这是最常见的位置。业务代码打的日志直接写 stdout,容器运行时把它落到宿主机上的文件里。

采集用 OTel Collector 的 filelog receiver,本质是 tail -f 这么一个动作,按 DaemonSet 部署,每个节点一份。它会记一个 checkpoint,比如某个文件读到了第 4096 字节,下次接着从 4096 读。Collector 容器本身重启也不影响,因为读到哪儿这件事,是它自己管的,不需要应用或者平台额外做什么。

应用侧要做的事就一件:日志写成第二节说的那种一行 JSON,字段命名在服务之间统一——traceId 不能一个服务叫 trace_id,一个塞进 extra 里,不然查的时候对不上。

四、PVC 上的文件:续传要自己挂对地方

有些日志不写 stdout,是应用自己写到了挂载的 PVC 文件里。stdout 那套 DaemonSet Collector 的 Pod 没挂这个 PVC,够不到里面的文件,只能在业务 Pod 里加一个 sidecar,跑 fluent-bit 去 tail 它。

fluent-bit 也记读到哪儿了,记在一个叫 position db 的 sqlite 文件里。假设它读到了第 8192 字节,这时候 sidecar 重启一次——不管是 Pod 重建还是 fluent-bit 自己挂了——如果这个 db 文件只在容器的可写层里,重启后它就不知道自己读到过 8192 了,会从第 0 字节重新开始读。后果是这 8192 字节的内容在 Loki 里出现了两次。这种问题不报错、不丢数据,只是重复,日常巡检根本看不出来。

解决办法很直接:把 position db 的路径单独挂一块持久化存储,跟日志文件本身共用同一个 PVC 的另一个子路径就行。重启后 fluent-bit 先读这个 db,看到”上次到 8192”,接着往下读。

五、中间件自己的文件:只能适配

MySQL 的慢查询日志默认写在 /var/lib/mysql/slow.log 这样的路径,格式是 MySQL 自己定的,业务团队改不了,甚至连要不要开启这个日志都得看 MySQL 的配置项。

这一类的采集方式看中间件自己给了什么接口:有的支持直接转发出去,没有的就得靠一个能访问到这个路径的 sidecar 去读。没有统一做法,见一个中间件配一个中间件,成本提前接受,不用纠结能不能优化,因为决定权本来就不在自己手上。

六、不落盘,直接走网络

应用内嵌 OTLP log exporter,日志生成后直接推给 Gateway,中间没有文件这一环。

好处是省掉了前面两节那些续传的麻烦事;代价是如果 Gateway 那一刻恰好不可用,这条日志能不能保住,全看 exporter 有没有做重试和本地缓冲——比如配了重试 3 次、本地缓冲队列 1000 条,超过这个量还是照丢不误。文件路径的日志好歹”数据还在磁盘上,等采集器恢复了还能补采”,网络直推没有这层兜底。

七、一句话规则

先问一句:这份日志落没落地。落地的,交给对应的东西去读(stdout 交给 Collector,PVC 交给 sidecar 自己管续传,中间件文件按它自己的接口来);没落地、直接走网络的,别管采集,去看 SDK 的重试和缓冲配置够不够,那才是真正决定丢不丢数据的地方。

本文由作者按照 CC BY 4.0 进行授权