3.6. 集群日志系统:EFK 与 Fluent Bit

kubectl logs 只能看当下,Pod 一删日志就散在节点磁盘里。要把全集群日志变成可检索、可聚合、可保留的数据,需要一条「采集 → 传输 → 存储 → 查询」的链路。本章用 Elasticsearch + Fluent Bit + Kibana 搭出这条链路,讲清每个 values 键在做什么,再与 Loki 做一次选型对照。

层级:

L3 · 平台

难度:

进阶

预计:

90 分钟

前置:

Helm 基础、PV/PVC 与日志采集模型

3.6.1. 你将学到

  • Kubernetes 日志从 CRI 文件到后端索引的完整路径,以及每一跳的边界。

  • EFK 各组件的角色划分:Fluent Bit 采集、Elasticsearch 存储与检索、Kibana 查询。

  • book-repo 中 elasticsearch、kibana、fluent-bit 三份 values 的关键键位与坑。

  • 索引命名、Kibana 检索与 EFK/Loki 的选型依据。

3.6.2. 为什么需要它

容器日志有三个绕不开的问题:Pod 是临时的,重建后旧日志随节点文件清理;日志分散在每个节点,不可能逐台 SSH;排障要按 namespace、Pod、关键字、状态码组合检索,grep 撑不住。指标能告诉你「错误率涨了」,只有日志能告诉你「哪个请求、什么堆栈」。EFK 是最经典的组合——Fluent Bit 采集、Elasticsearch 建倒排索引、Kibana 提供检索界面,代价是资源与运维成本都高于 Loki。

3.6.3. 核心概念

3.6.3.1. 日志采集模型

容器运行时把 stdout/stderr 写成 CRI 日志文件,路径形如 /var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log;/var/log/containers/ 下还有带 Pod、命名空间、容器名的符号链接,采集器用通配符就能发现全部容器。kubectl logs 由 kubelet 直接读这些文件,不经过 API Server 存储,所以 Pod 删除后日志通常就没了。

生产做法是在每个节点跑一个 DaemonSet:用 hostPath 挂载 /var/log/containers 与 /var/log/pods,通过 API 补充 Pod 元数据,给每行打上 namespace、pod、container、node 标签后发往后端。kubelet 默认按 containerLogMaxSize=10Mi、containerLogMaxFiles=5 轮转,采集器必须能处理文件被截断或重命名。

3.6.3.2. EFK 架构

三段职责:采集端是 Fluent Bit DaemonSet,负责 tail、解析、补元数据、输出;存储端是 Elasticsearch,按角色分为 master、coordinating、data、ingest;查询端是 Kibana,连 coordinating 节点的 9200 端口。

架构图

图:Pod 日志与 kubelet 日志在节点侧汇入 Fluent Bit,coordinating 只做路由与聚合,数据落在 data 节点,Kibana 通过 9200 检索。

原书用 bitnami 的 elasticsearch Chart,release 名为 es,因此 Service 是 es-elasticsearch-coordinating-only(fullname 规则为 <release>-<chart>)。Fluent Bit 与 Kibana 都指向这个名字,换 release 名要同步改两处 values。

3.6.3.3. values 里的关键配置

Elasticsearch 的 values 决定拓扑与容量,最该看懂的是这几项:

键

原书值

含义与判断

master.replicas

2

准主节点。偶数不满足多数派,一台故障就选不出 master,生产用 3

coordinating.replicas

2

客户端入口,Service 端口 9200,不存数据

data.replicas / size

2 / 10Gi

存分片与副本,storageClass: longhorn 动态供给

heapSize

512m / 128m / 2048m

JVM 堆,建议不超过容器 limit 的 50%,留出堆外内存

ingest.enabled

false

需要 ingest pipeline 时才开

metrics.enabled

true

用 Pod 注解暴露指标;有 Operator 时改开 serviceMonitor

Kibana 的 values 只有三件事值得记:persistence.storageClass: longhorn、ingress.hosts[0].name: kibana.ilinux.io(class 为 nginx),以及 elasticsearch.hosts 指向 es-elasticsearch-coordinating-only.logging.svc.cluster.local.、端口 9200。末尾那个点是绝对 FQDN 写法,能解析,但与其他清单风格不一致,排错时容易被误认为域名写错。

Fluent Bit 的 values 全部是「一段段配置文本」,拼进 ConfigMap 后成为 fluent-bit.conf:

fluent-bit-values.yaml(节选)
kind: DaemonSet
config:
  inputs: |
    [INPUT]
        Name tail
        Path /var/log/containers/*.log
        Parser cri
        Tag kube.*
        Mem_Buf_Limit 5MB
        Skip_Long_Lines On
  filters: |
    [FILTER]
        Name kubernetes
        Match kube.*
        Merge_Log On
        Keep_Log Off
        K8S-Logging.Exclude On
  outputs: |
    [OUTPUT]
        Name es
        Match kube.*
        Host es-elasticsearch-coordinating-only.logging.svc.cluster.local
        Logstash_Format On
        Logstash_Prefix k8s-cluster
        Replace_Dots On

几个键的实际作用:Parser cri 剥掉 CRI 头(时间戳 stdout F)并取到正确时间;原书用的是 Parser docker,在 containerd 集群上时间解析会失败,同目录的 fluent-fluent-bit-with-parsers-values.yaml 才是改好的版本。Merge_Log On 把容器输出的 JSON 展开成字段;Mem_Buf_Limit 5MB 与 Skip_Long_Lines On 用丢超长行换内存安全;Logstash_Format On + Logstash_Prefix 让索引按天滚成 k8s-cluster-2026.11.08;Replace_Dots On 把字段名里的点换成下划线,否则 ES 会把 kubernetes.pod_name 当成嵌套对象路径。

备注

Type 在 ES 7 之后无效 原书 outputs 里写了 Type flb_type,那是 ES 6 的 mapping type。ES 7 起类型被移除,这个键不再生效,字段映射要交给 index template 或 component template 管理。

3.6.3.4. 索引与检索

日志按天分索引,Kibana 里建 k8s-cluster-* 的 index pattern 才能检索,时间字段选 @timestamp。分索引的好处是生命周期管理简单:ILM 按「热 → 温 → 删」滚动,删除即删整个索引,比在大索引里 delete by query 便宜。代价是索引数随天数线性增长,分片要提前规划:单分片 10–50GB 是常见区间,日增量小就该用 rollover 而不是一天一个索引。

kubectl -n logging port-forward svc/es-elasticsearch-coordinating-only 9200:9200
curl -s 'http://127.0.0.1:9200/_cat/indices/k8s-cluster-*?v&h=index,docs.count,store.size'

3.6.3.5. EFK 与 Loki 的选型

维度

EFK

Loki

索引方式

倒排索引,全文可查

只索引标签,正文压缩

存储成本

高,通常 3–10 倍

低

查询能力

全文检索、聚合、KQL/Lucene

LogQL 标签定位 + 行过滤

资源占用

JVM 堆 + 分片,节点要求高

轻量,后端可用对象存储

运维复杂度

高,分片、副本、ILM 都要管

低

判断标准很简单:需要「任意字段全文搜索、聚合分析、给非运维人员用」选 EFK;需要「按 namespace/Pod 定位、低成本长期保留、与 Prometheus 共用 Grafana」选 Loki。两者也可以并存,热数据进 Loki,审计与合规数据进 EFK。

3.6.3.6. 资源与生产注意事项

  • master 必须奇数:2 个 master 是原书 values 的妥协,生产至少 3 个,且与 data 节点分离。

  • 堆与 limit 配套:heapSize 只是 JVM 堆,容器 limit 还要加上 Lucene 的堆外缓存;只设 requests 会拖垮节点。

  • 磁盘水位:默认 85% 触发只读、90% 开始迁分片、95% 写不进去,必须提前扩容或调 ILM。

  • 采集端要有背压:ES 不可用时内存缓冲会涨到 Mem_Buf_Limit 上限然后丢日志,生产应开 filesystem 缓冲并设 storage.total_limit_size。

3.6.4. 动手实验

实验:Helm 部署 Elasticsearch + Kibana + Fluent Bit

前置:Kubernetes v1.31;有 longhorn 或 nfs-csi StorageClass;Helm 3;节点内存 ≥ 8GiB。

3.6.4.1. 步骤 1 · 准备命名空间与仓库

kubectl create namespace logging
helm repo add bitnami https://charts.bitnami.com/bitnami && helm repo update
kubectl get sc   # 确认 values 里的 storageClass 真实存在

3.6.4.2. 步骤 2 · 部署 Elasticsearch

es-values.yaml
master:
  replicas: 3
  heapSize: 512m
  persistence: {enabled: true, storageClass: longhorn, size: 8Gi}
coordinating:
  replicas: 2
  service: {type: ClusterIP, port: 9200}
data:
  replicas: 2
  heapSize: 1024m
  persistence: {enabled: true, storageClass: longhorn, size: 10Gi}
helm install es bitnami/elasticsearch -n logging -f es-values.yaml
kubectl -n logging get sts,pvc
kubectl -n logging port-forward svc/es-elasticsearch-coordinating-only 9200:9200 &
curl -s 'http://127.0.0.1:9200/_cluster/health?pretty' | jq '{status,number_of_nodes}'

status 为 green 再继续;yellow 表示副本没分够,单节点实验环境正常,但说明副本数大于可用节点数。

3.6.4.3. 步骤 3 · 部署 Kibana

helm install kibana bitnami/kibana -n logging \
  --set persistence.storageClass=longhorn \
  --set 'elasticsearch.hosts[0]=es-elasticsearch-coordinating-only.logging.svc.cluster.local'
kubectl -n logging port-forward svc/kibana 5601:5601

打开 http://127.0.0.1:5601,进 Stack Management → Index Patterns 建 k8s-cluster-*,时间字段选 @timestamp;此时还没有数据,下一步补上采集端。

3.6.4.4. 步骤 4 · 部署 Fluent Bit

helm install fluent-bit ./fluent-bit-0.6.2.tgz -n logging -f fluent-bit-values.yaml
kubectl -n logging get ds fluent-bit
kubectl -n logging logs ds/fluent-bit --tail=20

仓库自带的 Chart 是 fluent-bit-0.6.2.tgz(对应 Fluent Bit 1.5.2)。values 里要确认三处:Parser cri(不是 docker)、outputs 的 Host 指向 coordinating Service、tolerations 让 DaemonSet 能落到控制平面节点。日志里出现 [output:es] 的连接失败会重试,先解决 ES 再排查采集。

3.6.4.5. 步骤 5 · 造日志并检索

kubectl run loggen --image=busybox:1.36 --restart=Never -- \
  sh -c 'for i in $(seq 1 50); do echo "{\"level\":\"info\",\"msg\":\"hello-efk\",\"i\":$i}"; sleep 1; done'
sleep 20 && curl -s 'http://127.0.0.1:9200/_cat/indices/k8s-cluster-*?v'
curl -s 'http://127.0.0.1:9200/k8s-cluster-*/_search?q=hello-efk&size=1' | jq '.hits.hits[0]._source'

输出应形如 {"level": "info", "msg": "hello-efk", "i": 50}。能查到 msg 与 level 两个字段,说明 Merge_Log 生效、JSON 被展开;只能查到整行字符串就回去检查 Merge_Log On 与 Merge_Parser。Kibana 的 Discover 里按 kubernetes.namespace_name: default 过滤,应能看到同一个 Pod 的日志。

3.6.4.6. 验证清单

  • es-elasticsearch-coordinating-only 可访问,_cluster/health 返回 green 或预期的 yellow。

  • Kibana 能打开,k8s-cluster-* index pattern 有文档数。

  • Fluent Bit DaemonSet 的 DESIRED 等于节点数,Pod 全部 Ready。

3.6.5. 常见问题与排错

现象

可能原因

处理方式

Fluent Bit 报 connection refused

ES Service 名或端口写错,release 名不是 es

用 kubectl -n logging get svc 对照 outputs 的 Host

索引里没有日志

Parser docker 用在 containerd 上,行解析失败

改 Parser cri 后 helm upgrade 重启 DaemonSet

字段是一整行字符串

没开 Merge_Log,或 JSON 不是单行

开 Merge_Log On,多行日志用 multiline 解析

索引变成只读

磁盘使用率超过 85%/95% 水位

扩容 PVC,或用 _cluster/settings 清 read_only_allow_delete

Pod Pending,PVC 不绑定

storageClass 不存在

kubectl get sc 对照 values,改完 helm upgrade

3.6.6. 进阶挑战

  1. 给 Fluent Bit 加 [FILTER] Name grep 丢弃 kube-system 的噪声,并加一个 [OUTPUT] Name loki 双写,比较两套后端的结果。

  2. 用 ILM 把 k8s-cluster-* 的保留期设为 7 天,热节点 2 天、温节点 5 天,验证 rollover 与删除。

  3. 把 ES 堆调到 2GiB 并配 limit 4GiB,压测 5000 行/秒,观察 GC 与写入延迟的变化。

3.6.7. 小结

  • 容器日志落在节点文件里,kubectl logs 只是 kubelet 的读取接口,生产必须用 DaemonSet 采集。

  • EFK 的分工是「Fluent Bit 采集、ES 存储检索、Kibana 查询」,coordinating 是入口,data 存分片。

  • values 里最关键的四个点:master 用奇数、heap 与 limit 配套、Parser cri、索引前缀与 index pattern 对齐。

  • EFK 强在全文检索与聚合,Loki 强在成本与运维简单;按查询模式选,不按流行度选。

3.6.8. 延伸阅读