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日志转换成我们想要的格式,并将日志输出到指定目的地。