How to gracefully restart kubernetes Pods

a4e5e71e69468b425806bac5520cd191.png



I have been upgrading the service grid Istio recently. After the upgrade, a necessary process is to restart all the Pods on the data plane, that is, the business Pods, so that the sidecars of these Pods can be updated to the new version.

Option 1

Because we have a large number of Pods in different environments, it is impossible to manually restart them one by one; we have done similar operations before:

kubectl delete --all pods --namespace=dev

In this way, the Pods in the dev namespace can be deleted with one click. Kubernetes will automatically restart these Pods to ensure the availability of the application.

But a big problem is that the scheduling pressure on kubernetes is great. Generally, there are at least hundreds of Pods under one namespace, and all of them need to be rescheduled and started to The load on kubernetes will be very high, and a little carelessness will have serious consequences.

So my first version of the plan at that time was to traverse all deployments, delete a Pod and then sleep for 5 minutes before deleting the next one. The pseudo code is as follows:

deployments, err := clientSet.AppsV1().Deployments(ns).List(ctx, metav1.ListOptions{})
if err != nil {
    return err
}
for _, deployment := range deployments.Items {
 podList, err := clientSet.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
     LabelSelector: fmt.Sprintf("app=%s", deployment.Name),
 })
 err = clientSet.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{})
 if err != nil {
     return err
 }
 log.Printf(" Pod %s rebuild success.\\
", pod.Name)
 time.Sleep(time.Minute * 5)
}

Existing problems

This solution is indeed simple and crude, but problems were discovered during testing.

When some services have only one Pod, the business will be down after deleting it directly, and there will be no extra copies to provide services.

This is certainly unacceptable.

There are even cases where the restart is not successful after deletion:

  • If there is no restart for a long time, the image cache is gone, or the image has even been deleted. In this case, it is impossible to start successfully.

  • There are also some Pods that have Init-Container that will do some things during startup. If it fails, it will not start successfully. In short, there are many situations that cause a Pod to fail to start normally, which will directly cause production problems online, so the first option is definitely not available.

Option 2

For this reason, I have prepared plan two:

147792e749712fe0bee291da53582e9a.png

image.png
  • First increase the number of replicas by 1. This will add a new Pod and use the latest sidecar image.

  • Wait for the newly created Pod to restart successfully.

  • Delete the original Pod after successful restart.

  • Then restore the number of copies to the previous number.

In this way, the original Pod can be restarted smoothly. At the same time, if the new Pod fails to start, it will not continue to restart other Deployment Pods. The old Pod will also be retained without any impact on the service itself.

Existing problems

It seems that there is no problem, but it is troublesome to implement and the process is very cumbersome. Here I have posted part of the core code:

func RebuildDeploymentV2(ctx context.Context, clientSet kubernetes.Interface, ns string) error {
 deployments, err := clientSet.AppsV1().Deployments(ns).List(ctx, metav1.ListOptions{})
 if err != nil {
  return err
 }

 for _, deployment := range deployments.Items {

  //Print each Deployment
  log.Printf("Ready deployment: %s\\
", deployment.Name)

  originPodList, err := clientSet.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
   LabelSelector: fmt.Sprintf("app=%s", deployment.Name),
  })
  if err != nil {
   return err
  }

  // Check if there are any Pods
  if len(originPodList.Items) == 0 {
   log.Printf(" No pod in %s\\
", deployment.Name)
   continue
  }

  // Skip Pods that have already been upgraded
  updateSkip := false
  for _, container := range pod.Spec.Containers {
   if container.Name == "istio-proxy" & amp; & amp; container.Image == "proxyv2:1.x.x" {
    log.Printf(" Pod: %s Container: %s has already upgraded, skip\\
", pod.Name, container.Name)
    updateSkip = true
   }
  }
  if updateSkip {
   continue
  }

  // Scale the Deployment, create a new pod.
  scale, err := clientSet.AppsV1().Deployments(ns).GetScale(ctx, deployment.Name, metav1.GetOptions{})
  if err != nil {
   return err
  }
  scale.Spec.Replicas = scale.Spec.Replicas + 1
  _, err = clientSet.AppsV1().Deployments(ns).UpdateScale(ctx, deployment.Name, scale, metav1.UpdateOptions{})
  if err != nil {
   return err
  }

  // Wait for pods to be scaled
  for {
   podList, err := clientSet.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
    LabelSelector: fmt.Sprintf("app=%s", deployment.Name),
   })
   if err != nil {
    log.Fatal(err)
   }
   if len(podList.Items) != int(scale.Spec.Replicas) {
    time.Sleep(time.Second * 10)
   } else {
    break
   }
  }

  // Wait for pods to be running
  for {
   podList, err := clientSet.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
    LabelSelector: fmt.Sprintf("app=%s", deployment.Name),
   })
   if err != nil {
    log.Fatal(err)
   }
   isPending := false
   for _, item := range podList.Items {
    if item.Status.Phase != v1.PodRunning {
     log.Printf("Deployment: %s Pod: %s Not Running Status: %s\\
", deployment.Name, item.Name, item.Status.Phase)
     isPending = true
    }
   }
   if isPending == true {
    time.Sleep(time.Second * 10)
   } else {
    break
   }
  }

  // Remove origin pod
  for _, pod := range originPodList.Items {
   err = clientSet.CoreV1().Pods(ns).Delete(context.Background(), pod.Name, metav1.DeleteOptions{})
   if err != nil {
    return err
   }
   log.Printf(" Remove origin %s success.\\
", pod.Name)
  }

  //Recover scale
  newScale, err := clientSet.AppsV1().Deployments(ns).GetScale(ctx, deployment.Name, metav1.GetOptions{})
  if err != nil {
   return err
  }
  newScale.Spec.Replicas = newScale.Spec.Replicas - 1
  newScale.ResourceVersion = ""
  newScale.UID = ""
  _, err = clientSet.AppsV1().Deployments(ns).UpdateScale(ctx, deployment.Name, newScale, metav1.UpdateOptions{})
  if err != nil {
   return err
  }
  log.Printf(" Depoloyment %s rebuild success.\\
", deployment.Name)
  log.Println()

 }

 return nil
}

It can be seen that there are quite a lot of codes.

Final plan

Is there a simpler way? When I communicated the above solution to the leadership, everyone was dumbfounded. It is too complicated: kubectl doesn’t have a direct rolling restart command?

? k rollout -h
Manage the rollout of one or many resources.

Available Commands:
  history view rollout history
  pause Mark the provided resource as paused
  restart Restart a resource
  resume Resume a paused resource
  status Show the status of the rollout
  undo Undo a previous rollout

kubectl rollout restart deployment/abcUse this command to rollover the abc deployment. This update operation occurs on the kubernetes server. Execute The steps are similar to the second option, except that Kubernetes implements it more rigorously than mine.

Later, I also mentioned this command in the official upgrade guide of Istio: 01d7758d92e0acfd7646c170f81d47b6.png

So you still have to read the official documentation carefully.

Integrate kubectl

Now that there is a ready-made command, just integrate this command into my script, and then call it in a loop when traversing the deployment under the namespace.

But this rollout command does not have this API in the SDK of kubernetes‘s client-go.

So I can only refer to the source code of kubectl and copy this part of the function; but fortunately, I can directly rely on kubect in my project.

require (
    k8s.io/api v0.28.2
    k8s.io/apimachinery v0.28.2
    k8s.io/cli-runtime v0.28.2
    k8s.io/client-go v0.28.2
    k8s.io/klog/v2 v2.100.1
    k8s.io/kubectl v0.28.2
)

54af72978b93671447038ee57f96bcb4.png

image.png

The RestartOptions structure used in the source code is publicly accessible, so I modified it with reference to its source code:

func TestRollOutRestart(t *testing.T) {
    kubeConfigFlags := defaultConfigFlags()
    streams, _, _, _ := genericiooptions.NewTestIOStreams()
    ns := "dev"
    kubeConfigFlags.Namespace = &ns
    matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
    f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
    deploymentName := "deployment/abc"
    r := & amp;rollout.RestartOptions{
       PrintFlags: genericclioptions.NewPrintFlags("restarted").WithTypeSetter(scheme.Scheme),
       Resources: []string{deploymentName},
       IOStreams: streams,
    }
    err := r.Complete(f, nil, []string{deploymentName})
    if err != nil {
       log.Fatal(err)
    }
    err = r.RunRestart()
    if err != nil {
       log.Fatal(err)
    }
}

Finally, after several debugs, it finally works. You only need to move this part of the logic into a loop and add sleep to restart the Pod regularly.

Reference links:

  • https://istio.io/latest/docs/setup/upgrade/canary/#data-plane

  • https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/rollout/rollout_restart.go

Recommended in the past

Implementing gRPC load balancing in a kubernetes environment

Technical Reading Weekly Issue 1

Some tips for using Helm to manage applications

Five minutes from getting started with k8s to practice – application configuration

Go language epic update-loop bug fix

27401e38c05d992640acd0f2b21a63cd.gif

Click to share

ee6618fe72c87779187789513261f1e0.gif

Click to favorite

0f4fb84fad3f18959b6bb99bc4fc87a8.gif

Like

1252f80f48bc3db8dd14135a5d3fd2cd.gif

Click to see