解读K8s Pod的13种典型异常

在K8s中,Pod作为工作负载的运行载体,是最为核心的一个资源对象。Pod具有复杂的生命周期,在其生命周期的每一个阶段,可能发生多种不同的异常情况。K8s作为一个复杂系统,异常诊断往往要求强大的知识和经验储备。结合实战经历以及EDAS用户真实场景的归纳,我们总结了K8s Pod的13种常见异常场景,给出各个场景的常见错误状态,分析其原因和排查思路。

Pod生命周期

在整个生命周期中,Pod会出现5种阶段(Phase)。

  • Pending:Pod被K8s创建出来后,起始于Pending阶段。在Pending阶段,Pod将经过调度,被分配至目标节点开始拉取镜像、加载依赖项、创建容器。
  • Running:当Pod所有容器都已被创建,且至少一个容器已经在运行中,Pod将进入Running阶段。
  • Succeeded:当Pod中的所有容器都执行完成后终止,并且不会再重启,Pod将进入Succeeded阶段。
  • Failed:若Pod中的所有容器都已终止,并且至少有一个容器是因为失败终止,也就是说容器以非0状态异常退出或被系统终止,Pod将进入Failed阶段。
  • Unkonwn:因为某些原因无法取得 Pod 状态,这种情况Pod将被置为Unkonwn状态。

一般来说,对于Job类型的负载,Pod在成功执行完任务之后将会以Succeeded状态为终态。而对于Deployment等负载,一般期望Pod能够持续提供服务,直到Pod因删除消失,或者因异常退出/被系统终止而进入Failed阶段。

nowjava.com

Pod的5个阶段是 Pod 在其生命周期中所处位置的简单宏观概述,并不是对容器或 Pod 状态的综合汇总。Pod有一些细分状态( PodConditions ),例如Ready/NotReady、Initialized、 PodScheduled/Unschedulable等。这些细分状态描述造成Pod所处阶段的具体成因是什么。比如,Pod 当前阶段是Pending,对应的细分状态是 Unschedulable,这就意味着Pod调度出现了问题。

容器也有其生命周期状态(State):Waiting、Running和 Terminated。并且也有其对应的状态原因(Reason),例如ContainerCreating、Error、OOMKilled、CrashLoopBackOff、Completed等。而对于发生过重启或终止的容器,上一个状态(LastState)字段不仅包含状态原因,还包含上一次退出的状态码(Exit Code)。例如容器上一次退出状态码是137,状态原因是OOMKilled,说明容器是因为OOM被系统强行终止。在异常诊断过程中,容器的退出状态是至关重要的信息。

除了必要的集群和应用监控,一般还需要通过kubectl命令搜集异常状态信息。

// 获取Pod当前对象描述文件
kubectl get pod <podName> -n <namespace> -o yaml 

// 获取Pod信息和事件(Events)
kubectl describe pod <podName> -n <namespace>

// 获取Pod容器日志
kubectl logs <podName> <containerName> -n <namespace>

// 在容器中执行命令
kubectl exec <podName> -n <namespace> -c <containerName> -- <CMD> <ARGS>


Pod异常场景

Pod在其生命周期的许多时间点可能发生不同的异常,按照Pod容器是否运行为标志点,我们将异常场景大致分为两类:

  1. 在Pod进行调度并创建容器过程中发生异常,此时Pod将卡在Pending阶段。
  2. Pod容器运行中发生异常,此时Pod按照具体场景处在不同阶段。

下文将对这具体的13种场景进行描述和分析。

nowjava.com


调度失败

常见错误状态:Unschedulable

Pod被创建后进入调度阶段,K8s调度器依据Pod声明的资源请求量和调度规则,为Pod挑选一个适合运行的节点。当集群节点均不满足Pod调度需求时,Pod将会处于Pending状态。造成调度失败的典型原因如下:

  • 节点资源不足

K8s将节点资源(CPU、内存、磁盘等)进行数值量化,定义出节点资源容量(Capacity)和节点资源可分配额(Allocatable)。资源容量是指 Kubelet 获取的计算节点当前的资源信息,而资源可分配额是Pod可用的资源。Pod容器有两种资源额度概念:请求值Request和限制值Limit,容器至少能获取请求值大小、至多能获取限制值的资源量。Pod 的资源请求量是Pod中所有容器的资源请求之和,Pod的资源限制量是Pod中所有容器的资源限制之和。K8s默认调度器按照较小的请求值作为调度依据,保障可调度节点的资源可分配额一定不小于Pod资源请求值。当集群没有一个节点满足Pod的资源请求量,则Pod将卡在Pending状态。

Pod因为无法满足资源需求而被Pending,可能是因为集群资源不足,需要进行扩容,也有可能是集群碎片导致。以一个典型场景为例,用户集群有10几个4c8g的节点,整个集群资源使用率在60%左右,每个节点都有碎片,但因为碎片太小导致扩不出来一个2c4g的Pod。一般来说,小节点集群会更容易产生资源碎片,而碎片资源无法供Pod调度使用。如果想最大限度地减少资源浪费,使用更大的节点可能会带来更好的结果。

  • 超过Namespace资源配额

K8s用户可以通过资源配额(Resource Quota)对Namespace进行资源使用量限制,包括两个维度:

1. 限定某个对象类型(如Pod)可创建对象的总数。

2. 限定某个对象类型可消耗的资源总数。

如果在创建或更新Pod时申请的资源超过了资源配额,则Pod将调度失败。此时需要检查Namespace资源配额状态,做出适当调整。

  • 不满足 NodeSelector节点选择器

Pod通过NodeSelector节点选择器指定调度到带有特定Label的节点,若不存在满足 NodeSelector的可用节点,Pod将无法被调度,需要对NodeSelector或节点Label进行合理调整。

  • 不满足亲和性

节点亲和性(Affinity)和反亲和性(Anti-Affinity)用于约束Pod调度到哪些节点,而亲和性又细分为软亲和(Preferred)和硬亲和(Required)。对于软亲和规则,K8s调度器会尝试寻找满足对应规则的节点,如果找不到匹配的节点,调度器仍然会调度该 Pod。而当硬亲和规则不被满足时,Pod将无法被调度,需要检查Pod调度规则和目标节点状态,对调度规则或节点进行合理调整。

  • 节点存在污点

K8s提供污点(Taints)和容忍(Tolerations)机制,用于避免 Pod 被分配到不合适的节点上。假如节点上存在污点,而 Pod 没有设置相应的容忍,Pod 将不会调度到该 节点。此时需要确认节点是否有携带污点的必要,如果不必要的话可以移除污点;若Pod可以分配到带有污点的节点,则可以给Pod增加污点容忍。

  • 没有可用节点

节点可能会因为资源不足、网络不通、Kubelet未就绪等原因导致不可用(NotReady)。当集群中没有可调度的节点,也会导致Pod卡在Pending状态。此时需要查看节点状态,排查不可用节点问题并修复,或进行集群扩容。


镜像拉取失败

常见错误状态:ImagePullBackOff

Pod经过调度后分配到目标节点,节点需要拉取Pod所需的镜像为创建容器做准备。拉取镜像阶段可能存在以下几种原因导致失败:

  • 镜像名字拼写错误或配置了错误的镜像

出现镜像拉取失败后首先要确认镜像地址是否配置错误。

  • 私有仓库的免密配置错误

集群需要进行免密配置才能拉取私有镜像。自建镜像仓库时需要在集群创建免密凭证Secret,在Pod指定ImagePullSecrets,或者将Secret嵌入ServicAccount,让Pod使用对应的ServiceAccount。而对于acr等镜像服务云产品一般会提供免密插件,需要在集群中正确安装免密插件才能拉取仓库内的镜像。免密插件的异常包括:集群免密插件未安装、免密插件Pod异常、免密插件配置错误,需要查看相关信息进行进一步排查。

  • 网络不通

网络不通的常见场景有三个:

1. 集群通过公网访问镜像仓库,而镜像仓库未配置公网的访问策略。对于自建仓库,可能是端口未开放,或是镜像服务未监听公网IP;对于acr等镜像服务云产品,需要确认开启公网的访问入口,配置白名单等访问控制策略。

2. 集群位于专有网络,需要为镜像服务配置专有网络的访问控制,才能建立集群节点与镜像服务之间的连接。

3. 拉取海外镜像例如http://gcr.io仓库镜像,需配置镜像加速服务。

  • 镜像拉取超时

常见于带宽不足或镜像体积太大,导致拉取超时。可以尝试在节点上手动拉取镜像,观察传输速率和传输时间,必要时可以对集群带宽进行升配,或者适当调整 Kubelet 的 --image-pull-progress-deadline 和 --runtime-request-timeout 选项。

  • 同时拉取多个镜像,触发并行度控制

常见于用户弹性扩容出一个节点,大量待调度Pod被同时调度上去,导致一个节点同时有大量Pod启动,同时从镜像仓库拉取多个镜像。而受限于集群带宽、镜像仓库服务稳定性、容器运行时镜像拉取并行度控制等因素,镜像拉取并不支持大量并行。这种情况可以手动打断一些镜像的拉取,按照优先级让镜像分批拉取。


依赖项错误

常见错误状态:Error

nowjava.com

在 Pod 启动之前,Kubelet将尝试检查与其他 K8s 元素的所有依赖关系。主要存在的依赖项有三种:PersistentVolume、ConfigMap和Secret。当这些依赖项不存在或者无法读取时,Pod容器将无法正常创建,Pod会处于Pending状态直到满足依赖性。当这些依赖项能被正确读取,但出现配置错误时,也会出现无法创建容器的情况。比如将一个只读的持久化存储卷PersistentVolume以可读写的形式挂载到容器,或者将存储卷挂载到/proc等非法路径,也会导致容器创建失败。


容器创建失败

常见错误状态:Error

Pod容器创建过程中出现了错误。常见原因包括:

  • 违反集群的安全策略,比如违反了 PodSecurityPolicy 等。
  • 容器无权操作集群内的资源,比如开启 RBAC 后,需要为 ServiceAccount 配置角色绑定。
  • 缺少启动命令,Pod描述文件和镜像Dockerfile中均未指定启动命令。
  • 启动命令配置错误。Pod配置文件可以通过command字段定义命令行,通过args字段给命令行定义参数。启动命令配置错误的情况非常多见,要格外注意命令及参数的格式。正确的填写方式可参考:

nowjava.com


初始化失败

常见错误状态:CrashLoopBackOff

K8s提供Init Container特性,用于在启动应用容器之前启动一个或多个初始化容器,完成应用程序所需的预置条件。Init container与应用容器本质上是一样的,但它们是仅运行一次就结束的任务,并且必须在执行完成后,系统才能继续执行下一个容器。如果 Pod 的Init Container执行失败,将会block业务容器的启动。通过查看Pod状态和事件定位到Init Container故障后,需要查看Init Container日志进一步排查故障点。


回调失败

常见错误状态:FailedPostStartHook或FailedPreStopHook事件

K8s提供PostStart和PreStop两种容器生命周期回调,分别在容器中的进程启动前或者容器中的进程终止之前运行。PostStart 在容器创建之后立即执行,但由于是异步执行,无法保证和容器启动命令的执行顺序相关联。PreStop 在容器终止之前被同步阻塞调用,常用于在容器结束前优雅地释放资源。如果PostStart或者PreStop 回调程序执行失败,容器将被终止,按照重启策略决定是否重启。当出现回调失败,会出现FailedPostStartHook或FailedPreStopHook事件,进一步结合容器打出的日志进行故障排查。


就绪探针失败

常见错误状态:容器已经全部启动,但是Pod处于NotReady状态,服务流量无法从Service达到Pod

K8s使用Readiness Probe(就绪探针)来确定容器是否已经就绪可以接受流量。只有当Pod 中的容器都处于就绪状态时,K8s 才认定该Pod 处于就绪状态,才会将服务流量转发到该容器。一般就绪探针失败分为几种情况:

  • 容器内应用原因: 健康检查所配置规则对应的端口或者脚本,无法成功探测,如容器内应用没正常启动等。
  • 探针配置不当:写错检查端口导致探测失败;检测间隔和失败阈值设置不合理,例如每次检查间隔1s,一次不通过即失败;启动延迟设置太短,例如应用正常启动需要15s,而设置容器启动10s后启用探针。
  • 系统层问题:节点负载高,导致容器进程hang住。
  • CPU资源不足:CPU资源限制值过低,导致容器进程响应慢。

需要特别说明的是,对于微服务应用,服务的注册和发现由注册中心管理,流量不会经过Service,直接从上游Pod流到下游Pod。然而注册中心并没有如K8s就绪探针的检查机制,对于启动较慢的JAVA应用来说,服务注册成功后所需资源仍然可能在初始化中,导致出现上线后流量有损的情况。对于这一类场景,EDAS提供延迟注册和服务预热等解决方案,解决K8s微服务应用上线有损的问题。


存活探针失败

常见错误状态:CrashLoopBackOff

K8s使用Liveness Probe(存活探针)来确定容器是否正在运行。如果存活态探测失败,则容器会被杀死,随之按照重启策略决定是否重启。存活探针失败的原因与就绪探针类似,然而存活探针失败后容器会被kill消失,所以排障过程要棘手得多。一个典型的用户场景是,用户在压测期间通过HPA弹性扩容出多个新Pod,然而新Pod一启动就被大流量阻塞,无法响应存活探针,导致Pod被kill。kill后又重启,重启完又挂掉,一直在Running和CrashLoopBackOff状态中振荡。微服务场景下可以使用延迟注册和服务预热等手段,避免瞬时流量打挂容器。如果是程序本身问题导致运行阻塞,建议先将Liveness探针移除,通过Pod启动后的监控和进程堆栈信息,找出流量涌入后进程阻塞的根因。


容器退出

常见错误状态:CrashLoopBackOff

容器退出分为两种场景:

  • 启动后立即退出,可能原因是:

1. 启动命令的路径未包含在环境变量PATH中。

2. 启动命令引用了不存在的文件或目录。

3. 启动命令执行失败,可能因为运行环境缺少依赖,也可能是程序本身原因。

4. 启动命令没有执行权限。

5. 容器中没有前台进程。容器应该至少包含一个long-running的前台进程,不能后台运行,比如通过nohup这种方式去启动进程,或是用tomcat的startup.sh脚本。

对于容器启动后立即退出的情况,通常因为容器直接消失,无法获取其输出流日志,很难直接通过现场定位问题。一个简易的排查方式是,通过设置特殊的启动命令卡住容器(比如使用tail -f /dev/null),然后进到容器中手动执行命令看看结果,确认问题原因。

  • 运行一段时间后退出,这种情况一般是容器内1进程Crash或者被系统终止导致退出。此时首先查看容器退出状态码,然后进一步查看上下文信息进行错误定位。这种情况发生时容器已经删除消失,无法进入容器中查看日志和堆栈等现场信息,所以一般推荐用户对日志、错误记录等文件配置持久化存储,留存更多现场信息。几种常见的状态码如下:


状态码含义分析
0正常退出容器的启动程序不是一个long-running的程序。如果正常退出不符合预期,需要检查容器日志,对程序的执行逻辑进行调整。
137外部终止137 表示容器已收到来自主机操作系统的 SIGKILL 信号。该信号指示进程立即终止,没有宽限期。可能原因包含:容器运行时将容器kill,例如docker kill命令;Linux 用户向进程发送 kill -9 命令触发;K8s尝试终止容器,超过优雅下线窗口期后直接kill容器;由节点系统触发,比如遭遇了OOM。
139段错误139表示容器收到了来自操作系统的 SIGSEGV 信号。这表示分段错误 —— 内存违规,由容器试图访问它无权访问的内存位置引起。
143优雅终止143 表示容器收到来自操作系统的 SIGTERM 信号,该信号要求容器正常终止。该退出码可能的原因是:容器引擎停止容器,例如使用 docker stop 停止了容器;K8s终止了容器,比如缩容行为将Pod删除。


OOMKilled

常见错误状态:OOMKilled

K8s中有两种资源概念:可压缩资源(CPU)和不可压缩资源(内存,磁盘 )。当CPU这种可压缩资源不足时,Pod只会“饥饿”,但不会退出;而当内存和磁盘IO这种不可压缩资源不足时,Pod会被kill或者驱逐。因为内存资源不足/超限所导致的Pod异常退出的现象被称为Pod OOMKilled。K8s存在两种导致Pod OOMKilled的场景:

  • Container Limit Reached,容器内存用量超限

Pod内的每一个容器都可以配置其内存资源限额,当容器实际占用的内存超额,该容器将被OOMKilled并以状态码137退出。OOMKilled往往发生在Pod已经正常运行一段时间后,可能是由于流量增加或是长期运行累积的内存逐渐增加。这种情况需要查看程序日志以了解为什么Pod使用的内存超出了预期,是否出现异常行为。如果发现程序只是按照预期运行就发生了OOM,就需要适当提高Pod内存限制值。一个很常见的错误场景是,JAVA容器设置了内存资源限制值Limit,然而JVM堆大小限制值比内存Limit更大,导致进程在运行期间堆空间越开越大,最终因为OOM被终止。对于JAVA容器来说,一般建议容器内存限制值Limit需要比JVM 最大堆内存稍大一些。

  • Limit Overcommit,节点内存耗尽

K8s有两种资源额度概念:请求值Request和限制值Limit,默认调度器按照较小的请求值作为调度依据,保障节点的所有Pod资源请求值总和不超过节点容量,而限制值总和允许超过节点容量,这就是K8s资源设计中的Overcommit(超卖)现象。超卖设计在一定程度上能提高吞吐量和资源利用率,但会出现节点资源被耗尽的情况。当节点上的Pod实际使用的内存总和超过某个阈值,K8s将会终止其中的一个或多个Pod。为了尽量避免这种情况,建议在创建Pod时选择大小相等或相近的内存请求值和限制值,也可以利用调度规则将内存敏感型Pod打散到不同节点。


Pod驱逐

常见错误状态:Pod Evicted

当节点内存、磁盘这种不可压缩资源不足时,K8s会按照QoS等级对节点上的某些Pod进行驱逐,释放资源保证节点可用性。当Pod发生驱逐后,上层控制器例如Deployment会新建Pod以维持副本数,新Pod会经过调度分配到其他节点创建运行。对于内存资源,前文已经分析过可以通过设置合理的请求值和限制值,避免节点内存耗尽。而对于磁盘资源,Pod在运行期间会产生临时文件、日志,所以必须对Pod磁盘容量进行限制,否则某些Pod可能很快将磁盘写满。类似限制内存、CPU 用量的方式,在创建Pod时可以对本地临时存储用量(ephemeral-storage)进行限制。同时,Kubelet驱逐条件默认磁盘可用空间在10%以下,可以调整云监控磁盘告警阈值以提前告警。


Pod失联

常见错误状态:Unkonwn
展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java