Asher Wang

Kubernetes 中的日志收集

容器化、Kubernetes 这类技术极大地提高了服务的发布、资源管理的效率,但也为一些服务运行中的基本需求提出了挑战,日志收集就是其中不可或缺的一环,本文介绍了一种基于 Sidecar 的方案,并主要介绍了遇到的问题以及解决方案。

Overview

关于在 Kubernetes 中收集日志,官方的 Logging Architecture 此文档提出的几种方案很值得参考,本文的方案也基于其 Streaming sidecar container 一节,推荐阅读。

20250218173759119

整体架构如上图所示,核心为 Pod 中的 Fluentbit Sidecar,它和 App Container 共同 mount 了一个 emptyDir 类型的 volume,App 负责将日志以格式化的方式输出到 emptyDir 以及 logrotate,Fluentbit 负责读取日志,并将其转发到集群中部署的 Fluentbit,然后统一由此 Fluentbit 转发到 ElastricSearch 存储。

这里选择 emptyDir 直接使用 Node 文件系统而非 PersistentVolume 是为了充分利用 Node 上的磁盘空间,同时强制为其配置 sizeLimit,保证其异常写大量日志时被驱逐,一次保证 Node 的磁盘空间可用,sizeLimit 的大小可以参考集群容量规划时单个 Node 上的 Pod 数和磁盘容量来制定。

理论上 Fluentbit Sidecar 可以直接读取日志然后转发到远端存储,但为什么这里还额外增加了一层 Fluentbit Agent 来统一接收集群中的日志,然后转发出去呢?原因主要是为了方便维护:首先如果每个 Fluentbit Sidecar 都直接转发日志到远端存储,因为涉及到跨网络传输,可能经常会因为网络波动而转发失败,此时会产生大量的错误报警(Fluentbit 需接 Prometheus 等监控),而将所有的日志都转发到集群内的 Fluentbit 可以显著减少异常。其次如果需要对远端存储做修改,比如修改日志格式、远端地址变更等,直接修改 Fluentbit Agent 服务然后整个集群就都可以无缝切换到新的配置了,无需重新修改每个 Sidecar 的配置了。

对于 Fluentbit 的可观测性(监控),可以参考此文档,接入之后便可对整个集群中每个服务的日志量有把握了,可以设置报警规则对异常日志的服务进行通知。

Problems

Update:Kubernetes 自从 v1.28 后正式支持了 sidecar container,使用了 Sidecar Containers 之后下文的两个问题都一并得到了解决,推荐使用此方案。但是如果暂时未能升级 >=v1.28 或使用云厂商版本无法开启此功能,下文内容仍然有参考价值。

方案本身很直观,但如果按照此方案实施并不会一帆风顺,存在以下一些问题需要解决。

1. App 和 Sidecar 的退出顺序

关于 Pod 在退出时的机制可以参看此文档,简单来说 kubelet 会先运行 preStop hook 的命令,然后给 Pod 中的每个 Container 发送 SIGTERM。当 App Container 新版本发布的时候,旧版本里的 App Container 和 Sidecar Container 都需要退出,但如果 Sidecar 先退出了,App Container 的日志就会存在未被收集进而丢失的问题,因此需要保证 Sidecar 一定要在 App 之后退出。

首先 Fluentbit 如何知道 App 已不再产生日志了呢?可以从监控指标入手,Fluentbit 有 fluentbit_input_records_total 这个指标,表示了 Fluentbit 总共读取的日志量,如果此指标在某个时间范围内不再变化了,就可以确定 App 不再产生日志了。

其次刚才提到的 Fluentbit 探测 App 是否产生新日志的逻辑,不可以在 preStop 阶段运行,因为 kubelet 会在 terminationGracePeriodSeconds 后给每个 Container 发送 TERM 信号,如果此时 App 仍然在产生日志,那 Fluentbit 仍然会提前退出。所以需要将 Fluentbit 检查 App 是否产生新日志的逻辑延迟到接收到 TERM 信号时。这就需要修改 Fluentbit 的启动命令,在其启动时 trap TERM 信号,然后执行检查逻辑最后退出。

 1# prestop.sh check app container log status
 2#!/bin/bash
 3sleep 5
 4preRecords=$(curl -s http://127.0.0.1:2020/api/v1/metrics/prometheus | grep 'fluentbit_input_records_total{name="tail.0"}' | awk '{print $2}')
 5while true
 6do
 7    sleep 10  # check every 10s
 8    records=$(curl -s http://127.0.0.1:2020/api/v1/metrics/prometheus | grep 'fluentbit_input_records_total{name="tail.0"}' | awk '{print $2}')
 9    if [ "$preRecords" == "$records" ]; then
10        break
11    fi
12    preRecords=$records
13done
14
15# startup.sh trap SIGTERM and SIGKILL
16#!/bin/bash
17# before singalterm fluent-bit, do prestop checking
18term_handler() {
19  if [ $pid -ne 0 ]; then
20    echo "enter prestop checking!"
21    prestop.sh
22    echo "pass prestop checking!"
23    kill -SIGTERM "$pid"
24    wait "$pid"
25  fi
26  exit ${?};
27}
28
29kill_handler() {
30  if [ $pid -ne 0 ]; then
31    kill -SIGKILL "$pid"
32    wait "$pid"
33  fi
34  exit ${?};
35}
36
37trap 'kill ${!}; term_handler' SIGTERM
38trap 'kill ${!}; kill_handler' SIGKILL
39fluent-bit -c "config.conf" &
40pid="$!"
41
42# wait forever
43while true
44do
45  tail -f /dev/null & wait ${!}
46done

通过修改 Fluentbit 的启动命令为该脚本,可以保证 Fluentbit 在 App Container 不再产生日志后再退出,从而不会漏收集日志。

2. Job 不退出

如果使用 Job 这种资源的话,又会遇到另一个问题,此时 App Container 是一个会自己退出的 Process,但 Fluentbit 只有在收到 TERM 信号后才会退出,这就导致 Job 的状态是只有 Pod 中所有的 Container 都成功退出后才 Complete,这就导致 App Container 退出后,Job 状态仍然为 Running 但其中 Container 为 1/2 不会正常结束。针对此问题的讨论可以参看此处

对于此问题可以使用 k8s-controller-sidecars 这个项目,它会在检测到 App Container 结束时给 Sidecar 发送一个 TERM 信号让其正常退出。

A Better Solution (?)

20250218193315210

通过阅读此文应该也感觉到这种方案并不如看起来那么简单,其中涉及的细节问题也比较多,也许在 Logging Architecture 文中一开始提到的,App Container 直接将日志输出到 Stdout 和 Stderr,部署一个日志收集的 DaemonSet,在每个 Node 上收集日志,而 logrotate 也交由 Kubernetes 本身的机制完成的方案更加简单可靠。

除此之外这种方案同时也会节省一些磁盘空间,我之前提到的方案中,App 可能会同时开启 Stdout(为了使用 kubectl logs 命令来方便查看实时日志)和磁盘写,这就会有两份日志,而这种方案则只有 Stdout/Stderr 一份日志。

Fin

这么看来自己前文一大堆似乎无甚用处,但前文关于第 2 点 Job 不退出的 Case 同样也可以用在解决其他 Sidecar 的退出问题(不推荐,参见前文 Update)中,其余的就权当记录给大家一些启发了。至于我更推荐的 “Better Solution” 也并未实际部署过,如果遇到什么问题并有解决方案也可分享于我,互相交流。

#logging #kubernetes