文末有kubernetes排障全景图,高清版请转发,关注后私信获取。

排查Kubernetes部署故障的3个步骤首先,检查Pod已经创建,并且正常。

可以用下面几个命令用来排查Pod故障:

kubectl logs <pod name> :用来查看Pod容器日志。

kubectl describe pod <pod name>:用于查看与Pod相关的事件列表。

kubectl get pod <pod name>:用于获取Pod的yaml定义。

**kubectl exec -ti <pod name> bash:**对进入Pod容器进行交互式终端。

其次,如果Pod正常,则应检查服务是否可以将流量分配给Pod。

如果的Pod正在运行且已就绪,但仍无法收到应用程序的响应,则应检查服务的配置是否正确。

服务的主要功能是根据流量的标签将流量路由到Pod。所以,先应该检查服务定位了多少个Pod,可以通过检查服务中的端点来查看:

kubectl describe service <service-name> | grep Endpoints

端点是一对<ip address:port>,并且在服务(至少)以Pod为目标时,应该至少有一个。

如果"端点"部分为空,则有两种原因:

没有运行带有正确标签的Pod,应检查是否在正确的命名空间。

服务的选择器标签中有错字;

如果可以看到端点列表,但仍然无法访问应用程序,则很大原因是服务中的targetPort配置有误。

可以通过使用kubectl port-forward连接到服务具体排查:

kubectl port-forward service/<service-name> 3000:80

最后,检查服务与入口之间的连接。

如果Pod运行正常,服务可以分配流量到Pod,则可能原因是入口配置有误:

根据入口可能使用不同控制器类型,需要按具体对应方法进行调试。

检查入口配置参数serviceName和servicePort配置是否正确。可以使用下面命令检查:

kubectl describe ingress <ingress-name>

如果"后端"列为空,则配置中肯定有一个错误。

如果可以在"后端"列中看到端口,但是仍然无法访问该应用程序,则可能是以下问题:

没有如何将入口发布到公网;没有如何将群集发布到公网;

可以通过直接连接到Ingress Pod来将基础结构问题与入口隔离开。

首先,查看入口控制器Pod列表:

kubectl get pods --all-namespaces

其次,使用kubectl describe命令查看端口:

kubectl describe pod Nginx-ingress-controller-6fc5bcc

最后,连接到Pod:

kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system

这样,访问计算机上的端口3000时,请求都会转发到Pod上的端口80。现在应用可以用吗?

如果可行,则问题出在基础架构中。应该检查如何将流量调度到群集。

如果还不行,则问题出在入口控制器中。应该调试入口控制器。常见的入口控制包括Nginx,HAProxy,Traefik等,可以查看具体控制器相关文档进行问题排查。此处我们以Nginx为例:

Ingress-nginx项目是Kubectl官方插件。可以使用kubectl ingress-nginx执行以下操作:

查看日志,后端,证书等;

连接到入口;

检查当前配置。

对应的命令有:

kubectl ingress-nginx lint:用于检查nginx.conf

**kubectl ingress-nginx backend:**用于检查后端(类似于kubectl describe ingress <ingress-name>)

kubectl ingress-nginx logs:查看控制器日志。

接下来对不同情况下的具体报错进行详细描述

容器退出码详解

当容器终止时,容器引擎使用退出码来报告容器终止的原因。容器故障是 pod 异常最常见的原因之一,了解容器退出码可以帮助排查时找到 pod 故障的根本原因。

使用以下命令查看 pod 错误:kubectl describe pod [name]

kubernetes技术简说(kubernetes故障排查及解决)(1)

以下是容器使用的最常见的退出码:

退出码

名称

含义

0

正常退出

开发者用来表明容器是正常退出

1

应用错误

容器因应用程序错误或镜像规范中的错误引用而停止

125

容器未能运行

Docker run 命令没有执行成功

126

命令调用错误

无法调用镜像中指定的命令

127

找不到文件或目录

找不到镜像中指定的文件或目录

128

退出时使用的参数无效

退出是用无效的退出码触发的(有效代码是 0-255 之间的整数)

134

异常终止 (SIGABRT)

容器使用 abort() 函数自行中止

137

立即终止 (SIGKILL)

容器被操作系统通过 SIGKILL 信号终止

139

分段错误 (SIGSEGV)

容器试图访问未分配给它的内存并被终止

143

优雅终止 (SIGTERM)

容器收到即将终止的警告,然后终止

255

退出状态超出范围

容器退出,返回可接受范围之外的退出代码,表示错误原因未知

退出码 0:正常退出

退出代码 0 由开发人员在任务完成后故意停止容器时触发。从技术上讲,退出代码 0 意味着前台进程未附加到特定容器。

如果容器以退出码 0 终止怎么办?

  1. 检查容器日志,确定哪个库导致容器退出;
  2. 查看现有库的代码,并确定它触发退出码 0 的原因,以及它是否正常运行。

退出码 1:应用错误

退出代码 1 表示容器由于以下原因之一停止:

  1. 应用程序错误:这可能是容器运行的代码中的简单编程错误,例如“除以零”,也可能是与运行时环境相关的高级错误,例如 Java、Python 等;
  2. 无效引用:这意味着镜像规范引用了容器镜像中不存在的文件。

如果容器以退出码 1 终止怎么办?

  1. 检查容器日志以查看是否找不到映像规范中列出的文件之一。如果这是问题所在,请更正镜像以指向正确的路径和文件名。
  2. 如果您找不到不正确的文件引用,请检查容器日志以查找应用程序错误,并调试导致错误的库。

退出码 125:容器未能运行

退出码 125 表示该命令用于运行容器。例如 docker run 在 shell 中被调用但没有成功执行。以下是可能发生这种情况的常见原因:

  1. 命令中使用了未定义的 flag,例如 docker run --abcd;
  2. 镜像中用户的定义命令在本机权限不足;
  3. 容器引擎与宿主机操作系统或硬件不兼容。

如果容器以 退出码 125 终止怎么办?

  1. 检查运行容器的命令语法是否正确;
  2. 检查运行容器的用户,或者镜像中执行命令的上下文,是否有足够的权限在宿主机上创建容器;
  3. 如果您的容器引擎提供了运行容器的 option,请尝试它们。例如,在 Docker 中,尝试 docker start 而不是 docker run;
  4. 测试您是否能够使用相同的用户名或上下文在主机上运行其他容器。如果不能,重新安装容器引擎,或者解决容器引擎和主机设置之间的底层兼容性问题。

退出码 126:命令调用错误

退出码 126 表示无法调用容器镜像中使用的命令。这通常是用于运行容器的持续集成脚本中缺少依赖项或错误的原因。

如果容器以退出码 126 终止怎么办?

  1. 检查容器日志,查看无法调用哪个命令;
  2. 尝试在没有命令的情况下运行容器以确保隔离问题;
  3. 对命令进行故障排除以确保您使用正确的语法,并且所有依赖项都可用;
  4. 更正容器规范并重试运行容器。

退出码 127:找不到文件或目录

退出码 127 表示容器中指定的命令引用了不存在的文件或目录。

如果容器以退出码 127 终止怎么办?

与退出码 126 相同,识别失败的命令,并确保容器镜像中引用的文件名或文件路径真实有效。

退出码 128:退出时使用的参数无效

退出码 128 表示容器内的代码触发了退出命令,但没有提供有效的退出码。Linux exit 命令只允许 0-255 之间的整数,因此如果进程以退出码 3.5 退出,则日志将报告退出代码 128。

如果容器以退出码 128 终止怎么办?

  1. 检查容器日志以确定哪个库导致容器退出。
  2. 确定有问题的库在哪里使用了 exit 命令,并更正它以提供有效的退出代码。

退出码 134:异常终止 (SIGABRT)

退出码 134 表示容器自身异常终止,关闭进程并刷新打开的流。此操作是不可逆的,类似 SIGKILL(请参阅下面的退出码 137)。进程可以通过执行以下操作之一来触发 SIGABRT:

如果容器以退出码 134 终止怎么办?

  1. 检查容器日志,查看哪个库触发了 SIGABRT 信号;
  2. 检查中止进程是否是预期内的(例如,因为库处于调试模式),如果不是,则对库进行故障排除,并修改以避免中止容器。

退出码 137:立即终止 (SIGKILL)

退出码 137 表示容器已收到来自主机操作系统的 SIGKILL 信号。该信号指示进程立即终止,没有宽限期。可能的原因是:

如果容器以退出码 137 终止怎么办?

  1. 检查主机上的日志,查看在容器终止之前发生了什么,以及在接收到 SIGKILL 之前是否之前收到过 SIGTERM 信号(优雅终止);
  2. 如果之前有 SIGTERM 信号,请检查您的容器进程是否处理 SIGTERM 并能够正常终止;
  3. 如果没有 SIGTERM 并且容器报告了 OOMKilled 错误,则排查主机上的内存问题。

退出码 139:分段错误 (SIGSEGV)

退出码 139 表示容器收到了来自操作系统的 SIGSEGV 信号。这表示分段错误 —— 内存违规,由容器试图访问它无权访问的内存位置引起。SIGSEGV 错误有三个常见原因:

如果容器以退出码 139 终止怎么办?

  1. 检查容器进程是否处理 SIGSEGV。在 Linux 和 Windows 上,您都可以处理容器对分段错误的响应。例如,容器可以收集和报告堆栈跟踪;
  2. 如果您需要对 SIGSEGV 进行进一步的故障排除,您可能需要将操作系统设置为即使在发生分段错误后也允许程序运行,以便进行调查和调试。然后,尝试故意造成分段错误并调试导致问题的库;
  3. 如果您无法复现问题,请检查主机上的内存子系统并排除内存配置故障。

退出码 143:优雅终止 (SIGTERM)

退出码 143 表示容器收到来自操作系统的 SIGTERM 信号,该信号要求容器正常终止,并且容器成功正常终止(否则您将看到退出码 137)。该退出码可能的原因是:

如果容器以退出码 143 终止怎么办?

检查主机日志,查看操作系统发送 SIGTERM 信号的上下文。如果您使用的是 Kubernetes,请检查 kubelet 日志,查看 pod 是否以及何时关闭。

一般来说,退出码 143 不需要故障排除。这意味着容器在主机指示后正确关闭。

退出码 255:退出状态超出范围

当您看到退出码 255 时,意味着容器的 entrypoint 以该状态停止。这意味着容器停止了,但不知道是什么原因。

如果容器以退出码 255 终止怎么办?

  1. 如果容器在虚拟机中运行,首先尝试删除虚拟机上配置的 overlay 网络并重新创建它们。
  2. 如果这不能解决问题,请尝试删除并重新创建虚拟机,然后在其上重新运行容器。
  3. 如果上述操作失败,则 bash 进入容器并检查有关 entrypoint 进程及其失败原因的日志或其他线索。
Pod 排错

排查过程常用的命名如下:

查看 Pod 状态: kubectl get pod <pod-name> -o wide 查看 Pod 的 yaml 配置: kubectl get pod <pod-name> -o yaml 查看 Pod 事件: kubectl describe pod <pod-name> 查看容器日志: kubectl logs <pod-name> [-c <container-name>]

Pod 有多种状态,这里罗列一下:

Error: Pod 启动过程中发生错误 NodeLost: Pod 所在节点失联 Unkown: Pod 所在节点失联或其它未知异常 Waiting: Pod 等待启动 Pending: Pod 等待被调度 ContainerCreating: Pod 容器正在被创建 Terminating: Pod 正在被销毁 CrashLoopBackOff: 容器退出,kubelet 正在将它重启 InvalidImageName: 无法解析镜像名称 ImageInspectError: 无法校验镜像 ErrImageNeverPull: 策略禁止拉取镜像 ImagePullBackOff: 正在重试拉取 RegistryUnavailable: 连接不到镜像中心 ErrImagePull: 通用的拉取镜像出错 CreateContainerConfigError: 不能创建 kubelet 使用的容器配置 CreateContainerError: 创建容器失败 RunContainerError: 启动容器失败 PreStartHookError: 执行 preStart hook 报错 PostStartHookError: 执行 postStart hook 报错 ContainersNotInitialized: 容器没有初始化完毕 ContainersNotReady: 容器没有准备完毕 ContainerCreating:容器创建中 PodInitializing:pod 初始化中 DockerDaemonNotReady:docker还没有完全启动 NetworkPluginNotReady: 网络插件还没有完全启动

Pod 一直处于 Pending 状态

**节点资源不够** 节点资源不够有以下几种情况: - CPU 负载过高 - 剩余可以被分配的内存不够 - 剩余可用 GPU 数量不够 (通常在机器学习场景,GPU 集群环境) 如果判断某个 Node 资源是否足够? 通过 `kubectl describe node <node-name>` 查看 node 资源情况,关注以下信息: - `Allocatable`: 表示此节点能够申请的资源总和 - `Allocated resources`: 表示此节点已分配的资源 (Allocatable 减去节点上所有 Pod 总的 request) 前者与后者相减,可得出剩余可申请的资源。如果这个值小于 Pod 的 request,就不满足 Pod 的资源要求,Scheduler 在 Predicates (预选) 阶段就会剔除掉这个 Node,也就不会调度上去。 **不满足 nodeSelector 与 affinity** 如果 Pod 包含 nodeSelector 指定了节点需要包含的 label,调度器将只会考虑将 Pod 调度到包含这些 label 的 Node 上,如果没有 Node 有这些 label 或者有这些 label 的 Node 其它条件不满足也将会无法调度。参考官方文档:[https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) 如果 Pod 包含 affinity(亲和性)的配置,调度器根据调度算法也可能算出没有满足条件的 Node,从而无法调度。affinity 有以下几类: - nodeAffinity: 节点亲和性,可以看成是增强版的 nodeSelector,用于限制 Pod 只允许被调度到某一部分 Node。 - podAffinity: Pod 亲和性,用于将一些有关联的 Pod 调度到同一个地方,同一个地方可以是指同一个节点或同一个可用区的节点等。 - podAntiAffinity: Pod 反亲和性,用于避免将某一类 Pod 调度到同一个地方避免单点故障,比如将集群 DNS 服务的 Pod 副本都调度到不同节点,避免一个节点挂了造成整个集群 DNS 解析失败,使得业务中断。 **Node 存在 Pod 没有容忍的污点** 如果节点上存在污点 (Taints),而 Pod 没有响应的容忍 (Tolerations),Pod 也将不会调度上去。通过 describe node 可以看下 Node 有哪些 Taints。 #### Pod 一直处于 ContainerCreating 或 Waiting 状态 **Pod 配置错误** - 检查是否打包了正确的镜像 - 检查配置了正确的容器参数 **挂载 Volume 失败** Volume 挂载失败也分许多种情况,先列下我这里目前已知的。 **磁盘爆满** 启动 Pod 会调 CRI 接口创建容器,容器运行时创建容器时通常会在数据目录下为新建的容器创建一些目录和文件,如果数据目录所在的磁盘空间满了就会创建失败并报错 **节点内存碎片化** 如果节点上内存碎片化严重,缺少大页内存,会导致即使总的剩余内存较多,但还是会申请内存失败, **limit 设置太小或者单位不对** 如果 limit 设置过小以至于不足以成功运行 Sandbox 也会造成这种状态,常见的是因为 memory limit 单位设置不对造成的 limit 过小,比如误将 memory 的 limit 单位像 request 一样设置为小 `m`,这个单位在 memory 不适用,会被 k8s 识别成 byte, 应该用 `Mi` 或 `M`。 **拉取镜像失败** 镜像拉取失败也分很多情况,这里列举下: - 配置了错误的镜像 - Kubelet 无法访问镜像仓库(比如默认 pause 镜像在 [gcr.io](http://gcr.io) 上,国内环境访问需要特殊处理) - 拉取私有镜像的 imagePullSecret 没有配置或配置有误 - 镜像太大,拉取超时(可以适当调整 kubelet 的 —image-pull-progress-deadline 和 —runtime-request-timeout 选项) **CNI 网络错误** 如果发生 CNI 网络错误通常需要检查下网络插件的配置和运行状态,如果没有正确配置或正常运行通常表现为: - 无法配置 Pod 网络 - 无法分配 Pod IP **controller-manager 异常** 查看 master 上 kube-controller-manager 状态,异常的话尝试重启。 **存在同名容器

**

如果节点上已有同名容器,创建 sandbox 就会失败,event: #### Pod 处于 CrashLoopBackOff 状态 Pod 如果处于 `CrashLoopBackOff` 状态说明之前是启动了,只是又异常退出了,只要 Pod 的 [restartPolicy](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy) 不是 Never 就可能被重启拉起,此时 Pod 的 `RestartCounts` 通常是大于 0 的。 重要的是要注意 `CrashLoopBackOff` 不是导致 pod 崩溃的实际错误。请记住,它只是显示`STATUS`列中发生的循环。您需要找到影响容器的潜在错误。 与实际应用程序相关的一些错误是: - **错误配置:** 就像配置文件中的错误配置 - **资源不可用:** 例如未挂载的 PersistentVolume - **错误的命令行参数:** 要么丢失,要么不正确的命令行参数 - **bug 和异常:** 这可以是任何异常,对你的应用来说都是非常具体的 最后是网络和权限的错误: - 您试图绑定被占用的端口。 - **内存限制太低**,容器被 `Out Of Memory` 杀死。 - **liveness 探针返回错误**,未报告 Pod 已 Ready。 - **只读文件系统**,或缺乏权限。 以上这些只是可能原因的列表,可能还有很多其他原因。 #### CrashLoopBackOff排障 1. 检查`pod 描述`。 kubectl describe pod命令提供特定 Pod 及其容器的详细信息 通过使用`kubectl describe pod`,您可以检查以下配置错误: - Pod 定义 - **容器** - 为容器拉取的 **镜像** - 为容器分配的 **资源** - 错误或缺少的 **参数** 1. 检查`pod 日志`。 您可以查看 pod 的所有容器的日志:`kubectl logs mypod --all-containers` 或者指定的容器:`kubectl logs mypod -c mycontainer` 1. 检查 **events**。 可以列出相关的事件:`kubectl get events` 使用以下命令列出单个 Pod 的所有事件:`kubectl get events --field-selector involvedObject.name=mypod` 1. 检查 **deployment**。 kubectl describe deployment mydeployment,如果deployment定义了所需的 Pod 状态,它可能包含导致 CrashLoopBackOff 的错误配置。

Pod 一直处于 Terminating 状态

**磁盘爆满** 如果 docker 的数据目录所在磁盘被写满,docker 无法正常运行,无法进行删除和创建操作,所以 kubelet 调用 docker 删除容器没反应。 **存在 “i” 文件属性** 如果容器的镜像本身或者容器启动后写入的文件存在 “i” 文件属性,此文件就无法被修改删除,而删除 Pod 时会清理容器目录,但里面包含有不可删除的文件,就一直删不了,Pod 状态也将一直保持 Terminating **存在 Finalizers** k8s 资源的 metadata 里如果存在 `finalizers`,那么该资源一般是由某程序创建的,并且在其创建的资源的 metadata 里的 `finalizers` 加了一个它的标识,这意味着这个资源被删除时需要由创建资源的程序来做删除前的清理,清理完了它需要将标识从该资源的 `finalizers` 中移除,然后才会最终彻底删除资源。比如 Rancher 创建的一些资源就会写入 `finalizers` 标识。

Pod 一直处于 Unknown 状态

通常是节点失联,没有上报状态给 apiserver,到达阀值后 controller-manager 认为节点失联并将其状态置为 `Unknown`。 可能原因: - 节点高负载导致无法上报 - 节点宕机 - 节点被关机 - 网络不通

Pod 一直处于 Error 状态

通常处于 Error 状态说明 Pod 启动过程中发生了错误。常见的原因包括: - 依赖的 ConfigMap、Secret 或者 PV 等不存在 - 请求的资源超过了管理员设置的限制,比如超过了 LimitRange 等 - 违反集群的安全策略,比如违反了 PodSecurityPolicy 等 - 容器无权操作集群内的资源,比如开启 RBAC 后,需要为 ServiceAccount 配置角色绑定

Pod 一直处于 ImagePullBackOff 状态

**http 类型 registry,地址未加入到 insecure-registry** dockerd 默认从 https 类型的 registry 拉取镜像,如果使用 https 类型的 registry,则必须将它添加到 insecure-registry 参数中,然后重启或 reload dockerd 生效。 **https 自签发类型 resitry,没有给节点添加 ca 证书** 如果 registry 是 https 类型,但证书是自签发的,dockerd 会校验 registry 的证书,校验成功才能正常使用镜像仓库,要想校验成功就需要将 registry 的 ca 证书放置到 `/etc/docker/certs.d/<registry:port>/ca.crt` 位置。 **私有镜像仓库认证失败** 如果 registry 需要认证,但是 Pod 没有配置 imagePullSecret,配置的 Secret 不存在或者有误都会认证失败。 **镜像文件损坏** 如果 push 的镜像文件损坏了,下载下来也用不了,需要重新 push 镜像文件。 **镜像拉取超时** 如果节点上新起的 Pod 太多就会有许多可能会造成容器镜像下载排队,如果前面有许多大镜像需要下载很长时间,后面排队的 Pod 就会报拉取超时。

Pod 健康检查失败

- Kubernetes 健康检查包含就绪检查(readinessProbe)和存活检查(livenessProbe) - pod 如果就绪检查失败会将此 pod ip 从 service 中摘除,通过 service 访问,流量将不会被转发给就绪检查失败的 pod - pod 如果存活检查失败,kubelet 将会杀死容器并尝试重启 **健康检查配置不合理** `initialDelaySeconds` 太短,容器启动慢,导致容器还没完全启动就开始探测,如果 successThreshold 是默认值 1,检查失败一次就会被 kill,然后 pod 一直这样被 kill 重启。 **节点负载过高** cpu 占用高(比如跑满)会导致进程无法正常发包收包,通常会 timeout,导致 kubelet 认为 pod 不健康。参考本书 [处理实践: 高负载](https://www.bookstack.cn/read/kubernetes-practice-guide/troubleshooting-handle-high-load.md) 一节。 **容器内进程端口监听挂掉** 使用 `netstat -tunlp` 检查端口监听是否还在,如果不在了,抓包可以看到会直接 reset 掉健康检查探测的连接。连接异常,从而健康检查失败。发生这种情况的原因可能在一个节点上启动了多个使用 `hostNetwork` 监听相同宿主机端口的 Pod,只会有一个 Pod 监听成功,但监听失败的 Pod 的业务逻辑允许了监听失败,并没有退出,Pod 又配了健康检查,kubelet 就会给 Pod 发送健康检查探测报文,但 Pod 由于没有监听所以就会健康检查失败。

容器进程主动退出

容器进程如果是自己主动退出(不是被外界中断杀死),退出状态码一般在 0-128 之间,根据约定,正常退出时状态码为 0,1-127 说明是程序发生异常,主动退出了,比如检测到启动的参数和条件不满足要求,或者运行过程中发生 panic 但没有捕获处理导致程序退出。除了可能是业务程序 BUG,还有其它许多可能原因。 **DNS 无法解析** 可能程序依赖 集群 DNS 服务,比如启动时连接数据库,数据库使用 service 名称或外部域名都需要 DNS 解析,如果解析失败程序将报错并主动退出。解析失败的可能原因: - 集群网络有问题,Pod 连不上集群 DNS 服务 - 集群 DNS 服务挂了,无法响应解析请求 - Service 或域名地址配置有误,本身是无法解析的地址 **程序配置有误** - 配置文件格式错误,程序启动解析配置失败报错退出 - 配置内容不符合规范,比如配置中某个字段是必选但没有填写,配置校验不通过,程序报错主动退出

网络排错LB 健康检查失败

可能原因:

DNS 解析异常

5 秒延时

如果DNS查询经常延时5秒才返回,通常是遇到内核 conntrack 冲突导致的丢包,

解析超时

如果容器内报 DNS 解析超时,先检查下集群 DNS 服务 (kube-dns/coredns) 的 Pod 是否 Ready,如果不是,请参考本章其它小节定位原因。如果运行正常,再具体看下超时现象。

解析外部域名超时

可能原因:

所有解析都超时

如果集群内某个 Pod 不管解析 Service 还是外部域名都失败,通常是 Pod 与集群 DNS 之间通信有问题。

可能原因:

Service 不通

集群 dns 故障

节点防火墙没放开集群容器网络 (iptables/安全组)

Service 无法解析

集群 DNS 没有正常运行(kube-dns或CoreDNS)

检查集群 DNS 是否运行正常:

kubelet 启动参数 --cluster-dns 可以看到 dns 服务的 cluster ip: $ ps -ef | grep kubelet ... /usr/bin/kubelet --cluster-dns=172.16.14.217 ... 找到 dns 的 service: $ kubectl get svc -n kube-system | grep 172.16.14.217 kube-dns ClusterIP 172.16.14.217 <none> 53/TCP,53/UDP 47d 看是否存在 endpoint: $ kubectl -n kube-system describe svc kube-dns | grep -i endpoints Endpoints: 172.16.0.156:53,172.16.0.167:53 Endpoints: 172.16.0.156:53,172.16.0.167:53 检查 endpoint 的 对应 pod 是否正常: $ kubectl -n kube-system get pod -o wide | grep 172.16.0.156 kube-dns-898dbbfc6-hvwlr 3/3 Running 0 8d 172.16.0.156 10.0.0.3

Pod 与 DNS 服务之间网络不通

检查下 pod 是否连不上 dns 服务,可以在 pod 里 telnet 一下 dns 的 53 端口:

  1. # 连 dns service 的 cluster ip
  2. $ telnet 172.16.14.217 53

如果检查到是网络不通,就需要排查下网络设置:

Kubernetes 网络排错骨灰级中文指南pod网络异常

网络异常大概分为如下几类:

kubernetes技术简说(kubernetes故障排查及解决)(2)

常用网络排查工具

tcpdump

tcpdump 网络嗅探器,将强大和简单结合到一个单一的命令行界面中,能够将网络中的报文抓取,输出到屏幕或者记录到文件中。

对于 Kubernetes 集群中的 Pod,由于容器内不便于抓包,通常视情况在 Pod 数据包经过的 veth 设备,docker0 网桥,CNI 插件设备(如 cni0,flannel.1 etc..)及 Pod 所在节点的网卡设备上指定 Pod IP 进行抓包。选取的设备根据怀疑导致网络问题的原因而定,比如范围由大缩小,从源端逐渐靠近目的端,比如怀疑是 CNI 插件导致,则在 CNI 插件设备上抓包。从 pod 发出的包逐一经过 veth 设备,cni0 设备,flannel0,宿主机网卡,到达对端,抓包时可按顺序逐一抓包,定位问题节点。

nsenter

nsenter 是一款可以进入进程的名称空间中。例如,如果一个容器以非 root 用户身份运行,而使用 docker exec 进入其中后,但该容器没有安装 sudo 或未 netstat ,并且您想查看其当前的网络属性,如开放端口,这种场景下将如何做到这一点?nsenter 就是用来解决这个问题的。

nsenter (namespace enter) 可以在容器的宿主机上使用 nsenter 命令进入容器的命名空间,以容器视角使用宿主机上的相应网络命令进行操作。当然需要拥有 root 权限

nsenter 的 c 使用语法为,nsenter -t pid -n <commond>,-t 接 进程 ID 号,-n 表示进入名称空间内,<commond> 为执行的命令。

实例:如我们有一个 Pod 进程 ID 为 30858,进入该 Pod 名称空间内执行 ifconfig ,如下列所示

$ ps -ef|grep tail root 17636 62887 0 20:19 pts/2 00:00:00 grep --color=auto tail root 30858 30838 0 15:55 ? 00:00:01 tail -f $ nsenter -t 30858 -n ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1480 inet 192.168.1.213 netmask 255.255.255.0 broadcast 192.168.1.255 ether 5e:d5:98:af:dc:6b txqueuelen 0 (Ethernet) RX packets 92 bytes 9100 (8.8 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 92 bytes 8422 (8.2 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 loop txqueuelen 1000 (Local Loopback) RX packets 5 bytes 448 (448.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 5 bytes 448 (448.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 net1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.1.0.201 netmask 255.255.255.0 broadcast 10.1.0.255 ether b2:79:f9:dd:2a:10 txqueuelen 0 (Ethernet) RX packets 228 bytes 21272 (20.7 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 216 bytes 20272 (19.7 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

如何定位 Pod 名称空间

首先需要确定 Pod 所在的节点名称

$ kubectl get pods -owide |awk '{print $1,$7}' NAME NODE netbox-85865d5556-hfg6v master-machine netbox-85865d5556-vlgr4 node01

如果 Pod 不在当前节点还需要用 IP 登录则还需要查看 IP(可选)

接下来,登录节点,获取容器 lD,如下列所示,每个 pod 默认有一个 pause 容器,其他为用户 yaml 文件中定义的容器,理论上所有容器共享相同的网络命名空间,排查时可任选一个容器。

$ docker ps |grep netbox-85865d5556-hfg6v 6f8c58377aae f78dd05f11ff "tail -f" 45 hours ago Up 45 hours k8s_netbox_netbox-85865d5556-hfg6v_default_4a8e2da8-05d1-4c81-97a7-3d76343a323a_0 b9c732ee457e registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.1 "/pause"

接下来获得获取容器在节点系统中对应的进程号,如下所示

$ docker inspect --format "{{ .State.Pid }}" 6f8c58377aae 30858

最后就可以通过 nsenter 进入容器网络空间执行命令了

paping

paping 命令可对目标地址指定端口以 TCP 协议进行连续 ping,通过这种特性可以弥补 ping ICMP 协议,以及 nmap , telnet 只能进行一次操作的的不足;通常情况下会用于测试端口连通性和丢包率

paping download:paping[2]

paping 还需要安装以下依赖,这取决于你安装的 paping 版本

mtr

mtr 是一个跨平台的网络诊断工具,将 tracerouteping 的功能结合到一个工具。与 traceroute 不同的是 mtr 显示的信息比起 traceroute 更加丰富:通过 mtr 可以确定网络的条数,并且可以同时打印响应百分比以及网络中各跳跃点的响应时间。

最简单的示例,就是后接域名或 IP,这将跟踪整个路由

$ mtr google.com Start: Thu Jun 28 12:10:13 2018 HOST: TecMint Loss% Snt Last Avg Best Wrst StDev 1.|-- 192.168.0.1 0.0% 5 0.3 0.3 0.3 0.4 0.0 2.|-- 5.5.5.211 0.0% 5 0.7 0.9 0.7 1.3 0.0 3.|-- 209.snat-111-91-120.hns.n 80.0% 5 7.1 7.1 7.1 7.1 0.0 4.|-- 72.14.194.226 0.0% 5 1.9 2.9 1.9 4.4 1.1 5.|-- 108.170.248.161 0.0% 5 2.9 3.5 2.0 4.3 0.7 6.|-- 216.239.62.237 0.0% 5 3.0 6.2 2.9 18.3 6.7 7.|-- bom05s12-in-f14.1e100.net 0.0% 5 2.1 2.4 2.0 3.8 0.5

-n 强制 mtr 打印 IP 地址而不是主机名

$ mtr -n google.com Start: Thu Jun 28 12:12:58 2018 HOST: TecMint Loss% Snt Last Avg Best Wrst StDev 1.|-- 192.168.0.1 0.0% 5 0.3 0.3 0.3 0.4 0.0 2.|-- 5.5.5.211 0.0% 5 0.9 0.9 0.8 1.1 0.0 3.|-- ??? 100.0 5 0.0 0.0 0.0 0.0 0.0 4.|-- 72.14.194.226 0.0% 5 2.0 2.0 1.9 2.0 0.0 5.|-- 108.170.248.161 0.0% 5 2.3 2.3 2.2 2.4 0.0 6.|-- 216.239.62.237 0.0% 5 3.0 3.2 3.0 3.3 0.0 7.|-- 172.217.160.174 0.0% 5 3.7 3.6 2.0 5.3 1.4

-b 同时显示 IP 地址与主机名

$ mtr -b google.com Start: Thu Jun 28 12:14:36 2018 HOST: TecMint Loss% Snt Last Avg Best Wrst StDev 1.|-- 192.168.0.1 0.0% 5 0.3 0.3 0.3 0.4 0.0 2.|-- 5.5.5.211 0.0% 5 0.7 0.8 0.6 1.0 0.0 3.|-- 209.snat-111-91-120.hns.n 0.0% 5 1.4 1.6 1.3 2.1 0.0 4.|-- 72.14.194.226 0.0% 5 1.8 2.1 1.8 2.6 0.0 5.|-- 108.170.248.209 0.0% 5 2.0 1.9 1.8 2.0 0.0 6.|-- 216.239.56.115 0.0% 5 2.4 2.7 2.4 2.9 0.0 7.|-- bom07s15-in-f14.1e100.net 0.0% 5 3.7 2.2 1.7 3.7 0.9

-c 跟一个具体的值,这将限制 mtr ping 的次数,到达次数后会退出

$ mtr -c5 google.com

如果需要指定次数,并且在退出后保存这些数据,使用 -r flag

$ mtr -r -c 5 google.com > 1 $ cat 1 Start: Sun Aug 21 22:06:49 2022 HOST: xxxxx.xxxxx.xxxx.xxxx Loss% Snt Last Avg Best Wrst StDev 1.|-- gateway 0.0% 5 0.6 146.8 0.6 420.2 191.4 2.|-- 212.xx.21.241 0.0% 5 0.4 1.0 0.4 2.3 0.5 3.|-- 188.xxx.106.124 0.0% 5 0.7 1.1 0.7 2.1 0.5 4.|-- ??? 100.0 5 0.0 0.0 0.0 0.0 0.0 5.|-- 72.14.209.89 0.0% 5 43.2 43.3 43.1 43.3 0.0 6.|-- 108.xxx.250.33 0.0% 5 43.2 43.1 43.1 43.2 0.0 7.|-- 108.xxx.250.34 0.0% 5 43.7 43.6 43.5 43.7 0.0 8.|-- 142.xxx.238.82 0.0% 5 60.6 60.9 60.6 61.2 0.0 9.|-- 142.xxx.238.64 0.0% 5 59.7 67.5 59.3 89.8 13.2 10.|-- 142.xxx.37.81 0.0% 5 62.7 62.9 62.6 63.5 0.0 11.|-- 142.xxx.229.85 0.0% 5 61.0 60.9 60.7 61.3 0.0 12.|-- xx-in-f14.1e100.net 0.0% 5 59.0 58.9 58.9 59.0 0.0

默认使用的是 ICMP 协议 -i ,可以指定 -u, -t 使用其他协议

mtr --tcp google.com

mtr 输出的数据

colum

describe

last

最近一次的探测延迟值

avg

探测延迟的平均值

best

探测延迟的最小值

wrst

探测延迟的最大值

stdev

标准偏差。越大说明相应节点越不稳定

丢包判断

任一节点的 Loss%(丢包率)如果不为零,则说明这一跳网络可能存在问题。导致相应节点丢包的原因通常有两种。

如果随后节点均没有丢包,则通常说明异常节点丢包是由于运营商策略限制所致。可以忽略相关丢包。 如果随后节点也出现丢包,则通常说明节点确实存在网络异常,导致丢包。对于这种情况,如果异常节点及其后续节点连续出现丢包,而且各节点的丢包率不同,则通常以最后几跳的丢包率为准。如链路测试在第 5、6、7 跳均出现了丢包。最终丢包情况以第 7 跳作为参考。

延迟判断

由于链路抖动或其它因素的影响,节点的 BestWorst 值可能相差很大。而 Avg(平均值)统计了自链路测试以来所有探测的平均值,所以能更好的反应出相应节点的网络质量。而 StDev(标准偏差值)越高,则说明数据包在相应节点的延时值越不相同(越离散)。所以标准偏差值可用于协助判断 Avg 是否真实反应了相应节点的网络质量。例如,如果标准偏差很大,说明数据包的延迟是不确定的。可能某些数据包延迟很小(例如:25ms),而另一些延迟却很大(例如:350ms),但最终得到的平均延迟反而可能是正常的。所以此时 Avg 并不能很好的反应出实际的网络质量情况。

这就需要结合如下情况进行判断:

Pod 网络排查流程图

kubernetes技术简说(kubernetes故障排查及解决)(3)

集群排错Node 全部消失

Rancher 清除 Node 导致集群异常

现象

安装了 rancher 的用户,在卸载 rancher 的时候,可能会手动执行 kubectl delete ns local 来删除这个 rancher 创建的 namespace,但直接这样做会导致所有 node 被清除,通过 kubectl get node 获取不到 node。

原因

看了下 rancher 源码,rancher 通过 nodes.management.cattle.io 这个 CRD 存储和管理 node,会给所有 node 创建对应的这个 CRD 资源,metadata 中加入了两个 finalizer,其中 user-node-remove_local 对应的 finalizer 处理逻辑就是删除对应的 k8s node 资源,也就是 delete ns local 时,会尝试删除 nodes.management.cattle.io 这些 CRD 资源,进而触发 rancher 的 finalizer 逻辑去删除对应的 k8s node 资源,从而清空了 node,所以 kubectl get node 就看不到 node 了,集群里的服务就无法被调度。

规避方案

不要在 rancher 组件卸载完之前手动 delete ns local。

Daemonset 没有被调度

Daemonset 的期望实例为 0,可能原因:

经典报错no space left on device

inotify watch 耗尽

节点 NotReady,kubelet 启动失败,看 kubelet 日志:

Jul 18 15:20:58 VM_16_16_centos kubelet[11519]: E0718 15:20:58.280275 11519 raw.go:140] Failed to watch directory "/sys/fs/cgroup/memory/kubepods": inotify_add_watch /sys/fs/cgroup/memory/kubepods/burstable/pod926b7ff4-7bff-11e8-945b-52540048533c/6e85761a30707b43ed874e0140f58839618285fc90717153b3cbe7f91629ef5a: no space left on device

系统调用 inotify_add_watch 失败,提示 no space left on device, 这是因为系统上进程 watch 文件目录的总数超出了最大限制,可以修改内核参数调高限制

cgroup 泄露

查看当前 cgroup 数量:

$ cat /proc/cgroups | column -t #subsys_name hierarchy num_cgroups enabled cpuset 5 29 1 cpu 7 126 1 cpuacct 7 126 1 memory 9 127 1 devices 4 126 1 freezer 2 29 1 net_cls 6 29 1 blkio 10 126 1 perf_event 3 29 1 hugetlb 11 29 1 pids 8 126 1 net_prio 6 29 1

cgroup 子系统目录下面所有每个目录及其子目录都认为是一个独立的 cgroup,所以也可以在文件系统中统计目录数来获取实际 cgroup 数量,通常跟 /proc/cgroups 里面看到的应该一致:

$ find -L /sys/fs/cgroup/memory -type d | wc -l 127

当 cgroup 泄露发生时,这里的数量就不是真实的了,低版本内核限制最大 65535 个 cgroup,并且开启 kmem 删除 cgroup 时会泄露,大量创建删除容器后泄露了许多 cgroup,最终总数达到 65535,新建容器创建 cgroup 将会失败,报 no space left on device

arp_cache: neighbor table overflow

节点内核报这个错说明当前节点 arp 缓存满了。

查看当前 arp 记录数:

$ arp -an | wc -l 1335

查看 gc 阀值:

$ sysctl -a | grep net.ipv4.neigh.default.gc_thresh net.ipv4.neigh.default.gc_thresh1 = 128 net.ipv4.neigh.default.gc_thresh2 = 512 net.ipv4.neigh.default.gc_thresh3 = 1024

当前 arp 记录数接近 gc_thresh3 比较容易 overflow,因为当 arp 记录达到 gc_thresh3 时会强制触发 gc 清理,当这时又有数据包要发送,并且根据目的 IP 在 arp cache 中没找到 mac 地址,这时会判断当前 arp cache 记录数加 1 是否大于 gc_thresh3,如果没有大于就会 时就会报错: neighbor table overflow!

解决方案

调整部分节点内核参数,将 arp cache 的 gc 阀值调高 (/etc/sysctl.conf):

net.ipv4.neigh.default.gc_thresh1 = 80000 net.ipv4.neigh.default.gc_thresh2 = 90000 net.ipv4.neigh.default.gc_thresh3 = 100000

并给 node 打下label,修改 pod spec,加下 nodeSelector 或者 nodeAffnity,让 pod 只调度到这部分改过内核参数的节点。

Cannot allocate memory

容器启动失败,报错 Cannot allocate memory。

PID 耗尽

如果登录 ssh 困难,并且登录成功后执行任意命名经常报 Cannot allocate memory,多半是 PID 耗尽了。

其他排错kubectl 执行 exec 或 logs 失败

通常是 apiserver —> kubelet:10250 之间的网络不通,10250 是 kubelet 提供接口的端口,kubectl exec 和 kubectl logs 的原理就是 apiserver 调 kubelet,kubelet 再调运行时 (比如 dockerd) 来实现的,所以要保证 kubelet 10250 端口对 apiserver 放通。检查防火墙、iptables 规则是否对 10250 端口或某些 IP 进行了拦截。

内核软死锁

原因

发生这个报错通常是内核繁忙 (扫描、释放或分配大量对象),分不出时间片给用户态进程导致的,也伴随着高负载,如果负载降低报错则会消失。

什么情况下会导致内核繁忙

排错技巧容器内抓包定位网络问题

在使用 kubernetes 跑应用的时候,可能会遇到一些网络问题,比较常见的是服务端无响应(超时)或回包内容不正常,如果没找出各种配置上有问题,这时我们需要确认数据包到底有没有最终被路由到容器里,或者报文到达容器的内容和出容器的内容符不符合预期,通过分析报文可以进一步缩小问题范围。那么如何在容器内抓包呢?本文提供实用的脚本一键进入容器网络命名空间(netns),使用宿主机上的tcpdump进行抓包。

使用脚本一键进入 pod netns 抓包

发现某个服务不通,最好将其副本数调为1,并找到这个副本 pod 所在节点和 pod 名称

function e() { set -eu ns=${2-"default"} pod=`kubectl -n $ns describe pod $1 | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'` pid=`docker inspect -f {{.State.Pid}} $pod` echo "entering pod netns for $ns/$1" cmd="nsenter -n --target $pid" echo $cmd $cmd }

e istio-galley-58c7c7c646-m6568 istio-system e proxy-5546768954-9rxg6 # 省略 NAMESPACE 默认为 default

tcpdump -i eth0 -w test.pcap port 80

脚本原理

我们解释下步骤二中用到的脚本的原理 查看指定 pod 运行的容器 ID kubectl describe pod <pod> -n mservice 获得容器进程的 pid docker inspect -f {{.State.Pid}} <container> 进入该容器的 network namespace nsenter -n --target <PID> 依赖宿主机的命名:kubectl, docker, nsenter, grep, head, sed

使用 Systemtap 定位疑难杂症

安装

CentOS

安装 systemtap:

yum install -y systemtap

默认没装 debuginfo,我们需要装一下,添加软件源 /etc/yum.repos.d/CentOS-Debug.repo:

[debuginfo] name=CentOS-$releasever - DebugInfo baseurl=http://debuginfo.centos.org/$releasever/$basearch/ gpgcheck=0 enabled=1 protect=1 priority=1

执行 stap-prep (会安装 kernel-debuginfo)

最后检查确保 kernel-debuginfo 和 kernel-devel 均已安装并且版本跟当前内核版本相同,如果有多个版本,就删除跟当前内核版本不同的包(通过uname -r查看当前内核版本)。

重点检查是否有多个版本的 kernel-devel:

$ rpm -qa | grep kernel-devel kernel-devel-3.10.0-327.el7.x86_64 kernel-devel-3.10.0-514.26.2.el7.x86_64 kernel-devel-3.10.0-862.9.1.el7.x86_64

如果存在多个,保证只留跟当前内核版本相同的那个,假设当前内核版本是 3.10.0-862.9.1.el7.x86_64,那么使用 rpm 删除多余的版本:

rpm -e kernel-devel-3.10.0-327.el7.x86_64 kernel-devel-3.10.0-514.26.2.el7.x86_64

使用 systemtap 揪出杀死容器的真凶

Pod 莫名其妙被杀死? 可以使用 systemtap 来监视进程的信号发送,原理是 systemtap 将脚本翻译成 C 代码然后调用 gcc 编译成 linux 内核模块,再通过 modprobe 加载到内核,根据脚本内容在内核做各种 hook,在这里我们就 hook 一下信号的发送,找出是谁 kill 掉了容器进程。

首先,找到被杀死的 pod 又自动重启的容器的当前 pid,describe 一下 pod:

...... Container ID: docker://5fb8adf9ee62afc6d3f6f3d9590041818750b392dff015d7091eaaf99cf1c945 ...... Last State: Terminated Reason: Error Exit Code: 137 Started: Thu, 05 Sep 2019 19:22:30 0800 Finished: Thu, 05 Sep 2019 19:33:44 0800

拿到容器 id 反查容器的主进程 pid:

$ docker inspect -f "{{.State.Pid}}" 5fb8adf9ee62afc6d3f6f3d9590041818750b392dff015d7091eaaf99cf1c945 7942

通过 Exit Code 可以看出容器上次退出的状态码,如果进程是被外界中断信号杀死的,退出状态码将在 129-255 之间,137 表示进程是被 SIGKILL 信号杀死的,但我们从这里并不能看出是被谁杀死的。

如果问题可以复现,我们可以使用下面的 systemtap 脚本来监视容器是被谁杀死的(保存为sg.stp):

global target_pid = 7942 probe signal.send{ if (sig_pid == target_pid) { printf("%s(%d) send %s to %s(%d)\n", execname(), pid(), sig_name, pid_name, sig_pid); printf("parent of sender: %s(%d)\n", pexecname(), ppid()) printf("task_ancestry:%s\n", task_ancestry(pid2task(pid()), 1)); } }

运行脚本:stap sg.stp

当容器进程被杀死时,脚本捕捉到事件,执行输出:

pkill(23549) send SIGKILL to server(7942) parent of sender: bash(23495) task_ancestry:swapper/0(0m0.000000000s)=>systemd(0m0.080000000s)=>vGhyM0(19491m2.579563677s)=>sh(33473m38.074571885s)=>bash(33473m38.077072025s)=>bash(33473m38.081028267s)=>bash(33475m4.817798337s)=>pkill(33475m5.202486630s)

通过观察 task_ancestry 可以看到杀死进程的所有父进程,在这里可以看到有个叫 vGhyM0 的奇怪进程名,通常是中了木马,需要安全专家介入继续排查。

kubernetes故障排查全景图

kubernetes技术简说(kubernetes故障排查及解决)(4)

kubernetes技术简说(kubernetes故障排查及解决)(5)


kubernetes技术简说(kubernetes故障排查及解决)(6)

,