k8s pod 抢占后陷入失败/关闭状态 (gke v1.20)

jbp*_*lps 8 node.js kubernetes google-kubernetes-engine

TL;DR - gke 1.20 可抢占节点导致 Pod 僵尸化并导致失败/关闭

我们已经使用 GKE 几年了,集群中包含稳定节点池和可抢占节点池。最近,自 gke v1.20 以来,我们开始看到抢占的 Pod 进入奇怪的僵尸状态,它们被描述为:

状态:失败

原因: 关机

消息:节点正在关闭,正在驱逐 Pod

当这种情况开始发生时,我们确信这与我们的 Pod 未能在抢占时正确处理 SIGTERM 有关。我们决定通过将服务软件简化为一个大部分处于睡眠状态的简单服务来消除其问题根源:

/* eslint-disable no-console */
let exitNow = false

process.on( 'SIGINT', () => {
  console.log( 'INT shutting down gracefully' )
  exitNow = true
} )

process.on( 'SIGTERM', () => {
  console.log( 'TERM shutting down gracefully' )
  exitNow = true
} )

const sleep = ( seconds ) => {
  return new Promise( ( resolve ) => {
    setTimeout( resolve, seconds * 1000 )
  } )
}

const Main = async ( cycles = 120, delaySec = 5 ) => {
  console.log( `Starting ${cycles}, ${delaySec} second cycles` )

  for ( let i = 1; i <= cycles && !exitNow; i++ ) {
    console.log( `---> ${i} of ${cycles}` )
    await sleep( delaySec ) // eslint-disable-line
  }

  console.log( '*** Cycle Complete - exiting' )
  process.exit( 0 )
}

Main()
Run Code Online (Sandbox Code Playgroud)

此代码使用 tini init 构建到 docker 映像中,以生成在 nodejs 下运行的 pod 进程(fermium-alpine 映像)。无论我们如何调整信号处理,吊舱似乎永远不会真正完全关闭,即使日志表明它们是这样的。

另一个奇怪的地方是,根据 Kubernetes Pod 日志,我们看到 Pod 终止开始,然后被取消:

2021-08-06 17:00:08.000 EDT 停止容器 preempt-pod

2021-08-06 17:02:41.000 EDT 取消删除 Pod preempt-pod

我们还尝试添加 preStop 15 秒延迟,只是为了看看是否有任何效果,但我们尝试的任何操作似乎都不重要 - 豆荚变成了僵尸。新副本在池中可用的其他节点上启动,因此它始终保持系统上成功运行的 Pod 的最小数量。

我们还使用 sim 维护事件来测试抢占周期:

gcloud 计算实例模拟维护事件节点 ID

jbp*_*lps 7

在浏览了各种帖子后,我最终决定每 9 分钟运行一次 cronjob,以避免 pod 处于关闭状态超过 10 分钟后触发 AlertManager。对我来说,这仍然感觉像是一种 hack,但它确实有效,并且它迫使我深入研究 k8s cronjob 和 RBAC。

这篇文章让我开始走上这条道路: 如何删除 Kubernetes 'shutdown' pod

以及由此产生的 cronjob 规范:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-accessor-role
  namespace: default
rules:
- apiGroups: [""] # "" indicates the core API group
  resources: ["pods"]
  verbs: ["get", "delete", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-access
  namespace: default
subjects:
- kind: ServiceAccount
  name: cronjob-sa
  namespace: default
roleRef:
  kind: Role
  name: pod-accessor-role
  apiGroup: ""
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: cronjob-sa
  namespace: default
---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: cron-zombie-killer
  namespace: default
spec:
  schedule: "*/9 * * * *"
  successfulJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        metadata:
          name: cron-zombie-killer
          namespace: default
        spec:
          serviceAccountName: cronjob-sa
          restartPolicy: Never
          containers:
          - name: cron-zombie-killer
            imagePullPolicy: IfNotPresent
            image: bitnami/kubectl
            command:
              - "/bin/sh"
            args:
              - "-c"
              - "kubectl get pods -n default --field-selector='status.phase==Failed' -o name | xargs kubectl delete -n default 2> /dev/null"
status: {}
Run Code Online (Sandbox Code Playgroud)

请注意,将 stderr 重定向到 /dev/null 只是为了避免当 kubectl get 找不到任何处于失败状态的 pod 时 kubectl delete 的错误输出。

更新添加了角色中缺少的“删除”动词,并添加了缺少的 RoleBinding

更新添加的 imagePullPolicy

  • 另一个提示:如果您需要跨命名空间删除 pod,例如通过在 kubectl 命令中使用 --all-namespaces-flag,那么您必须使用“ClusterRole”和“ClusterRoleBinding”类型。 (2认同)