本文首发于 https://robberphex.com/error-reporting-with-kubernetes-events/
组内有维护一个Kubernetes Webhook,可以拦截pod的创建请求,并做一些修改(比如添加环境变量、添加init-container等)。
业务逻辑本身很简单,但是如果过程中产生错误,就很难处理。要不直接阻止pod创建,那么就有可能导致应用无法启动。要么忽略业务逻辑,那么就会导致静默失败,谁也不知道这儿出现了一个错误。
于是,朴素的想法就是接入告警系统,但这会导致当前组件和具体的告警系统耦合起来。
在Kubernetes中,有Event机制,可以做到把一些事件,比如警告、错误等信息记录下来,就比较适合这个场景。
什么是Kubernetes中的事件/Event?
事件(Event)是 Kubernetes 中众多资源对象中的一员,通常用来记录集群内发生的状态变更,大到集群节点异常,小到 Pod 启动、调度成功等等。
比如我们Describe一个pod,就能看到这个pod对应的事件:
kubectl describe pod sc-b-68867c5dcb-sf9hn
可以看到,从调度、到启动、再到这个pod最终拉取镜像失败,都会通过event的方式记录下来。
我们来看下一个Event的结构:
$ k get events -o json | jq .items[10]
{
"apiVersion": "v1",
"count": 1,
"eventTime": null,
"firstTimestamp": "2021-12-04T17:02:14Z",
"involvedObject": {
"apiVersion": "v1",
"fieldPath": "spec.containers{sc-b}",
"kind": "Pod",
"name": "sc-b-68867c5dcb-sf9hn",
"namespace": "default",
"resourceVersion": "322554830",
"uid": "24df4a07-f41e-42c2-ba26-d90940303b00"
},
"kind": "Event",
"lastTimestamp": "2021-12-04T17:02:14Z",
"message": "Error: ErrImagePull",
"metadata": {
"creationTimestamp": "2021-12-04T17:02:14Z",
"name": "sc-b-68867c5dcb-sf9hn.16bd9bf933d60437",
"namespace": "default",
"resourceVersion": "1197082",
"selfLink": "/api/v1/namespaces/default/events/sc-b-68867c5dcb-sf9hn.16bd9bf933d60437",
"uid": "f928ff2d-c618-44a6-bf5a-5b0d3d20e95e"
},
"reason": "Failed",
"reportingComponent": "",
"reportingInstance": "",
"source": {
"component": "kubelet",
"host": "eci"
},
"type": "Warning"
}
可以看到,一个event,比较重要的几个字端:
- type - 事件类型,可以是Warning、Normal、Error等
- reason - 事件的原因,可以是Failed、Scheduled、Started、Completed等
- message - 事件的描述信息
- involvedObject - 这个事件对应的资源对象,可以是Pod、Node等
- source - 这个事件的来源,可以是kubelet、kube-apiserver等
- firstTimestamp,lastTimestamp - 这个事件的第一次和最后一次发生的时间
基于这些信息,我们就可以做一些集群级别的监控、告警了,比如阿里云的ACK,就会将Event发送到SLS中,然后根据对应的规则来做告警。
如何上报事件
前面说了什么是Kubernetes中的Event,但是我们必须要上报事件,才能让Kubernetes集群知道这个事件发生了,从而做出后续的监控和告警。
如何访问Kubernetes API
上报事件的第一步是访问Kubernetes API,这个API是基于Restful API的,Kubernetes也基于这个API,包装了SDK,直接可以用。
通过SDK连接到Kubernetes API,有两种方式:
第一种是通过kubeconfg文件来访问(从外部访问),第二种是通过serviceaccount访问(从Pod访问)。
为了简单起见,我们使用第一种方式作为例子:
package main
import (
"flag"
"fmt"
"path/filepath"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
func main() {
var kubeconfig *string
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
versionInfo, err := clientset.ServerVersion()
if err != nil {
panic(err)
}
fmt.Printf("Version: %#v\n", versionInfo)
}
运行这段代码,就可以连接到集群中,可以获取到Kubernetes Server版本了:
Version: &version.Info{Major:"1", Minor:"18+", GitVersion:"v1.18.8-aliyun.1", GitCommit:"27f24d2", GitTreeState:"", BuildDate:"2021-08-19T10:00:16Z", GoVersion:"go1.13.15", Compiler:"gc", Platform:"linux/amd64"}
如何创建、上报事件
在上面的例子中,有了clientset对象,我们现在就要依赖这个对象,在Kuberentes集群中创建一个事件:
now := time.Now()
message := "test message at " + now.Format(time.RFC3339)
// 命名空间为default
_, err = clientset.CoreV1().Events("default").Create(&apiv1.Event{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-",
},
Type: "Warning",
Message: message,
Reason: "OnePilotFail",
FirstTimestamp: metav1.NewTime(now),
LastTimestamp: metav1.NewTime(now),
InvolvedObject: apiv1.ObjectReference{
Namespace: "default",
Kind: "Deployment",
Name: "sc-b",
},
})
fmt.Printf("create event with err: %v\n", err)
在上面的例子中,我们在命名空间default下创建了一个名为test-
开头的Event,这个Event的类型是Warning。
我们也可以看下最终产生出来的Event:
kubectl get events -o json | jq .items[353]
{
"apiVersion": "v1",
"eventTime": null,
"firstTimestamp": "2021-12-04T17:27:06Z",
"involvedObject": {
"kind": "Deployment",
"name": "sc-b",
"namespace": "default"
},
"kind": "Event",
"lastTimestamp": "2021-12-04T17:27:06Z",
"message": "test message at 2021-12-05T01:27:06+08:00",
"metadata": {
"creationTimestamp": "2021-12-04T17:27:06Z",
"generateName": "test-",
"name": "test-vvjzp",
"namespace": "default",
"resourceVersion": "1198057",
"selfLink": "/api/v1/namespaces/default/events/test-vvjzp",
"uid": "f2bcdd1c-442f-4f61-921a-e18637ee5871"
},
"reason": "OnePilotFail",
"reportingComponent": "",
"reportingInstance": "",
"source": {},
"type": "Warning"
}
这样,关心对应事件的人,比如运维人员的人,就可以依据这些信息做监控、告警了。
使用场景
和业务事件不同,Kubernetes事件是集群中的资源,关注的人也多是集群的维护者。
所以这种事件上报机制,还是比较适合一些基础组件来使用,可以让集群维护者了解到当前集群的状态。
如果需要有更加灵活的告警、监控,那么可以使用更加贴近业务的、规则更加丰富的时间、告警系统。