调试 Service

对于新的 Kubernetes 安装而言,一个常见的问题是 Service 无法正常工作。你通过 Deployment(或其他工作负载控制器)运行了你的 Pod,并创建了 Service,但当你尝试访问它时却没有得到响应。本文档有望帮助你找出问题所在。

在 Pod 中运行命令

对于这里的许多步骤,你可能希望查看集群中运行的 Pod 看到的内容。最简单的方法是运行一个交互式的 busybox Pod。

kubectl run -it --rm --restart=Never busybox --image=gcr.io/google-containers/busybox sh

如果你已经有一个正在运行的 Pod,并且你更喜欢使用它,你可以使用以下命令在其内部运行一个命令:

kubectl exec <POD-NAME> -c <CONTAINER-NAME> -- <COMMAND>

设置

为了本次演练的目的,我们来运行一些 Pod。由于你可能正在调试自己的 Service,你可以替换为你自己的详细信息,或者你可以跟着操作以获取第二个数据点。

kubectl create deployment hostnames --image=registry.k8s.io/serve_hostname
deployment.apps/hostnames created

kubectl 命令会打印创建或修改的资源的类型和名称,这些信息可以在后续命令中使用。

我们将 Deployment 扩缩容到 3 个副本。

kubectl scale deployment hostnames --replicas=3
deployment.apps/hostnames scaled

请注意,这与你使用以下 YAML 启动 Deployment 的效果相同:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: hostnames
  name: hostnames
spec:
  selector:
    matchLabels:
      app: hostnames
  replicas: 3
  template:
    metadata:
      labels:
        app: hostnames
    spec:
      containers:
      - name: hostnames
        image: registry.k8s.io/serve_hostname

kubectl create deployment 命令会自动将标签 "app" 设置为 Deployment 的名称。

你可以确认你的 Pod 正在运行:

kubectl get pods -l app=hostnames
NAME                        READY     STATUS    RESTARTS   AGE
hostnames-632524106-bbpiw   1/1       Running   0          2m
hostnames-632524106-ly40y   1/1       Running   0          2m
hostnames-632524106-tlaok   1/1       Running   0          2m

你也可以确认你的 Pod 正在提供服务。你可以获取 Pod 的 IP 地址列表并直接测试它们。

kubectl get pods -l app=hostnames \
    -o go-template='{{range .items}}{{.status.podIP}}{{"\n"}}{{end}}'
10.244.0.5
10.244.0.6
10.244.0.7

本次演练中使用的示例容器通过 HTTP 在端口 9376 上提供其自身的主机名服务,但如果你正在调试自己的应用程序,你需要使用你的 Pod 正在监听的端口号。

在 Pod 内部

for ep in 10.244.0.5:9376 10.244.0.6:9376 10.244.0.7:9376; do
    wget -qO- $ep
done

这应该会产生类似如下的输出:

hostnames-632524106-bbpiw
hostnames-632524106-ly40y
hostnames-632524106-tlaok

如果此时你没有获得预期的响应,你的 Pod 可能不健康,或者它们监听的端口可能不是你认为的端口。你可能会发现 kubectl logs 命令对于查看发生了什么非常有用,或者你可能需要直接 kubectl exec 进入你的 Pod 并从那里进行调试。

假设到目前为止一切按计划进行,你可以开始调查为什么你的 Service 不工作了。

Service 是否存在?

细心的读者可能已经注意到你实际上还没有创建 Service——这是故意这样安排的。这一步有时会被遗忘,也是首先要检查的事情。

如果你尝试访问一个不存在的 Service,会发生什么?如果你有另一个 Pod 通过名称使用此 Service,你将得到类似如下的输出:

wget -O- hostnames
Resolving hostnames (hostnames)... failed: Name or service not known.
wget: unable to resolve host address 'hostnames'

首先要检查的是该 Service 是否确实存在:

kubectl get svc hostnames
No resources found.
Error from server (NotFound): services "hostnames" not found

我们来创建 Service。和之前一样,这是为了本次演练——你可以在这里使用你自己的 Service 的详细信息。

kubectl expose deployment hostnames --port=80 --target-port=9376
service/hostnames exposed

然后读取它:

kubectl get svc hostnames
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
hostnames   ClusterIP   10.0.1.175   <none>        80/TCP    5s

现在你知道 Service 存在了。

和之前一样,这与你使用 YAML 启动 Service 的效果相同:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: hostnames
  name: hostnames
spec:
  selector:
    app: hostnames
  ports:
  - name: default
    protocol: TCP
    port: 80
    targetPort: 9376

为了突出配置的完整范围,你在这里创建的 Service 使用了与 Pod 不同的端口号。对于许多实际的 Service,这些值可能相同。

是否有影响目标 Pod 的 Network Policy 入站规则?

如果你部署了任何可能影响到 hostnames-* Pod 的入站流量的 Network Policy 入站规则,则需要对此进行审查。

请参阅网络策略以获取更多详细信息。

Service 是否可以通过 DNS 名称工作?

客户端使用 Service 最常见的方式之一是通过 DNS 名称。

从同一 Namespace 的 Pod 中:

nslookup hostnames
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      hostnames
Address 1: 10.0.1.175 hostnames.default.svc.cluster.local

如果失败,可能是你的 Pod 和 Service 位于不同的 Namespace 中,尝试使用包含 Namespace 的限定名称(同样,从 Pod 内部):

nslookup hostnames.default
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      hostnames.default
Address 1: 10.0.1.175 hostnames.default.svc.cluster.local

如果这可行,你需要调整你的应用以使用跨 Namespace 的名称,或将你的应用和 Service 运行在同一个 Namespace 中。如果仍然失败,请尝试使用完全限定名称:

nslookup hostnames.default.svc.cluster.local
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      hostnames.default.svc.cluster.local
Address 1: 10.0.1.175 hostnames.default.svc.cluster.local

注意这里的后缀:"default.svc.cluster.local"。 "default" 是你所在的 Namespace。"svc" 表示这是一个 Service。"cluster.local" 是你的集群域,这在你的集群中可能有所不同。

你也可以从集群中的 Node 上尝试此操作:

nslookup hostnames.default.svc.cluster.local 10.0.0.10
Server:         10.0.0.10
Address:        10.0.0.10#53

Name:   hostnames.default.svc.cluster.local
Address: 10.0.1.175

如果你能够进行完全限定名称查找但不能进行相对名称查找,你需要检查 Pod 中的 /etc/resolv.conf 文件是否正确。从 Pod 内部:

cat /etc/resolv.conf

你应该看到类似如下的输出:

nameserver 10.0.0.10
search default.svc.cluster.local svc.cluster.local cluster.local example.com
options ndots:5

nameserver 行必须指向你的集群 DNS Service。这是通过 --cluster-dns 标志传递给 kubelet 的。

search 行必须包含适当的后缀,以便你能找到 Service 名称。在本例中,它查找本地 Namespace ("default.svc.cluster.local") 中的 Services,所有 Namespace ("svc.cluster.local") 中的 Services,最后查找集群中的名称 ("cluster.local")。根据你自己的安装情况,后面可能会有额外的记录(最多 6 条)。集群后缀通过 --cluster-domain 标志传递给 kubelet。在本文档中,假定集群后缀为 "cluster.local"。你自己的集群可能配置不同,在这种情况下,你应该在之前所有的命令中更改它。

options 行必须将 ndots 设置得足够高,以便你的 DNS 客户端库考虑搜索路径。Kubernetes 默认将其设置为 5,这足以覆盖其生成的所有 DNS 名称。

是否有任何 Service 可以通过 DNS 名称工作?

如果上述方法仍然失败,则表示你的 Service 的 DNS 查找不工作。你可以退一步看看还有什么不工作。Kubernetes master Service 应该总是可以工作的。从 Pod 内部:

nslookup kubernetes.default
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local

Name:      kubernetes.default
Address 1: 10.0.0.1 kubernetes.default.svc.cluster.local

如果失败,请参阅本文档的 kube-proxy 小节,或者回到本文档开头重新开始,但这次不是调试你自己的 Service,而是调试 DNS Service。

Service 是否可以通过 IP 工作?

假设你已经确认 DNS 工作正常,接下来要测试的是你的 Service 是否可以通过其 IP 地址工作。从集群中的一个 Pod 中,访问 Service 的 IP(从上面 kubectl get 的输出中获取)。

for i in $(seq 1 3); do 
    wget -qO- 10.0.1.175:80
done

这应该会产生类似如下的输出:

hostnames-632524106-bbpiw
hostnames-632524106-ly40y
hostnames-632524106-tlaok

如果你的 Service 工作正常,你应该会得到正确的响应。如果不是,可能有许多地方出了问题。请继续阅读。

Service 定义是否正确?

这听起来可能很傻,但你真的应该反复检查你的 Service 是否正确,并且与你的 Pod 的端口匹配。读取你的 Service 定义并验证它:

kubectl get service hostnames -o json
{
    "kind": "Service",
    "apiVersion": "v1",
    "metadata": {
        "name": "hostnames",
        "namespace": "default",
        "uid": "428c8b6c-24bc-11e5-936d-42010af0a9bc",
        "resourceVersion": "347189",
        "creationTimestamp": "2015-07-07T15:24:29Z",
        "labels": {
            "app": "hostnames"
        }
    },
    "spec": {
        "ports": [
            {
                "name": "default",
                "protocol": "TCP",
                "port": 80,
                "targetPort": 9376,
                "nodePort": 0
            }
        ],
        "selector": {
            "app": "hostnames"
        },
        "clusterIP": "10.0.1.175",
        "type": "ClusterIP",
        "sessionAffinity": "None"
    },
    "status": {
        "loadBalancer": {}
    }
}
  • 你尝试访问的 Service 端口是否列在 spec.ports[] 中?
  • targetPort 是否对你的 Pod 正确(有些 Pod 使用与 Service 不同的端口)?
  • 如果你打算使用数字端口,它是数字 (9376) 还是字符串 "9376"?
  • 如果你打算使用命名端口,你的 Pod 是否暴露了同名的端口?
  • 该端口的 protocol 是否对你的 Pod 正确?

Service 是否有 EndpointSlice?

如果你走到这一步,说明你已经确认你的 Service 定义正确并且可以通过 DNS 解析。现在我们来检查你运行的 Pod 是否实际被 Service 选中。

之前你看到 Pod 正在运行。你可以再次检查:

kubectl get pods -l app=hostnames
NAME                        READY     STATUS    RESTARTS   AGE
hostnames-632524106-bbpiw   1/1       Running   0          1h
hostnames-632524106-ly40y   1/1       Running   0          1h
hostnames-632524106-tlaok   1/1       Running   0          1h

-l app=hostnames 参数是 Service 上配置的标签选择器。

“AGE” 列显示这些 Pod 运行了大约一小时,这意味着它们运行正常且没有崩溃。

“RESTARTS” 列显示这些 Pod 没有频繁崩溃或重启。频繁重启可能导致间歇性连接问题。如果重启次数很高,请阅读更多关于如何调试 Pod 的信息。

在 Kubernetes 系统内部有一个控制循环,它会评估每个 Service 的选择器,并将结果保存在一个或多个 EndpointSlice 对象中。

kubectl get endpointslices -l k8s.io/service-name=hostnames

NAME              ADDRESSTYPE   PORTS   ENDPOINTS
hostnames-ytpni   IPv4          9376    10.244.0.5,10.244.0.6,10.244.0.7

这确认了 EndpointSlice 控制器为你的 Service 找到了正确的 Pod。如果 ENDPOINTS 列显示 <none>,你应该检查你的 Service 的 spec.selector 字段是否确实选择了你的 Pod 上的 metadata.labels 值。一个常见的错误是拼写错误或其他错误,例如 Service 选择 app=hostnames,而 Deployment 指定 run=hostnames,就像在 1.18 版本之前,kubectl run 命令也可能被用来创建 Deployment。

Pod 是否工作正常?

此时,你知道你的 Service 存在并且已经选中了你的 Pod。在本次演练开始时,你已经验证了 Pod 本身。我们再次检查 Pod 是否实际工作正常——你可以绕过 Service 机制,直接访问 Pod,就像上面 Endpoints 列出的那样。

从 Pod 内部:

for ep in 10.244.0.5:9376 10.244.0.6:9376 10.244.0.7:9376; do
    wget -qO- $ep
done

这应该会产生类似如下的输出:

hostnames-632524106-bbpiw
hostnames-632524106-ly40y
hostnames-632524106-tlaok

你期望 Endpoints 列表中的每个 Pod 返回其自己的主机名。如果不是这样(或者你的 Pod 的正确行为不是这样),你应该调查那里发生了什么。

kube-proxy 是否工作正常?

如果你走到这里,你的 Service 正在运行,有 EndpointSlice,并且你的 Pod 确实正在提供服务。此时,整个 Service 代理机制就值得怀疑了。我们来逐一确认。

Service 的默认实现以及大多数集群中使用的实现是 kube-proxy。这是一个运行在每个节点上的程序,它配置一套有限的机制来提供 Service 抽象。如果你的集群没有使用 kube-proxy,则以下部分不适用,你将不得不调查你正在使用的 Service 的具体实现。

kube-proxy 是否正在运行?

确认 kube-proxy 正在你的节点上运行。直接在节点上运行,你应该会得到类似如下的输出:

ps auxw | grep kube-proxy
root  4194  0.4  0.1 101864 17696 ?    Sl Jul04  25:43 /usr/local/bin/kube-proxy --master=https://kubernetes-master --kubeconfig=/var/lib/kube-proxy/kubeconfig --v=2

接下来,确认它没有出现明显故障,例如无法联系 master。为此,你需要查看日志。访问日志的方式取决于你的节点操作系统。在某些操作系统上,日志是一个文件,例如 /var/log/kube-proxy.log,而在其他操作系统上则使用 journalctl 访问日志。你应该看到类似如下的输出:

I1027 22:14:53.995134    5063 server.go:200] Running in resource-only container "/kube-proxy"
I1027 22:14:53.998163    5063 server.go:247] Using iptables Proxier.
I1027 22:14:54.038140    5063 proxier.go:352] Setting endpoints for "kube-system/kube-dns:dns-tcp" to [10.244.1.3:53]
I1027 22:14:54.038164    5063 proxier.go:352] Setting endpoints for "kube-system/kube-dns:dns" to [10.244.1.3:53]
I1027 22:14:54.038209    5063 proxier.go:352] Setting endpoints for "default/kubernetes:https" to [10.240.0.2:443]
I1027 22:14:54.038238    5063 proxier.go:429] Not syncing iptables until Services and Endpoints have been received from master
I1027 22:14:54.040048    5063 proxier.go:294] Adding new service "default/kubernetes:https" at 10.0.0.1:443/TCP
I1027 22:14:54.040154    5063 proxier.go:294] Adding new service "kube-system/kube-dns:dns" at 10.0.0.10:53/UDP
I1027 22:14:54.040223    5063 proxier.go:294] Adding new service "kube-system/kube-dns:dns-tcp" at 10.0.0.10:53/TCP

如果你看到有关无法联系 master 的错误消息,你应该仔细检查你的节点配置和安装步骤。

Kube-proxy 可以运行在几种模式之一。在上面列出的日志中,Using iptables Proxier 行表明 kube-proxy 正在以 "iptables" 模式运行。最常见的另一种模式是 "ipvs"。

Iptables 模式

在 "iptables" 模式下,你应该在节点上看到类似如下的输出:

iptables-save | grep hostnames
-A KUBE-SEP-57KPRZ3JQVENLNBR -s 10.244.3.6/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.3.6:9376
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 10.244.1.7/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.1.7:9376
-A KUBE-SEP-X3P2623AGDH6CDF3 -s 10.244.2.3/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.2.3:9376
-A KUBE-SERVICES -d 10.0.1.175/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR

对于每个 Service 的每个端口,KUBE-SERVICES 中应该有一条规则和一个 KUBE-SVC-<hash> 链。对于每个 Pod Endpoint,在 KUBE-SVC-<hash> 中应该有少量规则,并且有一个 KUBE-SEP-<hash> 链其中包含少量规则。具体的规则会根据你的精确配置(包括 node-ports 和 load-balancers)而有所不同。

IPVS 模式

在 "ipvs" 模式下,你应该在节点上看到类似如下的输出:

ipvsadm -ln
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
...
TCP  10.0.1.175:80 rr
  -> 10.244.0.5:9376               Masq    1      0          0
  -> 10.244.0.6:9376               Masq    1      0          0
  -> 10.244.0.7:9376               Masq    1      0          0
...

对于每个 Service 的每个端口,加上任何 NodePorts、外部 IP 和负载均衡器 IP,kube-proxy 会创建一个虚拟服务器。对于每个 Pod Endpoint,它会创建相应的真实服务器。在此示例中,Service hostnames(10.0.1.175:80) 有 3 个 Endpoint(10.244.0.5:9376, 10.244.0.6:9376, 10.244.0.7:9376)。

kube-proxy 是否正在进行代理?

假设你看到了上述情况之一,再次尝试从你的一个节点上通过 IP 访问你的 Service:

curl 10.0.1.175:80
hostnames-632524106-bbpiw

如果仍然失败,查看 kube-proxy 日志中类似如下的特定行:

Setting endpoints for default/hostnames:default to [10.244.0.5:9376 10.244.0.6:9376 10.244.0.7:9376]

如果你看不到这些,尝试重启 kube-proxy 并将 -v 标志设置为 4,然后再次查看日志。

边缘案例:Pod 无法通过 Service IP 访问自身

这听起来可能不太可能,但确实会发生,而且这是应该工作的。

这可能发生在网络没有正确配置“回环(hairpin)”流量时,通常是当 kube-proxyiptables 模式下运行并且 Pod 使用桥接网络连接时。Kubelet 暴露了一个 hairpin-mode 标志,它允许 Service 的 Endpoint 在尝试访问自己的 Service VIP 时负载均衡回到自身。hairpin-mode 标志必须设置为 hairpin-vethpromiscuous-bridge

排查此问题的常见步骤如下:

  • 确认 hairpin-mode 设置为 hairpin-vethpromiscuous-bridge。你应该会看到类似如下的输出。在以下示例中,hairpin-mode 设置为 promiscuous-bridge
ps auxw | grep kubelet
root      3392  1.1  0.8 186804 65208 ?        Sl   00:51  11:11 /usr/local/bin/kubelet --enable-debugging-handlers=true --config=/etc/kubernetes/manifests --allow-privileged=True --v=4 --cluster-dns=10.0.0.10 --cluster-domain=cluster.local --configure-cbr0=true --cgroup-root=/ --system-cgroups=/system --hairpin-mode=promiscuous-bridge --runtime-cgroups=/docker-daemon --kubelet-cgroups=/kubelet --babysit-daemons=true --max-pods=110 --serialize-image-pulls=false --outofdisk-transition-frequency=0
  • 确认有效的 hairpin-mode。为此,你需要查看 kubelet 日志。访问日志的方式取决于你的节点操作系统。在某些操作系统上,日志是一个文件,例如 /var/log/kubelet.log,而在其他操作系统上则使用 journalctl 访问日志。请注意,由于兼容性原因,实际的 hairpin 模式可能与 --hairpin-mode 标志不匹配。检查 kubelet.log 中是否有包含关键字 hairpin 的日志行。应该有日志行指示实际的 hairpin 模式,如下所示。
I0629 00:51:43.648698    3252 kubelet.go:380] Hairpin mode set to "promiscuous-bridge"
  • 如果实际的 hairpin 模式是 hairpin-veth,请确保 Kubelet 具有在节点上操作 /sys 的权限。如果一切工作正常,你应该会看到类似如下的输出:
for intf in /sys/devices/virtual/net/cbr0/brif/*; do cat $intf/hairpin_mode; done
1
1
1
1
  • 如果实际的 hairpin 模式是 promiscuous-bridge,请确保 Kubelet 具有在节点上操作 linux 桥接的权限。如果使用了 cbr0 桥接并且配置正确,你应该会看到:
ifconfig cbr0 |grep PROMISC
UP BROADCAST RUNNING PROMISC MULTICAST  MTU:1460  Metric:1
  • 如果以上方法都不奏效,请寻求帮助。

寻求帮助

如果你走到这一步,说明发生了非常奇怪的事情。你的 Service 正在运行,有 EndpointSlice,并且你的 Pod 确实正在提供服务。你的 DNS 工作正常,并且 kube-proxy 似乎没有出现异常行为。然而,你的 Service 却不工作。请告知我们发生了什么,以便我们协助调查!

通过 Slack论坛GitHub 联系我们。

后续步骤

访问故障排查概述文档以获取更多信息。

最后修改于 2025 年 4 月 9 日太平洋标准时间 5:08:更新文档以反映 Endpoints API 的弃用 (#49831) (649bda2cbd)