!! 大家好,我是乔克,一个爱折腾的运维工程,一个睡觉都被自己丑醒的云原生爱好者。

作者:乔克
公众号:运维开发故事
博客:www.jokerbai.com


Kubernetes中的事件监控

随着微服务以及云原生的发展,越来越多的企业都将业务部署运行到Kubernetes中,主要是想依托Kubernetes的可扩展、可伸缩、自动化以及高稳定性来保障业务的稳定性。

然而,Kubernetes本身是一个复杂的管理系统,它既然是作为企业业务的基础设施,其本身以及运行在集群内部的业务系统对于企业来说都变得非常重要。为此,在实际工作中,我们会借助需要的监控手段来提升Kubernetes本身以及业务的可观测性,常见的有:

  • 使用cAdvisor来获取容器的资源指标,比如cpu、内存;
  • 使用kube-state-metrics来获取资源对象的状态指标,比如Deployment、Pod的状态;
  • 使用metrics-server来获取集群范围内的资源数据指标;
  • 使用node-exporter等一系列官方以及非官方的exporter来获取特定组件的指标;

在大部分的监控场景中,我们都是对特定资源进行特定监控,比如Pod,Node等。但是,在Kubernetes中还有一些场景是无法通过资源来表述的,就是说它们不是特定的资源,比如Pod调度、重启,在Kubernetes中,这类场景主要称之为事件

在Kubernetes中,存在两种事件:

  • Warning事件,事件的状态转换是在非预期的状态之间产生。
  • Normal事件,期望达到的状态和目前的状态是一致的。

在这里,我们用Pod来进行说明。当创建Pod的时候,会先进入Pending状态,然后再进入Creating状态(主要是在拉取镜像),再进去NotReady状态(主要是应用启动并且等待健康检测通过),最后进入Running状态,这整个过程就会生成Normal事件。但是,如果在运行过程中,如果Pod因为一些异常原因进入其他状态,比如节点驱逐、OOM等,在这个状态转换的过程中,就会产生Warning事件。在Kubernetes中,我们可以通过其他办法来保障业务的稳定性,比如为了避免Pod调度到一个节点或者同可用区等而采用亲和性以及反亲和性调度,为了避免节点驱逐导致某个单个Pod不可用而采用的PDB等,也许某个Warning事件并不会对整个业务的稳定性带来致命的影响,但是如果能够通过监控事件的手段来感知集群的某个状态变化是有助于进行查漏补缺的,也有助于我们感知一些容易忽略的问题。

在Kubernetes中,所有事件都通过事件系统记录到APIServer中,并且最终存入在Etcd中,我们可以通过API或者kubectl进行查看,比如:


也可以查看某个对象的事件,比如:


事件包含了时间、类型、对象、原因以及描述等,通过事件我们能够知道应用的部署、调度、运行、停止等整个生命周期,也能通过事件去了解系统中正在发生的一些异常。在Kubernetes各个组件的源码中都会定义该组件可能会触发的事件类型,比如在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"
)
......
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

Kubernetes事件最终是存在Etcd中,默认只保存1小时,由于Etcd本身并不支持一些复杂的分析操作,只能被动地存在Etcd中,并不支持主动推送到其他系统,通常情况下只能手动去查看。

在实际中,我们对Kubernetes事件还有其他的需求,比如:

  • 希望对异常的事件做告警处理;
  • 希望查询更长事件的历史事件;
  • 希望对集群事件进行灵活的统计分析;

为此,我们需要单独对Kubernetes事件进行收集,以便适用于查询以及告警。

在社区中,有很多工具来做事件的收集以及告警,我常用的两个工具是:

  • kube-eventer:阿里云推出的事件收集工具;
  • kube-event-exporter:Github上另外一个事件收集工作;

在实际工作中,可以选择使用其中一个,基本都能满足收集以及告警功能。在这里,我将同时使用上面两个插件,用kube-eventer来进行告警,用kube-event-exporter将事件收集到ES中进行查看和分析。

使用kube-eventer进行事件告警

kube-eventer的告警通道可以是企业微信、钉钉以及webhook。可以根据需要进行选择,每个组件的具体使用方法在项目的docs/en目录中,这里选择使用webhook将告警发送到企业微信中。

(1)首先需要在企业微信群里创建一个webhook机器人,然后获取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_configmap=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发生时间: {{ .LastTimestamp }}\n详细信息: {{ .Message }}"}}
kind: ConfigMap
metadata:
  name: custom-webhook-body
  namespace: monitoring
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.

在webhook的配置中增加了level=Warning,表示只要Warning事件才会告警通知,除此之外,还可以通过namespaces字段来过来需要告警的命名空间,通过kinds字段来过滤需要告警的对象,比如只需要发送Node的Warning事件,则可以写成level=warning&kinds=Node。再比如,如果不想产生非常多的告警风暴,只发送某些特定原因的告警,比如系统OOM的事件,可以增加reason=SystemOOM等待。

当kube-eventer的Pod启动完成后,企业微信即可收到满足条件的事件告警,比如:


使用kube-event-exporter收集集群事件

上面使用kube-eventer进行事件告警,本质上并没有存储历史事件,而实际中可能需要查询历史事件并且对其做一些事件分析,而ES是常用于进行内容收集并通过kibana进行查看和分析,所以这里我们将使用kube-event-exporter收集Kubernetes事件到ES中。

kube-event-exporter可以直接将事件存入ES,也可以将事件发送到kafka,然后再通过Logstash消费Kafka中的数据将其存入ES。在这里,我基于环境现状,将事件发送给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:
            - -conf=/data/config.yaml
          volumeMounts:
            - mountPath: /data
              name: cfg
      volumes:
        - name: cfg
          configMap:
            name: event-exporter-cfg
  selector:
    matchLabels:
      app: event-exporter
      version: v1
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.

当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"
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.

部署之前,先在ES中创建template,可以在kibana中的dev tools中进行操作,语句如下:

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": {}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.

然后再在Kubernetes集群中部署Logstash。然后就可以在Kibana上查看收集到的事件了,如下:


只要数据有了,不论是查询还是做分析,都变得简单容易了。比如最简单得统计今天事件原因为Unhealthy所发生的总次数,可以在Kibana中创建图表,如下:


以上就是在Kubernetes中对集群事件进行收集和告警,这是站在巨人的肩膀上直接使用。在企业中还可以对其进行二次开放以将功能更丰富,比如支持对事件告警增加开关,可以任意开启或者关闭某个事件告警。

链接

[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



最后,求关注。如果你还想看更多优质原创文章,欢迎关注我们的公众号「运维开发故事」。

如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!

你还可以把我的公众号设为「星标」,这样当公众号文章更新时,你会在第一时间收到推送消息,避免错过我的文章更新。




我是 乔克,《运维开发故事》公众号团队中的一员,一线运维农民工,云原生实践者,这里不仅有硬核的技术干货,还有我们对技术的思考和感悟,欢迎关注我们的公众号,期待和你一起成长!