kubernetes服务优雅停止

吕晨曦 · · 1081 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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驱逐
  • OOM

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的主动驱逐

OOM

oom killer其实是内核自我保护的机制,当宿主机资源不足时,内核为了维护自身的稳定性,会杀死某些进程(其实就是Containaer),此时不会有任何api调用同时也不会调用PreStop。为了捕获到这种情况,只能在controller中增加对Pod的watch,针对Pod的DELETE且没有进行外部资源是否的进行补救处理

方案一

在不考虑kubelet驱逐的情况下,通过ValidatingAdmissionWebhook截取Pod Delete请求,并附加额外操作就能满足在资源没有释放完全之前不删除Pod

Webhook处理流程

image
具体说明可以参考官网

时序图

Pod Delete请求与资源释放的时序图
image

流程如下:

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

方案二

使用sidecar解决注册和反注册的优缺点分析

类别优点缺点
注册1. 实现简单
2. 问题单一,不会影响全局
3. 不会出现性能瓶颈
1. sidecar需要配置信息(注册中心地址)
2. 需要识别出每个Pod中哪些需要注册,哪些不需要注册
3. 需要识别出应用的健康状态
4. 需要解决sidecar升级问题
5. 造成资源浪费
6. sidecar异常会导致整个Pod异常
反注册同上1. 没有解决时长问题,需要为每个Pod单独配置优雅停止时间
2. 需要通过健康检查的方式处理OOM的情况
3. 无法处理驱逐情况的资源释放
4. 需要解决sidecar升级问题
5. 造成资源浪费
6. sidecar异常会导致整个Pod异常

说明:

  • 方案一适用于比较复杂的情况,比如需要处理数据、同步状态而且不知道需要多长才能完成
  • 方案二适用于简单的情况

结构图

image
说明:

  1. sidecar需要通过container的状态结合service中的endpoint判断服务的健康状态判断服务是否满足注册和反注册的条件
  2. sidecar需要捕获term信号,收到信后进行反注册(Pod正常删除)
  3. sidecar需要配置Prestop,在Prestop中调用sidecar的反注册函数(用于处理驱逐情况,kubelet驱逐过程中会主动调用container的Prestop,参考killPod),收到信后进行反注册

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:吕晨曦

查看原文:kubernetes服务优雅停止

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1081 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传