为何statefulset管理的pod会卡在pending状态
问题
接到测试同事反馈的一个小问题:
- 创建一个statefulset,container resource req明显过大,超过任何节点的allocatable值,此时pod会卡在pending状态
- edit上述statefuleset,将resource req改为合理的显然可调度的值
- 此时预期pending pod会被干掉,新的resource req的pod被创建,从而statefulset状态实现正常(这种预期本质上来自于常见的deployment逻辑)
- 然而3里的预期并未实现,实际情况是无事发生:
- pending pod不会被干掉
- 新pod不会创建出来
- 但是statefulset中的resource是修改成功了的
所以总结问题如下:statefulset控制的pod,状态流转到pending后,后续行为是什么?和无状态的deployment是否一致?
代码
根据之前看kubelet和kube-scheduler代码的经验,首先大致推测一下controller-manager在代码结构中的位置。通常核心逻辑还是位于pkg包下,按照字母排序从a往下遍历,很快便可以看到controller包。打开可以看到很多以object名称命名的目录。看到这里大概就应该知道找对了。果然在其中可以找到statefulset目录,应该就是statefulset控制器的代码了。
进入stateful_set.go
,可以看到熟悉的基于client-go实现的controller模式:基于informer,为statefulset的新增、修改、删除分别注册事件处理函数,将发生变化的statefulset入队(生产者模型);同时启动worker协程,不断从队列中取出statefulset,进行逻辑处理。
我们只需要关注worker端,最初的处理入口:
// pkg/controller/statefulset/stateful_set.go:407
func (ssc *StatefulSetController) sync(key string) error {
...
return ssc.syncStatefulSet(set, pods)
}
沿着函数的调用,来到第一处对本文问题提供了有效信息的地方:一处注释。我们看到注释中有一句:当有任何pod处在unhealthy状态时,不会有新pod被创建。从这句注释,大概就可以推测了,本文描述的问题,应该是statefulset设计上的本意,并非bug
// UpdateStatefulSet executes the core logic loop for a stateful set, applying the predictable and
// consistent monotonic update strategy by default - scale up proceeds in ordinal order, no new pod
// is created while any pod is unhealthy, and pods are terminated in descending order. The burst
// strategy allows these constraints to be relaxed - pods will be created and deleted eagerly and
// in no particular order. Clients using the burst strategy should be careful to ensure they
// understand the consistency implications of having unpredictable numbers of pods available.
func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {
// list all revisions and sort them
revisions, err := ssc.ListRevisions(set)
if err != nil {
return err
}
history.SortControllerRevisions(revisions)
currentRevision, updateRevision, err := ssc.performUpdate(set, pods, revisions)
if err != nil {
return utilerrors.NewAggregate([]error{err, ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)})
}
// maintain the set's revision history limit
return ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)
}
进一步,我们看最最核心的逻辑所处的函数:
func (ssc *defaultStatefulSetControl) updateStatefulSet(
set *apps.StatefulSet,
currentRevision *apps.ControllerRevision,
updateRevision *apps.ControllerRevision,
collisionCount int32,
pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
...
}
由于该函数长达300+行,不便一一展开。只记录和本文问题相关的关键点:
- 逻辑中会对statefulset下属的pod做状态判断,根据不同的状态,采取不同的行为。比如
isFailed
、isCreated
、isTerminating
、isRunningAndReady
等,处在pending状态的pod并不属于这些状态中的任何一个,因此在这些逻辑都不会操作pending状态下的pod。这就解释了为什么更新了statefulset后,不会有新的pod被创建出来 - 那么,为什么pending状态的pod没有被干掉?如果代码逻辑把这种情况考虑了,干掉这个pod,就可以修改后的pod创建出来了。但是查看代码发现,控制器删除pod的目标主要是超过副本数限制的pod,并没有考虑pending状态的pod。
- 综上,就会出现卡在pod无法调度,一直pending的状态,即使修改过后的statefulset是完全可调度的(只是用户知道,控制器当然不知道,因为调度信息在scheduler那边,所以逻辑设计成了现在的样子)。
最新版本
上述的代码来源于kubernetes 1.21版本。出于好奇,不知道最新的版本是否对pending状态的pod有所考虑?所以我们切到release-1.28分支,即当前最新的版本,查看是否有多加一些逻辑。发现确实增加了和pending pod相关的逻辑。
func (ssc *defaultStatefulSetControl) processReplica(
ctx context.Context,
set *apps.StatefulSet,
currentRevision *apps.ControllerRevision,
updateRevision *apps.ControllerRevision,
currentSet *apps.StatefulSet,
updateSet *apps.StatefulSet,
monotonic bool,
replicas []*v1.Pod,
i int) (bool, error) {
...
// If the Pod is in pending state then trigger PVC creation to create missing PVCs
if isPending(replicas[i]) {
logger.V(4).Info(
"StatefulSet is triggering PVC creation for pending Pod",
"statefulSet", klog.KObj(set), "pod", klog.KObj(replicas[i]))
if err := ssc.podControl.createMissingPersistentVolumeClaims(ctx, set, replicas[i]); err != nil {
return true, err
}
}
...
}
然而新增的逻辑考虑的是pod由于pvc没有创建而导致的pending。
workaround
业务层发现编辑了statefulset的resource后,删除一次其控制的所有pod(这种情况下其实只会有1个),使新statefulset从全新状态开始逐个创建pod,避免之前pending pod导致的阻塞。
进一步思考
一方面,从k8s官方设计的角度,尝试理解当前的逻辑:由于是分布式系统,controller-manager显然不能对资源调度情况有所干预,因此目前的实现里基本不会干预pending状态,因为pending状态的流转,主要是调度器负责的。
但是另一方面,遇到本文描述的这种情况,又确实不太符合k8s reconcile的设计理念:即对比spec和status的差别,自动化地实现二者的最终一致。
再进一步
上面对代码的主体逻辑有了了解后,是时候抠一下细节了。在代码中,有一些开关变量值得注意。比如这一行:monotonic := !allowsBurst(set)
,我们查看这个allowsBurst
函数:发现statefulset有个字段叫.spec.podManagementPolicy
,取值有两种:OrderedReady
和Parallel
,显然我们常见的statefulset默认行为是基于OrderedReady
策略的。
func allowsBurst(set *apps.StatefulSet) bool {
return set.Spec.PodManagementPolicy == apps.ParallelPodManagement
}
在代码中,当monotonic(意味单调)为true时,代码中的很多逻辑一旦判断pod状态不正常,就会fast return,pod的删除逻辑就不会被触发到。因此才有了上面的分析。
经过测试,把statefulset的spec.podManagementPolicy
改为Parallel
后,编辑statefulset的resource后,即可触发pod的重建,不论旧pod状态是否正常。
注意,这个方法虽然可以解决本文开头描述的问题,但是却会破坏k8s对statefulset pod的严格生命周期顺序管理,需要权衡利弊后决策。
参考
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。