暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

kubernetes Event 源码解析

原创 Spark 2021-12-16
222

简介: 众所周知,event在Kubernetes中起着举足轻重的作用,本文将为大家深入探讨一下Kubernetes中的事件机制。

51.png


镜像下载、域名解析、时间同步请点击 阿里巴巴开源镜像站

我们通过 kubectl describe [资源] 命令,可以在看到Event输出,并且经常依赖event进行问题定位,从event中可以分析整个POD的运行轨迹,为服务的客观测性提供数据来源,由此可见,event在Kubernetes中起着举足轻重的作用。

11.png


event并不只是kubelet中都有的,关于event的操作被封装在client-go/tools/record包,我们完全可以在写入自定义的event。
现在让我们来一步步揭开event的面纱。

一、Event定义

其实event也是一个资源对象,并且通过apiserver将event存储在etcd中,所以我们也可以通过 kubectl get event 命令查看对应的event对象。
以下是一个event的yaml文件:


  1. apiVersion: v1


  2. count: 1


  3. eventTime: null


  4. firstTimestamp: "2020-03-02T13:08:22Z"


  5. involvedObject:


  6. apiVersion: v1


  7. kind: Pod


  8. name: example-foo-d75d8587c-xsf64


  9. namespace: default


  10. resourceVersion: "429837"


  11. uid: ce611c62-6c1a-4bd8-9029-136a1adf7de4


  12. kind: Event


  13. lastTimestamp: "2020-03-02T13:08:22Z"


  14. message: Pod sandbox changed, it will be killed and re-created.


  15. metadata:


  16. creationTimestamp: "2020-03-02T13:08:30Z"


  17. name: example-foo-d75d8587c-xsf64.15f87ea1df862b64


  18. namespace: default


  19. resourceVersion: "479466"


  20. selfLink: /api/v1/namespaces/default/events/example-foo-d75d8587c-xsf64.15f87ea1df862b64


  21. uid: 9fe6f72a-341d-4c49-960b-e185982d331a


  22. reason: SandboxChanged


  23. reportingComponent: ""


  24. reportingInstance: ""


  25. source:


  26. component: kubelet


  27. host: minikube


  28. type: Normal

主要字段说明:

  • involvedObject: 触发event的资源类型
  • lastTimestamp:最后一次触发的时间
  • message:事件说明
  • metadata :event的元信息,name,namespace等
  • reason:event的原因
  • source:上报事件的来源,比如kubelet中的某个节点
  • type:事件类型,Normal或Warning

event字段定义可以看这里:types.go#L5078
接下来我们来看看,整个event是如何下入的。

二、写入事件

1、这里以kubelet为例,看看是如何进行事件写入的
2、文中代码以Kubernetes 1.17.3为例进行分析

先以一幅图来看下整个的处理流程

12.png


创建操作事件的客户端:
kubelet/app/server.go#L461


  1. // makeEventRecorder sets up kubeDeps.Recorder if it's nil. It's a no-op otherwise.


  2. func makeEventRecorder(kubeDeps *kubelet.Dependencies, nodeName types.NodeName) {


  3. if kubeDeps.Recorder != nil {


  4. return


  5. }


  6. //事件广播


  7. eventBroadcaster := record.NewBroadcaster()


  8. //创建EventRecorder


  9. kubeDeps.Recorder = eventBroadcaster.NewRecorder(legacyscheme.Scheme, v1.EventSource{Component: componentKubelet, Host: string(nodeName)})


  10. //发送event至log输出


  11. eventBroadcaster.StartLogging(klog.V(3).Infof)


  12. if kubeDeps.EventClient != nil {


  13. klog.V(4).Infof("Sending events to api server.")


  14. //发送event至apiserver


  15. eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: kubeDeps.EventClient.Events("")})


  16. } else {


  17. klog.Warning("No api server defined - no events will be sent to API server.")


  18. }


  19. }

通过 makeEventRecorder 创建了 EventRecorder 实例,这是一个事件广播器,通过它提供了StartLogging和StartRecordingToSink两个事件处理函数,分别将event发送给log和apiserver。
NewRecorder创建了 EventRecorder 的实例,它提供了 Event ,Eventf 等方法供事件记录。

EventBroadcaster

我们来看下EventBroadcaster接口定义:event.go#L113


  1. // EventBroadcaster knows how to receive events and send them to any EventSink, watcher, or log.


  2. type EventBroadcaster interface {


  3. //


  4. StartEventWatcher(eventHandler func(*v1.Event)) watch.Interface


  5. StartRecordingToSink(sink EventSink) watch.Interface


  6. StartLogging(logf func(format string, args ...interface{})) watch.Interface


  7. NewRecorder(scheme *runtime.Scheme, source v1.EventSource) EventRecorder


  8. Shutdown()


  9. }

具体实现是通过 eventBroadcasterImpl struct来实现了各个方法。
其中StartLogging 和 StartRecordingToSink 其实就是完成了对事件的消费,EventRecorder实现对事件的写入,中间通过channel实现了生产者消费者模型。

EventRecorder

我们先来看下EventRecorder 接口定义:event.go#L88,提供了一下4个方法


  1. // EventRecorder knows how to record events on behalf of an EventSource.


  2. type EventRecorder interface {


  3. // Event constructs an event from the given information and puts it in the queue for sending.


  4. // 'object' is the object this event is about. Event will make a reference-- or you may also


  5. // pass a reference to the object directly.


  6. // 'type' of this event, and can be one of Normal, Warning. New types could be added in future


  7. // 'reason' is the reason this event is generated. 'reason' should be short and unique; it


  8. // should be in UpperCamelCase format (starting with a capital letter). "reason" will be used


  9. // to automate handling of events, so imagine people writing switch statements to handle them.


  10. // You want to make that easy.


  11. // 'message' is intended to be human readable.


  12. //


  13. // The resulting event will be created in the same namespace as the reference object.


  14. Event(object runtime.Object, eventtype, reason, message string)


  15. // Eventf is just like Event, but with Sprintf for the message field.


  16. Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{})


  17. // PastEventf is just like Eventf, but with an option to specify the event's 'timestamp' field.


  18. PastEventf(object runtime.Object, timestamp metav1.Time, eventtype, reason, messageFmt string, args ...interface{})


  19. // AnnotatedEventf is just like eventf, but with annotations attached


  20. AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{})


  21. }

主要参数说明:

  • object 对应event资源定义中的 involvedObject
  • eventtype 对应event资源定义中的type,可选Normal,Warning.
  • reason :事件原因
  • message :事件消息

我们来看下当我们调用 Event(object runtime.Object, eventtype, reason, message string) 的整个过程。
发现最终都调用到了 generateEvent 方法:event.go#L316


  1. func (recorder *recorderImpl) generateEvent(object runtime.Object, annotations map[string]string, timestamp metav1.Time, eventtype, reason, message string) {


  2. .....


  3. event := recorder.makeEvent(ref, annotations, eventtype, reason, message)


  4. event.Source = recorder.source


  5. go func() {


  6. // NOTE: events should be a non-blocking operation


  7. defer utilruntime.HandleCrash()


  8. recorder.Action(watch.Added, event)


  9. }()


  10. }

最终事件在一个 goroutine 中通过调用 recorder.Action 进入处理,这里保证了每次调用event方法都是非阻塞的。
其中 makeEvent 的作用主要是构造了一个event对象,事件name根据InvolvedObject中的name加上时间戳生成:

注意看:对于一些非namespace资源产生的event,event的namespace是default


  1. func (recorder *recorderImpl) makeEvent(ref *v1.ObjectReference, annotations map[string]string, eventtype, reason, message string) *v1.Event {


  2. t := metav1.Time{Time: recorder.clock.Now()}


  3. namespace := ref.Namespace


  4. if namespace == "" {


  5. namespace = metav1.NamespaceDefault


  6. }


  7. return &v1.Event{


  8. ObjectMeta: metav1.ObjectMeta{


  9. Name: fmt.Sprintf("%v.%x", ref.Name, t.UnixNano()),


  10. Namespace: namespace,


  11. Annotations: annotations,


  12. },


  13. InvolvedObject: *ref,


  14. Reason: reason,


  15. Message: message,


  16. FirstTimestamp: t,


  17. LastTimestamp: t,


  18. Count: 1,


  19. Type: eventtype,


  20. }


  21. }

进一步跟踪Action方法,apimachinery/blob/master/pkg/watch/mux.go#L188:23


  1. // Action distributes the given event among all watchers.


  2. func (m *Broadcaster) Action(action EventType, obj runtime.Object) {


  3. m.incoming <- Event{action, obj}


  4. }

将event写入到了一个channel里面。
注意:
这个Action方式是apimachinery包中的方法,因为实现的sturt recorderImpl
将 *watch.Broadcaster 作为一个匿名struct,并且在 NewRecorder 进行 Broadcaster 赋值,这个Broadcaster其实就是 eventBroadcasterImpl 中的Broadcaster
到此,基本清楚了event最终被写入到了 Broadcaster 中的 incoming channel中,下面看下是怎么进行消费的。

三、消费事件

在 makeEventRecorder 调用的 StartLogging 和 StartRecordingToSink 其实就是完成了对事件的消费。

  • StartLogging直接将event输出到日志
  • StartRecordingToSink将事件写入到apiserver

两个方法内部都调用了 StartEventWatcher 方法,并且传入一个 eventHandler 方法对event进行处理


  1. func (e *eventBroadcasterImpl) StartEventWatcher(eventHandler func(*v1.Event)) watch.Interface {


  2. watcher := e.Watch()


  3. go func() {


  4. defer utilruntime.HandleCrash()


  5. for watchEvent := range watcher.ResultChan() {


  6. event, ok := watchEvent.Object.(*v1.Event)


  7. if !ok {


  8. // This is all local, so there's no reason this should


  9. // ever happen.


  10. continue


  11. }


  12. eventHandler(event)


  13. }


  14. }()


  15. return watcher


  16. }

其中 watcher.ResultChan 方法就拿到了事件,这里是在一个goroutine中通过func (m *Broadcaster) loop() ==>func (m *Broadcaster) distribute(event Event) 方法调用将event又写入了broadcasterWatcher.result
主要看下 StartRecordingToSink 提供的的eventHandler, recordToSink 方法:


  1. func recordToSink(sink EventSink, event *v1.Event, eventCorrelator *EventCorrelator, sleepDuration time.Duration) {


  2. // Make a copy before modification, because there could be multiple listeners.


  3. // Events are safe to copy like this.


  4. eventCopy := *event


  5. event = &eventCopy


  6. result, err := eventCorrelator.EventCorrelate(event)


  7. if err != nil {


  8. utilruntime.HandleError(err)


  9. }


  10. if result.Skip {


  11. return


  12. }


  13. tries := 0


  14. for {


  15. if recordEvent(sink, result.Event, result.Patch, result.Event.Count > 1, eventCorrelator) {


  16. break


  17. }


  18. tries++


  19. if tries >= maxTriesPerEvent {


  20. klog.Errorf("Unable to write event '%#v' (retry limit exceeded!)", event)


  21. break


  22. }


  23. // Randomize the first sleep so that various clients won't all be


  24. // synced up if the master goes down.


  25. // 第一次重试增加随机性,防止 apiserver 重启的时候所有的事件都在同一时间发送事件


  26. if tries == 1 {


  27. time.Sleep(time.Duration(float64(sleepDuration) * rand.Float64()))


  28. } else {


  29. time.Sleep(sleepDuration)


  30. }


  31. }


  32. }

其中event被经过了一个 eventCorrelator.EventCorrelate(event) 方法做预处理,主要是聚合相同的事件(避免产生的事件过多,增加 etcd 和 apiserver 的压力,也会导致查看 pod 事件很不清晰)
下面一个for循环就是在进行重试,最大重试次数是12次,调用 recordEvent 方法才真正将event写入到了apiserver。

事件处理

我们来看下EventCorrelate方法:


  1. // EventCorrelate filters, aggregates, counts, and de-duplicates all incoming events


  2. func (c *EventCorrelator) EventCorrelate(newEvent *v1.Event) (*EventCorrelateResult, error) {


  3. if newEvent == nil {


  4. return nil, fmt.Errorf("event is nil")


  5. }


  6. aggregateEvent, ckey := c.aggregator.EventAggregate(newEvent)


  7. observedEvent, patch, err := c.logger.eventObserve(aggregateEvent, ckey)


  8. if c.filterFunc(observedEvent) {


  9. return &EventCorrelateResult{Skip: true}, nil


  10. }


  11. return &EventCorrelateResult{Event: observedEvent, Patch: patch}, err


  12. }

分别调用了 aggregator.EventAggregate , logger.eventObserve , filterFunc 三个方法,分别作用是:

1、aggregator.EventAggregate:聚合event,如果在最近 10 分钟出现过 10 个相似的事件(除了 message 和时间戳之外其他关键字段都相同的事件),aggregator 会把它们的 message 设置为 (combined from similar events)+event.Message
2、logger.eventObserve:它会把相同的事件以及包含 aggregator 被聚合了的相似的事件,通过增加 Count 字段来记录事件发生了多少次。
3、filterFunc: 这里实现了一个基于令牌桶的限流算法,如果超过设定的速率则丢弃,保证了apiserver的安全。

我们主要来看下aggregator.EventAggregate方法:


  1. func (e *EventAggregator) EventAggregate(newEvent *v1.Event) (*v1.Event, string) {


  2. now := metav1.NewTime(e.clock.Now())


  3. var record aggregateRecord


  4. // eventKey is the full cache key for this event


  5. //eventKey 是将除了时间戳外所有字段结合在一起


  6. eventKey := getEventKey(newEvent)


  7. // aggregateKey is for the aggregate event, if one is needed.


  8. //aggregateKey 是除了message和时间戳外的字段结合在一起,localKey 是message


  9. aggregateKey, localKey := e.keyFunc(newEvent)


  10. // Do we have a record of similar events in our cache?


  11. e.Lock()


  12. defer e.Unlock()


  13. //从cache中根据aggregateKey查询是否存在,如果是相同或者相类似的事件会被放入cache中


  14. value, found := e.cache.Get(aggregateKey)


  15. if found {


  16. record = value.(aggregateRecord)


  17. }


  18. //判断上次事件产生的时间是否超过10分钟,如何操作则重新生成一个localKeys集合(集合中存放message)


  19. maxInterval := time.Duration(e.maxIntervalInSeconds) * time.Second


  20. interval := now.Time.Sub(record.lastTimestamp.Time)


  21. if interval > maxInterval {


  22. record = aggregateRecord{localKeys: sets.NewString()}


  23. }


  24. // Write the new event into the aggregation record and put it on the cache


  25. //将locakKey也就是message放入集合中,如果message相同就是覆盖了


  26. record.localKeys.Insert(localKey)


  27. record.lastTimestamp = now


  28. e.cache.Add(aggregateKey, record)


  29. // If we are not yet over the threshold for unique events, don't correlate them


  30. //判断localKeys集合中存放的类似事件是否超过10个,


  31. if uint(record.localKeys.Len()) < e.maxEvents {


  32. return newEvent, eventKey


  33. }


  34. // do not grow our local key set any larger than max


  35. record.localKeys.PopAny()


  36. // create a new aggregate event, and return the aggregateKey as the cache key


  37. // (so that it can be overwritten.)


  38. eventCopy := &v1.Event{


  39. ObjectMeta: metav1.ObjectMeta{


  40. Name: fmt.Sprintf("%v.%x", newEvent.InvolvedObject.Name, now.UnixNano()),


  41. Namespace: newEvent.Namespace,


  42. },


  43. Count: 1,


  44. FirstTimestamp: now,


  45. InvolvedObject: newEvent.InvolvedObject,


  46. LastTimestamp: now,


  47. //这里会对message加个前缀:(combined from similar events):


  48. Message: e.messageFunc(newEvent),


  49. Type: newEvent.Type,


  50. Reason: newEvent.Reason,


  51. Source: newEvent.Source,


  52. }


  53. return eventCopy, aggregateKey


  54. }

aggregator.EventAggregate方法中其实就是判断了通过cache和localKeys判断事件是否相似,如果最近 10 分钟出现过 10 个相似的事件就合并并加上前缀,后续通过logger.eventObserve方法进行count累加,如果message也相同,肯定就是直接count++。

四、总结

event处理的整个流程基本就是这样,我们可以概括为以下几点,也可以结合文中的图对比一起来看:

1、创建 EventRecorder 对象,通过其提供的 Event 等方法,创建好event对象
2、将创建出来的对象发送给 EventBroadcaster 中的channel中
3、EventBroadcaster 通过后台运行的goroutine,从管道中取出事件,并广播给提前注册好的handler处理
4、当输出log的handler收到事件就直接打印事件
5、当 EventSink handler收到处理事件就通过预处理之后将事件发送给apiserver
6、其中预处理包含三个动作,1、限流 2、聚合 3、计数
7、apiserver收到事件处理之后就存储在etcd中

回顾event的整个流程,可以看到event并不是保证100%事件写入(从预处理的过程来看),这样做是为了后端服务etcd的可用性,因为event事件在整个集群中产生是非常频繁的,尤其在服务不稳定的时候,而相比Deployment,Pod等其他资源,又没那么的重要。所以这里做了个取舍。

本文转自: kubernetes Event 源码解析-阿里云开发者社区

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论