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: 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
的退出流程
- 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 能力实现的设置,比方镜像)的更新