在K8s中,Pod作为工作负载的运行载体,是最为核心的一个资源对象。Pod具有复杂的生命周期,在其生命周期的每一个阶段,可能发生多种不同的异常情况。K8s作为一个复杂系统,异常诊断往往要求强大的知识和经验储备。结合实战经历以及EDAS用户真实场景的归纳,我们总结了K8s Pod的13种常见异常场景,给出各个场景的常见错误状态,分析其原因和排查思路。
本文篇幅超过7千字,通读全文大概需要20分钟。文章内容源自大量真实场景的沉淀和分析,建议收藏,以供查阅。
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阶段。
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容器是否运行为标志点,我们将异常场景大致分为两类:
- 在Pod进行调度并创建容器过程中发生异常,此时Pod将卡在Pending阶段。
- Pod容器运行中发生异常,此时Pod按照具体场景处在不同阶段。
下文将对这具体的13种场景进行描述和分析。
调度失败
常见错误状态: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. 拉取海外镜像例如gcr.io仓库镜像,需配置镜像加速服务。
- 镜像拉取超时
常见于带宽不足或镜像体积太大,导致拉取超时。可以尝试在节点上手动拉取镜像,观察传输速率和传输时间,必要时可以对集群带宽进行升配,或者适当调整 Kubelet 的 --image-pull-progress-deadline 和 --runtime-request-timeout 选项。
- 同时拉取多个镜像,触发并行度控制
常见于用户弹性扩容出一个节点,大量待调度Pod被同时调度上去,导致一个节点同时有大量Pod启动,同时从镜像仓库拉取多个镜像。而受限于集群带宽、镜像仓库服务稳定性、容器运行时镜像拉取并行度控制等因素,镜像拉取并不支持大量并行。这种情况可以手动打断一些镜像的拉取,按照优先级让镜像分批拉取。
依赖项错误
常见错误状态:Error
在 Pod 启动之前,Kubelet将尝试检查与其他 K8s 元素的所有依赖关系。主要存在的依赖项有三种:PersistentVolume、ConfigMap和Secret。当这些依赖项不存在或者无法读取时,Pod容器将无法正常创建,Pod会处于Pending状态直到满足依赖性。当这些依赖项能被正确读取,但出现配置错误时,也会出现无法创建容器的情况。比如将一个只读的持久化存储卷PersistentVolume以可读写的形式挂载到容器,或者将存储卷挂载到/proc等非法路径,也会导致容器创建失败。
容器创建失败
常见错误状态:Error
Pod容器创建过程中出现了错误。常见原因包括:
- 违反集群的安全策略,比如违反了 PodSecurityPolicy 等。
- 容器无权操作集群内的资源,比如开启 RBAC 后,需要为 ServiceAccount 配置角色绑定。
- 缺少启动命令,Pod描述文件和镜像Dockerfile中均未指定启动命令。
- 启动命令配置错误。Pod配置文件可以通过command字段定义命令行,通过args字段给命令行定义参数。启动命令配置错误的情况非常多见,要格外注意命令及参数的格式。正确的填写方式可参考:
初始化失败
常见错误状态: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
Pod处于Unkonwn状态,无法获取其详细信息,一般是因为所在节点Kubelet异常,无法向APIServer上报Pod信息。首先检查节点状态,通过Kubelet和容器运行时的日志信息定位错误,进行修复。如果无法及时修复节点,可以先将该节点从集群中删除。
无法被删除
常见错误状态:卡在Terminating
当一个Pod被执行删除操作后,却长时间处于Terminating状态,这种情况的原因有几种:
- Pod关联的finalizer未完成。首先查看Pod的metadata字段是否包含finalizer,通过一些特定上下文信息确认finalizer任务具体是什么,通常finalizer的任务未完成可能是因为与Volume相关。如果finalizer已经无法被完成,可以通过patch操作移除对应的Pod上的finalizer完成删除操作。
- Pod对中断信号没有响应。Pod没有被终止可能是进程对信号没有响应,可以尝试强制删除Pod。
- 节点故障。通过查看相同节点上的其他Pod状态确认是否节点故障,尝试重启Kubelet和容器运行时。如果无法修复,先将该节点从集群中删除。
EDAS排障工具链
EDAS对应用全生命周期的大部分异常都有沉淀和分析,降低用户学习成本,缩短排障时间。EDAS提供一系列解决方案和工具帮助用户解决应用生命周期中的异常问题,包括应用变更前的变更预检、应用变更和运行的事件追踪可观测、应用异常时的诊断工具。
应用变更预检
EDAS在应用变更任务下发前将经过预检环节,应用变更预检可以在应用部署前检查集群状态及变更参数是否有效,能够有效避免应用变更过程出错,降低变更风险。当前应用变更预检提供集群可用资源检查、集群健康检查、各项依赖配置检查等项目,对于非预期的预检结果给出分析和处置建议。例如对于集群资源余量不满足Pod调度需求的异常场景,变更预检结果将显示资源检查不通过,用户能够第一时间做出针对性调整。
应用事件观测
EDAS对应用生命周期中的事件进行追踪提供可观测能力。对于应用变更过程提供完整的事项展示,让用户能够白屏观测到变更中的每一个步骤和相关上下文信息。当出现异常变更情况时,将具体的事件和相关资源信息在白屏透出,并对异常事件进行分析解读并给出操作建议。例如给Pod配置了容器服务仓库镜像,但并未正确配置集群免密插件,EDAS将镜像拉取失败事件抛出,并引导用户检查镜像拉取权限。
诊断工具箱
对于异常Pod,通常需要连接到Pod容器,对业务进程进行诊断,必要时候还需要对异常进行复现。EDAS提供云原生工具箱,让用户在网页上连接Pod容器Shell,并且提供Arthas、Tcpdump等工具,弥补镜像软件工具包的缺失。对于Pod已经消失、不适合在业务Pod进行诊断等场景,云原生工具箱提供Pod复制能力,根据诊断场景不同,用户可以按需选择开启诊断Pod。
对于上文中提到的容器进程被大流量阻塞,导致Pod被Liveness打挂的场景,用户可以通过云原生工具箱,开启一个移除Liveness的诊断Pod,设置全链路流量控制规则,打入一些测试流量,使用Arthas提供的trace、stack、watch等工具精准定位问题
参考文档
- https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
- https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
- https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/
- https://docs.docker.com/engine/reference/commandline/pull/#concurrent-downloads
- https://developer.aliyun.com/article/1066441
- https://alibaba.github.io/arthas