小Cの已经记不起来的博客

微服务架构下 日志采集 的设计与权衡

上周排查一个超时问题,从网关到下游一共跨了五个服务,我开着五个终端窗口挨个 kubectl logs + grep,grep 完了还得自己对时间线,来回对了一个多小时才把整条链路的日志拼起来。拼完那一刻我就想,这事不能再这么干了,日志采集这块是时候好好收拾一下了。

趁记忆还热乎,把这次重构里做的几个决策和对应的权衡记下来,省得下次再纠结一遍。

先统一输出,再谈采集

很多日志采集的乱象,根子其实不在采集器,而在应用自己。有的服务往 stdout 打,有的往 /var/log/app/xxx.log 打,还有个老服务往本地打完之后,居然又起了个定时任务往 S3 传一份……先别急着上什么 ELK,第一步是把日志的出口收拢。

我的建议是按 12-Factor 那套来:应用只管往 stdout/stderr 写,不落盘、不自己轮转。理由很简单,容器一重启本地文件就没了,而且一旦落盘,你就得操心磁盘满了怎么办、轮转策略怎么配,这些都是和业务无关的破事。另外趁着统一输出的机会,把 trace_id 也塞进日志格式里,不然跨服务的日志永远只能靠人肉对时间戳。

当然现实不会那么美好,遗留系统改不动输出的情况太多了。我的折中方案是:新服务一律 stdout,老服务先写文件,采集端按文件方式接进来,后续慢慢迁。别想着一口吃成胖子,改日志输出这种事,排在需求后面就是排到天荒地老。

采集方式:DaemonSet、Sidecar 还是应用直发

Kubernetes 下常见的三种姿势,各有利弊,我只说我最后的选择和原因。

DaemonSet:每个节点跑一个采集器,挂载 /var/log 和 /var/lib/docker/containers,把节点上所有容器的日志都收走。优点是资源占用可控,应用完全无感知;缺点是权限给得比较宽,而且一个节点就一个进程,采集器挂了就是一整个节点的日志断流。

Sidecar:每个 Pod 蹭一个采集容器,隔离性好,还能按应用做不同的解析配置。但资源开销是乘以副本数的,我们这种几十个服务、上百个副本的规模,光采集器就能多吃掉不少内存,想想就肉疼。

应用直发:应用通过 logback 的 appender 直接往 Kafka 推。好处是链路少一跳,还能做异步缓冲;坏处是和业务代码绑死了,日志丢了都不知道该找谁。

我最后选的是 DaemonSet + Fluent Bit,理由很俗:便宜,够用。

采集器选型,我踩过的坑

Filebeat 我用了两年,功能确实全,但内存是真的能吃,节点上 Pod 一多、日志量一上来,动不动就 OOM,调了半天 queue.mem 也只是缓解。后来换 Fluent Bit,默认配置下内存占用只有 Filebeat 的零头,kube.* 这套容器日志路径规则也是原生支持的,换过来基本没费什么事。

不过 Fluent Bit 也有让我骂街的地方,多行合并(Java 那种几十行的堆栈)的配置前后改了好几版语法,网上搜到的文章一半是旧写法,照抄必挂。我现在的配置长这样:

[FILTER]
    Name                  multiline
    Match                 kube.*
    multiline.key_content log
    multiline.parser      java

顺带一提,堆栈这种多行日志一定要在采集端合并,不然进了存储之后一条异常被拆成二十几条,查起来能把人逼疯。

存储这关:ES 还是 Loki

这是整个链路里最大的一个权衡点,也是我纠结最久的。

Elasticsearch 查询能力强,全文检索随便造,但成本摆在那:副本得留,JVM 堆得给够,磁盘更是无底洞。Loki 的思路是只索引标签、不索引全文,存储便宜得多,接个对象存储就完事了。代价是查询时必须先锁定标签(比如 namespace、pod),然后对命中的 chunk 做暴力过滤,一旦查询范围撒大了,慢得你想砸键盘。

我的结论是看日志的“体温”:近 7 天的高频查询日志放 ES,历史日志压缩后丢对象存储,要查再说。全量、全期、高性能,这三样在预算面前只能挑两样,认清这一点之后反而不纠结了。

几个不算 advice 的 advice

写在最后

回头看,日志采集这事没什么黑科技,全是一地鸡毛式的权衡:成本和查询能力、实时性和稳定性、全量和采样。我的建议只有一个:从最简单的方案开始(stdout + DaemonSet + 单一后端),先把链路跑通、把监控补上,等真的痛了再升级。别一上来就照着大厂的架构图画,图上的每一层都是钱和人堆出来的……

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放