广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

建议收藏:Linkerd 日志收集方案 | 自动化全链路

我们踩坑多次,发现Linkerd在日志收集上的设计其实挺反直觉的。它默认不收集任何日志,只通过环境变量与sidecar配置控制。如果你真的想用Linkerd做全链路日志收集,得自己搞定日志转发,不能指望它自动帮你。我们折腾过Kubernetes的LogExtraction、Fluentd,也用过Grafana Loki,但真实落地的时候发

建议收藏:Linkerd 日志收集方案 | 自动化全链路
配图来源于网络和AI生成,仅供参考。
▌ 技术引导
我们踩坑多次,发现Linkerd在日志收集上的设计其实挺反直觉的。它默认不收集任何日志,只通过环境变量与sidecar配置控制。如果你真的想用Linkerd做全链路日志收集,得自己搞定日志转发,不能指望它自动帮你。我们折腾过Kubernetes的LogExtraction、Fluentd,也用过Grafana Loki,但真实落地的时候发现配置断点特别多,尤其是sidecar的sidecar-env配置和日志格式处理。最致命的是,很多生产环境的微服务没开标准日志输出,导致Linkerd根本抓不到任何数据。而且Linkerd的默认日志打点策略非常保守,比如只记录HTTP请求的入口和出口,不会记录中间的代理行为,这在调试时真的会让人抓狂。最后我们决定用更直接的方式,把所有日志通过标准输出打到一个统一的输出端口,再用一个通用的日志代理去收集,这样的方式反而更稳定,也更容易维护。

▌ 技术参考

一 Linkerd日志收集的核心机制基于sidecar代理的插件系统,它的日志能力天生受限。默认情况下,Linkerd不会收集任何日志,除非你手动配置sidecar-env中的LOG_FORMAT和LOG_LEVEL参数。这些参数决定了日志输出的格式和级别,但它们只影响Linkerd自身的日志输出,对于服务本身的日志是无能为力的。如果你希望捕获服务的stdout和stderr,必须额外配置日志转发机制。比如在Kubernetes中,可以通过设置容器的logOptions中的"max-size"和"max-files"来控制日志保留策略,但这些设置对Linkerd本身没有意义。我们真正踩到的那个坑是,很多服务在启动时没有正确打开日志输出,导致Linkerd根本无法抓到任何数据。

二 要想在Linkerd中实现全链路日志收集,必须使用额外的日志代理。常见的做法是把所有服务的日志导向一个共同的输出端口,比如通过docker的日志驱动配置,或者在Kubernetes中使用标准的logging sidecar。比如在Docker中,可以通过--log-driver=json-file和--log-opt flags来指定日志格式和路径,这可能是个可选的方案。不过在Kubernetes中,更推荐使用sidecar的方式,比如在Deployment中加入一个sidecar容器,用来监听主容器的标准输出,并进行格式化后再写入统一的存储。这个方案的关键在于确保主容器的日志输出格式是可控的,比如JSON格式,这样sidecar才能准确解析和转发。我们曾在测试环境中因为服务没有用标准日志格式导致日志代理根本无法处理数据,这是个大坑。

三 Linkerd的sidecar代理本身支持日志插件,但默认没有启用。你得在部署Linkerd时显式地开启日志插件。比如在helm部署时,需要在values.yaml中设置sidecar.env.LOG_FORMAT和sidecar.env.LOG_LEVEL,这两个参数控制Linkerd自身的日志输出方式。但是,如果你希望收集服务本身的日志,就得借助外部工具。比如在Kubernetes中,你可以通过设置KUBE_LOGS_FORMAT=json来启用JSON日志格式,然后让Linkerd的sidecar使用lindb插件来收集这些日志。不过我们发现,这种方案在实际中存在很大的性能损耗,尤其是在高并发场景下,日志转发容易导致延迟,甚至被丢弃。最终我们决定用Fluentd来做日志聚合,因为它对日志格式的兼容性更强,而且性能更稳定。

四 如果使用Fluentd作为日志收集中间件,那么在Linkerd的sidecar中需要配置Fluentd的插件。比如在Linkerd的sidecar-env中,可以设置LOG_FORMAT=json,这样Fluentd就能正确解析日志内容。然后将日志转发到一个专用的Fluentd Pod中,这个Pod负责收集并转发到Loki或者Elasticsearch。Fluentd的配置需要特别注意,比如在match块中设置tag和output_type为forward,这样就不会出现日志丢失的情况。另外,在Fluentd的输出配置中,要确保端口和地址正确,否则日志就会卡在Fluentd这一层。我们遇到过一次,因为Fluentd的端口被防火墙屏蔽,导致Linkerd无法将日志转发出去,这真是个送分题。

五 在实际部署中,我们发现Linkerd的sidecar日志支持非常有限,尤其是在处理非标准日志格式时,需要自己写插件。比如如果你的服务使用的是log4j或者syslog格式,Linkerd的sidecar可能无法正确解析。这时候就需要在Fluentd中配置相应的parse规则,或者在Kubernetes中使用custom logging driver来统一日志格式。我们曾用过一个叫做logstash的中间件,把所有的日志统一处理成JSON格式,再通过Kubernetes的logging system重定向到一个Loggregator,这样反而更可靠。但logstash的资源消耗很高,尤其在高吞吐场景下,容易成为瓶颈。

六 Linkerd的日志转发在Kubernetes中有一个重要的限制,就是它只能将日志发送到特定的端点,比如某个Fluentd Pod的端口。这意味着你需要在Linkerd的sidecar中显式配置输出地址和端口。比如在Linkerd的ConfigMap中设置linkerd-proxy.env.LOG_ENDPOINT="http://fluentd:24224",这样sidecar就知道把日志发送到哪里。不过这个配置需要和Kubernetes的logging system协同工作,否则可能会出现日志无法到达的情况。我们曾经因为Kubernetes的logging system没有正确配置,导致日志一直堆积在sidecar中,最终不得不手动清理。这说明Linkerd的日志收集机制并不是一个完全自动化的系统,还需要配合其他组件。

七 在高并发场景下,Linkerd的日志性能确实是个问题。我们做过一个压测,当服务每秒处理超过5000个请求时,Linkerd的日志转发就会出现明显的延迟。这时候,我们发现必须调整sidecar的缓冲策略。比如在sidecar-env中增加LOG_BUFFER_SIZE=1024,这样可以提升日志处理能力。但缓冲太大也会导致日志丢失,所以需要在配置中设置合理的LOG_FLUSH_INTERVAL=5000。另外,我们还发现,使用JSON格式的日志反而比文本日志更消耗资源,因为解析成本更高。所以最终我们决定用compact的text格式,并在Fluentd中做额外的格式化处理,这样既保证了可读性,又降低了资源消耗。

八 适用场景上,Linkerd日志收集方案更适合那些已经基于Linkerd进行服务间通信的系统,并且对服务日志的格式有统一要求。比如在混合使用Linkerd和Istio的环境中,可以考虑将日志统一通过Linkerd的sidecar进行转发。不过这种方案在Kubernetes中存在一些局限,尤其是在服务的日志格式不一致时,需要额外的处理。我们曾在一个项目中因为日志格式不一致,导致多个Fluentd实例无法正确处理日志,最后不得不采用一个统一的日志代理方案。此外,Linkerd的日志收集能力无法覆盖所有的调试需求,尤其是需要捕获完整请求链路时,还是得依赖其他工具。

九 另一个常见的踩坑点是,Linkerd的日志插件在某些版本中存在兼容性问题。比如在我们用Linkerd 2.11.0时,发现日志插件无法正确解析某些服务的traceID字段,导致日志无法被正确归类。这时候需要手动在sidecar-env中配置额外的环境变量,比如设置LOG_EXTRACTION_HEADERS="trace-id",这样日志就可以包含traceID。不过这个配置在某些版本中可能不支持,所以我们必须在部署前测试。还有一次,我们在使用Fluentd时,发现日志被错误地归类到某个无效的日志路径下,后来才发现是Fluentd的match规则配置有误,导致日志没有被正确转发。

十 如果你不想用Fluentd,也可以考虑用Loki作为日志存储。Linkerd的sidecar可以直接配置Loki作为输出目标,比如在sidecar-env中设置LOG_ENDPOINT="http://loki:3100/loki/api/v1/push"。不过Loki的性能在高并发下比较脆弱,尤其是在没有使用Grafana Loki的高效日志处理模式时,很容易出现日志堆积。我们曾尝试直接把日志写入Loki,结果发现延迟很高,最终不得不换用Loki的流式处理模式,配合Tailer和Babel。此外,Loki的日志解析能力也有限,尤其是对于复杂格式的日志,可能需要在Linkerd的sidecar中做额外的预处理,或者在Loki的接收端配置正确的解析规则。

十一 如果你希望更细粒度地控制日志行为,可以在Linkerd的sidecar中配置log-level参数。比如设置LOG_LEVEL=debug,这样就能看到更多底层的行为信息。不过这个参数在某些场景下会导致日志量激增,甚至让sidecar变成性能瓶颈。我们曾在调试一个分布式事务错误时,把日志级别调成debug,结果发现sidecar的CPU和内存使用率飙升,最终不得不调整回来。此外,Linkerd的日志插件无法自动处理所有的日志字段,比如traceID、spanID等,必须在Kubernetes的配置中手动设置,否则这些字段就可能丢失。

十二 在日志存储方面,我们曾经尝试将日志直接写入本地文件,然后通过Kubernetes的LogCollector进行收集。但这种方法在高并发下表现极差,因为本地文件的写入速度无法跟上请求量。最终我们换用了Loki,但配置时遇到了一些坑,比如需要在Loki的配置中设置正确的接收端口和认证方式。我们还发现,Loki的标签系统在日志归类上非常灵活,但配置不当会导致标签混乱。为此,我们手动在每个Pod的metadata中设置了标签,比如在Deployment的metadata中添加labels: app: myapp,这样在Loki中就能根据标签快速过滤日志。

十三 Linkerd的sidecar日志转发功能对服务的启动顺序非常敏感。如果服务启动缓慢,或者日志输出速度跟不上,就会导致日志丢失。我们曾遇到一次,服务启动时日志堆积,最终在sidecar中出现了大量未处理的日志。后来我们调整了sidecar的缓冲策略,增加了LOG_BUFFER_SIZE=8192,并在Kubernetes中设置了一个日志收集的Pod作为中间层,这样就能保证日志不会丢失。此外,Kubernetes的logging system也需要合理配置,比如使用Multiline模式来处理日志。

十四 如果你想用Linkerd做日志收集,还需要考虑日志的存储和查询。比如在Loki中,可以使用日志的流式处理模式,这样就能保证日志的实时性。不过Loki的查询性能在大数据量下会明显下降,这时候就需要用到Grafana Loki的索引优化策略。我们曾使用过一个叫做logql的查询工具,但发现它对复杂查询的支持有限,最终还是回归到使用Grafana Loki自身的查询界面。另外,日志存储的成本也是一个重要因素,尤其是当日志量非常大时,纯基于Loki的日志存储方案可能会超出预算。

十五 如果你对Linkerd的日志处理不太满意,可以考虑用Linkerd的监控能力来补充。比如通过Linkerd的metrics导出器,可以收集到服务的请求量、响应时间等指标,但这些指标无法代替日志。我们曾经用Linkerd的监控功能来辅助调试,但最终还是得用日志来定位具体的问题。所以,Linkerd的日志处理方案更像是一个辅助工具,而不是一个完整的解决方案。在这种情况下,我们更倾向于使用Fluentd作为日志收集中间件,再结合Loki做存储,这样既能保证日志的完整性,又能在查询时有更好的体验。