乐趣区

关于kubernetes:kubernetes服务优雅停止

Graceful shutdown

优雅进行(Graceful shutdown),在进行程序之前先实现资源清理工作。比方:

  • 操作数据:清理、转移数据。数据库节点产生重启时须要思考
  • 反注册:程序退出之前告诉网关或服务注册核心,服务下线后再进行服务,此时不会有任何流量受到服务进行的影响。

Prestop Hook

个别状况当 Pod 进行后,k8s 会把 Pod 从 service 中摘除,同时程序外部对 SIGTERM 信号进行解决就能够满足优雅进行的需要。但如果 Pod 通过注册核心向外裸露 ip,并间接承受内部流量,则须要做一些额定的事件。此时就须要用到 Prestop hook,目前 kubernetes 提供目前提供了 ExecHTTP 两种形式,应用时须要通过 Pod 的 .spec.containers[].lifecycle.preStop 字段为 Pod 中的每个容器独自配置, 比方:

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","nginx -s quit; while killall -0 nginx; do sleep 1; done"]

pod 删除流程

为了不便了解 Prestop Hook 工作原理,上面阐明一下 Pod 的退出流程

  1. API-Server 承受到申请后更新 Pod 中的 DeletionTimestamp 以及 DeletionGracePeriodSeconds。Pod 进入 Terminating 状态
  2. Pod 会进行进行的相干解决

    • 如果存在 Prestop Hook,kubelet 会调用每个容器的 preStop hook,如果 preStop hook 的运行工夫超出了 grace period,kubelet 会发送 SIGTERM 并再等 2 秒(能够通过调整参数 terminationGracePeriodSeconds 以适应每个 pod 的退出流程,默认 30s)
    • kubelet 发送 TERM 信号给每个 container 中的 1 号过程
  3. 在优雅退出的同时,k8s 会将 Pod 从对应的 Service 上摘除
  4. grace period 超出之后,kubelet 发送 SIGKILL 给 Pod 中的所有运行容器;同上清理 pause 状态的 container
  5. Kubelet 向 API-Server 发送申请,强制删除 Pod(通过将 grace period 设置为 0)
  6. API Server 删除 Pod 在 etcd 中的数据

详情参考官网阐明:https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/

问题

  1. 无奈预测 Pod 会在多久之内实现优雅退出, 导致某些非凡资源不能开释或者牵引
  2. 优雅退出的代码逻辑须要很久能力解决实现或者存在 BUG(此问题能够要求业务进行革新,上面不再阐明)
  3. 程序代码没有解决 SIGTERM(此问题能够要求业务进行革新,上面不再阐明)

解决方案

为了保障业务稳固和数据安全,同时缩小人工接入,须要额定形式帮助实现进行后的解决流程。

删除起因

为了找到具体的解决方案须要明确 Pod 的删除起因有哪些

  • kubectl 命令删除
  • kubernetes go client 调用 api 删除
  • Pod update
  • kubelet 驱赶

kubectl 命令删除

其实都是 通过 api 调用实现 Pod 删除

调用 api 删除

通过 api 调用实现 Pod 删除

Pod update

大抵能够分为两类

Deployment

Deployment通过 ReplicasSet 实现对应的操作

func (dc *DeploymentController) scaleReplicaSet(rs *apps.ReplicaSet, newScale int32, deployment *apps.Deployment, scalingOperation string) (bool, *apps.ReplicaSet, error) {sizeNeedsUpdate := *(rs.Spec.Replicas) != newScale

    annotationsNeedUpdate := deploymentutil.ReplicasAnnotationsNeedUpdate(rs, *(deployment.Spec.Replicas), *(deployment.Spec.Replicas)+deploymentutil.MaxSurge(*deployment))

    scaled := false
    var err error
    if sizeNeedsUpdate || annotationsNeedUpdate {rsCopy := rs.DeepCopy()
        *(rsCopy.Spec.Replicas) = newScale
        deploymentutil.SetReplicasAnnotations(rsCopy, *(deployment.Spec.Replicas), *(deployment.Spec.Replicas)+deploymentutil.MaxSurge(*deployment))
        rs, err = dc.client.AppsV1().ReplicaSets(rsCopy.Namespace).Update(context.TODO(), rsCopy, metav1.UpdateOptions{})
        if err == nil && sizeNeedsUpdate {
            scaled = true
            dc.eventRecorder.Eventf(deployment, v1.EventTypeNormal, "ScalingReplicaSet", "Scaled %s replica set %s to %d", scalingOperation, rs.Name, newScale)
        }
    }
    return scaled, rs, err
}

下面的代码中通过 dc.client.AppsV1().ReplicaSets(rsCopy.Namespace).Update(context.TODO(), rsCopy, metav1.UpdateOptions{}) 形式,调整 ReplicaSets 中的设置实现对 Pod 数量的调整。

manageReplicasReplicasSet 中外围的办法, 它会计算 ReplicasSet 须要创立或者删除多少个 Pod 并调用 API-Server 的接口进行操作, 上面是调用删除 Pod 接口

func (r RealPodControl) DeletePod(namespace string, podID string, object runtime.Object) error {accessor, err := meta.Accessor(object)
    if err != nil {return fmt.Errorf("object does not have ObjectMeta, %v", err)
    }
    klog.V(2).InfoS("Deleting pod", "controller", accessor.GetName(), "pod", klog.KRef(namespace, podID))
    if err := r.KubeClient.CoreV1().Pods(namespace).Delete(context.TODO(), podID, metav1.DeleteOptions{}); err != nil {if apierrors.IsNotFound(err) {klog.V(4).Infof("pod %v/%v has already been deleted.", namespace, podID)
            return err
        }
        r.Recorder.Eventf(object, v1.EventTypeWarning, FailedDeletePodReason, "Error deleting: %v", err)
        return fmt.Errorf("unable to delete pods: %v", err)
    }
    r.Recorder.Eventf(object, v1.EventTypeNormal, SuccessfulDeletePodReason, "Deleted pod: %v", podID)

    return nil
}

最终是 通过 api 实现对 Pod 的删除

DaemonSet

DaemoSet 删除 Pod 有几种状况

  • 降级
  • 调整 nodeSelector、容忍等导致某个节点不再部署
降级

Pod 降级策略由 Spec.Update.Strategy 字段指定,目前反对 OnDelete 和 RollingUpdate 两种模式

OnDelete
须要用户手动删除旧 Pod, 而后 DaemonSets Contro‖er 会利用更新后的 Spec.Template 创立新 Pod。通过 api 调用实现 Pod 删除

RollingUpdate
删除旧 Pod 操作, 函数 syncNodes 中实现具体操作,syncNodes删除逻辑如下

...
    klog.V(4).Infof("Pods to delete for daemon set %s: %+v, deleting %d", ds.Name, podsToDelete, deleteDiff)
    deleteWait := sync.WaitGroup{}
    deleteWait.Add(deleteDiff)
    for i := 0; i < deleteDiff; i++ {go func(ix int) {defer deleteWait.Done()
            if err := dsc.podControl.DeletePod(ds.Namespace, podsToDelete[ix], ds); err != nil {dsc.expectations.DeletionObserved(dsKey)
                if !apierrors.IsNotFound(err) {klog.V(2).Infof("Failed deletion, decremented expectations for set %q/%q", ds.Namespace, ds.Name)
                    errCh <- err
                    utilruntime.HandleError(err)
                }
            }
        }(i)
    }
    deleteWait.Wait()
...

下面最终是通过调用 podControl.DeletePod 实现的删除,是 通过 api 调用实现 Pod 删除

节点调整
func (dsc *DaemonSetsController) manage(ds *apps.DaemonSet, nodeList []*v1.Node, hash string) error {
    // 1、获取已存在 daemon pod 与 node 的映射关系
    nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
    ......
    // 2、判断每一个 node 是否须要运行 daemon pod
    var nodesNeedingDaemonPods, podsToDelete []string
    for _, node := range nodeList {
        nodesNeedingDaemonPodsOnNode, podsToDeleteOnNode, err := dsc.podsShouldBeOnNode(node, nodeToDaemonPods, ds)
        if err != nil {continue}
        nodesNeedingDaemonPods = append(nodesNeedingDaemonPods, nodesNeedingDaemonPodsOnNode...)
        podsToDelete = append(podsToDelete, podsToDeleteOnNode...)
    }
    // 3、判断是否启动了 ScheduleDaemonSetPods feature-gates 个性,若启用了则对不存在 node 上的 
    // daemon pod 进行删除 
    if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {podsToDelete = append(podsToDelete, getUnscheduledPodsWithoutNode(nodeList, nodeToDaemonPods)...)
    }
    // 4、为对应的 node 创立 daemon pod 以及删除多余的 pods
    if err = dsc.syncNodes(ds, podsToDelete, nodesNeedingDaemonPods, hash); err != nil {return err}
    return nil
}

syncNodes中的删除逻辑曾经在下面进行了阐明,是 通过 api 调用实现 Pod 删除

kubelet 驱赶

驱赶函数调用链
m.evictPod() => m.killPodFunc() = killPodNow() 的返回值 => podWorkers.UpdatePod() => podWorkers.managePodLoop() => podWorkers.syncPodFn() = kubelet.syncPod()。最终就是调用 kubelet 的 syncPod() 办法,把 podPhase=Failed 更新进去
syncPod 外面调用了 statusManager.SetPodStatus(pod, apiPodStatus),通过 statusManager 将Pod 信息同步到API-Server, 并没有调用接口删除Pod, 代码如下:

func (kl *Kubelet) syncPod(o syncPodOptions) error {
    // pull out the required options
    pod := o.pod
    mirrorPod := o.mirrorPod
    podStatus := o.podStatus
    updateType := o.updateType

    // if we want to kill a pod, do it now!
    if updateType == kubetypes.SyncPodKill {
        killPodOptions := o.killPodOptions
        if killPodOptions == nil || killPodOptions.PodStatusFunc == nil {return fmt.Errorf("kill pod options are required if update type is kill")
        }
        apiPodStatus := killPodOptions.PodStatusFunc(pod, podStatus)
        kl.statusManager.SetPodStatus(pod, apiPodStatus)
        // we kill the pod with the specified grace period since this is a termination
        if err := kl.killPod(pod, nil, podStatus, killPodOptions.PodTerminationGracePeriodSecondsOverride); err != nil {kl.recorder.Eventf(pod, v1.EventTypeWarning, events.FailedToKillPod, "error killing pod: %v", err)
            // there was an error killing the pod, so we return that error directly
            utilruntime.HandleError(err)
            return err
        }
        return nil
    }
    

statusManager通过 syncPod 实现状态同步

func (m *manager) syncPod(uid types.UID, status versionedPodStatus) {
    // 1、判断是否须要同步状态
    if !m.needsUpdate(uid, status) {klog.V(1).Infof("Status for pod %q is up-to-date; skipping", uid)
        return
    }
    // 2、获取 pod 的 oldStatus
    pod, err := m.kubeClient.CoreV1().Pods(status.podNamespace).Get(status.podName, metav1.GetOptions{})
    if errors.IsNotFound(err) {return}
    if err != nil {return}
    translatedUID := m.podManager.TranslatePodUID(pod.UID)
    // 3、查看 pod UID 是否曾经扭转
    if len(translatedUID) > 0 && translatedUID != kubetypes.ResolvedPodUID(uid) {return}
    // 4、同步 pod 最新的 status 至 apiserver
    oldStatus := pod.Status.DeepCopy()
    newPod, patchBytes, err := statusutil.PatchPodStatus(m.kubeClient, pod.Namespace, pod.Name, *oldStatus, mergePodStatus(*oldStatus, status.status))
    if err != nil {return}
    pod = newPod
...

kubelet 的驱赶带来了很多不确定性,其实能够通过 自定义调度性能 来代替,生产环境应该防止 kubelet 的被动驱赶

计划

在不思考 kubelet 驱赶的状况下,通过 ValidatingAdmissionWebhook 截取 Pod Delete 申请,并附加额定操作就能满足在资源没有开释齐全之前不删除 Pod

Webhook 解决流程


具体阐明能够参考官网

时序图

Pod Delete 申请与资源开释的时序图

流程如下:

  1. 通过 API 删除Pod
  2. API-Server接管到申请后,调用内部 WebHook 进行校验
  3. WebHook须要先辨认出 Pod 是否须要开释资源。同时须要查看资源是否进行了开释,如果资源已开释,则批准删除;如果须要首先创立一个 CRD 实例,同时拒绝请求,Pod将不会被删除(创立 CRD 目标次要是针对用户手动删除的这种状况,其余删除都是由各种资源的 controller 触发的,为了满足状态须要会一直触发删除申请)
  4. controller 发现新的 CRD 资源创立当前清理 Pod 内部资源(注册核心、数据等)
  5. 如果清理未实现,整个流程会因为 controller 的管制循环回到第 4 步
  6. 清理实现后由 controller 删除对应的Pod

影响

  • Pod delete: 清理工作未实现时,Pod 无奈删除
  • Pod update: 清理工作未实现时,不能进行 Pod 非凡资源(须要重启 Pod 能力实现的设置,比方镜像)的更新
退出移动版