Client-go之tools(EventBroadcaster)

本文为《Kubernetes 源码剖析》读书笔记,书籍简介:http://www.broadview.com.cn/book/6104

一 简介

Kubernetes的事件(Event)是一种资源对象(Resource Object),用于展示集群内发生的情况,Kubernetes系统中的各个组件会将运行时发生的各种事件上报给Kubernetes API Server。例如,调度器做了什么决定,某些Pod为什么被从节点中驱逐。可以通过kubectl get event kubectldescribe pod命令显示事件,查看Kubernetes集群中发生了哪些事件。执行这些命令后,默认情况下只会显示最近(1小时内)发生的事件。
注意:此处的Event事件是Kubernetes所管理的Event资源对象,而非Etcd集群监控机制产生的回调事件,需要注意区分。
由于Kubernetes的事件是一种资源对象,因此它们存储在Kubernetes API Server的Etcd集群中。为避免磁盘空间被填满,故强制执行保留策略:在最后一次的事件发生后,删除1小时之前发生的事件。Kubernetes系统以Pod资源为核心,Deployment、StatefulSet、ReplicaSet、DaemonSet、CronJob等,最终都会创建出Pod。因此Kubernetes事件也是围绕Pod进行的,在Pod生命周期内的关键步骤中都会产生事件消息。Event资源数据结构体定义在core资源组下,代码示例如下:

//事件是集群中某处事件的报告。
type Event struct {
	metav1.TypeMeta `json:",inline"`
	//标准对象的元数据。 更多信息:https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
	metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
	//此事件涉及的对象。
	InvolvedObject ObjectReference `json:"involvedObject" protobuf:"bytes,2,opt,name=involvedObject"`
	//这应该是一个简短的,机器可理解的字符串,该字符串给出了转换为对象当前状态的原因。
// TODO:提供格式的确切规范。
// +可选
	Reason string `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"`
//此操作状态的可读描述。
// TODO:决定最大长度。
// +可选
	Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`
//报告此事件的组件。 应该是机器可以理解的短字符串。
// +可选
	Source EventSource `json:"source,omitempty" protobuf:"bytes,5,opt,name=source"`
	//首次记录事件的时间。 (服务器收到时间以TypeMeta表示。)
// +可选
	FirstTimestamp metav1.Time `json:"firstTimestamp,omitempty" protobuf:"bytes,6,opt,name=firstTimestamp"`
	//最近一次记录此事件的时间。
	// +optional
	LastTimestamp metav1.Time `json:"lastTimestamp,omitempty" protobuf:"bytes,7,opt,name=lastTimestamp"`
	// 此事件发生的次数。
	// +optional
	Count int32 `json:"count,omitempty" protobuf:"varint,8,opt,name=count"`
	// 此事件的类型(正常,警告),将来可能会添加新的类型
	// +optional
	Type string `json:"type,omitempty" protobuf:"bytes,9,opt,name=type"`
	//首次观察到此事件的时间。
	// +optional
	EventTime metav1.MicroTime `json:"eventTime,omitempty" protobuf:"bytes,10,opt,name=eventTime"`
	// 有关此事件表示的事件系列的数据,如果是单例事件,则为nil。
	// +optional
	Series *EventSeries `json:"series,omitempty" protobuf:"bytes,11,opt,name=series"`
	// 针对对象已采取/未采取什么措施。
	// +optional
	Action string `json:"action,omitempty" protobuf:"bytes,12,opt,name=action"`
	// 可选的辅助对象,用于更复杂的操作。
	// +optional
	Related *ObjectReference `json:"related,omitempty" protobuf:"bytes,13,opt,name=related"`
	// 发出此事件的控制器的名称,例如 `kubernetes.io / kubelet`。
	// +optional
	ReportingController string `json:"reportingComponent" protobuf:"bytes,14,opt,name=reportingComponent"`
	// 控制器实例的ID,例如 `kubelet-xyzf`。
	// +optional
	ReportingInstance string `json:"reportingInstance" protobuf:"bytes,15,opt,name=reportingInstance"`
}

event分为 警告跟正常事件

// 事件类型的有效值(将来可能会添加新类型)  Type
const (
	// 仅提供信息,不会造成任何问题
	EventTypeNormal string = "Normal"
	//这些事件是为了警告某些问题
	EventTypeWarning string = "Warning"
)

二 工作流程介绍

在这里插入图片描述

1.EventRecorder处理过程

event方法的实现是

func (recorder *recorderImpl) Event(object runtime.Object, eventtype, reason, message string) {
	recorder.generateEvent(object, nil, metav1.Now(), eventtype, reason, message)
}
func (recorder *recorderImpl) generateEvent(object runtime.Object, annotations map[string]string, timestamp metav1.Time, eventtype, reason, message string) {
	//获得object的上级管理者 比如pod 的上级是
	ref, err := ref.GetReference(recorder.scheme, object)
	if err != nil {
		klog.Errorf("Could not construct reference to: '%#v' due to: '%v'. Will not report event: '%v' '%v' '%v'", object, err, eventtype, reason, message)
		return
	}
	...
		recorder.Action(watch.Added, event)
	}()
}

在这里插入图片描述

2.EventBroadcaster处理过程

//动作将给定事件分配给所有观察者。
//这里吧对应写入了管道里 
func (m *Broadcaster) Action(action EventType, obj runtime.Object) {
	m.incoming <- Event{action, obj}
}
//循环从m.incoming接收并分发给所有观察者。
//既然回放在管理里面,这里就会有一个从管道中不断读取的
func (m *Broadcaster) loop() {
	// 故意不在此处捕获崩溃。 是的,如果watch.Broadcaster中存在错误,请降低该过程。
	for event := range m.incoming {
		if event.Type == internalRunFunctionMarker {
			event.Object.(functionFakeRuntimeObject)()
			continue
		}
		//分发
		m.distribute(event)
	}
	m.closeAll()
	m.distributing.Done()
}
//分发将事件发送给所有观察者。 阻塞。
func (m *Broadcaster) distribute(event Event) {
	m.lock.Lock()
	defer m.lock.Unlock()
	if m.fullChannelBehavior == DropIfChannelFull {
	//这里也是通过管道老传递的 result  chan Event
		for _, w := range m.watchers {
			select {
			case w.result <- event:
			case <-w.stopped:
			default: // 如果无法将事件排队,请不要阻塞。
			}
		}
	} else {
		for _, w := range m.watchers {
			select {
			case w.result <- event:
			case <-w.stopped:
			}
		}
	}
}

3.broadcasterWatcher处理过程

func (f *FakeWatcher) ResultChan() <-chan Event {
	return f.result
}
// StartEventWatcher开始将从此EventBroadcaster接收的事件发送到给定的事件处理函数。如果需要,返回值可以忽略或用于停止记录。
func (eventBroadcaster *eventBroadcasterImpl) StartEventWatcher(eventHandler func(*v1.Event)) watch.Interface {
	watcher := eventBroadcaster.Watch()
	go func() {
		defer utilruntime.HandleCrash()
		//通过上看可以看出ResultChan 是拿到了f.result 也就是说当在这个管道写入的时候这里就会获取到
		for watchEvent := range watcher.ResultChan() {
			event, ok := watchEvent.Object.(*v1.Event)
			if !ok {
				// This is all local, so there's no reason this should
				// ever happen.
				continue
			}
			eventHandler(event)
		}
	}()
	return watcher
}

// StartRecordingToSink开始将从指定的eventBroadcaster接收到的事件发送到给定的接收器。 如果需要,可以忽略返回值或将其用于停止记录。
func (eventBroadcaster *eventBroadcasterImpl) StartRecordingToSink(sink EventSink) watch.Interface {
	eventCorrelator := NewEventCorrelatorWithOptions(eventBroadcaster.options)
	return eventBroadcaster.StartEventWatcher(
		func(event *v1.Event) {
			recordToSink(sink, event, eventCorrelator, eventBroadcaster.sleepDuration)
		})
}
func NewEventCorrelatorWithOptions(options CorrelatorOptions) *EventCorrelator {...}

// StartLogging开始将从此EventBroadcaster接收到的事件发送到给定的日志记录功能。 如果需要,可以忽略返回值或将其用于停止记录。
func (eventBroadcaster *eventBroadcasterImpl) StartLogging(logf func(format string, args ...interface{})) watch.Interface {
	return eventBroadcaster.StartEventWatcher(
		func(e *v1.Event) {
			logf("Event(%#v): type: '%v' reason: '%v' %v", e.InvolvedObject, e.Type, e.Reason, e.Message)
		})
}

// EventSink知道如何存储事件(由client.Client实现。)EventSink必须遵守将嵌入在“ event”中的名称空间。 假定EventSink将返回与pkg / client的REST客户端相同类型的错误。
type EventSink interface {
	Create(event *v1.Event) (*v1.Event, error)
	Update(event *v1.Event) (*v1.Event, error)
	Patch(oldEvent *v1.Event, data []byte) (*v1.Event, error)
}
// EventCorrelator处理所有传入事件并执行分析,以免使系统不堪重负。
//它可以过滤所有传入事件,以查看是否应从进一步处理中过滤掉该事件。
//它可以聚合经常发生的类似事件,以保护系统免受用户难以区分的垃圾邮件事件的侵害。
//它执行重复数据删除以确保将多次观察到的事件压缩为单个事件,并增加计数。
type EventCorrelator struct {
	// the function to filter the event
	//过滤事件的功能
	filterFunc EventFilterFunc
	// the object that performs event aggregation
	//执行事件聚合的对象
	aggregator *EventAggregator
	// the object that observes events as they come through
	//观察事件经过的对象
	logger *eventLogger
}

4.分析

● EventRecorder:事件(Event)生产者,也称为事件记录器Kubernetes系统组件通过EventRecorder记录关键性事件。
● EventBroadcaster:事件(Event)消费者,也称为事件广播器。EventBroadcaster消费EventRecorder记录的事件并将其分发给目前所有已连接的broadcasterWatcher。分发过程有两种机制,分别是非阻塞(Non-Blocking)分发机制和阻塞(Blocking)分发机制。
● broadcasterWatcher:观察者(Watcher)管理,用于定义事件的处理方式,例如上报事件至Kubernetes API Server。

三 各部分详解

1.EventRecorder

EventRecorder拥有如下4种记录方法

// EventRecorder知道如何代表EventSource记录事件。
type EventRecorder interface {
	//事件根据给定的信息构造一个事件,并将其放入发送队列中。
	//“对象”是此事件涉及的对象。 事件将进行引用-或您也可以直接将引用传递给该对象。
	//此事件的“类型”,可以是“正常”,“警告”之一。
	//可能在将来的“原因”中添加新类型,这是生成此事件的原因。
	//“原因”应该简短而独特; 它应该采用UpperCamelCase格式(以大写字母开头)。
	//“原因”将用于自动处理事件,因此,想象人们编写switch语句来处理事件。
	//您想使它变得容易。“消息”旨在使人可读。
	//结果事件将在与引用对象相同的名称空间中创建。
	Event(object runtime.Object, eventtype, reason, message string)
	// Eventf类似于Event,但是message域使用Sprintf。
	Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{})
	//PastEventf和Eventf一样,但是具有指定事件的“时间戳”字段的选项。
	PastEventf(object runtime.Object, timestamp metav1.Time, eventtype, reason, messageFmt string, args ...interface{})
	//AnnotatedEventf就像eventf一样,但是附加了注释
	AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{})
}

以Event方法为例,记录当前发生的事件,Event→recorder.generateEvent→recorder.Action

func (m *Broadcaster) Action(action EventType, obj runtime.Object) {
	m.incoming <- Event{action, obj}
}

Action函数通过goroutine实现异步操作,该函数将事件写入m.incommit Chan中,完成事件生产过程。

2.EventBroadcaster

EventBroadcaster消费EventRecorder记录的事件并将其分发给目前所有已连接的broadcasterWatcher

func NewBroadcaster() EventBroadcaster {
	return &eventBroadcasterImpl{
		Broadcaster:   watch.NewBroadcaster(maxQueuedEvents, watch.DropIfChannelFull),
		sleepDuration: defaultSleepDuration,
	}
}

在实例化过程中,会通过watch.NewBroadcaster函数在内部启动goroutine(即m.loop函数)来监控m.incoming,并将监控的事件通过m.distribute函数分发给所有已连接的broadcasterWatcher。分发过程有两种机制,分别是非阻塞分发机制和阻塞分发机制。在非阻塞分发机制下使用DropIfChannelFull标识,在阻塞分发机制下使用WaitIfChannelFull标识,默认为DropIfChannelFull标识

// distribute sends event to all watchers. Blocking.
func (m *Broadcaster) distribute(event Event) {
	m.lock.Lock()
	defer m.lock.Unlock()
	if m.fullChannelBehavior == DropIfChannelFull {
		for _, w := range m.watchers {
			select {
			case w.result <- event:
			case <-w.stopped:
			default: // Don't block if the event can't be queued.
			}
		}
	} else {
		for _, w := range m.watchers {
			select {
			case w.result <- event:
			case <-w.stopped:
			}
		}
	}
}

在分发过程中,DropIfChannelFull标识位于select多路复用中,使用default关键字做非阻塞分发,当w.result缓冲区满的时候,事件会丢失。WaitIfChannelFull标识也位于select多路复用中,没有default关键字,当w.result缓冲区满的时候,分发过程会阻塞并等待。注意:Kubernetes中的事件与其他的资源不同,它有一个很重要的特性,那就是它可以丢失。因为随着Kubernetes系统集群规模越来越大,上报的事件越来越多,每次上报事件都要对Etcd集群进行读/写,这样会给Etcd集群带来很大的压力。如果某个事件丢失了,并不会影响集群的正常工作,事件的重要性远低于集群的稳定性,所以可以看到源码中当w.result缓冲区满的时候,在非阻塞分发机制下事件会丢失

3.broadcasterWatcher

broadcasterWatcher是每个Kubernetes系统组件自定义处理事件的方式。例如,上报事件至Kubernetes API Server

● StartLogging:将事件写入日志中。
● StartRecordingToSink:将事件上报至Kubernetes API Server并存储至Etcd集群。
以kube-scheduler组件为例,该组件作为一个broadcasterWatcher,通过StartLogging函数将事件输出至klog stdout标准输出,通过StartRecordingToSink函数将关键性事件上报至Kubernetes APIServer

if _, err := cc.Client.Discovery().ServerResourcesForGroupVersion(eventsv1beta1.SchemeGroupVersion.String()); err == nil {
	cc.Broadcaster = events.NewBroadcaster(&events.EventSinkImpl{Interface: cc.EventClient.Events("")})
	cc.Recorder = cc.Broadcaster.NewRecorder(scheme.Scheme, cc.ComponentConfig.SchedulerName)
} else {
	recorder := cc.CoreBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: cc.ComponentConfig.SchedulerName})
	cc.Recorder = record.NewEventRecorderAdapter(recorder)
}

StartLogging和StartRecordingToSink函数依赖于StartEventWatcher函数,该函数内部运行了一个goroutine,用于不断监控EventBroadcaster来发现事件并调用相关函数对事件进行处理。下面重点介绍一下StartRecordingToSink函数,kube-scheduler组件将v1core.EventSinkImpl作为上报事件的自定义函数。上报事件有3种方法,分别是Create(即Post方法)、Update(即Put方法)、Patch(Patch方法)。以Create方法为例,Create→e.Interface.CreateWithEventNamespace代码示例如下:

// CreateWithEventNamespace创建一个新事件。 
//返回服务器返回的事件或错误的副本。 
//从事件推论出要在其中创建事件的名称空间。 
//它必须与该事件客户端的名称空间匹配,或者此事件客户端必须使用“”名称空间创建。
func (e *events) CreateWithEventNamespace(event *v1beta1.Event) (*v1beta1.Event, error) {
	if e.ns != "" && event.Namespace != e.ns {
		return nil, fmt.Errorf("can't create an event with namespace '%v' in namespace '%v'", event.Namespace, e.ns)
	}
	result := &v1beta1.Event{}
	err := e.client.Post().
		NamespaceIfScoped(event.Namespace, len(event.Namespace) > 0).
		Resource("events").
		Body(event).
		Do().
		Into(result)
	return result, err
}

上报过程通过RESTClient发送Post请求,将事件发送至Kubernetes API Server,最终存储在Etcd集群中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来自万古的忧伤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值