kubernetes提供了用于存活性检查的liveness和用于服务就绪检查的readiness功能,这两个功能为服务的可靠性提供了极大的帮组;接下来我们就来分析下在kubelet源码中是如何实现这块功能的。
概念
kubelet中提供了liveness和readiness探针,这两种探针都支持基于HTTP/TCP/Command的形势;而且他们的配置都是一致的,只是各自的用途不同而已。
liveness
探针
存活性检查,主要用于检测pod是否健康;一旦该配置指定的指标未能达标,kubelet即认为该pod不健康,就会试图杀掉该pod,然后重新启动一个副本,从而保障pod永远是“活着”的。readiness
探针
就绪检查,主要用于检测某个pod所提供的服务当前是否可用;如果该配置指定的指标未能达标,kubelet就不会将该pod作为service的endpoints,也就意味着,不会将外部的访问流量分配给该pod处理。
实现流程
代码实现逻辑首先从核心对象来了解主要的数据结构和封装,然后分别从readiness探测结果的守护go Routine到探针与Pod联动过程来了解探测和结果处理动作。
核心对象
如上图所示,实现代码中主要需涉及以下核心对象:
probe_manager
封装了pod的状态变化与探针联动的所有操作以及probe对应的worker的添加、删除等。worker
封装了操作具体do_probe行为任务;探针探测的主要逻辑就是在该对象中执行。其方法分析如下:- newWoker用户初始化出属于readiness或者liveness的worker;
- run中按照pod.probe.spec.PeriodSeconds中指定的周期,执行worker的w.doProbe操作;
- doProbe调用w.probeManager.prober.probe来执行探测,然后对结果进行处理。
当成功或者失败的次数小于设置的threshold的时候,需要继续执行;否则将结果通过w.resultsManager.Set写到channel中; - doProbe经过多层调用,最终命令是通过runProbe函数来执行的;在该函数中,分别支持了exec、http以及TCPsocket类型的调用探测方式。
probe
具体探针探测的流程,按照三种探测类型来实现(HTTP/TCP/Command),对应代码细节如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38func (pb *prober) runProbe(probeType probeType, p *v1.Probe, pod *v1.Pod, status v1.PodStatus, container v1.Container, containerID kubecontainer.ContainerID) (probe.Result, string, error) {
timeout := time.Duration(p.TimeoutSeconds) * time.Second
if p.Exec != nil {
command := kubecontainer.ExpandContainerCommandOnlyStatic(p.Exec.Command, container.Env)
return pb.exec.Probe(pb.newExecInContainer(container, containerID, command, timeout))
}
if p.HTTPGet != nil {
scheme := strings.ToLower(string(p.HTTPGet.Scheme))
host := p.HTTPGet.Host
if host == "" {
host = status.PodIP
}
port, err := extractPort(p.HTTPGet.Port, container)
if err != nil {
return probe.Unknown, "", err
}
path := p.HTTPGet.Path
url := formatURL(scheme, host, port, path)
headers := buildHeader(p.HTTPGet.HTTPHeaders)
if probeType == liveness {
return pb.livenessHttp.Probe(url, headers, timeout)
} else { // readiness
return pb.readinessHttp.Probe(url, headers, timeout)
}
}
if p.TCPSocket != nil {
port, err := extractPort(p.TCPSocket.Port, container)
if err != nil {
return probe.Unknown, "", err
}
host := p.TCPSocket.Host
if host == "" {
host = status.PodIP
}
return pb.tcp.Probe(host, port, timeout)
}
return probe.Unknown, "", fmt.Errorf("Missing probe handler for %s:%s", format.Pod(pod), container.Name)
}可以从上面代码中看到,分别有三个判断,分别对应为:p.Exec、p.HTTPGet 和 p.TCPSocket。
启动流程
kubelet通过probe_manager.start()来启动probe服务,该服务只是持续的从readiness channel中获取readiness的结果(所以,不如说这是在启动readiness的服务)。
readinessManager的结果通过调用probeManager.Start()来从channel中将结果获取出来,并通过statusManager写出去。经过一连串的动作,最终到kubelet的主循环逻辑中触发podStatusChannel收消息后的业务逻辑。
Pod联动
当pod被kubelet处理时,如果该pod配置了liveness或者readiness探针规则,probeManager的AddPod方法中会启动分别goRoutine来为readiness和liveness启动各自的worker(这里可以参考”核心对象”节的描述)。
各自的worker都会调用相同的probeManager来执行probe操作;这里两种探针使用的probe是行为是一样的。当基于probeManager获取到探针探测结果后,会调用各自的resultManager来处理结果。这里readiness和liveness各自对结果的处理有些不一样。
readiness
将结果写入到livenessManager channel,这就和前面“启动流程”一节分析的一致,只是写入channel后其他流程便由kubelet的主逻辑来处理。1
2
3
4
5
6func (m *manager) updateReadiness() {
update := <-m.readinessManager.Updates()
ready := update.Result == results.Success
m.statusManager.SetContainerReadiness(update.PodUID, update.ContainerID, ready)
}liveness
livenessManager的结果,直接在syncLoopIteration中读出来然后执行pod的update操作。