本文为《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集群中