调试服务
对于新安装的 Kubernetes,一个相当常见的问题是服务(Service)无法正常工作。虽然你已经通过 Deployment(或其他工作负载控制器)运行了 Pod 并创建了服务,但在尝试访问它时却得不到响应。本文将帮助你查明问题所在。
在 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。由于你可能正在调试自己的服务,你可以替换为你自己的详细信息,或者按照步骤获取第二个参考点。
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
标签 "app" 会被 kubectl create deployment 自动设置为 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
本演示中使用的示例容器通过 9376 端口上的 HTTP 提供其自身的主机名,但如果你正在调试自己的应用,则需要使用 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 并从那里进行调试。
假设到目前为止一切都按计划进行,你可以开始调查服务不起作用的原因。
服务是否存在?
敏锐的读者会注意到你实际上还没有创建服务 —— 这是故意的。这是有时会被遗忘的一步,也是首先要检查的事情。
如果你尝试访问一个不存在的服务会发生什么?如果你有另一个 Pod 按名称消费此服务,你会得到类似以下内容:
wget -O- hostnames
Resolving hostnames (hostnames)... failed: Name or service not known.
wget: unable to resolve host address 'hostnames'
首先要检查的是该服务是否确实存在:
kubectl get svc hostnames
No resources found.
Error from server (NotFound): services "hostnames" not found
让我们创建服务。如前所述,这是为了演示 —— 你可以在此处使用自己服务的详细信息。
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
现在你知道服务已经存在了。
如前所述,这与使用 YAML 启动服务的效果相同:
apiVersion: v1
kind: Service
metadata:
labels:
app: hostnames
name: hostnames
spec:
selector:
app: hostnames
ports:
- name: default
protocol: TCP
port: 80
targetPort: 9376
为了突出配置的全面性,你在此处创建的服务使用了与 Pod 不同的端口号。对于许多现实世界中的服务,这些值可能是相同的。
是否有任何网络策略(Network Policy)入口规则影响目标 Pod?
如果你部署了任何可能影响 hostnames-* Pod 入站流量的网络策略入口规则,则需要对其进行检查。
请参阅 网络策略 了解更多详情。
服务可以通过 DNS 名称工作吗?
客户端消费服务最常用的方式之一是通过 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 和服务处于不同的命名空间,请尝试使用限定命名空间的名称(同样在 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
如果这可行,你需要调整应用以使用跨命名空间的名称,或者在同一个命名空间中运行应用和服务。如果这仍然失败,请尝试使用全限定域名(FQDN):
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" 是你正在操作的命名空间。"svc" 表示这是一个服务。"cluster.local" 是你的集群域,在你自己的集群中可能会有所不同。
你也可以从集群中的节点(Node)尝试此操作:
说明
10.0.0.10 是集群的 DNS 服务 IP,你的可能不同。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 服务。这是通过 --cluster-dns 标志传递给 kubelet 的。
search 行必须包含适当的后缀,以便你找到服务名称。在这种情况下,它正在查找本地命名空间中的服务 ("default.svc.cluster.local")、所有命名空间中的服务 ("svc.cluster.local"),以及集群中的名称 ("cluster.local")。根据你自己的安装情况,之后可能还会有其他记录(总计最多 6 条)。集群后缀是通过 --cluster-domain 标志传递给 kubelet 的。在本文档中,假定集群后缀为 "cluster.local"。你自己的集群配置可能不同,在这种情况下,你应该在之前的所有命令中更改它。
options 行必须将 ndots 设置得足够高,以便你的 DNS 客户端库考虑搜索路径。Kubernetes 默认将其设置为 5,这足以覆盖它生成的所有 DNS 名称。
是否有任何服务可以通过 DNS 名称工作?
如果上述操作仍然失败,则 DNS 查找对你的服务不起作用。你可以退一步看看还有什么不起作用。Kubernetes 的 master 服务应该始终有效。在 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 部分,甚至返回文档顶部重新开始,但不要调试自己的服务,而是调试 DNS 服务。
服务可以通过 IP 工作吗?
假设你已确认 DNS 正常工作,下一步是测试你的服务是否可以通过其 IP 地址工作。从集群中的 Pod 访问服务的 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
如果你的服务工作正常,你应该会得到正确的响应。如果没有,可能有许多地方出错了。请继续阅读。
服务定义正确吗?
这听起来可能很傻,但你确实应该反复检查你的服务是否正确,并且与 Pod 的端口匹配。回读你的服务并验证它:
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": {}
}
}
- 你尝试访问的服务端口是否列在
spec.ports[]中? targetPort对你的 Pod 来说是否正确(某些 Pod 使用与服务不同的端口)?- 如果你打算使用数字端口,它是数字 (9376) 还是字符串 "9376"?
- 如果你打算使用命名端口,你的 Pod 是否暴露了同名的端口?
- 端口的
protocol对你的 Pod 来说是否正确?
服务是否有任何 EndpointSlices?
如果你进行到这一步,说明你已确认服务定义正确且已被 DNS 解析。现在让我们检查你运行的 Pod 是否确实被服务选中了。
之前你看到 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 参数是服务上配置的标签选择器。
"AGE" 列显示这些 Pod 大约有一小时的历史,这意味着它们运行良好且没有崩溃。
"RESTARTS" 列显示这些 Pod 没有频繁崩溃或被重启。频繁重启可能会导致间歇性的连接问题。如果重启次数很高,请阅读更多关于如何 调试 Pod 的信息。
Kubernetes 系统内部有一个控制循环,它评估每个服务的选择器,并将结果保存到一个或多个 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 控制器已为你的服务找到了正确的 Pod。如果 ENDPOINTS 列为 <none>,你应该检查服务的 spec.selector 字段是否确实选择了 Pod 上的 metadata.labels 值。一个常见的错误是存在拼写错误或其他错误,例如服务选择了 app=hostnames,但 Deployment 指定了 run=hostnames(如 1.18 之前的版本中,kubectl run 命令也可以用于创建 Deployment)。
Pod 工作正常吗?
至此,你已知晓服务存在并已选中 Pod。在本次演示开始时,你验证了 Pod 本身。让我们再次检查 Pod 是否确实在工作 —— 你可以绕过服务机制,直接访问上面 Endpoints 列出的 Pod。
说明
这些命令使用 Pod 端口 (9376),而不是服务端口 (80)。在 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 工作正常吗?
如果你进行到这里,说明你的服务正在运行,有 EndpointSlices,并且你的 Pod 确实在提供服务。此时,整个服务代理机制都值得怀疑。让我们一步步确认它。
服务的默认实现(也是大多数集群上使用的实现)是 kube-proxy。这是一个运行在每个节点上的程序,负责配置用于提供服务抽象的一小组机制之一。如果你的集群不使用 kube-proxy,则以下部分将不适用,你必须调查你正在使用的任何服务实现。
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
对于每个服务的每个端口,在 KUBE-SERVICES 中应该有 1 条规则,以及一个 KUBE-SVC-<hash> 链。对于每个 Pod 端点,在该 KUBE-SVC-<hash> 中应该有少量规则,以及一个包含少量规则的 KUBE-SEP-<hash> 链。具体的规则将根据你的具体配置(包括 node-port 和负载均衡器)而有所不同。
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
...
对于每个服务的每个端口,加上任何 NodePort、外部 IP 和负载均衡器 IP,kube-proxy 都会创建一个虚拟服务器。对于每个 Pod 端点,它将创建相应的真实服务器。在此示例中,服务 hostnames (10.0.1.175:80) 有 3 个端点 (10.244.0.5:9376, 10.244.0.6:9376, 10.244.0.7:9376)。
kube-proxy 正在代理吗?
假设你确实看到了上述情况之一,请再次尝试从其中一个节点通过 IP 访问你的服务:
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 无法通过服务 IP 到达自身
这听起来可能不太可能,但它确实会发生,而且理应可以工作。
当网络未针对 "hairpin"(发夹)流量进行正确配置时,可能会发生这种情况,通常发生在 kube-proxy 以 iptables 模式运行且 Pod 通过网桥连接时。Kubelet 暴露了一个 hairpin-mode 标志,如果服务的端点尝试访问自己的服务虚拟 IP (VIP),该标志允许它们负载均衡回自身。hairpin-mode 标志必须设置为 hairpin-veth 或 promiscuous-bridge。
排查此问题的常用步骤如下:
- 确认
hairpin-mode设置为hairpin-veth或promiscuous-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-mode标志不匹配。检查 kubelet.log 中是否有带有关键字hairpin的日志行。应该有指示生效发夹模式的日志行,如下所示:
I0629 00:51:43.648698 3252 kubelet.go:380] Hairpin mode set to "promiscuous-bridge"
- 如果生效的发夹模式是
hairpin-veth,请确保Kubelet有权操作节点上的/sys。如果一切正常,你应该看到类似:
for intf in /sys/devices/virtual/net/cbr0/brif/*; do cat $intf/hairpin_mode; done
1
1
1
1
- 如果生效的发夹模式是
promiscuous-bridge,请确保Kubelet有权操作节点上的 linux 网桥。如果使用了cbr0网桥并进行了正确配置,你应该看到:
ifconfig cbr0 |grep PROMISC
UP BROADCAST RUNNING PROMISC MULTICAST MTU:1460 Metric:1
- 如果以上方法均无效,请寻求帮助。
寻求帮助
如果你进行到这一步,说明发生了一些非常奇怪的事情。你的服务正在运行,有 EndpointSlices,并且你的 Pod 确实在提供服务。DNS 工作正常,kube-proxy 似乎也没有异常。然而你的服务就是无法工作。请告知我们发生了什么,以便我们协助调查!
接下来
访问 排错概览文档 了解更多信息。