Kubernetes中的事件收集以及監(jiān)控告警
Kubernetes中的事件監(jiān)控
隨著微服務(wù)以及云原生的發(fā)展,越來越多的企業(yè)都將業(yè)務(wù)部署運行到Kubernetes中,主要是想依托Kubernetes的可擴(kuò)展、可伸縮、自動化以及高穩(wěn)定性來保障業(yè)務(wù)的穩(wěn)定性。
然而,Kubernetes本身是一個復(fù)雜的管理系統(tǒng),它既然是作為企業(yè)業(yè)務(wù)的基礎(chǔ)設(shè)施,其本身以及運行在集群內(nèi)部的業(yè)務(wù)系統(tǒng)對于企業(yè)來說都變得非常重要。為此,在實際工作中,我們會借助需要的監(jiān)控手段來提升Kubernetes本身以及業(yè)務(wù)的可觀測性,常見的有:
- 使用cAdvisor來獲取容器的資源指標(biāo),比如cpu、內(nèi)存;
- 使用kube-state-metrics來獲取資源對象的狀態(tài)指標(biāo),比如Deployment、Pod的狀態(tài);
- 使用metrics-server來獲取集群范圍內(nèi)的資源數(shù)據(jù)指標(biāo);
- 使用node-exporter等一系列官方以及非官方的exporter來獲取特定組件的指標(biāo);
在大部分的監(jiān)控場景中,我們都是對特定資源進(jìn)行特定監(jiān)控,比如Pod,Node等。但是,在Kubernetes中還有一些場景是無法通過資源來表述的,就是說它們不是特定的資源,比如Pod調(diào)度、重啟,在Kubernetes中,這類場景主要稱之為事件
。
在Kubernetes中,存在兩種事件:
- Warning事件,事件的狀態(tài)轉(zhuǎn)換是在非預(yù)期的狀態(tài)之間產(chǎn)生。
- Normal事件,期望達(dá)到的狀態(tài)和目前的狀態(tài)是一致的。
在這里,我們用Pod來進(jìn)行說明。當(dāng)創(chuàng)建Pod的時候,會先進(jìn)入Pending狀態(tài),然后再進(jìn)入Creating狀態(tài)(主要是在拉取鏡像),再進(jìn)去NotReady狀態(tài)(主要是應(yīng)用啟動并且等待健康檢測通過),最后進(jìn)入Running狀態(tài),這整個過程就會生成Normal事件。但是,如果在運行過程中,如果Pod因為一些異常原因進(jìn)入其他狀態(tài),比如節(jié)點驅(qū)逐、OOM等,在這個狀態(tài)轉(zhuǎn)換的過程中,就會產(chǎn)生Warning事件。在Kubernetes中,我們可以通過其他辦法來保障業(yè)務(wù)的穩(wěn)定性,比如為了避免Pod調(diào)度到一個節(jié)點或者同可用區(qū)等而采用親和性以及反親和性調(diào)度,為了避免節(jié)點驅(qū)逐導(dǎo)致某個單個Pod不可用而采用的PDB等,也許某個Warning事件并不會對整個業(yè)務(wù)的穩(wěn)定性帶來致命的影響,但是如果能夠通過監(jiān)控事件的手段來感知集群的某個狀態(tài)變化是有助于進(jìn)行查漏補(bǔ)缺的,也有助于我們感知一些容易忽略的問題。
在Kubernetes中,所有事件都通過事件系統(tǒng)記錄到APIServer中,并且最終存入在Etcd中,我們可以通過API或者kubectl進(jìn)行查看,比如:
也可以查看某個對象的事件,比如:
事件包含了時間、類型、對象、原因以及描述等,通過事件我們能夠知道應(yīng)用的部署、調(diào)度、運行、停止等整個生命周期,也能通過事件去了解系統(tǒng)中正在發(fā)生的一些異常。在Kubernetes各個組件的源碼中都會定義該組件可能會觸發(fā)的事件類型,比如在kubelet的源碼中定義了許多的事件類型,如下:
package events
// Container event reason list
const (
CreatedContainer = "Created"
StartedContainer = "Started"
FailedToCreateContainer = "Failed"
FailedToStartContainer = "Failed"
KillingContainer = "Killing"
PreemptContainer = "Preempting"
BackOffStartContainer = "BackOff"
ExceededGracePeriod = "ExceededGracePeriod"
)
// Pod event reason list
const (
FailedToKillPod = "FailedKillPod"
FailedToCreatePodContainer = "FailedCreatePodContainer"
FailedToMakePodDataDirectories = "Failed"
NetworkNotReady = "NetworkNotReady"
)
......
Kubernetes事件最終是存在Etcd中,默認(rèn)只保存1小時,由于Etcd本身并不支持一些復(fù)雜的分析操作,只能被動地存在Etcd中,并不支持主動推送到其他系統(tǒng),通常情況下只能手動去查看。
在實際中,我們對Kubernetes事件還有其他的需求,比如:
- 希望對異常的事件做告警處理;
- 希望查詢更長事件的歷史事件;
- 希望對集群事件進(jìn)行靈活的統(tǒng)計分析;
為此,我們需要單獨對Kubernetes事件進(jìn)行收集,以便適用于查詢以及告警。
在社區(qū)中,有很多工具來做事件的收集以及告警,我常用的兩個工具是:
- kube-eventer:阿里云推出的事件收集工具;
- kube-event-exporter:Github上另外一個事件收集工作;
在實際工作中,可以選擇使用其中一個,基本都能滿足收集以及告警功能。在這里,我將同時使用上面兩個插件,用kube-eventer來進(jìn)行告警,用kube-event-exporter將事件收集到ES中進(jìn)行查看和分析。
使用kube-eventer進(jìn)行事件告警
kube-eventer的告警通道可以是企業(yè)微信、釘釘以及webhook??梢愿鶕?jù)需要進(jìn)行選擇,每個組件的具體使用方法在項目的docs/en
目錄中,這里選擇使用webhook將告警發(fā)送到企業(yè)微信中。
(1)首先需要在企業(yè)微信群里創(chuàng)建一個webhook機(jī)器人,然后獲取webhook地址。
(2)在Kubernetes集群中部署kube-eventer。
# cat kube-eventer.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
name: kube-eventer
name: kube-eventer
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: kube-eventer
template:
metadata:
labels:
app: kube-eventer
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
dnsPolicy: ClusterFirstWithHostNet
serviceAccount: kube-eventer
containers:
- image: registry.aliyuncs.com/acs/kube-eventer:v1.2.7-ca03be0-aliyun
name: kube-eventer
command:
- "/kube-eventer"
- "--source=kubernetes:https://kubernetes.default.svc.cluster.local"
## .e.g,dingtalk sink demo
#- --sink=dingtalk:[your_webhook_url]&label=[your_cluster_id]&level=[Normal or Warning(default)]
#- --sink=webhook:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=07055f32-a04e-4ad7-9cb1-d22352769e1c&level=Warning&label=oa-k8s
- --sink=webhook:http://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=888888-888-8888-8888-d35c52ff2e0b&level=Warning&header=Content-Type=application/json&custom_body_cnotallow=custom-webhook-body&custom_body_configmap_namespace=monitoring&method=POST
env:
# If TZ is assigned, set the TZ value as the time zone
- name: TZ
value: "Asia/Shanghai"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: true
- name: zoneinfo
mountPath: /usr/share/zoneinfo
readOnly: true
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 500m
memory: 250Mi
volumes:
- name: localtime
hostPath:
path: /etc/localtime
- name: zoneinfo
hostPath:
path: /usr/share/zoneinfo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-eventer
rules:
- apiGroups:
- ""
resources:
- events
- configmaps
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kube-eventer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-eventer
subjects:
- kind: ServiceAccount
name: kube-eventer
namespace: monitoring
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-eventer
namespace: monitoring
---
apiVersion: v1
data:
content: >-
{"msgtype": "text","text": {"content": "集群事件告警\n事件級別: {{ .Type }}\n名稱空間: {{ .InvolvedObject.Namespace }}\n事件類型: {{ .InvolvedObject.Kind }}\n事件對象: {{ .InvolvedObject.Name }}\n事件原因: {{ .Reason }}\n發(fā)生時間: {{ .LastTimestamp }}\n詳細(xì)信息: {{ .Message }}"}}
kind: ConfigMap
metadata:
name: custom-webhook-body
namespace: monitoring
在webhook的配置中增加了level=Warning,表示只要Warning事件才會告警通知,除此之外,還可以通過namespaces字段來過來需要告警的命名空間,通過kinds字段來過濾需要告警的對象,比如只需要發(fā)送Node的Warning事件,則可以寫成level=warning&kinds=Node。再比如,如果不想產(chǎn)生非常多的告警風(fēng)暴,只發(fā)送某些特定原因的告警,比如系統(tǒng)OOM的事件,可以增加reasnotallow=SystemOOM
等待。
當(dāng)kube-eventer的Pod啟動完成后,企業(yè)微信即可收到滿足條件的事件告警,比如:
使用kube-event-exporter收集集群事件
上面使用kube-eventer進(jìn)行事件告警,本質(zhì)上并沒有存儲歷史事件,而實際中可能需要查詢歷史事件并且對其做一些事件分析,而ES是常用于進(jìn)行內(nèi)容收集并通過kibana進(jìn)行查看和分析,所以這里我們將使用kube-event-exporter收集Kubernetes事件到ES中。
kube-event-exporter可以直接將事件存入ES,也可以將事件發(fā)送到kafka,然后再通過Logstash消費Kafka中的數(shù)據(jù)將其存入ES。在這里,我基于環(huán)境現(xiàn)狀,將事件發(fā)送給Kafka,然后再消費收集到ES。
(1)部署kube-event-exporter
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: monitoring
name: event-exporter
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: event-exporter
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
namespace: monitoring
name: event-exporter
---
apiVersion: v1
kind: ConfigMap
metadata:
name: event-exporter-cfg
namespace: monitoring
data:
config.yaml: |
logLevel: error
logFormat: json
route:
routes:
- match:
- receiver: "kafka"
drop:
- kind: "Service"
receivers:
- name: "kafka"
kafka:
clientId: "kubernetes"
topic: "kubenetes-event"
brokers:
- "192.168.100.50:9092"
- "192.168.100.51:9092"
- "192.168.100.52:9092"
compressionCodec: "snappy"
layout: #optional
kind: "{{ .InvolvedObject.Kind }}"
namespace: "{{ .InvolvedObject.Namespace }}"
name: "{{ .InvolvedObject.Name }}"
reason: "{{ .Reason }}"
message: "{{ .Message }}"
type: "{{ .Type }}"
timestamp: "{{ .GetTimestampISO8601 }}"
cluster: "sda-pre-center"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: event-exporter
namespace: monitoring
spec:
replicas: 1
template:
metadata:
labels:
app: event-exporter
version: v1
spec:
serviceAccountName: event-exporter
containers:
- name: event-exporter
image: ghcr.io/resmoio/kubernetes-event-exporter:latest
imagePullPolicy: IfNotPresent
args:
- -cnotallow=/data/config.yaml
volumeMounts:
- mountPath: /data
name: cfg
volumes:
- name: cfg
configMap:
name: event-exporter-cfg
selector:
matchLabels:
app: event-exporter
version: v1
當(dāng)kube-event-exporter的Pod啟動過后,可以在kafka中查看到收集的事件,如下:
(2)部署logstash將事件存入ES
kind: Deployment
apiVersion: apps/v1
metadata:
name: kube-event-logstash
namespace: log
labels:
app: kube-event-logstash
spec:
replicas: 1
selector:
matchLabels:
app: kube-event-logstash
template:
metadata:
creationTimestamp: null
labels:
app: kube-event-logstash
annotations:
kubesphere.io/restartedAt: '2024-02-22T09:03:36.215Z'
spec:
volumes:
- name: kube-event-logstash-pipeline-config
configMap:
name: kube-event-logstash-pipeline-config
defaultMode: 420
containers:
- name: kube-event-logstash
image: 'logstash:7.8.0'
env:
- name: XPACK_MONITORING_ELASTICSEARCH_HOSTS
value: 'http://192.168.100.100:8200'
- name: XPACK_MONITORING_ELASTICSEARCH_USERNAME
value: jokerbai
- name: XPACK_MONITORING_ELASTICSEARCH_PASSWORD
value: JeA9BiAgnNRzVrp5JRVQ4vYX
- name: PIPELINE_ID
value: kube-event-logstash
- name: KAFKA_SERVER
value: '192.168.100.50:9092,192.168.100.51:9092,192.168.100.52:9092'
- name: ES_SERVER
value: 'http://192.168.100.100:8200'
- name: ES_USER_NAME
value: jokerbai
- name: ES_USER_PASSWORD
value: JeA9BiAgnNRzVrp5JRVQ4vYX
- name: NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: PIPELINE_BATCH_SIZE
value: '4000'
- name: PIPELINE_BATCH_DELAY
value: '100'
- name: PIPELINE_WORKERS
value: '4'
- name: LS_JAVA_OPTS
value: '-Xms2g -Xmx3500m'
resources:
limits:
cpu: '2'
memory: 4Gi
requests:
cpu: '2'
memory: 4Gi
volumeMounts:
- name: kube-event-logstash-pipeline-config
mountPath: /usr/share/logstash/pipeline
livenessProbe:
tcpSocket:
port: 9600
initialDelaySeconds: 39
timeoutSeconds: 5
periodSeconds: 30
successThreshold: 1
failureThreshold: 2
readinessProbe:
tcpSocket:
port: 9600
initialDelaySeconds: 39
timeoutSeconds: 5
periodSeconds: 30
successThreshold: 1
failureThreshold: 2
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
securityContext: {}
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node/category
operator: In
values:
- app
schedulerName: default-scheduler
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
---
kind: ConfigMap
apiVersion: v1
metadata:
name: kube-event-logstash-pipeline-config
namespace: log
data:
logstash.conf: |-
input {
kafka {
id => "kafka_plugin_id"
bootstrap_servers => "${KAFKA_SERVER}"
client_id => "logstash"
group_id => "logstash"
decorate_events => true
topics => ["kubenetes-event"]
codec => json {
charset => "UTF-8"
}
}
}
output {
elasticsearch {
hosts => "${ES_SERVER}"
user => "${ES_USER_NAME}"
password => "${ES_USER_PASSWORD}"
index => "kubernetes-event-%{+YYYY.MM}"
manage_template => false
template_name => "kubernetes-event"
}
}
部署之前,先在ES中創(chuàng)建template,可以在kibana中的dev tools中進(jìn)行操作,語句如下:
PUT _template/kubernetes-event
{
"index_patterns" : [
"*kubernetes-event*"
],
"settings": {
"index": {
"highlight": {
"max_analyzed_offset": "10000000"
},
"number_of_shards": "2",
"number_of_replicas": "0"
}
},
"mappings": {
"properties": {
"cluster": {
"type": "keyword"
},
"kind": {
"type": "keyword"
},
"message": {
"type": "text"
},
"name": {
"type": "keyword"
},
"namespace": {
"type": "keyword"
},
"reason": {
"type": "keyword"
},
"type": {
"type": "keyword"
},
"timestamp": {
"type": "keyword"
}
}
},
"aliases": {}
}
然后再在Kubernetes集群中部署Logstash。然后就可以在Kibana上查看收集到的事件了,如下:
只要數(shù)據(jù)有了,不論是查詢還是做分析,都變得簡單容易了。比如最簡單得統(tǒng)計今天事件原因為Unhealthy所發(fā)生的總次數(shù),可以在Kibana中創(chuàng)建圖表,如下:
以上就是在Kubernetes中對集群事件進(jìn)行收集和告警,這是站在巨人的肩膀上直接使用。在企業(yè)中還可以對其進(jìn)行二次開放以將功能更豐富,比如支持對事件告警增加開關(guān),可以任意開啟或者關(guān)閉某個事件告警。
鏈接
[1] https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/events/event.go?spm=a2c6h.12873639.article-detail.7.c8585c576FGh7o&file=event.go。
[2] https://github.com/AliyunContainerService/kube-eventer。
[3] https://github.com/resmoio/kubernetes-event-exporter。