Kubernetes Events介绍(中)

上一回,历经千辛万苦终于破解了Events的姓名之谜,(Kubernetes(K8s)Events介绍(上))寻得Events真身。那么Events的身世究竟如何?根据Pod怎样才能找到对应的Events?本回将一一揭开谜底。

顺藤摸瓜

前面说到在DockerManager里定义了EventRecorder的成员,它的方法Event()、Eventf()、PastEventf()都可以用来构造Events实例,略有区别的地方是Eventf()调用了Sprintf()来输出Events message,PastEventf()可创建指定时间发生的Events。

一方面可以推测所有拥有EventsRecorder成员的Kubernetes资源定义都可以产生Events。经过暴力搜索发现,EventsRecorder主要被K8s的重要组件ControllerManager和Kubelet使用。比如,负责管理注册、注销等的NodeController,会将Node的状态变化信息记录为Events。DeploymentController会记录回滚、扩容等的Events。他们都在ControllerManager启动时被初始化并运行。与此同时Kubelet除了会记录它本身运行时的Events,比如:无法为Pod挂载卷、无法带宽整型等,还包含了一系列像docker_manager这样的小单元,它们各司其职,并记录相关Events。

另一方面在调查的时候发现,Events分为两类,并定义在kubernetes/pkg/api/types.go里,分别是EventTypeNormal和EventTypeWarning,它们分别表示该Events“仅表示信息,不会造成影响”和“可能有些地方不太对”。

在types.go里,还找到了Event数据结构的定义:

type Event struct {
    unversioned.TypeMeta `json:",inline"`
    ObjectMeta           `json:"metadata,omitempty"`

    // Required. The object that this event is about.
    InvolvedObject ObjectReference `json:"involvedObject,omitempty"`

    Reason string `json:"reason,omitempty"`
    Message string `json:"message,omitempty"`
    Source EventSource `json:"source,omitempty"`
    FirstTimestamp unversioned.Time `json:"firstTimestamp,omitempty"`
    LastTimestamp unversioned.Time `json:"lastTimestamp,omitempty"`
    Count int32 `json:"count,omitempty"`
    Type string `json:"type,omitempty"`
}

除了标准的Kubernetes资源必备的unversioned.TypeMeta和ObjectMeta成员外,Event结构体还包含了Events相关的对象、原因、内容、消息源、首次记录时间、最近记录时间、记录统计和类型。

另外还定义了EventsList的结构类型,这就是我们使用kubectl get events和GET /api/v1/namespaces/{namespace}/events获取Events列表时K8s使用的数据结构。

在Events的定义里,比较重要的有两个成员,一个是InvolvedObject, 另一个是Source。

首先,InvolvedObject表示的是这个Events所属的资源。它的类型是ObjectReference,定义如下:

type ObjectReference struct 
{
    Kind            string    `json:"kind,omitempty"`
    Namespace       string    `json:"namespace,omitempty"`
    Name            string    `json:"name,omitempty"`
    UID             types.UID `json:"uid,omitempty"`
    APIVersion      string    `json:"apiVersion,omitempty"`
    ResourceVersion string    `json:"resourceVersion,omitempty"`
    FieldPath       string    `json:"fieldPath,omitempty"`
}

ObjectReference里包含的信息足够我们唯一确定该资源实例。

然后,Source表示的是该Events的来源,它的类型是EventSource,定义如下:

type EventSource struct {    
    // Component from which the event is generated.
    Component string `json:"component,omitempty"`
    // Host name on which the event is generated.
    Host string `json:"host,omitempty"`
}

来龙无去脉

前面的研究已经为我们大致画出了Events的内部轮廓。回到开始时的问题: 既然Events的名字跟发生它的Pod的名字不同,那么kubectl describe pod时如何找到对应的Events的?我们可以大胆推测,正是通过Events定义里的InvolvedObject成员来锁定了它们之间的关系。

在前面分析kubeadm原理的文章中已经介绍过Kubernetes的命令都是利用第三方包Cobra生成的,kubectl describe也不例外,它定义在kubernetes/pkg/kubectl/cmd/describe.go里。

Kubernetes中只有部分资源可以被describe,可以称为“DescribableResources”,通过一个资源类型(unversioned.GroupKind)和对应描述器(Describer)的Map映射相关联。这些资源有我们常见的Pod、ReplicationController、Service、Node、Deloyment、ReplicaSet等等。注意这些资源里并不包含Events。

显而易见,我们需要去仔细看看Pod的Describer做了什么。PodDescriber只有一个方法,那就是Describe(),实现在kubernetes/pkg/kubectl/describe.go里。

Describe()方法首先通过namespace和name唯一确定所请求的Pod。如果出错并且ShowEvents标识为true的情况下,会根据FieldSelector找到Events,并说明“获取Pod出错,但发现了Events”。如果请求Pod未出错且ShowEvents标识为true,则通过GetReference()方法找到相关的Events。

不管哪种方式,只要找到的Events不为空,总是会通过DescribeEvents()方法将Events列表按特定格式输出。即下图:

 

20161226093445

 

这么一说就明白了,原来我们在kubectl describe pod时得到的返回的结果不仅包含了Pod的信息,还有Events的信息,它们来自的是不同的处理过程。

到此我们已经摸清了Events的来龙。具体来说对于describe pod时看到的Events,它是由Kubelet的DockerManager生成,在执行kubectl命令时通过PodDescriber进行采集。显然如果我们不执行kubectl命令的时候这些Events仍然是存在的,那么这个时候这些Events会流向何处?也就说,我们还没捋顺Events的去脉。

Kubelet在启动的时候会初始化一个EventRecorder,这个EventRecorder又被交于Kubelet上每个小manager使用,比如DockerManager。它将产生的Events的Source成员进行初始化:Componets为“kubelet”,Host为该节点的名字。

狡兔三窟

在追寻Events的去脉前,我们先来看看PodDescriber是如何采集这些Events的。

前面简单描述了PodDescriber的Describe()方法的作用,如果不够明朗,下面贴出它的源码:

func (d *PodDescriber) Describe(namespace, name string, describerSettings DescriberSettings) (string, error) {    
    pod, err := d.Pods(namespace).Get(name)    
    if err != nil {        
        // 获取Pod失败时
        if describerSettings.ShowEvents {            
            eventsInterface := d.Events(namespace)            
            selector := eventsInterface.GetFieldSelector(&name, &namespace, nil, nil)            
            options := api.ListOptions{FieldSelector: selector}            events, err2 := eventsInterface.List(options)            
            if describerSettings.ShowEvents && err2 == nil && len(events.Items) > 0 {                
                return tabbedString(func(out io.Writer) error {
                    fmt.Fprintf(out, "Pod '%v': error '%v', but found events.\n", name, err)                    
DescribeEvents(events, out)                    
                    return nil
                })
            }
        }        
        return "", err
    }    
    var events *api.EventList
    // 获取Pod成功
    if describerSettings.ShowEvents {        
        if ref, err := api.GetReference(pod); err != nil {
            glog.Errorf("Unable to construct reference to '%#v': %v", pod, err)
        } else {
            ref.Kind = ""
        // 通过Events().Search()获取
  1. 获取Pod失败时
    Events的GetFieldSelector()方法同时根据InvolvedObject的名称、命名空间、资源类型和UID生成一个FieldSelector。使用它作为ListOptions,可以选中满足这个Selector对应的资源。如果选中的Events不为空,说明“获取Pod出错,但发现了Events”的情况,并将其按照特定的格式打印。
  2. 获取Pod成功,GetReference()失败
    GetReference()根据传入的K8s资源实例,构造它的引用说明。如果执行失败,记录失败日志,并直接执行describePod(),将目前获取的结果输出到屏幕上。
  3. 获取Pod成功,GetReference()成功
    GetReference()成功后,调用Events的Search()方法,寻找关于该Pod的所有Events。最终执行describePod(),并将目前获取的结果输出到屏幕上。

当然,即使是Events的Search()方法,内部执行的仍是先GetFieldSelector()再Events List()的过程。这是因为DockerManager在生成Event的时候会调用它的makeEvent()方法(代码在上篇引用过,这里不再赘述),将Pod关联到该Events的InvolvedObject上。GetFieldSelector()返回的是一个field.Selector接口实例,它定义在kubernetes/pkg/fields/selector.go里,通过它的Matches()方法可以选中含有该Field且对应值相同的Events。

Kubernetes里,FieldSelector和LabelSelector的设计异曲同工,不同的是Field匹配的是该资源的域,比如Name、Namespace,而Label匹配的是Labels域里的键值对。

20161219151628

作者立尧微信公众号