浅析Kubelet如何上报状态
在K8S集群中,由运行在每个节点的Kubelet定期上报心跳到ApiServer,以此来判断Node是否存活,若Node超过一定时间没有上报心跳,则该节点的状态会被设置为NotReady,同时运行在该节点的容器状态也会被设置为Unknown状态。
在K8S中,一个Node的状态包含以下信息:
Addresses
Condition
Capacity
Info
Address主要包含以下几个字段:
HostName:即主机名,可以通过kubelet的–hostname-override参数进行覆盖。
ExternalIP:通常是可以外部路由的Node IP(从集群外可访问)。
InternalIP:通常是仅可以在集群内部路由的Node IP地址。
Condition主要包含以下内容:
Ready:如果节点是健康的并已经准备好接收接收Pod则为True;False表示节点不健康而且不能接收Pod;Unknow则表示节点控制器最近node-monitor-grace-period期间(默认40秒)没有收到节点的消息。
DiskPressure:True表示节点存在磁盘压力,即磁盘可用量低,否则为False。
MemoryPressure:True表示节点存在内存压力,即内存可用量低,否则为False。
PIDPressure:True表示节点存在进程压力,即节点上进程过多;否则为False。
Capacity:述节点上的可用资源:CPU、内存和可以调度到节点上的 Pod 的个数上限。
capacity:表示节点拥有的资源总量。
allocatable:表示节点上可供普通Pod消耗的资源量
Info:描述节点的一般信息,如内核版本、Kubernetes 版本(kubelet 和 kube-proxy 版本)、 容器运行时详细信息,以及 节点使用的操作系统。
当一个Node处于非Ready状态超过Pod-eviction-timeout的值(默认为5分钟,在kube-controller-manager中定义),kube-controller-manager不会force delete pod,运行在该节点的Pod会一直处于Terminating或者Unknow状态,直到Node从集群中删除,或者kubelet状态变为Ready,在Node NotReady期间,不同的控制器处理方式不同,依次如下:
Daemonset:Pod的状态变为Nodelost
Deployment:先变为Nodelost,然后变成Unknown,最后会在其他正常的节点重新创建。
StaticPod:先变为Nodelost,然后一直处于Unknown(staticPod即为/etc/kubernetes/manifest下的yaml文件)
Statefulset:先变为Nodelost,然后一直处于Unknown
当Kubelet再次变为Ready状态时,以上控制器的处理方式如下:
Daemonset:Pod不会重新创建,旧Pod的状态直接变为Running。
Deployment:则会将运行在该节点的旧Pod删除。
Statefulset:会将Pod重新进行创建。
Static Pod:则会被删除。
Kubelet上报状态有两种方式,分别是NodeStatus和NodeLease。具体实现方式如下:
NodeStatus:由Kubelet定期向Apiserver上报心跳,超时没有上报则会将Node标记为UnReady,Node上的Pod也会标记为NodeLost或者Unknow。默认更新时间5分钟。
NodeLease上报:在命名空间kube-node-lease会为每个节点都关联一个Lease对象,由节点定期更新Lease对象。默认每隔更新周期为10S。
状态的信息依靠NodeLease来实现上报,减少apiserver的压力,并且其上传的信息相对于Node的信息少很多,比较轻量。
宿主机是否Ready取决于很多条件,包含运行时判定、网络判定、基本资源判定等。源码内容如下:
func (kl *Kubelet) defaultNodeStatusFuncs() []func(*v1.Node) error {
// if cloud is not nil, we expect the cloud resource sync manager to exist
var nodeAddressesFunc func() ([]v1.NodeAddress, error)
if kl.cloud != nil {
nodeAddressesFunc = kl.cloudResourceSyncManager.NodeAddresses
}
var validateHostFunc func() error
if kl.appArmorValidator != nil {
validateHostFunc = kl.appArmorValidator.ValidateHost
}
var setters []func(n *v1.Node) error
setters = append(setters,
nodestatus.NodeAddress(kl.nodeIPs, kl.nodeIPValidator, kl.hostname, kl.hostnameOverridden, kl.externalCloudProvider, kl.cloud, nodeAddressesFunc),
nodestatus.MachineInfo(string(kl.nodeName), kl.maxPods, kl.podsPerCore, kl.GetCachedMachineInfo, kl.containerManager.GetCapacity,
kl.containerManager.GetDevicePluginResourceCapacity, kl.containerManager.GetNodeAllocatableReservation, kl.recordEvent),
nodestatus.VersionInfo(kl.cadvisor.VersionInfo, kl.containerRuntime.Type, kl.containerRuntime.Version),
nodestatus.DaemonEndpoints(kl.daemonEndpoints),
nodestatus.Images(kl.nodeStatusMaxImages, kl.imageManager.GetImageList),
nodestatus.GoRuntime(),
)
// Volume limits
setters = append(setters, nodestatus.VolumeLimits(kl.volumePluginMgr.ListVolumePluginWithLimits))
setters = append(setters,
nodestatus.MemoryPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderMemoryPressure, kl.recordNodeStatusEvent),
nodestatus.DiskPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderDiskPressure, kl.recordNodeStatusEvent),
nodestatus.PIDPressureCondition(kl.clock.Now, kl.evictionManager.IsUnderPIDPressure, kl.recordNodeStatusEvent),
nodestatus.ReadyCondition(kl.clock.Now, kl.runtimeState.runtimeErrors, kl.runtimeState.networkErrors, kl.runtimeState.storageErrors, validateHostFunc, kl.containerManager.Status, kl.shutdownManager.ShutdownStatus, kl.recordNodeStatusEvent),
nodestatus.VolumesInUse(kl.volumeManager.ReconcilerStatesHasBeenSynced, kl.volumeManager.GetVolumesInUse),
// TODO(mtaufen): I decided not to move this setter for now, since all it does is send an event
// and record state back to the Kubelet runtime object. In the future, I'd like to isolate
// these side-effects by decoupling the decisions to send events and partial status recording
// from the Node setters.
kl.recordNodeSchedulableEvent,
)
return setters
}
大部分情况下,我们只需要关注运行时的判定,即runtimeErrors,而runtimeErrors的判定条件有两个,分别如下:
距离最近一次运行时同步操作的时间间隔超过指定阈值(默认是30s)
运行时健康检查未通过。
对应的源码如下:
func (s *runtimeState) runtimeErrors() error {
s.RLock()
defer s.RUnlock()
errs := []error{}
if s.lastBaseRuntimeSync.IsZero() {
errs = append(errs, errors.New(“container runtime status check may not have completed yet”))
} else if !s.lastBaseRuntimeSync.Add(s.baseRuntimeSyncThreshold).After(time.Now()) {
errs = append(errs, errors.New(“container runtime is down”))
}
for _, hc := range s.healthChecks {
if ok, err := hc.fn(); !ok {
errs = append(errs, fmt.Errorf(“%s is not healthy: %v”, hc.name, err))
}
}
if s.runtimeError != nil {
errs = append(errs, s.runtimeError)
}
return utilerrors.NewAggregate(errs)
}
正常情况下,kubelet 每隔 5s 会将 lastBaseRuntimeSync 设置为当前时间,而宿主状态异常时,这个时间戳一直未被更新。也即 updateRuntimeUp 一直被阻塞在设置 lastBaseRuntimeSync 之前的某一步。
具体的函数调用链为:
initializeRuntimeDependentModules -> kl.cadvisor.Start -> cc.Manager.Start -> self.createContainer -> m.createContainerLocked -> container.NewContainerHandler -> factory.CanHandleAndAccept -> self.client.ContainerInspect
暂无,后期更新
PLEG是Pod Lifecycle Events Generator的缩写,基本上它的执行逻辑,是定期检查节点上Pod运行情况,如果发现感兴趣的变化,PLEG就会把这种变化包装成Event发送给Kubelet的主同步机制syncLoop去处理。但是,在PLEG的Pod检查机制不能定期执行的时候,NodeStatus机制就会认为,这个节点的状况是不对的,从而把这种状况同步到API Server。
5.1 PLEG的工作流程
kubelet中的NodeStatus机制会定期检查集群节点状况,并把节点状况同步到API Server。而NodeStatus判断节点就绪状况的一个主要依据,就是PLEG。
PLEG定期检查节点上Pod运行情况,并且会把pod 的变化包装成Event发送给Kubelet的主同步机制syncLoop去处理。但是,在PLEG的Pod检查机制不能定期执行的时候,NodeStatus机制就会认为这个节点的状况是不对的,从而把这种状况同步到API Server,我们就会看到 not ready 。
PLEG有两个关键的时间参数,一个是检查的执行间隔,另外一个是检查的超时时间。以默认情况为准,PLEG检查会间隔一秒,换句话说,每一次检查过程执行之后,PLEG会等待一秒钟,然后进行下一次检查;而每一次检查的超时时间是三分钟,如果一次PLEG检查操作不能在三分钟内完成,那么这个状况,会被NodeStatus机制当做集群节点NotReady的凭据,同步给API Server。
PLEG Start就是启动一个协程,每个relistPeriod(1s)就调用一次relist,根据最新的PodStatus生成PodLiftCycleEvent。relist是PLEG的核心,它从container runtime中查询属于kubelet管理containers/sandboxes的信息,并与自身维护的 pods cache 信息进行对比,生成对应的 PodLifecycleEvent,然后输出到 eventChannel 中,通过 eventChannel 发送到 kubelet syncLoop 进行消费,然后由 kubelet syncPod 来触发 pod 同步处理过程,最终达到用户的期望状态。
从 Docker 1.11 版本开始,Docker 容器运行就不是简单通过 Docker Daemon 来启动了,而是通过集成 containerd、runc 等多个组件来完成的。虽然 Docker Daemon 守护进程模块在不停的重构,但是基本功能和定位没有太大的变化,一直都是 CS 架构,守护进程负责和 Docker Client 端交互,并管理 Docker 镜像和容器。现在的架构中组件 containerd 就会负责集群节点上容器的生命周期管理,并向上为 Docker Daemon 提供 gRPC 接口。
PLEG在每次迭代检查中会调用runc的 relist() 函数干的事情,是定期重新列出节点上的所有容器,并与上一次的容器列表进行对比,以此来判断容器状态的变换。相当于docker ps来获取所有容器,在通过docker Inspect来获取这些容器的详细信息。
当Node节点的某个容器状态异常时,kubelet执行docker inspect操作也会被夯死。从而会导致PLET is not health。此时docker 无法进行任何操作。
我们需要借助pprof工具来进行深入分析,通过socat结合pprof工具输出docker的堆栈信息,进一步分析,使用方法如下:
1、 在异常节点安装socat命令
yum install socat -y
2、 执行如下命令,bind填写本机暴露的地址,端口自定义
socat -d -d TCP-LISTEN:18080,fork,bind=192.168.36.13 UNIX:/var/run/docker.sock
3、 通过页面访问http://192.168.36.13:18080/debug/pprof/