调试 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 上尝试此操作:
注意
10.0.0.10 是集群的 DNS Service 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 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 端口 (9376),而不是 Service 端口 (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 是否工作正常?
如果你走到这里,你的 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-proxy
在 iptables
模式下运行并且 Pod 使用桥接网络连接时。Kubelet
暴露了一个 hairpin-mode
标志,它允许 Service 的 Endpoint 在尝试访问自己的 Service 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 模式可能与--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 却不工作。请告知我们发生了什么,以便我们协助调查!
后续步骤
访问故障排查概述文档以获取更多信息。