一文搞懂 Kubernetes 部署故障排除

141 阅读14分钟

问题定位流程图

TL;DR:这里有一个流程图可以帮助你调试 Kubernetes 中的部署


当你希望在 Kubernetes 中部署一个应用程序时,通常需要定义三个组件:

  • Deployment(部署)  — 用于创建应用程序副本的配置。
  • Service(服务)  — 内部负载均衡器,将流量路由到 Pods。
  • Ingress(入口)  — 描述流量从集群外部到达 Service 的方式。

这里是一个快速预览。

部署简单的 Hello World 应用程序的 YAML 配置示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    track: canary
spec:
  selector:
    matchLabels:
      any-name: my-app
  template:
    metadata:
      labels:
        any-name: my-app
    spec:
      containers:
        - name: cont1
          image: ghcr.io/learnk8s/app:1.0.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    name: app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
      paths:
      - backend:
          service:
            name: my-service
            port:
              number: 80
        path: /
        pathType: Prefix

这个定义比较长,很容易忽视组件之间的关系。

例如:

  • 什么时候使用端口80,什么时候使用端口8080?
  • 是否应该为每个 Service 创建新的端口以避免冲突?
  • 标签名称是否重要?是否应该在各处保持相同?

在专注于调试之前,让我们回顾一下这三个组件之间的关系。

Deployment和Service的连接

令人惊讶的是,Service 和 Deployment 并没有直接连接。

相反,Service 直接指向 Pods,跳过了 Deployment。

因此,你应该注意的是 Pods 和 Services 之间的关系。

你应该记住三件事:

  1. Service 的选择器(selector)应该至少匹配一个 Pod 的标签。
  2. Service 的 targetPort 应该与 Pod 的 containerPort 匹配。
  3. Service 的 port 可以是任何数字。多个 Service 可以使用相同的端口,因为它们具有不同的 IP 地址。

下图总结了如何连接端口:

如果查看 YAML,标签和 ports/targetPort 应该匹配。

yamlCopy code
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    track: canary
spec:
  selector:
    matchLabels:
      any-name: my-app
  template:
    metadata:
      labels:
        any-name: my-app
    spec:
      containers:
        - name: cont1
          image: ghcr.io/learnk8s/app:1.0.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    any-name: my-app

关于 Deployment 顶部的 track: canary 标签呢?是否也应该匹配?
该标签属于 Deployment,Service 的选择器不使用它来路由流量。
换句话说,你可以安全地删除它或为其分配不同的值。

那么 matchLabels 选择器呢?
它始终必须匹配 Pod 的标签,并且它由 Deployment 用于跟踪 Pods。
假设你进行了正确的更改,该如何测试呢?

你可以使用以下命令检查 Pods 是否具有正确的标签:

kubectl get pods --show-labels

NAME                  READY   STATUS    LABELS
my-deployment-pv6pd   1/1     Running   any-name=my-app,pod-template-hash=7d6979fb54
my-deployment-f36rt   1/1     Running   any-name=my-app,pod-template-hash=7d6979fb54

并且可以查看pod的端口

kubectl get pod <pod name> --template='{{(index (index .spec.containers 0).ports 0).containerPort}}{{"\n"}}'

或者如果你有属于多个应用程序的 Pods:

kubectl get pods --selector any-name=my-app --show-labels

其中 any-name=my-app 是标签 any-name: my-app

仍然遇到问题?

你还可以连接到 Pod!

你可以使用 kubectl 中的 port-forward 命令连接到 Service 并测试连接:

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

Forwarding from 127.0.0.1:3000 -> 80
Forwarding from [::1]:3000 -> 80

其中:

  • service/<service name>  是 Service 的名称 — 在当前的 YAML 中是 "my-service"。
  • 3000 是你希望在计算机上打开的端口。
  • 80 是 Service 在端口字段中暴露的端口。

如果你可以连接,那么设置是正确的。

如果不能,你很可能放错了标签或端口不匹配。

连接 Service 和 Ingress

将应用程序暴露给外部的下一步是配置 Ingress。

Ingress 必须知道如何找到 Service,然后连接 Pods 并路由流量。

Ingress 通过名称和暴露的端口检索正确的Service。

Ingress 和 Service 中应该匹配两个要素:

  1. Ingress 的 service.port 应该与 Service 的端口匹配。
  2. Ingress 的 service.name 应该与 Service 的名称匹配。

以下图表总结了如何连接端口:

在实践中,你应该查看以下几行:

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
    - port: 80
      targetPort: 8080
  selector:
    any-name: my-app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
      paths:
      - backend:
          service:
            name: my-service
            port:
              number: 80
        path: /
        pathType: Prefix

如何测试 Ingress 是否正常工作呢?

你可以使用与之前相同的策略,使用 kubectl port-forward,但是连接到 Ingress controller 而不是连接到 Service。

首先,使用以下命令检索 Ingress controller 的 Pod 名称:

kubectl get pods --all-namespaces

NAMESPACE   NAME                              READY STATUS
kube-system coredns-5644d7b6d9-jn7cq          1/1   Running
kube-system etcd-minikube                     1/1   Running
kube-system kube-apiserver-minikube           1/1   Running
kube-system kube-controller-manager-minikube  1/1   Running
kube-system kube-proxy-zvf2h                  1/1   Running
kube-system kube-scheduler-minikube           1/1   Running
kube-system nginx-ingress-controller-6fc5bcc  1/1   Running

识别 Ingress Pod(可能位于不同的 Namespace 中),并使用描述命令检索其端口:

kubectl describe pod nginx-ingress-controller-6fc5bcc --namespace kube-system | grep Ports
Ports:         80/TCP, 443/TCP, 18080/TCP

最后,连接到 Pod:

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

Forwarding from 127.0.0.1:3000 -> 80
Forwarding from [::1]:3000 -> 80

其中:

  • nginx-ingress-controller-6fc5bcc 是 Ingress controller 的名称。
  • 3000 是你希望在计算机上打开的端口。
  • 80 是 Ingress controller 在端口字段中暴露的端口。

此时,每当你访问计算机上的端口3000时,请求都会转发到 Pod 上的端口80。

如果现在可以工作,问题就在基础设施中。你应该调查流量如何路由到你的集群。

如果仍然不起作用,问题就在 Ingress controller 中。你应该调试 Ingress。

如果仍然无法使 Ingress controller 正常工作,你应该开始调试它。

有许多不同版本的 Ingress controllers。

常见的选项包括 Nginx、HAProxy、Traefik 等。

你应该查阅 Ingress controller 的文档,找到调试指南。

由于 Ingress Nginx 是最流行的 Ingress controller,我们在下一节中包含了一些关于它的提示。

关于端口和标签应匹配的简要回顾:

  1. Service Selector 应与 Pod 的标签匹配。
  2. 服务 targetPort 应与 Pod 内容器中的 containerPort 匹配。
  3. Service 端口可以是任意数字。多个服务可以使用相同的端口,因为它们具有不同的分配的 IP 地址。
  4. Ingress 的 service.port 应与服务中的 port 匹配。
  5. Service的名称应与 Ingress 中的 service.name 字段匹配。

了解如何构建你的 YAML 定义只是故事的一部分。

当出现问题时会发生什么?

也许 Pod 没有启动,或者它正在崩溃。

调试 Kubernetes 部署的三个步骤

在深入调试损坏的部署之前,了解 Kubernetes 如何工作的心理模型至关重要。

由于每个部署中有三个组件,你应该按顺序调试它们,从底层开始。

  1. 确保你的 Pods 正在运行

  2. 关注将流量路由到 Pods 的 Service

  3. 检查 Ingress 是否正确配置。

1.调试 Pods

大多数情况下,问题出现在 Pod 本身。

你应该确保你的 Pods 处于运行状态并准备就绪。

如何检查呢?

kubectl get pods

NAME                    READY STATUS            RESTARTS  AGE
app1                    0/1   ImagePullBackOff  0         47h
app2                    0/1   Error             0         47h
app3-76f9fcd46b-xbv4k   1/1   Running           1         47h

在上面的输出中,最后一个 Pod 正在运行并准备就绪 。然而,前两个 Pod 既不在运行状态也不准备就绪。

如何调查出错原因?

有四个有用的命令用于调试 Pods:

  • kubectl logs <pod name>  有助于检索 Pod 中容器的日志。
  • kubectl describe pod <pod name>  有助于检索与 Pod 关联的事件列表。
  • kubectl get pod <pod name>  有助于提取存储在 Kubernetes 中的 Pod 的 YAML 定义。
  • kubectl exec -ti <pod name> -- bash 有助于在 Pod 的容器中运行交互式命令。

应该使用哪一个?

并没有一种适用于所有情况的方法。相反,你应该组合使用它们。

常见的 Pods 错误

Pods 可能会出现启动和运行时错误。

启动错误包括:

  • ImagePullBackoff
  • ImageInspectError
  • ErrImagePull
  • ErrImageNeverPull
  • RegistryUnavailable
  • InvalidImageName

运行时错误包括:

  • CrashLoopBackOff
  • RunContainerError
  • KillContainerError
  • VerifyNonRootError
  • RunInitContainerError
  • CreatePodSandboxError
  • ConfigPodSandboxError
  • KillPodSandboxError
  • SetupNetworkError
  • TeardownNetworkError

有些错误比其他错误更常见。

以下是最常见错误的列表以及如何修复它们。

ImagePullBackOff

当 Kubernetes 无法检索 Pod 中一个容器的镜像时,会出现此错误。

有三个常见的原因:

  1. 镜像名称无效 。 例如,你拼写错误了名称,或者镜像不存在。
  2. 为镜像指定了不存在的标签。
  3. 你尝试检索的镜像属于私有注册表,而 Kubernetes 没有访问它的权限凭据。

前两种情况可以通过纠正镜像名称和标签来解决。

对于最后一种情况,你应该将凭据添加到私有注册表中的一个 Secret,并在 Pods 中引用它。官方文档中有一个关于如何做到这一点的例子。

CrashLoopBackOff

如果容器无法启动,Kubernetes 会显示 CrashLoopBackOff 消息作为状态。

通常,容器无法启动有以下的情况:

  1. 应用程序中有阻止启动的错误。
  2. 配置容器时出现错误
  3. 存活性探测失败了太多次。

你应该尝试从该容器检索日志以查明失败的原因。

如果由于容器重新启动太快而看不到日志,你可以使用以下命令:

kubectl logs <pod-name> --previous

它会打印前一个容器的错误消息。

RunContainerError

当容器无法启动时,就会出现这个错误。

这甚至发生在容器内部的应用程序启动之前。

问题通常是由于错误配置,例如:

  1. 挂载不存在的卷,如 ConfigMap 或 Secrets。
  2. 将只读卷挂载为可读写。

你应该使用 kubectl describe pod <pod-name>  来检查和分析错误。

处于 Pending 状态的 Pods

当创建一个 Pod 时,Pod 保持在 Pending 状态。

为什么呢?

假设你的调度器组件运行正常,原因可能有:

  1. 集群没有足够的资源,如 CPU 和内存,来运行 Pod。
  2. 当创建 Pod 会使命名空间超出配额时,当前命名空间有一个 ResourceQuota 对象。
  3. Pod 绑定到一个处于 Pending 状态的 PersistentVolumeClaim。

你最好的选择是检查 kubectl describe 命令中的事件部分:

kubectl describe pod <pod name>

对于由于 ResourceQuotas 导致的错误,你可以使用以下命令检查集群的日志:

kubectl get events --sort-by=.metadata.creationTimestamp

查看节点剩余的资源配额

kubectl describe nodes

处于未就绪状态的 Pods

如果一个 Pod 正在运行但未就绪,这意味着 Readiness 探测失败。

当 Readiness 探测失败时,Pod 不会附加到 Service,并且不会将任何流量转发到该实例。

失败的 Readiness 探测是特定于应用程序的错误,因此你应该检查 kubectl describe 中的事件部分以识别错误。

2.调试Service

如果你的 Pods 正在运行并准备就绪,但仍然无法从你的应用程序收到响应,你应该检查Service是否配置正确。

Service 被设计为根据它们的标签将流量路由到 Pods。

因此,你应该首先检查的是Service针对多少个 Pods 进行了配置。

你可以通过检查服务中的 Endpoints 来实现:

kubectl describe service my-service

Name:                     my-service
Namespace:                default
Selector:                 app=my-app
IP:                       10.100.194.137
Port:                     <unset>  80/TCP
TargetPort:               8080/TCP
Endpoints:                172.17.0.5:8080

一个 Endpoint 是一对  <ip address:port> ,并且至少应该有一个,当服务至少针对一个 Pod 时。

如果 "Endpoints" 部分为空,有两种解释:

  1. 你没有任何以正确标签运行的 Pod(提示:你应该检查你是否在正确的命名空间中)。
  2. 服务的 selector 标签中有拼写错误。

如果你看到一列 Endpoints,但仍然无法访问你的应用程序,那么服务中的 targetPort 可能是问题所在。

如何测试Service?

无论服务的类型如何,你都可以使用 kubectl port-forward 来连接它:

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

# 或者不想指定端口想随机分配,可以使用以下命令
kubectl port-forward service/airec-server :8080

其中:

  • <service-name>  是Service的名称。

  • 3000 是你希望在计算机上打开的端口。

  • 80 是服务暴露的端口。

3.调试 Ingress

如果你已经阅读到这一部分,则:

  1. Pods 正在运行并准备就绪。
  2. Service 将流量分发到 Pod。

但你仍然无法看到应用程序的响应。

这很可能意味着 Ingress 的配置存在问题。

由于 Ingress 控制器是集群中的第三方组件,根据 Ingress 控制器的类型,有不同的调试技术。

但在深入研究 Ingress 特定工具之前,有一些直接的检查可以进行。

Ingress 使用 service.name 和 service.port 来连接到 Service。

你应该检查这些是否正确配置。

你可以使用以下命令检查 Ingress 是否正确配置:

kubectl describe ingress my-ingress

Name:             my-ingress
Namespace:        default
Rules:
  Host        Path  Backends
  ----        ----  --------
  *
              /   my-service:80 (<error: endpoints "my-service" not found>)

如果 Backend 列为空,那么配置中可能存在错误。

如果在 Backend 列中看到端点,但仍然无法访问应用程序,则问题可能是:

  1. 你是如何将 Ingress 公开到公共互联网的。
  2. 你是如何将集群公开到公共互联网的。

你可以通过直接连接到 Ingress Pod 来隔离与 Ingress 相关的基础设施问题。

首先,检索 Ingress 控制器的 Pod(可能位于不同的命名空间中):

kubectl get pods --all-namespaces

NAMESPACE   NAME                              READY STATUS
kube-system coredns-5644d7b6d9-jn7cq          1/1   Running
kube-system etcd-minikube                     1/1   Running
kube-system kube-apiserver-minikube           1/1   Running
kube-system kube-controller-manager-minikube  1/1   Running
kube-system kube-proxy-zvf2h                  1/1   Running
kube-system kube-scheduler-minikube           1/1   Running
kube-system nginx-ingress-controller-6fc5bcc  1/1   Running

通过 descirbe 它以检索端口:

kubectl describe pod nginx-ingress-controller-6fc5bcc --namespace kube-system | grep Ports
    Ports:         80/TCP, 443/TCP, 8443/TCP
    Host Ports:    80/TCP, 443/TCP, 0/TCP

最后,连接到该 Pod:

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

Forwarding from 127.0.0.1:3000 -> 80
Forwarding from [::1]:3000 -> 80

在这一点上,每当你在计算机上访问端口 3000 时,请求都会被转发到 Pod 上的端口 80。

现在它能工作吗?

  • 如果可以,问题就在基础设施中。你应该调查流量是如何路由到你的集群的。
  • 如果不能工作,问题就在 Ingress 控制器中。你应该调试 Ingress。
  • 如果仍然无法使 Ingress 控制器工作,你应该开始调试它。

有许多不同版本的 Ingress 控制器。

流行的选项包括 Nginx、HAProxy、Traefik 等。

你应该查阅你的 Ingress 控制器的文档以找到调试指南。

由于 Ingress Nginx 是最受欢迎的 Ingress 控制器,我们在下一节中包含了一些建议。

调试 Ingress Nginx

Ingress-nginx 项目有一个官方的 Kubectl 插件

你可以使用 kubectl ingress-nginx 来:

  • 检查日志、后端、证书等。
  • 连接到 Ingress。
  • 检查当前的配置。

你应该尝试的三个命令是:

  • kubectl ingress-nginx lint,用于检查 nginx.conf
  • kubectl ingress-nginx backend,检查后端(类似于 kubectl describe ingress <ingress-name> )。
  • kubectl ingress-nginx logs,检查日志。

请注意,你可能需要使用 --namespace <name>  为 Ingress 控制器指定正确的命名空间。

总结

在 Kubernetes 中进行故障排除可能是一项艰巨的任务,如果你不知道从哪里开始。

你应该始终记住从底层开始解决问题:从 Pods 开始,逐步向上移动到 Service 和 Ingress。

你在本文中学到的相同的调试技术可以应用于其他对象,如:

  • 失败的 Jobs 和 CronJobs。
  • StatefulSets 和 DaemonSets。

实际例子

Case-1: NodeHasDiskPressure

表现:节点有磁盘压力导致Pod全部被驱逐


# 查看错误事件
$ kubectl get events|grep Warning| less
# 看到磁盘有压力
The node had condition: [DiskPressure].

# 查看Pod 被驱逐的列表,发现都在一台机器上。
$ kubectl get pods -o=wide|grep Evicted

# 查看pod 的描述,发现请求的存储空间不足
$ kubectl describe pod <pod name>

Events:
  Type     Reason   Age   From     Message
  ----     ------   ----  ----     -------
  Warning  Evicted  32m   kubelet  The node was low on resource: ephemeral-storage. Container airec-server was using 13620Ki, which exceeds its request of 0.

# 因为都集中在同一个node上并且节点申请临时资源存储不足,所以猜测是node存在问题
$ kubectl describe node <node name>

Events:
  Type     Reason                 Age                    From        Message
  ----     ------                 ----                   ----        -------
  Warning  FreeDiskSpaceFailed    22m                    kubelet     failed to garbage collect required amount of images. Wanted to free 9421824819 bytes, but freed 0 bytes
  Warning  EvictionThresholdMet   15m (x29 over 15d)     kubelet     Attempting to reclaim ephemeral-storage

Case-2 : Postgres DB 部署报错

报错信息

chown: changing ownership of '/var/lib/postgresql/data/pgdata': Operation not permitted

通过搜索错误找到 github issue中的解决方法 :github.com/docker-libr…

原因是 postgres DB 使用NFS 作为挂在卷的时候需要 no_root_squash 这个权限

# 查看导出权限
$ cat /etc/exports
/data/nfs/k8s *(rw,sync,no_subtree_check)

# 编辑配置文件 ,加入 no_root_squash 权限
$ vi /etc/exports 
/data/nfs/k8s *(rw,sync,no_subtree_check,**no_root_squash**)

# 重启 nfs 服务
$ /etc/init.d/nfs-kernel-server restart

Refrence

  1. 本文部分内容翻译自learnk8s上的文章A visual guide on troubleshooting Kubernetes deployments
  2. zhuanlan.zhihu.com/p/33