此係列文章一共分為三部分,分為filebeat部分,logstash部分,es部分。這裡會按照每天幾百億條的資料量來考慮,去設計、部署、優化這個日誌系統,來最大限度的利用資源,並達到一個最優的效能。本篇主要講解
filebeat
這一塊
介紹
版本:filebeat-7.12.0
是關於k8s的日誌採集,部署方式是採用DaemonSet
的方式,採集時按照k8s叢集的namespace
進行分類,然後根據namespace
的名稱建立不同的topic
到kafka中
k8s日誌檔案說明
一般情況下,容器中的日誌在輸出到標準輸出(stdout)時,會以*-json.log
的命名方式儲存在/var/lib/docker/containers
目錄中,當然如果修改了docker
的資料目錄,那就是在修改後的資料目錄中了,例如:
# tree /data/docker/containers
/data/docker/containers
├── 009227c00e48b051b6f5cb65128fd58412b845e0c6d2bec5904f977ef0ec604d
│ ├── 009227c00e48b051b6f5cb65128fd58412b845e0c6d2bec5904f977ef0ec604d-json.log
│ ├── checkpoints
│ ├── config.v2.json
│ ├── hostconfig.json
│ └── mounts
這裡能看到,有這麼個檔案: /data/docker/containers/container id/*-json.log
,然後k8s預設會在/var/log/containers
和/var/log/pods
目錄中會生成這些日誌檔案的軟連線,如下所示:
cattle-node-agent-tvhlq_cattle-system_agent-8accba2d42cbc907a412be9ea3a628a90624fb8ef0b9aa2bc6ff10eab21cf702.log
etcd-k8s-master01_kube-system_etcd-248e250c64d89ee6b03e4ca28ba364385a443cc220af2863014b923e7f982800.log
然後,會看到這個目錄下存在了此宿主機上的所有容器日誌,檔案的命名方式為:
[podName]_[nameSpace]_[depoymentName]-[containerId].log
上面這個是deployment
的命名方式,其他的會有些不同,例如:DaemonSet
,StatefulSet
等,不過所有的都有一個共同點,就是
*_[nameSpace]_*.log
到這裡,知道這個特性,就可以往下來看Filebeat
的部署和配置了。
filebeat部署
部署採用的DaemonSet
方式進行,這裡沒有啥可說的,參照官方文件直接部署即可
---
apiVersion: v1
data:
filebeat.yml: |-
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-basic_*log
fields:
log_topic: bim5d-basic
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-cost_*log
fields:
log_topic: bim5d-cost
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_giot-integration-test_*log
fields:
log_topic: giot-integration-test
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_infra-integration-test_*log
fields:
log_topic: infra-integration-test
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_gss-integration-test_*log
fields:
log_topic: gss-integration-test
env: 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"]
topic: '%{[fields.log_topic]}'
partition.round_robin:
reachable_only: true
kind: 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
啟動的話直接kubectl apply -f
啟動即可,部署不是本篇的重點,這裡不做過多介紹。
官方部署參考:https://raw.githubusercontent.com/elastic/beats/7.12/deploy/kubernetes/filebeat-kubernetes.yaml
filebeat配置簡單介紹
這裡先簡單介紹下filebeat
的配置結構
filebeat.inputs:
filebeat.config.modules:
processors:
output.xxxxx:
結構大概是這麼個結構,完整的資料流向簡單來說就是下面這個圖:
前面也說了,我是根據根據名稱空間做分類,每一個名稱空間就是一個topic,如果要收集多個叢集,同樣也是使用名稱空間做分類,只不過topic的命名就需要加個k8s的叢集名,這樣方便去區分了,那既然是通過名稱空間來獲取日誌,那麼在配置inputs
的時候就需要通過寫正則將指定名稱空間下的日誌檔案取出,然後讀取,例如:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-basic_*log
fields:
log_topic: bim5d-basic
env: dev
這裡我的名稱空間為bim5d-basic
,然後通過正則*_bim5d-basic_*log
來獲取帶有此名稱空間名的日誌檔案,隨後又加了個自定義欄位,方便下面建立topic
時使用。
這裡是寫了一個名稱空間,如果有多個,就排開寫就行了,如下所示:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-basic_*log
fields:
log_topic: bim5d-basic
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-cost_*log
fields:
log_topic: bim5d-cost
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_giot-integration-test_*log
fields:
log_topic: giot-integration-test
env: dev
這種寫法有一個不好的地方就是,如果名稱空間比較多,那麼整個配置就比較多,維護起來可能是個問題,所以建議將filebeat的配置檔案通過版本控制來管理起來
注意: 日誌的型別,要設定成
container
上面說了通過名稱空間建立topic
,我這裡加了一個自定義的欄位log_topic
,就是後面的topic
的名稱,但是這裡有很多的名稱空間,那在輸出的時候,如何動態去建立呢?
output.kafka:
hosts: ["10.0.105.74:9092","10.0.105.76:9092","10.0.105.96:9092"]
topic: '%{[fields.log_topic]}'
partition.round_robin:
reachable_only: true
注意這裡的寫法:%{[fields.log_topic]}
那麼完整的配置如下所示:
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-basic_*log
fields:
log_topic: bim5d-basic
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-cost_*log
fields:
log_topic: bim5d-cost
env: dev
- type: container
enabled: true
paths:
- /var/log/containers/*_giot-integration-test_*log
fields:
log_topic: giot-integration-test
env: dev
output.kafka:
hosts: ["10.0.105.74:9092","10.0.105.76:9092","10.0.105.96:9092"]
topic: '%{[fields.log_topic]}'
partition.round_robin:
reachable_only: true
如果是不對日誌做任何處理,到這裡就結束了,但是這樣又視乎在檢視日誌的時候少了點什麼? 沒錯!到這裡你僅僅知道日誌內容,和該日誌來自於哪個名稱空間,但是你不知道該日誌屬於哪個服務,哪個pod,甚至說想檢視該服務的映象地址等,但是這些資訊在我們上面的配置方式中是沒有的,所以需要進一步的添磚加瓦。
這個時候就用到了一個配置項,叫做: processors
, 看下官方的解釋
You can define processors in your configuration to process events before they are sent to the configured output
簡單來說就是處理日誌
下面來重點講一下這個地方,非常有用和重要
filebeat的processors使用介紹
新增K8s的基本資訊
在採集k8s的日誌時,如果按照上面那種配置方式,是沒有關於pod的一些資訊的,例如:
- Pod Name
- Pod UID
- Namespace
- Labels
- 等等等等
那麼如果想新增這些資訊,就要使用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-04-19T07:07:36.065Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.11.2"
},
"log": {
"offset": 377708,
"file": {
"path": "/var/log/containers/geip-gateway-test-85545c868b-6nsvc_geip-function-test_geip-gateway-test-server-885412c0a8af6bfa7b3d7a341c3a9cb79a85986965e363e87529b31cb650aec4.log"
}
},
"fields": {
"env": "dev",
"log_topic": "geip-function-test"
},
"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": "packages.gxxxxxn.com/docxxxxxp/gexxxxxxxst:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c"
},
"id": "885412c0a8af6bfa7b3d7a341c3a9cb79a85986965e363e87529b31cb650aec4",
"runtime": "docker"
},
"kubernetes": {
"labels": {
"pod-template-hash": "85545c868b",
"app": "geip-gateway-test"
},
"container": {
"name": "geip-gateway-test-server",
"image": "packages.xxxxxxx.com/dxxxxxp/gxxxxxxxxt: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": "gxxxxxxxxst-85545c868b-6nsvc",
"uid": "1e678b63-fb3c-40b5-8aad-892596c5bd4d"
},
"namespace": "geip-function-test",
"replicaset": {
"name": "geip-gateway-test-85545c868b"
}
}
}
可以看到kubernetes這個key的value有關於pod的資訊,還有node的一些資訊,還有namespace資訊等,基本上關於k8s的一些關鍵資訊都包含了,非常的多和全。
但是,問題又來了,這一條日誌資訊有點太多了,有一半多不是我們想要的資訊,所以,我們需要去掉一些對於我們沒有用的欄位
刪除不必要的欄位
processors:
- drop_fields:
#刪除的多餘欄位
fields:
- host
- ecs
- log
- agent
- input
- stream
- container
ignore_missing: true
元資訊:@metadata是不能刪除的
新增日誌時間
通過上面的日誌資訊,可以看到是沒有單獨的一個關於日誌時間的欄位的,雖然裡面有一個@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
的欄位,在後面使用的時候,就可以使用這個欄位了。
重新拼接k8s源資訊
實際上,到這個程度就已經完成了我們的所有需求了,但是新增完k8s的資訊之後,多了很多無用的欄位,而我們如果想去掉那些沒用的欄位用drop_fields
也可以,例如下面這種寫法:
processors:
- drop_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
這樣寫也可以把無用的欄位去掉,但是結構層級沒有變化,巢狀了很多層,最終結果可能變成這個樣子
{
"@timestamp": "2021-04-19T07:07:36.065Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.11.2"
},
"fields": {
"env": "dev",
"log_topic": "geip-function-test"
},
"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": "geip-gateway-test-server",
"image": "packages.xxxxxxx.com/dxxxxxp/gxxxxxxxxt:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c"
},
"node": {
"hostname": "k8s-node-09"
},
"pod": {
"name": "gxxxxxxxxst-85545c868b-6nsvc"
},
"namespace": "geip-function-test"
}
}
這樣在後面使用es建立template的時候,就會巢狀好多層,查詢起來也很不方便,既然這樣那我們就優化下這個層級結構,繼續script
這個外掛
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);
}
這裡單獨建立了一個欄位k8s
,欄位裡包含:podName
, nameSpace
, imageAddr
, hostName
等關鍵資訊,最後再把kubernetes
這個欄位drop掉就可以了。最終結果如下:
{
"@timestamp": "2021-04-19T07:07:36.065Z",
"@metadata": {
"beat": "filebeat",
"type": "_doc",
"version": "7.11.2"
},
"fields": {
"env": "dev",
"log_topic": "geip-function-test"
},
"time": "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": "gxxxxxxxxst-85545c868b-6nsvc",
"nameSpace": "geip-function-test",
"imageAddr": "packages.xxxxxxx.com/dxxxxxp/gxxxxxxxxt:3.3.1-ent-release-SNAPSHOT.20210402191241_87c9b1f841c",
"hostName": "k8s-node-09"
}
}
這樣看起來就非常清爽了。
下面貼一些一個完整的示例:
configmap參考
apiVersion: v1
data:
filebeat.yml: |-
filebeat.inputs:
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-basic_*log
fields:
namespace: bim5d-basic
k8s: cluster01
env: prod
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
- type: container
enabled: true
paths:
- /var/log/containers/*_bim5d-cost_*log
fields:
namespace: bim5d-cost
k8s: cluster01
env: prod
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:
- add_kubernetes_metadata:
#新增k8s描述欄位
default_indexers.enabled: true
default_matchers.enabled: true
host: ${NODE_NAME}
matchers:
- logs_path:
logs_path: "/var/log/containers/"
- drop_event.when.regexp:
or:
kubernetes.pod.name: "filebeat.*"
kubernetes.pod.name: "external-dns.*"
kubernetes.pod.name: "coredns.*"
- drop_fields:
#刪除的多餘欄位
fields:
- host
- tags
- ecs
- log
- prospector
- agent
- input
- beat
- offset
- stream
- container
- kubernetes
ignore_missing: true
- timestamp:
field: start_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'
output.kafka:
hosts: ["10.0.105.74:9092","10.0.105.76:9092","10.0.105.96:9092"]
topic: '%{[fields.k8s]}-%{[fields.namespace]}'
partition.round_robin:
reachable_only: true
kind: ConfigMap
metadata:
name: filebeat-daemonset-config
namespace: default
總結
個人認為讓filebeat在收集日誌的第一層做一些處理,能縮短整個過程的處理時間,因為瓶頸大多在es和logstash,所以一些耗時的操作儘量在filebeat這塊去處理,如果處理不了在使用logstash,另外一個非常容易忽視的一點就是,對於日誌內容的簡化,這樣能顯著降低日誌的體積,我做過測試,同樣的日誌條數,未做簡化的體積達到20G,而優化後的體積不到10G,這樣的話對於整個es叢集來說可以說是非常的友好和作用巨大了。
另外,可以通過版本控制,來管理filebeat的配置檔案,這樣在維護時也能有個記錄,變化管理。
歡迎各位朋友關注我的公眾號,來一起學習進步哦