Filebeat在Kubernetes集群中的最佳实践
1 背景
在Kubernetes还未兴起的时代,业务部署几乎所有应用都采用单机部署,当压力增大时,IDC架构只能横向拓展服务器集群,增加算力,云计算兴起后,可以动态的调整已有服务器的配置,来分担压力,或者可以通过弹性伸缩来实现根据业务量大小、集群负载、监控等情况,动态的横向调整后端服务器的数量。日志作为应用系统的一部分,通常在系统异常时,用来排查故障,寻找原因,传统的日志处理凡是通常结合grep
和Linux
常见的文本命令工具进行分析。
为了支持更快的开发、迭代效率,近年来开始了容器化改造,并开始了拥抱 Kubernetes 生态、业务全量上云等工作。在这阶段,日志无论从规模、种类都呈现爆炸式的增长,对日志进行数字化、智能化分析的需求也越来越高,因此统一的日志平台应运而生。
2 Kubernetes日志系统收集难点
单纯日志系统的解决方案非常多,相对也比较成熟,这里就不再去赘述,我们此次只针对 Kubernetes 上的日志系统建设而论。Kubernetes 上的日志方案相比我们之前基于物理机、虚拟机场景的日志方案有很大不同,例如:
- 日志的形式变得更加复杂,不仅有物理机/虚拟机上的日志,还有容器的标准输出、容器内的文件、容器事件、Kubernetes 事件等等信息需要采集;
- 环境的动态性变强,在 Kubernetes 中,机器的宕机、下线、上线、Pod销毁、扩容/缩容等都是常态,这种情况下日志的存在是瞬时的(例如如果 Pod 销毁后该 Pod 日志就不可见了),所以日志数据必须实时采集到服务端。同时还需要保证日志的采集能够适应这种动态性极强的场景;
- 日志的种类变多,上图是一个典型的 Kubernetes 架构,一个请求从客户端需要经过 CDN、Ingress、Service Mesh、Pod 等多个组件,涉及多种基础设施,其中的日志种类增加了很多,例如 K8s 各种系统组件日志、审计日志、ServiceMesh 日志、Ingress 等;
3 Kubernetes日志文件说明
关于Kubenetes
的日志采集,部署方式是采用DaemonSet
的方式,采集时按照k8s集群的namespace
进行分类,然后根据namespace
的名称创建不同的topic
到kafka中
一般情况下,Kubernetes默认会在/var/log/containers
和/var/log/pods
目录中会生成这些日志文件的软连接,如下所示:
然后,会看到这个目录下存在了此宿主机上的所有容器日志,文件的命名方式为:
[podName]_[nameSpace]_[depoymentName]-[containerId].log
复制代码
上面这个是deployment
的命名方式,其他的会有些不同,例如:DaemonSet
,StatefulSet
等,不过所有的都有一个共同点,就是
*_[nameSpace]_*.log
复制代码
到这里,知道这个特性,就可以往下来看Filebeat
的部署和配置了。
4 Filebeat
4.1 部署
部署采用的DaemonSet
方式进行
---
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: log
data:
filebeat.yml: |-
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_default_*.log
fields:
namespace: default
env: dev
k8s: cluster-dev
- type: container
enabled: true
paths:
- /var/log/containers/*_kube-system_*.log
fields:
namespace: kube-system
env: dev
k8s: cluster-dev
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
output.kafka:
hosts: ["175.27.159.78:9092","175.27.159.78:9093","175.27.159.78:9094"]
topic: '%{[fields.k8s]}-%{[fields.namespace]}'
partition.round_robin:
reachable_only: true
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: filebeat
namespace: log
labels:
k8s-app: filebeat
spec:
selector:
matchLabels:
k8s-app: filebeat
template:
metadata:
labels:
k8s-app: filebeat
spec:
serviceAccountName: filebeat
terminationGracePeriodSeconds: 30
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: filebeat
image: docker.elastic.co/beats/filebeat:7.12.0
args: [
"-c", "/etc/filebeat.yml",
"-e",
]
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
securityContext:
runAsUser: 0
# If using Red Hat OpenShift uncomment this:
#privileged: true
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: config
mountPath: /etc/filebeat.yml
readOnly: true
subPath: filebeat.yml
- name: data
mountPath: /usr/share/filebeat/data
- name: varlibdockercontainers
mountPath: /data/docker/containers
readOnly: true
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: config
configMap:
defaultMode: 0640
name: filebeat-config
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /data/docker/containers
# data folder stores a registry of read status for all files, so we don't send everything again on a Filebeat pod restart
- name: data
hostPath:
# When filebeat runs as non-root user, this directory needs to be writable by group (g+w).
path: /var/lib/filebeat-data
type: DirectoryOrCreate
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: filebeat
subjects:
- kind: ServiceAccount
name: filebeat
namespace: log
roleRef:
kind: ClusterRole
name: filebeat
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: filebeat
labels:
k8s-app: filebeat
rules:
- apiGroups: [""] # "" indicates the core API group
resources:
- namespaces
- pods
- nodes
verbs:
- get
- watch
- list
- apiGroups: ["apps"]
resources:
- replicasets
verbs: ["get", "list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: filebeat
namespace: log
labels:
k8s-app: filebeat
复制代码
[root@master filebeat]# kubectl apply -f filebeat-daemonset.yaml
configmap/filebeat-daemonset-config-test created
daemonset.apps/filebeat created
clusterrolebinding.rbac.authorization.k8s.io/filebeat created
clusterrole.rbac.authorization.k8s.io/filebeat created
serviceaccount/filebeat created
复制代码
4.2 Filebeat配置文件介绍
这里先简单介绍下filebeat
的配置结构
filebeat.inputs:
filebeat.config.modules:
processors:
output.xxxxx:
复制代码
结构大概是这么个结构,完整的数据流向简单来说就是下面这个图:
4.3 inputs
根据命名空间做分类,每一个命名空间就是一个topic,如果要收集多个集群,同样也是使用命名空间做分类,只不过topic的命名就需要加个k8s的集群名,这样方便去区分了,那既然是通过命名空间来获取日志,那么在配置inputs
的时候就需要通过写正则将指定命名空间下的日志文件取出,然后读取,例如:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_default_*.log
fields:
namespace: default
env: dev
k8s: cluster-dev
复制代码
这里是写了一个命名空间,如果有多个,就排开写就行了,如下所示:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_default_*.log
fields:
namespace: default
env: dev
k8s: cluster-dev
- type: container
enabled: true
paths:
- /var/log/containers/*_kube-system_*.log
fields:
namespace: kube-system
env: dev
k8s: cluster-dev
复制代码
上面说了通过命名空间创建topic
,我这里加了一个自定义的字段namespace
,就是后面的topic
的名称,但是这里有很多的命名空间,那在输出的时候,如何动态去创建呢?
output.kafka:
hosts: ["10.0.105.74:9092","10.0.105.76:9092","10.0.105.96:9092"]
topic: '%{[fields.namespace]}'
partition.round_robin:
reachable_only: true
复制代码
那么目前,完整的配置文件如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: log
data:
filebeat.yml: |-
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_default_*.log
fields:
namespace: default
env: dev
k8s: cluster-dev
- type: container
enabled: true
paths:
- /var/log/containers/*_kube-system_*.log
fields:
namespace: kube-system
env: dev
k8s: cluster-dev
filebeat.config.modules:
path: ${path.config}/modules.d/*.yml
reload.enabled: false
output.kafka:
hosts: ["175.27.159.78:9092","175.27.159.78:9093","175.27.159.78:9094"]
topic: '%{[fields.k8s]}-%{[fields.namespace]}'
partition.round_robin:
reachable_only: true
复制代码
4.4 processors
如果是不对日志做任何处理,到这里就结束了,但是这样又视乎在查看日志的时候少了点什么? 到这里仅仅知道日志内容,和该日志来自于哪个命名空间,但是你=不知道该日志属于哪个服务,哪个pod,甚至说想查看该服务的镜像地址等,但是这些信息在我们上面的配置方式中是没有的,所以需要进一步的添砖加瓦。
这个时候就用到了一个配置项,叫做: processors
4.4.1 添加K8s的基本信息
在采集k8s的日志时,如果按照上面那种配置方式,是没有关于pod的一些信息的,例如:
- Pod Name
- Pod UID
- Namespace
- Labels
添加前日志示例
{
"@timestamp": "2021-05-06T02:47:09.256Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.12.0"
},
"log": {
"file": {
"path": "/var/log/containers/metrics-server-5549c7694f-7vb66_kube-system_metrics-server-9108765e17c7e325abd665fb0f53c8f4b3077c698cb88392099dfbafb0475709.log"
},
"offset": 15842
},
"stream": "stderr",
"message": "E0506 02:47:09.254911 1 reststorage.go:160] unable to fetch pod metrics for pod log/filebeat-s67ds: no metrics known for pod",
"input": {
"type": "container"
},
"fields": {
"env": "dev",
"k8s": "cluster-dev",
"namespace": "kube-system"
},
"ecs": {
"version": "1.8.0"
},
"host": {
"name": "node-03"
},
"agent": {
"hostname": "node-03",
"ephemeral_id": "1c87559a-cfca-4708-8f28-e4fc6441943c",
"id": "f9cf0cd4-eccf-4d8b-bd24-2bff25b4083b",
"name": "node-03",
"type": "filebeat",
"version": "7.12.0"
}
}
复制代码
那么如果想添加这些信息,就要使用processors
中的一个工具,叫做: add_kubernetes_metadata
, 字面意思就是添加k8s的一些元数据信息,使用方法可以先来看一段示例:
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
复制代码
host
: 指定要对filebeat起作用的节点,防止无法准确检测到它,比如在主机网络模式下运行filebeat
matchers
: 匹配器用于构造与索引创建的标识符相匹配的查找键
logs_path
: 容器日志的基本路径,如果未指定,则使用Filebeat运行的平台的默认日志路径
加上这个k8s的元数据信息之后,就可以在日志里面看到k8s的信息了,看一下添加k8s信息后的日志格式
{
"@timestamp": "2021-05-06T03:01:58.512Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.12.0"
},
"agent": {
"hostname": "node-03",
"ephemeral_id": "c0f94fc0-b128-4eb9-b9a3-387f4cae44b7",
"id": "f9cf0cd4-eccf-4d8b-bd24-2bff25b4083b",
"name": "node-03",
"type": "filebeat",
"version": "7.12.0"
},
"ecs": {
"version": "1.8.0"
},
"stream": "stdout",
"input": {
"type": "container"
},
"host": {
"name": "node-03"
},
"container": {
"id": "6791d22d210507becd7306ead1eeda9a4c558b5ca0630ed5af4f8b1b220fb4a7",
"runtime": "docker",
"image": {
"name": "nginx:1.10"
}
},
"kubernetes": {
"namespace": "default",
"replicaset": {
"name": "nginx-5b946576d4"
},
"labels": {
"app": "nginx",
"pod-template-hash": "5b946576d4"
},
"container": {
"name": "nginx",
"image": "nginx:1.10"
},
"deployment": {
"name": "nginx"
},
"node": {
"name": "node-03",
"uid": "4340750b-1bb4-4d61-a9aa-4715c7326988",
"labels": {
"kubernetes_io/arch": "amd64",
"kubernetes_io/hostname": "node-03",
"kubernetes_io/os": "linux",
"beta_kubernetes_io/arch": "amd64",
"beta_kubernetes_io/os": "linux"
},
"hostname": "node-03"
},
"namespace_uid": "8d1dad4b-bea0-469d-9858-51147822de79",
"pod": {
"name": "nginx-5b946576d4-6kftk",
"uid": "cc8c943a-919c-4e15-9cde-05358b8588c1"
}
},
"log": {
"offset": 2039,
"file": {
"path": "/var/log/containers/nginx-5b946576d4-6kftk_default_nginx-6791d22d210507becd7306ead1eeda9a4c558b5ca0630ed5af4f8b1b220fb4a7.log"
}
},
"message": "2021-05-06 11:01:58 10.234.2.11 - - \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.29.0\" \"-\"",
"fields": {
"k8s": "cluster-dev",
"namespace": "default",
"env": "dev"
}
}
复制代码
可以看到kubernetes这个key的value有关于pod的信息,还有node的一些信息,还有namespace信息等,基本上关于k8s的一些关键信息都包含了,非常的多和全。
但是,问题又来了,这一条日志信息有点太多了,有一半多不是我们想要的信息,所以,我们需要去掉一些对于我们没有用的字段
4.4.2 删除不必要的字段
processors:
- drop_fields:
#删除的多余字段
fields:
- host
- ecs
- log
- agent
- input
- stream
- container
ignore_missing: true
复制代码
4.4.3 添加日志时间
通过上面的日志信息,可以看到是没有单独的一个关于日志时间的字段的,虽然里面有一个@timestamp
,但不是北京时间,而我们要的是日志的时间,message
里面倒是有时间,但是怎么能把它取到并单独添加一个字段呢,这个时候就需要用到script
了,需要写一个js脚本来替换。
processors:
- script:
lang: javascript
id: format_time
tag: enable
source: >
function process(event) {
var str=event.Get("message");
var time=str.split(" ").slice(0, 2).join(" ");
event.Put("time", time);
}
- timestamp:
field: time
timezone: Asia/Shanghai
layouts:
- '2006-01-02 15:04:05'
- '2006-01-02 15:04:05.999'
test:
- '2019-06-22 16:33:51'
复制代码
添加完成后,会多一个time
的字段,在后面使用的时候,就可以使用这个字段了。
{
"@timestamp": "2021-05-06T04:32:10.560Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.12.0"
},
"message": "2021-05-06 11:32:10 10.234.2.11 - - \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.29.0\" \"-\"",
"fields": {
"k8s": "cluster-dev",
"namespace": "default",
"env": "dev"
},
"time": "2021-05-06 11:32:10",
"kubernetes": {
"replicaset": {
"name": "nginx-deployment-6c4b886b"
},
"labels": {
"app": "nginx-deployment",
"pod-template-hash": "6c4b886b"
},
"container": {
"name": "nginx",
"image": "nginx:1.19.5"
},
"deployment": {
"name": "nginx-deployment"
},
"node": {
"uid": "07d8a1a4-e10f-4331-adf0-2fd7d5817c2d",
"labels": {
"beta_kubernetes_io/os": "linux",
"kubernetes_io/arch": "amd64",
"kubernetes_io/hostname": "node-02",
"kubernetes_io/os": "linux",
"beta_kubernetes_io/arch": "amd64"
},
"hostname": "node-02",
"name": "node-02"
},
"namespace_uid": "8d1dad4b-bea0-469d-9858-51147822de79",
"pod": {
"name": "nginx-deployment-6c4b886b-6rbhw",
"uid": "78a28548-3d34-4df6-9a76-c651b39ff934"
},
"namespace": "default"
}
}
复制代码
4.4.4 优化k8s数据信息结构
这里单独创建了一个字段k8s
,字段里包含:podName
, nameSpace
, imageAddr
, hostName
等关键信息,最后再把kubernetes
这个字段drop掉就可以了。最终结果如下:
processors:
- script:
lang: javascript
id: format_k8s
tag: enable
source: >
function process(event) {
var k8s=event.Get("kubernetes");
var newK8s = {
podName: k8s.pod.name,
nameSpace: k8s.namespace,
imageAddr: k8s.container.name,
hostName: k8s.node.hostname
}
event.Put("k8s", newK8s);
}
复制代码
日志
{
"@timestamp": "2021-05-06T05:33:25.351Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.12.0"
},
"fields": {
"k8s": "cluster-dev",
"namespace": "default",
"env": "dev"
},
"k8s": {
"hostName": "node-02",
"podName": "nginx-deployment-6c4b886b-6rbhw",
"nameSpace": "default",
"imageAddr": "nginx"
},
"message": "06/May/2021:05:33:25 +0000 10.234.2.11 - - \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.29.0\" \"-\""
}
复制代码
链接:https://juejin.cn/post/6974581299881181220
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。