1. kubernetes 健康检查

打造一个完全不出事的容器是不可能的,因此需要对容器进行健康检查,以 check 容器是否处于可用状态。

健康检查可分为进程级健康检查和业务级健康检查两种:

  • 进程级健康检查即检查容器进程是否存活,kubelet 定期通过 docker daemon 获取所有 docker 进程的运行情况,如果发现某个容器未正常运行,则重启该容器。
  • 业务级健康检查是更细粒度的健康检查,试想容器进程处于死锁状态,此时容器是不可用的,但是容器进程是运行的,kubelet 不会重启该容器。针对这种情况 kubernetes 设计了 probe 探针来检查容器是否可用。

probe 又分两种:活性探针(liveness probe)和业务探针(readiness probe)。

1.1 liveness probe

以 livenss probe 为例,在启动容器时执行一段死锁代码如下:

#coding:utf8
import threading
import time

num = 0
lock = threading.Lock()

def func(n):
    lock.acquire()
    print n
    if(n == 5):
        print "deadLock"
        raise Exception('deadLock')
    lock.release()

if __name__ == "__main__":
    t4 = threading.Thread(target=func, args=(5,))
    t1 = threading.Thread(target=func, args=(8,))
    t2 = threading.Thread(target=func, args=(4,))
    t3 = threading.Thread(target=func, args=(2,))

    t4.start()
    t1.start()
    t2.start()
    t3.start()

启动包含该死锁代码的容器:

$ docker run -it centos_python2:v1 python2 /home/deadLock.py
5
deadLock here
Exception in thread Thread-1:
Traceback (most recent call last):
  File "/usr/lib64/python2.7/threading.py", line 805, in __bootstrap_inner
    self.run()
  File "/usr/lib64/python2.7/threading.py", line 758, in run
    self.__target(*self.__args, **self.__kwargs)
  File "/home/deadLock.py", line 13, in func
    raise Exception('Error: deadLock ')
Exception: Error: deadLock

此时容器停留在死锁状态,这时候容器是无法正常工作的!

kubernetes 的 liveness probe 通过三种方式对应用进行健康检查:

  1. HTTP GET
  2. Container Exec
  3. TCP Socket

这里仅介绍第二种方式 Container Exec(详细信息看 这里),它通过在容器中检查执行命令的退出码来检查容器应用是否正常,如退出码为 0 则认为容器应用工作正常。如下所示:

[root@chunqiu ~ ]# kubectl exec -it httpd /bin/bash -n ci
bash-5.0$ /usr/bin/genapik8s --is-alive
bash-5.0$ echo $?
0

bash-5.0$ /usr/bin/genapik8s --is-alive2
unknown option: --is-alive2
bash-5.0$ echo $?
3

这里展示了一个容器应用正常工作的容器,使用 genapik8s 检查容器是否可用。对应的 helm chat 如下:

livenessProbe:
    exec:
      command:
      - /usr/bin/genapik8s
      - --is-alive
    initialDelaySeconds: 15
    periodSeconds: 5
    successThreshold: 1
    failureThreshold: 3

其中,initialDelaySeconds 表示容器启动到执行健康检查的延迟时间,延迟时间的设计是为了容器进程有时间完成必要的初始化工作,而不是在还未启动好之前就被 kubelet 重启了。

同理,将 livenessProbe 应用在前面死锁的容器,容器中执行命令的退出码不为 0,使得 kubelet 发现到该容器无法正常工作,进而重启该容器。

1.2 readiness probe

如果某些容器应用只是暂时性出了问题,而不想 kubelet 对其重启该怎么办呢?kubernetes 提供了 readiness probe 来解决这类问题。类似于 liveness probe,readiness probe 会在容器中执行检查操作,如果检查失败 kubelet 不会重启容器,而是将容器所属的 pod 从 endpoint 列表中删除,这样访问 pod 的请求就会被路由到其它 pod 上。

根据这一特性,可以构造“相同”副本的 pod,一个为 active,一个为 standby。它们都通过 service 建立连接,service 会将请求路由到 active 的 pod,而不会路由到 standby 的 pod。

这里仅以单一副本举例,不深入讨论 active 和 standby pod 的情况。构造带 readiness probe 的 pod:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: readinesstester
spec:
  replicas: 1
  selector:
    matchLabels:
      app: readinessprobe
  template:
    metadata:
      labels:
        app: readinessprobe
    spec:
      containers:
      - name: httpd
        image: docker-httpd:0.2.4
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 5
          periodSeconds: 5

container 内包含一个 readinessProbe 探针,它会执行命令 cat /tmp/healthy 检查容器是否健康。容器中 healthy 是不存在的,执行会失败。创建 service 如下:

apiVersion: v1
kind: Service
metadata:
  name: readinessservice
spec:
  selector:
    app: readinessprobe
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 80

检查 pod 状态:

[root@chunqiu readinessProbe ]# kubectl get pods -o wide
NAME                               READY   STATUS             RESTARTS   AGE     IP              NODE
readinesstester-85bbbd7947-dzsm6   0/1     Running            0          3h34m   10.10.44.173    chunqiu-node-2

[root@chunqiu readinessProbe ]# kubectl describe pods readinesstester-85bbbd7947-dzsm6
...
Status:               Running
Controlled By:  ReplicaSet/readinesstester-85bbbd7947
Containers:
  httpd:
    ...
    State:          Running
      Started:      Sun, 30 May 2021 11:57:21 +0800
    Ready:          False
    Restart Count:  0
    Readiness:      exec [cat /tmp/healthy] delay=5s timeout=1s period=5s #success=1 #failure=3
Events:
  Type     Reason     Age                       From     Message
  ----     ------     ----                      ----     -------
  Warning  Unhealthy  3m13s (x2460 over 3h28m)  kubelet  Readiness probe failed: cat: /tmp/healthy: No such file or directory

这里仅列出需要重点关注的信息。从 pod 状态来看, pod 并未 ready,不过 pod 并未被重启,且 pod 是 running 的。进一步查看 service 对应的 pod endpoint 是否存在:

[root@chunqiu readinessProbe ]# kubectl describe service readinessservice
Name:              readinessservice
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=readinessprobe
Type:              ClusterIP
IP:                10.254.194.117
Port:              <unset>  8080/TCP
TargetPort:        80/TCP
Endpoints:
Session Affinity:  None
Events:            <none>

可以看到未 ready 的 pod endpoints 并未放到 service 中,虽然 pod 有自己的 ip。

2. Deployment 和 replicaSet

这一节我们这么部署 pod 和 service 看看会发生什么。

首先创建不带 readinessProbe 的 pod:

[root@chunqiu readinessProbe ]# kubectl get pods -o wide
NAME                              READY   STATUS     RESTARTS   AGE     IP              NODE
readinesstester-cd586b86d-b4n7t   1/1     Running    0          26s     10.10.44.179    chunqiu-node-2

[root@chunqiu readinessProbe ]# kubectl describe service readinessservice
Name:              readinessservice
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=readinessprobe
Type:              ClusterIP
IP:                10.254.194.117
Port:              <unset>  8080/TCP
TargetPort:        80/TCP
Endpoints:         10.10.44.179:80
Session Affinity:  None
Events:            <none>

很正常,接着将 readinessProbe 加到 pod 内,重新配置 pod,查看 pod 状态:

[root@chunqiu readinessProbe ]# kubectl get pods -o wide
NAME                               READY   STATUS             RESTARTS   AGE     IP              NODE
readinesstester-85bbbd7947-xj9gv   0/1     Running            0          29s     10.10.44.161    chunqiu-node-2
readinesstester-cd586b86d-b4n7t    1/1     Running            0          3m51s   10.10.44.179    chunqiu-node-2

发现有两个名为 readinesstester... 的 pod 存在,且状态还不一致,这是为什么呢?

自上而下通过 Deployment 和 replicatSet 查看 pod 干了什么:

[root@chunqiu readinessProbe ]# kubectl get deployments.apps
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
readinesstester   1/1     1            1           5m39s

[root@chunqiu readinessProbe ]# kubectl describe deployments.apps readinesstester
...
Events:
  Type    Reason             Age    From                   Message
  ----    ------             ----   ----                   -------
  Normal  ScalingReplicaSet  5m48s  deployment-controller  Scaled up replica set readinesstester-cd586b86d to 1
  Normal  ScalingReplicaSet  2m26s  deployment-controller  Scaled up replica set readinesstester-85bbbd7947 to 1

根据 Events 可以看到 deployment-controller scale up 一个 replicaSet readinesstester-85bbbd7947,由这个 replicaSet 接管新的带 readinessProbe pod 的创建:

[root@chunqiu readinessProbe ]# kubectl get replicasets.apps
NAME                         DESIRED   CURRENT   READY   AGE
readinesstester-85bbbd7947   1         1         0       9m13s
readinesstester-cd586b86d    1         1         1       12m

[root@chunqiu readinessProbe ]# kubectl describe replicasets.apps readinesstester-85bbbd7947
Name:           readinesstester-85bbbd7947
Replicas:       1 current / 1 desired
Pods Status:    1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Events:
  Type    Reason            Age    From                   Message
  ----    ------            ----   ----                   -------
  Normal  SuccessfulCreate  9m24s  replicaset-controller  Created pod: readinesstester-85bbbd7947-xj9gv
...

scale up 的 replicaSet 创建出 Pod,该 pod 的状态并未 ready,导致 deployment 的滚动更新策略一直卡在等待新 replicaSet 创建完成这里,因此会出现两个并行的 pod。从这个示例也可以看出 Deployment 是为了应用的更新而设计的。

芝兰生于空谷,不以无人而不芳。