Graceful shutdown
优雅进行(Graceful shutdown),在进行程序之前先实现资源清理工作。比方:
- 操作数据:清理、转移数据。数据库节点产生重启时须要思考
- 反注册:程序退出之前告诉网关或服务注册核心,服务下线后再进行服务,此时不会有任何流量受到服务进行的影响。
Prestop Hook
个别状况当Pod
进行后,k8s会把Pod
从service中摘除,同时程序外部对SIGTERM信号进行解决就能够满足优雅进行的需要。但如果Pod
通过注册核心向外裸露ip,并间接承受内部流量,则须要做一些额定的事件。此时就须要用到Prestop hook,目前kubernetes提供目前提供了 Exec
和 HTTP
两种形式,应用时须要通过 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
的退出流程
- API-Server承受到申请后更新Pod中的DeletionTimestamp以及DeletionGracePeriodSeconds。Pod 进入 Terminating 状态
Pod会进行进行的相干解决
- 如果存在Prestop Hook,kubelet 会调用每个容器的 preStop hook,如果 preStop hook 的运行工夫超出了 grace period,kubelet 会发送 SIGTERM 并再等 2 秒(能够通过调整参数
terminationGracePeriodSeconds
以适应每个pod的退出流程,默认30s) - kubelet 发送 TERM信号给每个container中的1号过程
- 如果存在Prestop Hook,kubelet 会调用每个容器的 preStop hook,如果 preStop hook 的运行工夫超出了 grace period,kubelet 会发送 SIGTERM 并再等 2 秒(能够通过调整参数
- 在优雅退出的同时,k8s 会将 Pod 从对应的 Service 上摘除
- grace period 超出之后,kubelet 发送 SIGKILL 给Pod中的所有运行容器;同上清理pause状态的container
- Kubelet向API-Server发送申请,强制删除Pod(通过将grace period设置为0)
- API Server删除Pod在etcd中的数据
详情参考官网阐明:https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
问题
- 无奈预测 Pod 会在多久之内实现优雅退出,导致某些非凡资源不能开释或者牵引
- 优雅退出的代码逻辑须要很久能力解决实现或者存在BUG(此问题能够要求业务进行革新,上面不再阐明)
- 程序代码没有解决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
数量的调整。
manageReplicas
是ReplicasSet
中外围的办法,它会计算 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申请与资源开释的时序图
流程如下:
- 通过API删除
Pod
API-Server
接管到申请后,调用内部WebHook
进行校验WebHook
须要先辨认出Pod
是否须要开释资源。同时须要查看资源是否进行了开释,如果资源已开释,则批准删除;如果须要首先创立一个CRD
实例,同时拒绝请求,Pod
将不会被删除(创立CRD
目标次要是针对用户手动删除的这种状况,其余删除都是由各种资源的controller触发的,为了满足状态须要会一直触发删除申请)- controller发现新的
CRD
资源创立当前清理Pod
内部资源(注册核心、数据等) - 如果清理未实现,整个流程会因为 controller 的管制循环回到第 4 步
- 清理实现后由controller删除对应的
Pod
影响
- Pod delete: 清理工作未实现时,Pod无奈删除
- Pod update: 清理工作未实现时,不能进行Pod非凡资源(须要重启Pod能力实现的设置,比方镜像)的更新