在 kubernetes 使用过程当中,根据集群的配置不一样,每每会由于以下状况的一种或几种致使节点 NotReady:node
当出现这种状况的时候,会出现节点 NotReady,进而当kube-controller-manager 中的--pod-eviction-timeout
定义的值,默认 5 分钟后,将触发 Pod eviction 动做。api
对于不一样类型的 workloads,其对应的 pod 处理方式由于 controller-manager 中各个控制器的逻辑不通而不一样。总结以下:网络
deployment
: 节点 NotReady 触发 eviction 后,pod 将会在新节点重建(若是有 nodeSelector 或者亲和性要求,会处于 Pending 状态),故障节点的 Pod 仍然会保留处于 Unknown 状态,因此此时看到的 pod 数多于副本数。statefulset
: 节点 NotReady 一样会对 StatefulSet 触发 eviction 操做,可是用户看到的 Pod 会一直处于 Unknown 状态没有变化。daemonSet
: 节点 NotReady 对 DaemonSet 不会有影响,查询 pod 处于 NodeLost 状态并一直保持。这里说到,对于 deployment
和 statefulSet
类型资源,当节点 NotReady 后显示的 pod 状态为 Unknown。 这里实际上 etcd 保存的状态为 NodeLost,只是显示时作了处理,与 daemonSet
作了区分。对应代码中的逻辑为:app
### node controller // 触发 NodeEviction 操做时会 DeletePods,这个删除为 GracefulDelete, // apiserver rest 接口对 PodObj 添加了 DeletionTimestamp func DeletePods(kubeClient clientset.Interface, recorder record.EventRecorder, nodeName, nodeUID string, daemonStore extensionslisters.DaemonSetLister) (bool, error) { ... for _, pod := range pods.Items { ... // Set reason and message in the pod object. if _, err = SetPodTerminationReason(kubeClient, &pod, nodeName); err != nil { if apierrors.IsConflict(err) { updateErrList = append(updateErrList, fmt.Errorf("update status failed for pod %q: %v", format.Pod(&pod), err)) continue } } // if the pod has already been marked for deletion, we still return true that there are remaining pods. if pod.DeletionGracePeriodSeconds != nil { remaining = true continue } // if the pod is managed by a daemonset, ignore it _, err := daemonStore.GetPodDaemonSets(&pod) if err == nil { // No error means at least one daemonset was found continue } glog.V(2).Infof("Starting deletion of pod %v/%v", pod.Namespace, pod.Name) recorder.Eventf(&pod, v1.EventTypeNormal, "NodeControllerEviction", "Marking for deletion Pod %s from Node %s", pod.Name, nodeName) if err := kubeClient.CoreV1().Pods(pod.Namespace).Delete(pod.Name, nil); err != nil { return false, err } remaining = true } ... } ### staging apiserver REST 接口 // 对于优雅删除,到这里其实已经中止,再也不进一步删除,剩下的交给 kubelet watch 到变化后去作 delete func (e *Store) Delete(ctx genericapirequest.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) { ... if graceful || pendingFinalizers || shouldUpdateFinalizers { err, ignoreNotFound, deleteImmediately, out, lastExisting = e.updateForGracefulDeletionAndFinalizers(ctx, name, key, options, preconditions, obj) } // !deleteImmediately covers all cases where err != nil. We keep both to be future-proof. if !deleteImmediately || err != nil { return out, false, err } ... } // stagging/apiserver中的 rest 接口调用,设置了 DeletionTimestamp 和 DeletionGracePeriodSeconds func (e *Store) updateForGracefulDeletionAndFinalizers(ctx genericapirequest.Context, name, key string, options *metav1.DeleteOptions, preconditions storage.Preconditions, in runtime.Object) (err error, ignoreNotFound, deleteImmediately bool, out, lastExisting runtime.Object) { ... if options.GracePeriodSeconds != nil { period := int64(*options.GracePeriodSeconds) if period >= *objectMeta.GetDeletionGracePeriodSeconds() { return false, true, nil } newDeletionTimestamp := metav1.NewTime( objectMeta.GetDeletionTimestamp().Add(-time.Second * time.Duration(*objectMeta.GetDeletionGracePeriodSeconds())). Add(time.Second * time.Duration(*options.GracePeriodSeconds))) objectMeta.SetDeletionTimestamp(&newDeletionTimestamp) objectMeta.SetDeletionGracePeriodSeconds(&period) return true, false, nil } ... } ### node controller // SetPodTerminationReason 尝试设置 Pod状态和缘由到 Pod 对象中 func SetPodTerminationReason(kubeClient clientset.Interface, pod *v1.Pod, nodeName string) (*v1.Pod, error) { if pod.Status.Reason == nodepkg.NodeUnreachablePodReason { return pod, nil } pod.Status.Reason = nodepkg.NodeUnreachablePodReason pod.Status.Message = fmt.Sprintf(nodepkg.NodeUnreachablePodMessage, nodeName, pod.Name) var updatedPod *v1.Pod var err error if updatedPod, err = kubeClient.CoreV1().Pods(pod.Namespace).UpdateStatus(pod); err != nil { return nil, err } return updatedPod, nil } ### 命令行输出 // 打印输出时状态的切换,若是 "DeletionTimestamp 不为空" 且 "podStatus 为 NodeLost 状态"时, // 显示的状态为 Unknown func printPod(pod *api.Pod, options printers.PrintOptions) ([]metav1alpha1.TableRow, error) { ... if pod.DeletionTimestamp != nil && pod.Status.Reason == node.NodeUnreachablePodReason { reason = "Unknown" } else if pod.DeletionTimestamp != nil { reason = "Terminating" } ... }
当节点恢复后,不一样的 workload 对应的 pod 状态变化也是不一样的。less
deployment
: 根据上一节描述,此时 pod 已经有正确的 pod 在其余节点 running,此时故障节点恢复后,kubelet 执行优雅删除,删除旧的 PodObj。statefulset
: statefulset 会从Unknown 状态变为 Terminating 状态,执行优雅删除,detach PV,而后执行从新调度与重建操做。daemonset
: daemonset 会从 NodeLost 状态直接变成 Running 状态,不涉及重建。spa
咱们每每会考虑下面两个问题,statefulset 为何没有重建? 如何保持单副本 statefulset 的高可用呢?插件
关于为何没重建命令行
首先简单介绍下 statefulset 控制器的逻辑。rest
Statefulset 控制器经过 StatefulSetControl
以及 StatefulPodControl
2个模块协调完成对 statefulSet 类型 workload 的状态管理(StatefulSetStatusUpdater)和扩缩控制(StatefulPodControl)。实际上,StatefulsetControl是对 StatefulPodControl 的调用来增删改 Pod。code
StatefulSet 在 podManagementPolicy
为默认值 OrderedReady
时,会按照整数顺序单调递增的依次建立 Pod,不然在 Parallel
时,虽然是按整数,可是 Pod 是同时调度与建立。
具体的逻辑在核心方法 UpdateStatefulSet
中,见图:
咱们看到的 Stateful Pod 一直处于 Unknown
状态的缘由就是由于这个控制器屏蔽了对该 Pod 的操做。由于在第一节介绍了,NodeController 的 Pod Eviction 机制已经把 Pod 标记删除,PodObj 中包含的 DeletionTimestamp
被设置,StatefulSet Controller 代码检查 IsTerminating
符合条件,便直接 return 了。
// updateStatefulSet performs the update function for a StatefulSet. This method creates, updates, and deletes Pods in // the set in order to conform the system to the target state for the set. The target state always contains // set.Spec.Replicas Pods with a Ready Condition. If the UpdateStrategy.Type for the set is // RollingUpdateStatefulSetStrategyType then all Pods in the set must be at set.Status.CurrentRevision. // If the UpdateStrategy.Type for the set is OnDeleteStatefulSetStrategyType, the target state implies nothing about // the revisions of Pods in the set. If the UpdateStrategy.Type for the set is PartitionStatefulSetStrategyType, then // all Pods with ordinal less than UpdateStrategy.Partition.Ordinal must be at Status.CurrentRevision and all other // Pods must be at Status.UpdateRevision. If the returned error is nil, the returned StatefulSetStatus is valid and the // update must be recorded. If the error is not nil, the method should be retried until successful. func (ssc *defaultStatefulSetControl) updateStatefulSet( ... for i := range replicas { ... // If we find a Pod that is currently terminating, we must wait until graceful deletion // completes before we continue to make progress. if isTerminating(replicas[i]) && monotonic { glog.V(4).Infof( "StatefulSet %s/%s is waiting for Pod %s to Terminate", set.Namespace, set.Name, replicas[i].Name) return &status, nil } ... } } // isTerminating returns true if pod's DeletionTimestamp has been set func isTerminating(pod *v1.Pod) bool { return pod.DeletionTimestamp != nil }
那么如何保证单副本高可用?
每每应用中有一些 pod 无法实现多副本,可是又要保证集群可以自愈,那么这种某个节点 Down 掉或者网卡坏掉等状况,就会有很大影响,要如何可以实现自愈呢?
对于这种 Unknown
状态的 Stateful Pod ,能够经过 force delete
方式去删除。关于 ForceDelete,社区是不推荐的,由于可能会对惟一的标志符(单调递增的序列号)产生影响,若是发生,对 StatefulSet 是致命的,可能会致使数据丢失(多是应用集群脑裂,也多是对 PV 多写致使)。
kubectl delete pods <pod> --grace-period=0 --force
可是这样删除仍然须要一些保护措施,以 Ceph RBD 存储插件为例,当执行force delete 前,根据经验,用户应该先设置 ceph osd blacklist
,防止当迁移过程当中网络恢复后,容器继续向 PV 写入数据将文件系统弄坏。由于 force delete
是将 PodObj 直接从 ETCD 强制清理,这样 StatefulSet Controller
将会新建新的 Pod 在其余节点, 可是故障节点的 Kubelet 清理这个旧容器须要时间,此时势必存在 2 个容器mount 了同一块 PV(故障节点Pod 对应的容器与新迁移Pod 建立的容器),可是若是此时网络恢复,那么2 个容器可能同时写入数据,后果将是严重的