当前位置 博文首页 > 阳阳的博客:【K8S】健康检查初探

    阳阳的博客:【K8S】健康检查初探

    作者:[db:作者] 时间:2021-08-15 13:25

    健康检查的意义

    为了保证集群的稳定性,需要及时的对故障服务进行处理。无论是通过下线的方式,不再将请求转发到该服务,还是通过重启的方式,使服务得以自动恢复,都是有效的处理方式。


    健康检查的策略

    而如何检测出服务是否有故障,k8s提供了以下的健康检查策略。

    • livenessProbe? ? ? ? ?存活性探针
    • readinessProbe? ? ? ?就绪性探针

    livenessProbe,k8s利用存活性探针去检测一个容器的运行状态,如果容器处于running的健康状态,此时不作任何处理。如果容器崩溃,即检查失败,则k8s会根据之前设定的重启策略去重新启动该容器。

    重启策略有以下几种:

    • always:默认的重启策略,当容器停止,会创建新的容器
    • onFailure:容器异常退出,重启容器
    • Never:当容器终止退出,从不重启容器

    readinessProbe,就绪性探针旨在告诉k8s集群知道我们启动的容器在什么时候可以接收请求。如果就绪性探针检查通过,说明该容器可以接收请求,控制器就会将此容器所在的Pod加入到Service的EndPoint列表中,那么Service则会将流量分发到该Pod上。反之,如果就绪性探针检查失败,表明该Pod还没准备好,不可以接收请求,那么控制器就会将此容器所在的Pod从对应的Service的EndPoint列表中移除。

    当下次就绪性探针检查成功后,Service则会将流量再重新转发到该Pod上。

    两者的区别简单来讲,

    livenessProbe,容器不存活,就重启该容器

    readinessProbe,容器不就绪,就不转发请求给该容器


    健康检查的检测方法

    每种探针都支持以下三种方法:

    • Exec:通过执行命令来检查服务是否正常,针对复杂检测或无HTTP接口的服务,命令返回值为0则表示容器健康。
    • HTTPGet:通过发送http或https请求检查服务是否正常,返回【200,400)状态码则表明容器健康。
    • TCPSocket:尝试在指定端口上建立TCP连接。如果它可以建立连接,容器被认为是健康的; 如果它不能被认为是不健康的。这常用于对gRPC或FTP服务的探测。

    探针探测的结果有以下几种:

    • Success:目标容器通过了检查。
    • Failure:目标容器未通过检查。
    • Unknown:未能执行检查,因此不采取任何措施。

    LivenessProbe探针配置

    示例一:通过exec方式做健康探测

    exec-liveness.yaml ?

    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        test: liveness
      name: liveness-exec
    spec:
      containers:
      - name: liveness
        image: k8s.gcr.io/busybox
        args:
        - /bin/sh
        - -c
        - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
        livenessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 5
          periodSeconds: 5

    在该配置文件中,对容器执行livenessProbe检查,periodSeconds字段指定kubelet每5s执行一次检查,检查的命令为cat /tmp/healthy,initialDelaySeconds字段告诉kubelet应该在执行第一次检查之前等待5秒。

    如果命令执行成功,则返回0,那么kubelet就认为容器是健康的,如果为非0,则Kubelet会Kill掉容器并根据重启策略来决定是否需要重启。

    当容器启动时,它会执行以下命令:

     /bin/sh -c "touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600"

    对于容器的前30秒,有一个/tmp/healthy文件。因此,在前30秒内,该命令cat /tmp/healthy返回成功代码。30秒后,cat /tmp/healthy返回失败代码。

    创建Pod:

       kubectl create -f exec-liveness.yaml

    在30秒内,查看Pod事件:

     kubectl describe pod liveness-exec

    输出表明尚未探测到失败:

       FirstSeen    LastSeen    Count   From            SubobjectPath           Type        Reason      Message
    --------- --------    -----   ----            -------------           --------    ------      -------
    24s       24s     1   {default-scheduler }                    Normal      Scheduled   Successfully assigned liveness-exec to worker0
    23s       23s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Pulling     pulling image "k8s.gcr.io/busybox"
    23s       23s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Pulled      Successfully pulled image "k8s.gcr.io/busybox"
    23s       23s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Created     Created container with docker id 86849c15382e; Security:[seccomp=unconfined]
    23s       23s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Started     Started container with docker id 86849c15382e

    35秒后,再次查看Pod事件:

    kubectl describe pod liveness-exec

    在输出的中显示探测失败,并且容器已被杀死并重新创建。

    FirstSeen LastSeen    Count   From            SubobjectPath           Type        Reason      Message
    --------- --------    -----   ----            -------------           --------    ------      -------
    37s       37s     1   {default-scheduler }                    Normal      Scheduled   Successfully assigned liveness-exec to worker0
    36s       36s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Pulling     pulling image "k8s.gcr.io/busybox"
    36s       36s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Pulled      Successfully pulled image "k8s.gcr.io/busybox"
    36s       36s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Created     Created container with docker id 86849c15382e; Security:[seccomp=unconfined]
    36s       36s     1   {kubelet worker0}   spec.containers{liveness}   Normal      Started     Started container with docker id 86849c15382e
    2s        2s      1   {kubelet worker0}   spec.containers{liveness}   Warning     Unhealthy   Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory

    再等30秒,确认Container已重新启动:

    kubectl get pod liveness-exec

    下面输出中RESTARTS的次数已增加:

    AME            READY     STATUS    RESTARTS   AGE
    liveness-exec   1/1       Running   1          1m

    示例二:通过HTTP方式做健康探测

    http-liveness.yaml ?

    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        test: liveness
      name: liveness-http
    spec:
      containers:
      - name: liveness
        image: k8s.gcr.io/liveness
        args:
        - /server
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
            httpHeaders:
            - name: X-Custom-Header
              value: Awesome
          initialDelaySeconds: 3
          periodSeconds: 3

    在配置文件中,使用k8s.gcr.io/liveness镜像,创建出一个Pod,其中periodSeconds字段指定kubelet每3秒执行一次探测,initialDelaySeconds字段告诉kubelet延迟等待3秒,探测方式为向容器中运行的服务发送HTTP GET请求,请求8080端口下的/healthz, 任何大于或等于200且小于400的代码表示成功。任何其他代码表示失败。

    创建此Pod

    kubectl create -f http-liveness.yaml

    10秒后,查看Pod事件以验证liveness探测失败并且Container已重新启动:

    kubectl describe pod liveness-http

    httpGet探测方式有如下可选的控制字段

    • host:要连接的主机名,默认为Pod IP,可以在http request head中设置host头部。
    • scheme: 用于连接host的协议,默认为HTTP。
    • path:http服务器上的访问URI。
    • httpHeaders:自定义HTTP请求headers,HTTP允许重复headers。
    • port: 容器上要访问端口号或名称。

    示例三:通过TCP方式做健康探测

    Kubelet将尝试在指定的端口上打开容器上的套接字,如果能建立连接,则表明容器健康。

    tcp-liveness-readiness.yaml ?

    apiVersion: v1
    kind: Pod
    metadata:
      name: goproxy
      labels:
        app: goproxy
    spec:
      containers:
      - name: goproxy
        image: k8s.gcr.io/goproxy:0.1
        ports:
        - containerPort: 8080
        readinessProbe:
          tcpSocket:
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          tcpSocket:
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20

    TCP检查方式和HTTP检查方式非常相似,示例中两种探针都使用了,在容器启动5秒后,kubelet将发送第一个readinessProbe探针,这将连接到容器的8080端口,如果探测成功,则该Pod将被标识为ready,10秒后,kubelet将进行第二次连接。

    除此之后,此配置还包含了livenessProbe探针,在容器启动15秒后,kubelet将发送第一个livenessProbe探针,仍然尝试连接容器的8080端口,如果连接失败则重启容器。

    创建该pod:

    kubectl create -f tcp-liveness-readiness.yaml
    

    15秒后,查看Pod事件以验证活动探测:

    kubectl describe pod goproxy

    当容器有多个端口时,通常会给每个端口命名,所以在使用探针探测时,也可以直接写自定义的端口名称

    ports:
    - name: liveness-port
      containerPort: 8080
      hostPort: 8080
    livenessProbe:
      httpGet:
        path: /healthz
        port: liveness-port

    ReadinessProbe探针配置:

    ReadinessProbe探针的使用场景livenessProbe稍有不同,有的时候应用程序可能暂时无法接受请求,比如Pod已经Running了,但是容器内应用程序尚未启动成功,在这种情况下,如果没有ReadinessProbe,则Kubernetes认为它可以处理请求了,然而此时,我们知道程序还没启动成功是不能接收用户请求的,所以不希望kubernetes把请求调度给它,则使用ReadinessProbe探针。

    ReadinessProbe和livenessProbe可以使用相同探测方式,只是对Pod的处置方式不同,ReadinessProbe是将Pod IP:Port从对应的EndPoint列表中删除,而livenessProbe则Kill容器并根据Pod的重启策略来决定作出对应的措施。

    ReadinessProbe探针探测容器是否已准备就绪,如果未准备就绪则kubernetes不会将流量转发给此Pod。

    ReadinessProbe探针与livenessProbe一样也支持exec、httpGet、TCP的探测方式,配置方式相同,只不过是将livenessProbe字段修改为ReadinessProbe。

     readinessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

    ReadinessProbe探针的HTTP、TCP的探测方式也与livenessProbe的基本一致。

    示例四: ReadinessProbe示例

    现在来看一个加入ReadinessProbe探针和一个没有ReadinessProbe探针的示例:

    该示例中,创建了一个deploy,名为JavaApp,启动的容器运行一个java应用程序,程序监听端口为8080。

    # 没有加入ReadinessProbe
    cat JavaApp.yaml
    ---
    kind: Deployment
    apiVersion: apps/v1
    metadata:
      name: JavaApp
      namespace: default
    spec:
      replicas: 1
      selector:
        matchLabels:
          test: JavaApp
      template: 
        metadata:
          labels:
             test: JavaApp
        spec:
          affinity:
            nodeAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 1
                preference:
                  matchExpressions:
                  - key: env
                    operator: In
                    values:
                    - testing
          containers:
          - image: 192.168.1.183:8081/jar/JavaApp:29
            name: JavaApp
            ports:  
            - containerPort: 8080
          imagePullSecrets:
          - name: myregistrykey
    ---       
    kind: Service
    apiVersion: v1
    metadata:
      name: JavaApp
      namespace: default
    spec:
      selector:
          test: JavaApp
      ports:
        - protocol: TCP
          port: 8080

    创建:

    kubectl create -f JavaApp.yaml

    刚创建后,等几秒钟后,查看Pod状态:

    $  kubectl get pod
    NAME                     READY     STATUS    RESTARTS   AGE
    JavaApp-579b45567c-tdsrk    1/1      Running        0            6s

    从上面可以看到,Pod刚启动6s,自身状态已Running,其READ字段,1/1 表示1个容器状态已准备就绪了,此时,对于kubernetes而言,它已经可以接收请求了,而实际上服务还无法访问,因为JAVA程序还尚启动起来,本人实验中的JAVA程序启动时间大概需要50s,50s后方可正常访问,所以针对此类程序,必须配置ReadinessProbe。

    #加入readinessProbe
    kind: Deployment
    apiVersion: apps/v1
    metadata:
      name: JavaApp
      namespace: default
    spec:
      replicas: 1
      selector:
        matchLabels:
          test: JavaApp
      template: 
        metadata:
          labels:
            test: JavaApp
        spec:
          affinity:
            nodeAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 1
                preference:
                  matchExpressions:
                  - key: env
                    operator: In
                    values:
                    - testing
          containers:
          - image: 192.168.1.183:8081/jar/JavaApp:29
            name: JavaApp
            ports:  
            - containerPort: 8080
            readinessProbe:
              tcpSocket:
                port: 8080
              initialDelaySeconds: 60
              periodSeconds: 10
          imagePullSecrets:
          - name: myregistrykey
    ---
    kind: Service
    apiVersion: v1
    metadata:
      name: JavaApp
      namespace: default
    spec:
      selector:
        test: JavaApp
      ports:
        - protocol: TCP
          port: 8080

    在该配置文件中,ReadinessProbe探针的探测方式为tcpSocket,因为程序监听在8080端口,所以这里探测为对8080建立连接。程序启动时间大概50多秒,所以这里第一次探测时间是在Pod Runing后60秒后,间隔10秒后执行第二次探测。

    创建Pod:

    kubectl create -f JavaApp.yaml

    创建后等50秒查看状态:

     $ kubectl get pod -o wide
    NAME                         READY     STATUS    RESTARTS   AGE          IP             NODE
    JavaApp-64b58cfd5c-kpc4s    0/1       Running   0           55s     172.26.91.109   192.168.1.180

    Pod虽然已处于Runnig状态,但是由于第一次探测时间未到,所以READY字段为0/1,即容器的状态为未准备就绪,在未准备就绪的情况下,其Pod对应的Service下的Endpoint也为空,所以才不会有任何请求被调度进来。

    $ kubectl get endpoints
       NAME           ENDPOINTS   
      JavaApp

    当通过第一次探测的检查通过后,容器的状态自然会转为READ状态。

    $ kubectl get pod
    NAME                      READY     STATUS    RESTARTS   AGE
    JavaApp-64b58cfd5c-qj886   1/     Running       0          1m
    
    $ kubectl get endpoints 
    NAME                ENDPOINTS                          AGE
    JavaApp            172.26.91.113:8080                 1m

    此后根据指定的间隔时间10s后再次探测,如果不通过,则kubernetes就会将Pod IP从EndPoint列表中移除。


    配置探针(Probe)相关属性

    探针(Probe)有许多可选字段,可以用来更加精确的控制Liveness和Readiness两种探针的行为(Probe):

    • initialDelaySeconds:Pod启动后延迟多久才进行检查,单位:秒。
    • periodSeconds:检查的间隔时间,默认为10,单位:秒。
    • timeoutSeconds:探测的超时时间,默认为1,单位:秒。
    • successThreshold:探测失败后认为成功的最小连接成功次数,默认为1,在Liveness探针中必须为1,最小值为1。
    • failureThreshold:探测失败的重试次数,重试一定次数后将认为失败,在readiness探针中,Pod会被标记为未就绪,默认为3,最小值为1。
    cs