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处理流程

kubernetes服务优雅停止_第1张图片
具体说明可以参考官网

时序图

Pod Delete请求与资源释放的时序图
kubernetes服务优雅停止_第2张图片

流程如下:

  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才能完成的设置,比如镜像)的更新

你可能感兴趣的:(kubernetes)