Kubernetes日志采集——使用Fluent Bit采集和转发Kubernetes日志(三)
1、概览
本文主要讲解下如何编写Fluent Bit配置文件来采集和转发Kubernetes日志。如果对Kubernetes日志管理机制和Fluent Bit不熟悉,请先阅读《从 Docker 到 Kubernetes 日志管理机制详解》、《Kubernetes日志采集——Fluent Bit详细介绍(一)》、《Kubernetes日志采集——Fluent Bit插件详细配置(二)》这三篇博文。
2、Kubernetes 的日志种类
在 Kubernetes 中日志也主要有两大类:
- Kuberntes 集群组件日志;
- 应用 Pod 日志;
所以,使用Fluent Bit采集Kubernetes日志就是采集Kuberntes 集群组件日志和应用 Pod 日志。
2.1 Kuberntes 集群组件日志
Kuberntes 集群组件日志分为两类:
- 运行在容器中的 Kubernetes scheduler 和 kube-proxy等。
- 未运行在容器中的 kubelet 和容器 runtime,比如 Docker。
在使用 systemd 机制的服务器上,kubelet 和容器 runtime 写入日志到 journald(常用的centos7正是使用 systemd 机制)。如果没有 systemd,他们写入日志到 /var/log 目录的 .log 文件。
2.2 应用 Pod 日志
Kubernetes Pod 的日志管理是基于 Docker 引擎的,Kubernetes 并不管理日志的轮转策略,日志的存储都是基于 Docker 的日志管理策略。k8s 集群调度的基本单位就是 Pod,而 Pod 是一组容器,所以 k8s 日志管理基于 Docker 引擎这一说法也就不难理解了,最终日志还是要落到一个个容器上面。
3、在Kubernetes集群部署Fluentbit
由于在Kubernetes部署Fluent-bit Daemonset比较简单,本文就不再介绍Fluent-bit Daemonset的安装过程。
下面粘贴一下Fluentbit Daemonset的配置文件,对于Fluentbit Daemonset的配置文件着重看下volumeMounts部分。
- 把节点的/var/log/journal目录挂载到fluent-bit容器内,通过/var/log/journal目录即可采集Kuberntes 集群组件日志。
- 把节点的/var/log目录和/data/docker-data/containers(docker数据盘路径)目录挂载到fluent-bit容器内,通过/var/log/containers/目录即可采集当前节点所有Pod下所有容器的所有日志文件(这里如果有疑问可以参见《从 Docker 到 Kubernetes 日志管理机制详解》)。
apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
app.kubernetes.io/name: fluent-bit
name: fluent-bit
namespace: logging-system
spec:
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/name: fluent-bit
template:
metadata:
creationTimestamp: null
labels:
app.kubernetes.io/name: fluent-bit
name: fluent-bit
namespace: logging-system
spec:
containers:
- env:
- name: NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
image: fluent-bit:v1.8.3
imagePullPolicy: IfNotPresent
name: fluent-bit
ports:
- containerPort: 2020
name: metrics
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /fluent-bit/config
name: config
readOnly: true
- mountPath: /var/log/
name: varlogs
readOnly: true
- mountPath: /var/log/journal
name: systemd
readOnly: true
- mountPath: /fluent-bit/tail
name: positions
- mountPath: /data/docker-data/containers
name: varlibcontainers0
readOnly: true
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: fluent-bit
serviceAccountName: fluent-bit
terminationGracePeriodSeconds: 30
tolerations:
- operator: Exists
volumes:
- name: config
secret:
defaultMode: 420
secretName: fluent-bit-config
- hostPath:
path: /var/log
type: ""
name: varlogs
- hostPath:
path: /var/log/journal
type: ""
name: systemd
- emptyDir: {}
name: positions
- hostPath:
path: /data/docker-data/containers
type: ""
name: varlibcontainers0
updateStrategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
type: RollingUpdate
注意:配置文件中的fluent-bit:v1.8.3镜像是基于官方镜像二次构建的,所以不要直接粘贴以上Fluentbit Daemonset的配置文件到其他k8s环境部署。另外,fluent-bit采集和转发的配置是通过密钥的形式挂载到fluent-bit容器内部,由于二次构建了镜像所以fluent-bit启动时能直接加载此密钥里面的配置文件。
4、通过编写Fluent Bit配置文件来采集和转发Kubernetes日志
此章节是本文的核心,通过配置Fluent Bit输入、解析、过滤、缓存和输出模块来采集和转发Kubernetes日志。

这里先整体粘贴下Fluent Bit输入、解析、过滤、缓存和输出模块的配置,下文会依次解释Input、Parser、Filter、Buffer、Routing 和 Output模块的配置。
[Service]
Parsers_File parsers.conf
[Input]
Name systemd
Path /var/log/journal
DB /fluent-bit/tail/docker.db
DB.Sync Normal
Tag service.docker
Systemd_Filter _SYSTEMD_UNIT=docker.service
[Input]
Name systemd
Path /var/log/journal
DB /fluent-bit/tail/kubelet.db
DB.Sync Normal
Tag service.kubelet
Systemd_Filter _SYSTEMD_UNIT=kubelet.service
[Input]
Name tail
Path /var/log/containers/*.log
Exclude_Path /var/log/containers/*_cloudbases-logging-system_events-exporter*.log,/var/log/containers/kube-auditing-webhook*_cloudbases-logging-system_kube-auditing-webhook*.log
Refresh_Interval 10
Skip_Long_Lines true
DB /fluent-bit/tail/pos.db
DB.Sync Normal
Mem_Buf_Limit 5MB
Parser docker
Tag kube.*
[Filter]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Labels false
Annotations false
[Filter]
Name nest
Match kube.*
Operation lift
Nested_under kubernetes
Add_prefix kubernetes_
[Filter]
Name modify
Match kube.*
Remove stream
Remove kubernetes_pod_id
Remove kubernetes_host
Remove kubernetes_container_hash
[Filter]
Name nest
Match kube.*
Operation nest
Wildcard kubernetes_*
Nest_under kubernetes
Remove_prefix kubernetes_
[Filter]
Name lua
Match service.*
script /fluent-bit/config/systemd.lua
call add_time
time_as_table true
[Output]
Name es
Match_Regex (?:kube|service)\.(.*)
Host elasticsearch-logging-data.logging-system.svc
Port 9200
Logstash_Format true
Logstash_Prefix cb-logstash-log
Time_Key @timestamp
Generate_ID true
注意:docker容器日志默认都是以JSON 的格式写到文件中,每一条 json 日志中默认包含 log, stream, time 三个字段。
{
"log": ....,
"stream": .....,
"time": .......
}
以下图这条容器日志为例,下面会详细说明此条日志在Fluent Bit不同模块的日志格式:

4.1 全局配置——SERVICE
[Service]
Parsers_File parsers.conf
这里Parsers_File引用了parsers.conf配置文件,在[Input]模块会使用parsers.conf文件中定义的[PARSER]将 Input 抽取的非结构化数据转化为标准的结构化数据,下面粘贴一下parsers.conf配置文件内容:
[PARSER]
Name apache
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache2
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>.*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache_error
Format regex
Regex ^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])?( \[client (?<client>[^\]]*)\])? (?<message>.*)$
[PARSER]
Name nginx
Format regex
Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
# https://rubular.com/r/IhIbCAIs7ImOkc
Name k8s-nginx-ingress
Format regex
Regex ^(?<host>[^ ]*) - (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*) "(?<referer>[^\"]*)" "(?<agent>[^\"]*)" (?<request_length>[^ ]*) (?<request_time>[^ ]*) \[(?<proxy_upstream_name>[^ ]*)\] (\[(?<proxy_alternative_upstream_name>[^ ]*)\] )?(?<upstream_addr>[^ ]*) (?<upstream_response_length>[^ ]*) (?<upstream_response_time>[^ ]*) (?<upstream_status>[^ ]*) (?<reg_id>[^ ]*).*$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name json
Format json
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
# --
# Since Fluent Bit v1.2, if you are parsing Docker logs and using
# the Kubernetes filter, it's not longer required to decode the
# 'log' key.
#
# Command | Decoder | Field | Optional Action
# =============|==================|=================
#Decode_Field_As json log
[PARSER]
Name docker-daemon
Format regex
Regex time="(?<time>[^ ]*)" level=(?<level>[^ ]*) msg="(?<msg>[^ ].*)"
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
[PARSER]
Name syslog-rfc5424
Format regex
Regex ^\<(?<pri>[0-9]{1,5})\>1 (?<time>[^ ]+) (?<host>[^ ]+) (?<ident>[^ ]+) (?<pid>[-0-9]+) (?<msgid>[^ ]+) (?<extradata>(\[(.*?)\]|-)) (?<message>.+)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
Time_Keep On
[PARSER]
Name syslog-rfc3164-local
Format regex
Regex ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$
Time_Key time
Time_Format %b %d %H:%M:%S
Time_Keep On
[PARSER]
Name syslog-rfc3164
Format regex
Regex /^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$/
Time_Key time
Time_Format %b %d %H:%M:%S
Time_Keep On
[PARSER]
Name mongodb
Format regex
Regex ^(?<time>[^ ]*)\s+(?<severity>\w)\s+(?<component>[^ ]+)\s+\[(?<context>[^\]]+)]\s+(?<message>.*?) *(?<ms>(\d+))?(:?ms)?$
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
Time_Key time
[PARSER]
# https://rubular.com/r/3fVxCrE5iFiZim
Name envoy
Format regex
Regex ^\[(?<start_time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)? (?<protocol>\S+)" (?<code>[^ ]*) (?<response_flags>[^ ]*) (?<bytes_received>[^ ]*) (?<bytes_sent>[^ ]*) (?<duration>[^ ]*) (?<x_envoy_upstream_service_time>[^ ]*) "(?<x_forwarded_for>[^ ]*)" "(?<user_agent>[^\"]*)" "(?<request_id>[^\"]*)" "(?<authority>[^ ]*)" "(?<upstream_host>[^ ]*)"
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
Time_Keep On
Time_Key start_time
[PARSER]
# http://rubular.com/r/tjUt3Awgg4
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
[PARSER]
Name kube-custom
Format regex
Regex (?<tag>[^.]+)?\.?(?<pod_name>[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace_name>[^_]+)_(?<container_name>.+)-(?<docker_id>[a-z0-9]{64})\.log$
4.2 输入——Input(包含解析模块引用)
通过配置[Input]模块采集Kubernetes集群组件日志和应用容器日志。
[Input]
Name systemd //使用systemd输入插件从systemd或journaled读取日志
Path /var/log/journal //采集k8s未运行在容器中的集群组件日志: Docker
DB /fluent-bit/tail/docker.db
DB.Sync Normal
Tag service.docker //定义Tag用于识别数据源
Systemd_Filter _SYSTEMD_UNIT=docker.service //采集当前节点docker服务日志(_SYSTEM_UNIT必须加下划线)
[Input]
Name systemd
Path /var/log/journal //采集k8s未运行在容器中的集群组件日志: Kubelet
DB /fluent-bit/tail/kubelet.db
DB.Sync Normal
Tag service.kubelet
Systemd_Filter _SYSTEMD_UNIT=kubelet.service //采集当前节点kubelet服务日志
[Input]
Name tail //使用tail输入插件
Path /var/log/containers/*.log //采集k8s应用Pod日志和运行在容器中的集群组件日志(Kubernetes scheduler 和 kube-proxy、etcd等)
Exclude_Path /var/log/containers/*_cloudbases-logging-system_events-exporter*.log,/var/log/containers/kube-auditing-webhook*_cloudbases-logging-system_kube-auditing-webhook*.log //使用通配符排除日志文件采集
Refresh_Interval 10 //刷新监视文件列表的时间间隔
Skip_Long_Lines true //当受监视的文件由于行很长而达到缓冲区容量时,默认停止监视该文件
DB /fluent-bit/tail/pos.db
DB.Sync Normal
Mem_Buf_Limit 5MB //缓存使用的内存限制如果达到了极限,input就会暂停读取;当刷新数据到output后,它将恢复读取
Parser docker //使用docker解析插件将Input抽取的非结构化容器日志转化为标准的结构化数据
Tag kube.*
应用容器日志通过配置[Input]模块中引用docker解析插件将Input抽取的非结构化容器日志转化为标准的结构化数据,此时容器的日志格式为:

docker解析插件内容如下:
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
4.3 过滤——Filter
对Input模块采集的格式化数据进行过滤和修改。一个数据管道中可以包含多个 Filter,Filter 会顺序执行,其执行顺序与配置文件中的顺序一致。
4.3.1 使用kubernetes过滤器插件为应用容器日志和运行在容器中的k8s集群组件日志添加kubernetes元数据
[Filter]
Name kubernetes
Match kube.* //匹配输入模块中的Tag,即匹配上文中的使用tail插件的那个input模块
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Labels false //不将标签添加到容器日志中
Annotations false //不将注解添加到容器日志中
经过kubernetes过滤器插件后,此时容器的日志格式为:

4.3.2 使用nest过滤器插件对应用容器和运行在容器中的k8s集群组件的嵌套日志进行操作
[Filter]
Name nest
Match kube.*
Operation lift //通过lift模式,从记录的将指定map中的key value都提取出来放到上一层
Nested_under kubernetes //指定需要提取的map名
Add_prefix kubernetes_ //添加前缀
经过nest过滤器插件后,此时容器的日志格式为:

4.3.3 通过modify调整应用容器和运行在容器中的k8s集群组件日志字段
[Filter]
Name modify
Match kube.*
Remove stream //移除stream字段
Remove kubernetes_pod_id //移除kubernetes_pod_id字段
Remove kubernetes_host //移除kubernetes_host字段
Remove kubernetes_container_hash //移除kubernetes_container_hash字段
4.3.4 使用nest过滤器插件对应用容器和运行在容器中的k8s集群组件的嵌套日志进行操作
[Filter]
Name nest
Match kube.*
Operation nest //通过nest模式,从记录中指定一组key value合并,并放到一个map里
Wildcard kubernetes_* //选择日志记录中以kubernetes_为前缀的key,将这些key value放到一个map里
Nest_under kubernetes //存放key value的map名
Remove_prefix kubernetes_ //移除这些key的前缀
经过nest过滤器插件后,此时容器的日志格式为:

经过以上4个过滤器插件后,将应用容器日志和运行在容器中的k8s集群组件日志过滤和修改成了想要的格式,当以上配置不满足公司业务需求时,对应调整过滤器模块配置即可。
4.3.5 通过lua过滤器插件处理kubernetes非容器化集群组件(Docker、Kubelet)日志
需要注意的是经过systemd输入插件采集的日志直接是格式化的,并不需要解析。
示例日志如下:
service.kubelet: [1657004329.109221000, {"PRIORITY"=>"6", "_UID"=>"0", "_GID"=>"0", "_CAP_EFFECTIVE"=>"1fffffffff", "_SYSTEMD_SLICE"=>"system.slice", "_BOOT_ID"=>"f1b154f127cf479f9c150f84038fd70b", "_MACHINE_ID"=>"d96b070ae8844338a3170e4ee73453f8", "_HOSTNAME"=>"node1", "_TRANSPORT"=>"stdout", "_STREAM_ID"=>"f984c86546344c88811be43d44b93394", "SYSLOG_FACILITY"=>"3", "SYSLOG_IDENTIFIER"=>"kubelet", "_PID"=>"79465", "_COMM"=>"kubelet", "_EXE"=>"/usr/local/bin/kubelet", "_CMDLINE"=>"/usr/local/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --cgroup-driver=cgroupfs --network-plugin=cni --pod-infra-container-image=harbor.openserver.cn:443/big_data-cloudbases/pause:3.4.1 --node-ip=10.20.30.31 --hostname-override=node1", "_SYSTEMD_CGROUP"=>"/system.slice/kubelet.service", "_SYSTEMD_UNIT"=>"kubelet.service", "MESSAGE"=>"E0705 14:58:49.108605 79465 cadvisor_stats_provider.go:151] "Unable to fetch pod etc hosts stats" err="failed to get stats failed command 'du' ($ nice -n 19 du -x -s -B 1) on path /var/lib/kubelet/pods/57d16b3d-23f5-4a40-87a6-9e547793a519/etc-hosts with error exit status 1" pod="ingress-nginx/ingress-nginx-admission-create-2q7xc""}]
接着看下kubernetes非容器化集群组件的过滤器配置:
[Filter]
Name lua
Match service.*
script /fluent-bit/config/systemd.lua //脚本文件
call add_time //调用脚本add_time方法
time_as_table true
其中script脚本内容如下,逻辑为:新生成个空的日志记录,然后将采集到的systemd服务日志记录的指定字段放到新的日志记录中,然后返回新组装的日志记录,将新组装的日志记录输出到指定目的地。这样就可以将采集到的systemd服务日志过滤和修改成我们想要的日志内容。
function add_time(tag, timestamp, record) //record是获取的日志记录
new_record = {} //初始化一个空的日志记录
//时间格式化
timeStr = os.date("!*t", timestamp["sec"])
t = string.format("%4d-%02d-%02dT%02d:%02d:%02d.%sZ",
timeStr["year"], timeStr["month"], timeStr["day"],
timeStr["hour"], timeStr["min"], timeStr["sec"],
timestamp["nsec"])
//初始化空的kubernetes map 并新增数据
kubernetes = {}
kubernetes["pod_name"] = record["_HOSTNAME"]
kubernetes["container_name"] = record["SYSLOG_IDENTIFIER"]
kubernetes["namespace_name"] = "kube-system"
//把新增的数据都放到空的map中
new_record["time"] = t
new_record["log"] = record["MESSAGE"]
new_record["kubernetes"] = kubernetes
return 1, timestamp, new_record
4.4 输出——Output
将数据发送到不同的目的地。
[Output]
Name es //输出插件使用es
Match_Regex (?:kube|service)\.(.*) //在输出模块配置中指定 Match 规则,Match输入模块中的Tag,这样通过标签和匹配规则就能将数据路由到一个或多个目的地
Host elasticsearch-logging-data.logging-system.svc //es地址
Port 9200 //es端口
Logstash_Format true
Logstash_Prefix cb-logstash-log
Time_Key @timestamp
Generate_ID true
5、总结
Fluent Bit采集Kubernetes日志就是采集Kuberntes 集群组件日志和应用 Pod 日志,其中Kuberneters集群组件日志又分为:
- 运行在容器中的 Kubernetes scheduler 和 kube-proxy等。
- 未运行在容器中的 kubelet 和容器 runtime,比如 Docker。
通过分析Kubernetes日志种类就能明确出日志采集点,对于容器日志采集/var/log/containers/路径即可,对于非容器服务根据服务名通过systemd插件采集即可,至此就可以将Kubernetes日志采集到。接下来再通过编写Fluent Bit输入、解析、过滤、缓存和输出模块的配置就可以将Kubernetes日志转换成我们想要的格式,并将日志输出到指定目的地。
浙公网安备 33010602011771号