Health Check of k8s

Keywords: Linux kubelet Kubernetes Web Server Database

Powerful self-healing capability is an important feature of container orchestration engines such as Kubernetes. The default way to implement self-healing is to automatically rekindle the failing container. In addition, users can use Liveness and Readines detection mechanism to set up more detailed health checks, and then achieve the following requirements:

  1. Zero downtime deployment.
  2. Avoid deploying invalid mirrors.
  3. More secure rolling upgrades.

Next, learn Kubernetes'Helth Check function through practice.

Default health check

First, learn Kubernetes default health screening mechanism:

Each container starts with a process specified by the Dockerfile CMD or ENTRYPOINT. If the return code is not zero when the process exits, Kubernetes will restart the container according to restart policy if the container fails.

Let's simulate a container failure scenario. The Pod configuration file is as follows:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: healthcheck
  name: healthcheck
spec:
  restartPolicy: OnFailure
  containers:
  - name: healthcheck
    image: busybox
    args:
    - /bin/sh
    - -c
    - sleep 10; exit 1

Pod's restart policy is set to OnFailure by default to Always.

sleep 10; exit 1 simulates a container failure after 10 seconds of startup.

Execute kubectl application to create a Pod named healthcheck.

# kubectl apply -f healthcheck.yml
pod/healthcheck created

Look at the status of the Pod in a few minutes:

# kubectl get pod healthcheck 
NAME          READY   STATUS             RESTARTS   AGE
healthcheck   0/1     CrashLoopBackOff   4          3m39s

You can see that the container has been restarted four times.

In the example above, the return value of the container process is non-zero, while Kubernetes believes that the container has failed and needs to be restarted. However, there are many cases where failures occur, but the process will not quit. For example, when accessing the Web server, 500 internal errors may be system overload or resource deadlock. At this time, the httpd process does not exit abnormally. In this case, restarting the container may be the most direct and effective solution. How can we use the Health Check mechanism to deal with such scenarios?

The answer is Liveness detection.

Liveness detection

Liveness probes allow users to customize the conditions under which containers are healthy. If the probe fails, Kubernetes restarts the container.

Or as an example, create the following Pod:

Start the process by creating the file / TMP / health first and deleting it after 30 seconds. In our settings, if the file / TMP / health exists, the container is assumed to be in normal condition, otherwise it will fail.

The liveness Probe section defines how to perform Liveness probes:

  1. The method of detection is to check whether the / TMP / health file exists through the cat command. If the command is successfully executed and the return value is zero, Kubernetes considers the Liveness probe to be successful; if the return value of the command is non-zero, the Liveness probe fails.
  2. Initial DelaySeconds: 10 Specifies that the container will start executing Liveness probes after starting 10, and we usually set it according to the application's startup preparation time. For example, if it takes 30 seconds for an application to start normally, the initial DelaySeconds value should be greater than 30.
  3. PeriododSeconds: 5 specifies that Liveness probes are performed every five seconds. If Kubernetes fails to perform three Liveness probes in a row, the container will be killed and restarted.

Create Pod liveness below:

# kubectl apply -f liveness.yaml
pod/liveness created

From the configuration file, in the first 30 seconds, / TMP / health exists, the cat command returns 0, and the Liveness probe succeeds. During this period, the Events section of kubectl description pod liveness displays the normal log.

# kubectl describe pod liveness 
Events:
  Type    Reason     Age   From                Message
  ----    ------     ----  ----                -------
  Normal  Scheduled  31s   default-scheduler   Successfully assigned default/liveness to k8s-node2
  Normal  Pulling    30s   kubelet, k8s-node2  Pulling image "busybox"
  Normal  Pulled     30s   kubelet, k8s-node2  Successfully pulled image "busybox"
  Normal  Created    30s   kubelet, k8s-node2  Created container liveness
  Normal  Started    29s   kubelet, k8s-node2  Started container liveness

After 35 seconds, the log will show that / TMP / health no longer exists and that Liveness detection failed. In a few tens of seconds, after several failed probes, the container will be restarted.

Events:
  Type     Reason     Age               From                Message
  ----     ------     ----              ----                -------
  Normal   Scheduled  47s               default-scheduler   Successfully assigned default/liveness to k8s-node2
  Normal   Pulling    46s               kubelet, k8s-node2  Pulling image "busybox"
  Normal   Pulled     46s               kubelet, k8s-node2  Successfully pulled image "busybox"
  Normal   Created    46s               kubelet, k8s-node2  Created container liveness
  Normal   Started    45s               kubelet, k8s-node2  Started container liveness
  Warning  Unhealthy  3s (x3 over 13s)  kubelet, k8s-node2  Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
  Normal   Killing    3s                kubelet, k8s-node2  Container liveness failed liveness probe, will be restarted
# kubectl get pod liveness 
NAME       READY   STATUS    RESTARTS   AGE
liveness   1/1     Running   1          76s

In addition to Liveness detection, the Kubernetes Health Check mechanism also includes Readines detection.

Readines Detection

Users can tell Kubernetes when to self-heal by restarting containers through Liveness detection; Readiness detection tells Kubernetes when to add containers to Service load balancing pool.

The configuration syntax of Readiness probes is exactly the same as Liveness probes. Here's an example:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: readiness
  name: readiness
spec:
  restartPolicy: OnFailure
  containers:
  - name: readiness
    image: busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    readinessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 10
      periodSeconds: 5

This configuration file just replaces liveness in the previous example with readiness, so let's see what's different.

[root@k8s-master ~]# kubectl get pod readiness 
NAME        READY   STATUS    RESTARTS   AGE
readiness   0/1     Running   0          10s
[root@k8s-master ~]# kubectl get pod readiness 
NAME        READY   STATUS    RESTARTS   AGE
readiness   1/1     Running   0          20s
[root@k8s-master ~]# kubectl get pod readiness 
NAME        READY   STATUS    RESTARTS   AGE
readiness   1/1     Running   0          35s
[root@k8s-master ~]# kubectl get pod readiness 
NAME        READY   STATUS    RESTARTS   AGE
readiness   0/1     Running   0          61s
[root@k8s-master ~]# kubectl describe pod readiness 

The READY state of Pod readiness has undergone the following changes:

  1. When it was first created, the READY state was unavailable.
  2. Fifteen seconds later (initial DelaySeconds + periodSeconds), the first Readines probe was performed and successfully returned, setting READY as available.
  3. Thirty seconds later, / TMP / health was deleted and READY was set to be unavailable after three successive failed Readines probes.

You can also see the log of Readines detection failure through kubectl description pod Readiness.

Events:
  Type     Reason     Age                From                Message
  ----     ------     ----               ----                -------
  Normal   Scheduled  95s                default-scheduler   Successfully assigned default/readiness to k8s-node2
  Normal   Pulling    94s                kubelet, k8s-node2  Pulling image "busybox"
  Normal   Pulled     94s                kubelet, k8s-node2  Successfully pulled image "busybox"
  Normal   Created    93s                kubelet, k8s-node2  Created container readiness
  Normal   Started    93s                kubelet, k8s-node2  Started container readiness
  Warning  Unhealthy  4s (x12 over 59s)  kubelet, k8s-node2  Readiness probe failed: cat: can't open '/tmp/healthy': No such file or directory

Here's a comparison between Liveness detection and Readiness detection:

  1. Liveness detection and Readiness detection are two Health Check mechanisms. If not specifically configured, Kubernetes will adopt the same default behavior for the two detections, that is, to judge whether the detection is successful by determining whether the return value of the container start process is zero.
  2. The configuration methods of the two probes are exactly the same, and the configuration parameters supported are the same. The difference lies in the behavior after detection failure: Liveness detection is to restart the container; Readiness detection is to set the container unavailable and not receive requests forwarded by Service.
  3. Liveness probes and Readiness probes are executed independently and do not depend on each other, so they can be used separately or simultaneously. Liveness detection is used to determine whether the container needs to be restarted to achieve self-healing; Readiness detection is used to determine whether the container is ready to provide services to the outside world.

Use Health Check in business scenarios.

Use Health Check in Rolling Update

The previous section discussed the application of Health Check in Scale Up. Another important application scenario of Health Check is Rolling Update. Imagine the following:

There is a functioning multi-copy application, and then the application is updated (for example, using a higher version of image), Kubernetes will start the new copy, and the following events occur:

  1. Normally, a new copy takes 10 seconds to prepare and cannot respond to business requests until then.
  2. However, due to human configuration errors, replicas are always unable to complete the preparatory work (for example, unable to connect to the back-end database).

Think about it: What happens if you don't configure Health Check?

Because the new replica itself does not exit abnormally, the default Halith Check mechanism assumes that the container is ready and will gradually replace the existing replica with the new replica. As a result, when all the old replicas are replaced, the entire application will not be able to process requests and provide services to the outside world. If this happens in an important production system, the consequences will be very serious.

If Health Check is configured correctly, the new copy will be added to Service only if it passes Readines detection; if it does not pass detection, the existing copy will not be completely replaced, and the business will still be normal.

Here is an example to practice the application of Health Check in Rolling Update.

Simulate a 10-copy application with the following configuration file app.v1.yml:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 10
  template:
    metadata:
      labels:
        run: app
    spec:
      containers:
      - name: app
        image: busybox
        args:
        - /bin/sh
        - -c
        - sleep 3000
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 10
          periodSeconds: 5

After 10 seconds, the copy can be detected by Readines.

# kubectl get deployments. app
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
app    0/10    10           0           8s

# kubectl get pod
NAME                   READY   STATUS    RESTARTS   AGE
app-6dd7f876c4-575v5   1/1     Running   0          25s
app-6dd7f876c4-9kwk9   1/1     Running   0          25s
app-6dd7f876c4-bx4pf   1/1     Running   0          25s
app-6dd7f876c4-f6qf2   1/1     Running   0          25s
app-6dd7f876c4-fxp2m   1/1     Running   0          25s
app-6dd7f876c4-k76mr   1/1     Running   0          25s
app-6dd7f876c4-mfqsq   1/1     Running   0          25s
app-6dd7f876c4-whkc7   1/1     Running   0          25s
app-6dd7f876c4-x9q87   1/1     Running   0          25s
app-6dd7f876c4-xf8dv   1/1     Running   0          25s

Next, scroll to update the application. The configuration file app.v2.yml is as follows:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 10
  template:
    metadata:
      labels:
        run: app
    spec:
      containers:
      - name: app
        image: busybox
        args:
        - /bin/sh
        - -c
        - sleep 3000
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 10
          periodSeconds: 5

Obviously, because / TMP / health does not exist in the new copy, it cannot be detected by Readines. Verification is as follows:

# kubectl apply -f app.yml --record 
deployment.apps/app configured
[root@k8s-master ~]# kubectl get deployments. app
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
app    8/10    5            8           80s

# kubectl get pod
NAME                   READY   STATUS    RESTARTS   AGE
app-6dd7f876c4-575v5   1/1     Running   0          2m3s
app-6dd7f876c4-9kwk9   1/1     Running   0          2m3s
app-6dd7f876c4-f6qf2   1/1     Running   0          2m3s
app-6dd7f876c4-fxp2m   1/1     Running   0          2m3s
app-6dd7f876c4-k76mr   1/1     Running   0          2m3s
app-6dd7f876c4-whkc7   1/1     Running   0          2m3s
app-6dd7f876c4-x9q87   1/1     Running   0          2m3s
app-6dd7f876c4-xf8dv   1/1     Running   0          2m3s
app-7d7559dd99-6w2kn   0/1     Running   0          49s
app-7d7559dd99-jnbxg   0/1     Running   0          49s
app-7d7559dd99-mxbwg   0/1     Running   0          49s
app-7d7559dd99-n59vq   0/1     Running   0          49s
app-7d7559dd99-t49cp   0/1     Running   0          49s

This screenshot contains a lot of information and deserves our detailed analysis.

Focus first on the kubectl get pod output:

  1. Judging from Pod's AGE column, the last five Pods are new copies and are currently in the NOT READY state.
  2. Old copies were reduced from 10 to 8.

Look again at the output of kubectl get deployment app:

  1. DESIRED 10 indicates that the desired state is 10 copies of READY.
  2. CURRENT 13 represents the total number of current copies: eight old copies + five new copies.
  3. UP-TO-DATE 5 indicates the current number of copies that have been updated: five new copies.
  4. AVAILABLE 8 represents the number of copies currently in READY status: eight old copies.

In our settings, new copies are never detectable through Readines, so this state will remain unchanged.

Above we simulated a scenario where rolling updates failed. Fortunately, however, Health Check helped us block defective copies, while retaining most of the old copies, and the business was not affected by the failure of the update.

Next we have to answer: Why are five new copies created and only two old ones destroyed?

The reason is that rolling updates control the number of replica substitutions by parameters maxSurge and maxUnavailable.

maxSurge

This parameter controls the number of replicas exceeding the upper limit of DESIRED during rolling updates. MaxSurge can be a specific integer (such as 3), or it can be 100 percent, rounded up. The default value of maxSurge is 25%.

In the example above, DESIRED is 10, and the maximum number of copies is:
roundUp(10 + 10 * 25%) = 13

So we see that CURRENT is 13.

maxUnavailable

This parameter controls the maximum proportion of unavailable copies in DESIRED during rolling updates. MaxUnavailable can be a specific integer (for example, 3), or it can be a hundred percent, rounding down. The default value of maxUnavailable is 25%.

In the example above, DESIRED is 10, so the number of copies available is at least:
10 - roundDown(10 * 25%) = 8

So we see AVAILABLE is 8.

The larger the maxSurge value, the more new copies are initially created; the larger the maxUnavailable value, the more old copies are initially destroyed.

Ideally, the process of rolling updates in our case should be as follows:

  1. First, three new copies are created to bring the total number of copies to 13.
  2. Then destroy two old copies to reduce the number of available copies to eight.
  3. When these two old copies are destroyed successfully, two new copies can be created to keep the total number of copies to 13.
  4. When a new copy is detected by Readines, the number of available copies increases to more than 8.
  5. Further more old copies can be destroyed, bringing the number of available copies back to 8.
  6. The destruction of old copies makes the total number of copies less than 13, which allows more new copies to be created.
  7. This process will continue, and eventually all the old copies will be replaced by the new ones, and the rolling updates will be completed.
    Our actual situation is that in Step 4, we got stuck and the new copy could not be detected by Readines. This process can be viewed in the log section of the kubectl describe deployment app.
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  11m   deployment-controller  Scaled up replica set app-6dd7f876c4 to 10
  Normal  ScalingReplicaSet  10m   deployment-controller  Scaled up replica set app-7d7559dd99 to 3
  Normal  ScalingReplicaSet  10m   deployment-controller  Scaled down replica set app-6dd7f876c4 to 8
  Normal  ScalingReplicaSet  10m   deployment-controller  Scaled up replica set app-7d7559dd99 to 5

If the scroll update fails, you can roll back to the previous version through kubectl rollout undo.

# kubectl rollout history deployment app
deployment.extensions/app 
REVISION  CHANGE-CAUSE
1         kubectl apply --filename=app.yml --record=true
2         kubectl apply --filename=app.yml --record=true
# kubectl get deployments. app
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
app    8/10    5            8           14m
 kubectl get pod
NAME                   READY   STATUS    RESTARTS   AGE
app-6dd7f876c4-575v5   1/1     Running   0          14m
app-6dd7f876c4-9kwk9   1/1     Running   0          14m
app-6dd7f876c4-f6qf2   1/1     Running   0          14m
app-6dd7f876c4-fxp2m   1/1     Running   0          14m
app-6dd7f876c4-k76mr   1/1     Running   0          14m
app-6dd7f876c4-whkc7   1/1     Running   0          14m
app-6dd7f876c4-x9q87   1/1     Running   0          14m
app-6dd7f876c4-xf8dv   1/1     Running   0          14m
app-7d7559dd99-6w2kn   0/1     Running   0          13m
app-7d7559dd99-jnbxg   0/1     Running   0          13m
app-7d7559dd99-mxbwg   0/1     Running   0          13m
app-7d7559dd99-n59vq   0/1     Running   0          13m
app-7d7559dd99-t49cp   0/1     Running   0          13m

If you want to customize maxSurge and maxUnavailable, you can configure it as follows:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: app
spec:
  strategy:
    rollingUpdate:
      maxSurge: 35%
      maxUnavailable: 35%
  replicas: 10
  template:
    metadata:
      labels:
        run: app
    spec:
      containers:
      - name: app
        image: busybox
        args:
        - /bin/sh
        - -c
        - sleep 3000
        readinessProbe:
          exec:
            command:
            - cat
            - /tmp/healthy
          initialDelaySeconds: 10
          periodSeconds: 5

Summary
This chapter discusses two mechanisms of Kubernetes health examination: Liveness detection and Readines detection, and practices the application of health examination in Scale Up and Rolling Update scenarios.

Posted by dinger on Thu, 10 Oct 2019 11:54:38 -0700