This series of articles is divided into three parts: FileBeat, Logstash and ES. The logging system will be designed, deployed and optimized to maximize resource utilization and achieve optimal performance based on tens of billions of pieces of data per day. This article focuses on fileBeat
introduce
Version: filebeat – 7.12.0
This is about the log collection of K8S. The deployment mode is DaemonSet. During the collection, the namespace of K8S cluster is classified, and then different topics are created into Kafka according to the name of namespace
K8s Log file description
In general, in a container log in the output to the standard output (stdout), with * – json. The naming of the log are stored in/var/lib/docker/containers directory, of course, if changed the docker data directory, that is in the modified data directory, such as:
# tree /data/docker/containers/ data/docker/containers ├ ─ ─ 009227 c00e48b051b6f5cb65128fd58412b845e0c6d2bec5904f977ef0ec604d │ ├ ─ ─ 009227 c00e48b051b6f5cb65128fd58412b845e0c6d2bec5904f977ef0ec604d - json. The log │ ├ ─ ─ checkpoints │ ├ ─ ─ config. The v2. The json │ ├ ─ ─ Hostconfig. Json │ └ ─ ─ mountsCopy the code
You can see here, there’s this file: / data/docker/containers/container id / * – json. The log, then k8s will default in/var/log/containers and/var/log/pods will generate these log files in a directory of soft connection, as shown below:
cattle-node-agent-tvhlq_cattle-system_agent-8accba2d42cbc907a412be9ea3a628a90624fb8ef0b9aa2bc6ff10eab21cf702.log
etcd-k8s-master01_kube-system_etcd-248e250c64d89ee6b03e4ca28ba364385a443cc220af2863014b923e7f982800.log
Copy the code
You will then see that this directory contains all of the container logs for this host, named as:
[podName]_[nameSpace]_[depoymentName]-[containerId].log
Copy the code
This is the way of naming deployment. Others may vary a little, such as DaemonSet, StatefulSet, etc. But all have one thing in common: deployment
*_[nameSpace]_*.log
Copy the code
At this point, knowing this feature, you can move on to the deployment and configuration of Filebeat.
Filebeat deployment
The deployment was carried out by DaemonSet. There is nothing to be said here. The deployment can be carried out directly according to the official documents
---
apiVersion: v1
data:
filebeat.yml: |- filebeat.inputs: - type: container enabled: true paths: - /var/log/containers/*_test-1_*log fields: namespace: test-1 env: dev k8s: cluster-dev - type: container enabled: true paths: - /var/log/containers/*_test-2_*log fields: namespace: test-2 env: dev k8s: cluster-dev filebeat.config.modules: path: ${path.config}/modules.d/*.yml reload.enabled: false output.kafka: hosts: [10.0.105.74: "9092", "10.0.105.76:9092", "10.0.105.96:9092] the topic: '%{[fields.k8s]}-%{[fields.namespace]}' partition.round_robin: reachable_only: truekind: ConfigMap
metadata:
name: filebeat-daemonset-config-test
namespace: default
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: filebeat
namespace: kube-system
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: /var/lib/docker/containers
readOnly: true
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: config
configMap:
defaultMode: 0640
name: filebeat-config
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: varlog
hostPath:
path: /var/log
# 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: kube-system
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: kube-system
labels:
k8s-app: filebeat
Copy the code
Kubectl apply-f is not the focus of this article.
The official deployment reference: raw.githubusercontent.com/elastic/bea…
Filebeat configuration introduction
Here is the configuration structure of FileBeat
filebeat.inputs:
filebeat.config.modules:
processors:
output.xxxxx:
Copy the code
The structure is roughly like this. The complete data flow can be simply described as the following figure:
If you want to collect more than one cluster, you will also use the same namespace to do the classification, but the topic name needs to add k8S cluster name, so that it is convenient to separate. Write the re in inputs to fetch and read the log file in the specified namespace, for example:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_test-1_*log
fields:
namespace: test-1
env: dev
k8s: cluster-dev
Copy the code
I used the bim5D-BASIC namespace and used the *_test-1_*log to get the log file with this namespace name. I also added a custom field for topic creation. If there is more than one namespace, it can be arranged as follows:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_test-1_*log
fields:
namespace: test-1
env: dev
k8s: cluster-dev
- type: container
enabled: true
paths:
- /var/log/containers/*_test-2_*log
fields:
namespace: test-2
env: dev
k8s: cluster-dev
Copy the code
The downside of this is that if you have a lot of namespaces, you have a lot of configuration. Don’t worry, there is a more concise way to write this below
Note: The log type must be set to Container
I added a custom field named namespace, which is the name of the following topic. However, there are many namespaces in this topic, so how to create the topic dynamically when exporting?
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
Copy the code
Note the syntax: %{[fields.namespace]}
The complete configuration looks like this:
apiVersion: v1
data:
filebeat.yml: |- filebeat.inputs: - type: container enabled: true paths: - /var/log/containers/*_test-1_*log fields: namespace: test-1 env: dev k8s: cluster-dev - type: container enabled: true paths: - /var/log/containers/*_test-2_*log fields: namespace: test-2 env: dev k8s: cluster-dev filebeat.config.modules: path: ${path.config}/modules.d/*.yml reload.enabled: false output.kafka: hosts: [10.0.105.74: "9092", "10.0.105.76:9092", "10.0.105.96:9092] the topic: '%{[fields.k8s]}-%{[fields.namespace]}' partition.round_robin: reachable_only: truekind: ConfigMap
metadata:
name: filebeat-daemonset-config-test
namespace: default
Copy the code
If you didn’t do anything with the log, you’d end up here, but what’s missing when you’re looking at the log? That’s right! You only know the log content and which namespace the log comes from, but you do not know which service the log belongs to, which POD, or even want to view the mirror address of the service, etc. However, this information is not available in our above configuration mode, so you need to add more information.
This time we use a configuration item called: Processors. Check out the official explanation
You can define processors in your configuration to process events before they are sent to the configured output
In a nutshell, it’s dealing with logs
Now let’s focus on this place, which is very useful and important
Processors with FileBeat
Example Add K8s basic information
When collecting k8S logs, if the above configuration is followed, there is no information about POD, for example:
- Pod Name
- Pod UID
- Namespace
- Labels
- Etc., etc.
To add this information, use a tool in Processors called add_kubernetes_metadata, which simply adds k8s metadata. Here’s an example of how to do this: processors processors add_kubernetes_metadata
processors:
- add_kubernetes_metadata:
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
Copy the code
Host: specifies the node to operate on fileBeat in case it cannot be accurately detected, such as running FileBeat matchers in host network mode: the matcher is used to construct a lookup key logs_path that matches the identifier created by the index: The base path for container logs. If not specified, the default log path for the platform on which Filebeat is run
After adding the k8S metadata information, you can see the k8S information in the log.
{
"@timestamp": "The 2021-04-19 T07:07:36. 065 z"."@metadata": {
"beat": "filebeat"."type": "_doc"."version": "7.11.2"
},
"log": {
"offset": 377708."file": {
"path": "/var/log/containers/test-server-85545c868b-6nsvc_test-1_test-server-885412c0a8af6bfa7b3d7a341c3a9cb79a85986965e363e8752 9b31cb650aec4.log"}},"fields": {
"env": "dev"."k8s": "cluster-dev"
"namespace": "test-1"
},
"host": {
"name": "filebeat-fv484"
},
"agent": {
"id": "7afbca43-3ec1-4cee-b5cb-1de1e955b717"."name": "filebeat-fv484"."type": "filebeat"."version": "7.11.2"."hostname": "filebeat-fv484"."ephemeral_id": "8fd29dee-da50-4c88-88d5-ebb6bbf20772"
},
"ecs": {
"version": "1.6.0"
},
"stream": "stdout"."message": "2021-04-19 15:07:36.065 INFO 23 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration"."input": {
"type": "container"
},
"container": {
"image": {
"name": "hub.test.com/test/test-server:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c"
},
"id": "885412c0a8af6bfa7b3d7a341c3a9cb79a85986965e363e87529b31cb650aec4"."runtime": "docker"
},
"kubernetes": {
"labels": {
"pod-template-hash": "85545c868b"."app": "geip-gateway-test"
},
"container": {
"name": "test-server"."image": "hub.test.com/test/test-server:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c"
},
"node": {
"uid": "511d9dc1-a84e-4948-b6c8-26d3f1ba2e61"."labels": {
"kubernetes_io/hostname": "k8s-node-09"."kubernetes_io/os": "linux"."beta_kubernetes_io/arch": "amd64"."beta_kubernetes_io/os": "linux"."cloudt-global": "true"."kubernetes_io/arch": "amd64"
},
"hostname": "k8s-node-09"."name": "k8s-node-09"
},
"namespace_uid": "4fbea846-44b8-4d4a-b03b-56e43cff2754"."namespace_labels": {
"field_cattle_io/projectId": "p-lgxhz"."cattle_io/creator": "norman"
},
"pod": {
"name": "test-server-85545c868b-6nsvc"."uid": "1e678b63-fb3c-40b5-8aad-892596c5bd4d"
},
"namespace": "test-1"."replicaset": {
"name": "test-server-85545c868b"}}}Copy the code
Kubernetes key value contains pod information, node information, namespace information, etc., basically contains some key information about K8S.
However, the problem is that there is too much information in this log. More than half of the information is not what we want, so we need to remove some fields that are not useful to us
Delete unnecessary fields
processors:
- drop_fields:
# Delete unnecessary fields
fields:
- host
- ecs
- log
- agent
- input
- stream
- container
ignore_missing: true
Copy the code
Meta information: @metadata cannot be deleted
Adding log Time
It can be seen from the above log information that there is no separate field about the log time. Although there is a @timestamp in it, it is not Beijing time, and what we want is the log time. There is time in message, but how can we get it and add a separate field? At this point, you need to use script, you need to write a JS script to replace.
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:
- 'the 2006-01-02 15:04:05'
- 'the 2006-01-02 15:04:05. 999'
test:
- 'the 2019-06-22 16:33:51'
Copy the code
After the addition, there will be a time field, in later use, can use this field.
Reassembles k8S source information
In fact, at this point we have completed all of our requirements, but after adding k8S information, there are many useless fields, and we can also use drop_fields to remove the useless fields, for example:
processors:
- drop_fields:
# Delete unnecessary fields
fields:
- kubernetes.pod.uid
- kubernetes.namespace_uid
- kubernetes.namespace_labels
- kubernetes.node.uid
- kubernetes.node.labels
- kubernetes.replicaset
- kubernetes.labels
- kubernetes.node.name
ignore_missing: true
Copy the code
You can also get rid of useless fields, but the structure level is the same, there are many layers nested, and the end result might look like this
{
"@timestamp": "The 2021-04-19 T07:07:36. 065 z"."@metadata": {
"beat": "filebeat"."type": "_doc"."version": "7.11.2"
},
"fields": {
"env": "dev"."k8s": "cluster-dev"
"namespace": "test-1"
},
"message": "2021-04-19 15:07:36.065 INFO 23 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration"."kubernetes": {
"container": {
"name": "test-server"."image": "hub.test.com/test/test-server:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c"
},
"node": {
"hostname": "k8s-node-09"
},
"pod": {
"name": "test-server-85545c868b-6nsvc"
},
"namespace": "test-1"}}Copy the code
In this way, when using ES to create template, there will be a lot of nested layers, and it is very inconvenient to query. In this case, we should optimize the hierarchy and continue script plug-in
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); }Copy the code
K8s = podName, nameSpace, imageAddr, hostName; drop kubernetes = kubernetes; The final result is as follows:
{
"@timestamp": "The 2021-04-19 T07:07:36. 065 z"."@metadata": {
"beat": "filebeat"."type": "_doc"."version": "7.11.2"
},
"fields": {
"env": "dev"."k8s": "cluster-dev"
"namespace": "test-1"
},
"time": "The 2021-04-19 15:07:36. 065"."message": "2021-04-19 15:07:36.065 INFO 23 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration"."k8s": {
"podName": "test-server-85545c868b-6nsvc"."nameSpace": "test-1"."imageAddr": "hub.test.com/test/test-server:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c"."hostName": "k8s-node-09"}}Copy the code
So it looks very clean. However, it is still a little tedious, because if you add a new namespace later, then you need to change the configuration again every time, which is also very tedious, so is there a better way? There are some answers.
The final optimization
Since we can create a topic from output.kafka by specifying fields, we can take advantage of this and set it like this:
output.kafka:
hosts: ["10.0.105.74:9092"."10.0.105.76:9092"."10.0.105.96:9092"]
topic: '%{[fields.k8s]}-%{[k8s.nameSpace]}' Get the namespace by injecting k8S meta information into the past
partition.round_robin:
reachable_only: true
Copy the code
We also created a K8S field under fields to distinguish between different K8S clusters, so we can optimize the configuration file to look like this
apiVersion: v1
data:
filebeat.yml: |- filebeat.inputs: - type: container enabled: true paths: - /var/log/containers/*.log multiline.pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}|^[1-9]\d*\.[1-9]\d*\.[1-9]\d*\.[1-9]\d*' multiline.negate: true multiline.match: after multiline.timeout: 10s filebeat.config.modules: path: ${path.config}/modules.d/*.yml reload.enabled: false processors: - drop_event.when.regexp: or: kubernetes.pod.name: "filebeat.*" kubernetes.pod.name: "external-dns.*" kubernetes.pod.name: "coredns.*" kubernetes.pod.name: "eureka.*" kubernetes.pod.name: "zookeeper.*" - 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:999 'test: - '2019-06-22 16:33:51' # select timestamp, timestamp, timestamp, timestamp, timestamp, timestamp, timestamp, timestamp, timestamp, timestamp, timestamp, timestamp format_time_utc tag: enable source: > function process(event) { var utc_time=event.Get("@timestamp"); var T_pos = utc_time.indexOf('T'); var Z_pos = utc_time.indexOf('Z'); var year_month_day = utc_time.substr(0, T_pos); var hour_minute_second = utc_time.substr(T_pos+1, Z_pos-T_pos-1); var new_time = year_month_day + " " + hour_minute_second; timestamp = new Date(Date.parse(new_time)); timestamp = timestamp.getTime(); timestamp = timestamp/1000; var timestamp = timestamp + 8*60*60; var bj_time = new Date(parseInt(timestamp) * 1000 + 8* 3600 * 1000); var bj_time = bj_time.toJSON().substr(0, 19).replace('T', ' '); event.Put("time_utc", bj_time); } -timestamp: field: time_UTC layouts: - '2006-01-02 15:04:05' - '2006-01-02 15:04:999 'test: - '2019-06-22 16:33:51' - add_fields: target: '' fields: env: prod - add_kubernetes_metadata: default_indexers.enabled: true default_matchers.enabled: true host: ${NODE_NAME} matchers: - logs_path: logs_path: "/var/log/containers/" - 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, k8sName: "sg-saas-pro-hbali" } event.Put("k8s", newK8s); } -drop_fields: # drop fields: - host - tags - ecs - log - prospector - agent - input - beat - offset - stream - container - kubernetes ignore_missing: Kafka: hosts: ["10.127.91.90:9092","10.127.91.91:9092","10.127.91.92:9092"] topic: '%{[k8s.k8sName]}-%{[k8s.nameSpace]}' partition.round_robin: reachable_only: truekind: ConfigMap
metadata:
name: filebeat-daemonset-config
namespace: default
Copy the code
A few tweaks have been made to keep in mind the way we created our topic %{[k8s.k8sname]}-%{[k8s.namespace]}, which will be used later when we use logstash.
conclusion
Personally, I think it can shorten the processing time of the whole process to let FileBeat do some processing in the first layer of log collection. Because most of the bottleneck lies in ES and Logstash, some time-consuming operations should be dealt with in fileBeat as far as possible. If they cannot be dealt with with logstash, another point that is easy to ignore is that, As for the simplification of log content, it can significantly reduce the log volume. I have done a test, and for the same log number, the volume without simplification reaches 20G, while the volume after optimization is less than 10G. In this way, it can be said that it is very friendly and plays a great role for the whole ES cluster.
Welcome friends to pay attention to my public number, to learn and progress together oh