Graceful shutdown

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

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

Prestop Hook

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

apiVersion: v1kind: Podmetadata:  name: lifecycle-demospec:  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能力实现的设置,比方镜像)的更新