如何優雅重啟 Kubernetes 的 Pod
方案 1
因為我們不同環境的 Pod 數不少,不可能手動一個個重啟;之前也做過類似的操作:
kubectl delete --all pods --namespace=dev
這樣可以一鍵將 dev 這個命名空間下的 Pod 刪掉,kubernetes 之后會自動將這些 Pod 重啟,保證和應用的可用性。
但這有個大問題是對 kubernetes 的調度壓力較大,一般一個 namespace 下少說也是幾百個 Pod,全部需要重新調度啟動對 kubernetes 的負載會很高,稍有不慎就會有嚴重的后果。
所以當時我的第一版方案是遍歷所有的 deployment,刪除一個 Pod 后休眠 5 分鐘再刪下一個,偽代碼如下:
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.\n", pod.Name)
time.Sleep(time.Minute * 5)
}
存在的問題
這個方案確實是簡單粗暴,但在測試的時候就發現了問題。
當某些業務只有一個 Pod 的時候,直接刪掉之后這個業務就掛了,沒有多余的副本可以提供服務了。
這肯定是不能接受的。
甚至還有刪除之后沒有重啟成功的:
- 長期沒有重啟導致鏡像緩存沒有了,甚至鏡像已經被刪除了,這種根本就沒法啟動成功。
- 也有一些 Pod 有 Init-Container 會在啟動的時候做一些事情,如果失敗了也是沒法啟動成功的。 總之就是有多種情況導致一個 Pod 無法正常啟動,這在線上就會直接導致生產問題,所以方案一肯定是不能用的。
方案二
為此我就準備了方案二:
image.png
- 先將副本數+1,這是會新增一個 Pod,也會使用最新的 sidecar 鏡像。
- 等待新建的 Pod 重啟成功。
- 重啟成功后刪除原有的 Pod。
- 再將副本數還原為之前的數量。
這樣可以將原有的 Pod 平滑的重啟,同時如果新的 Pod 啟動失敗也不會繼續重啟其他 Deployment 的 Pod,老的 Pod 也是一直保留的,對服務本身沒有任何影響。
存在的問題
看起來是沒有什么問題的,就是實現起來比較麻煩,流程很繁瑣,這里我貼了部分核心代碼:
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\n", 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\n", deployment.Name)
continue
}
// Skip Pods that have already been upgraded
updateSkip := false
for _, container := range pod.Spec.Containers {
if container.Name == "istio-proxy" && container.Image == "proxyv2:1.x.x" {
log.Printf(" Pod: %s Container: %s has already upgrade, skip\n", 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\n", 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.\n", 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.\n", deployment.Name)
log.Println()
}
return nil
}
看的出來代碼是比較多的。
最終方案
有沒有更簡單的方法呢,當我把上述的方案和領導溝通后他人都傻了,這也太復雜了:kubectl 不是有一個直接滾動重啟的命令嗎。
? 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/abc 使用這個命令可以將 abc 這個 deployment 進行滾動更新,這個更新操作發生在 kubernetes 的服務端,執行的步驟和方案二差不多,只是 kubernetes 實現的比我的更加嚴謹。
后來我在查看 Istio 的官方升級指南中也是提到了這個命令:
所以還是得好好看官方文檔。
整合 kubectl
既然有現成的了,那就將這個命令整合到我的腳本里即可,再遍歷 namespace 下的 deployment 的時候循環調用就可以了。
但這個 rollout 命令在 kubernetes 的 client-go 的 SDK 中是沒有這個 API 的。
所以我只有參考 kubectl 的源碼,將這部分功能復制過來;不過好在可以直接依賴 kubect 到我的項目里。
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
)
源碼里使用到的 RestartOptions 結構體是公共訪問的,所以我就參考它源碼魔改了一下:
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 := &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)
}
}
最終在幾次 debug 后終于可以運行了,只需要將這部分邏輯移動到循環里,加上 sleep 便可以有規律的重啟 Pod 了。
參考鏈接: