一次请求在集群里经过了什么
同样是「服务调不通」:一个 Service 的选择器少写了一个字母,请求 1 毫秒就返回连接被拒绝;另一个 Service 的端口写错了,端点列表里明明有 3 个 Pod,结果也是连接被拒绝;加了一条只放行业务端口的出站策略,按名字访问要等 20 秒才报解析失败;一条入站策略挡住的请求,则是等到客户端超时。症状不同,是因为请求在不同的层停下了。
在 Pod 里访问 http://web/,请求依次经过:DNS 把名字解析成 Service 的 ClusterIP;节点上的 kube-proxy 规则把 ClusterIP 换成某个后端 Pod 的 IP;后端列表来自 EndpointSlice;包到达目标 Pod 之前,还要经过 CNI 执行的 NetworkPolicy。
本文在 kind 搭建的 Kubernetes 1.36.4 集群(1 个控制面、2 个工作节点,默认 CNI kindnet,kube-proxy 为 iptables 模式)里,用一个 3 副本的服务逐层验证。实验见文末。
一、Service、EndpointSlice 与 kube-proxy
Service web 的选择器是 app: web。EndpointSlice 控制器找到匹配且就绪的 3 个 Pod,把它们的 IP、就绪状态和所在节点写进 EndpointSlice。Service 本身只是一个稳定的虚拟 IP(ClusterIP),没有进程在上面监听。
真正转发的是每个节点上的 kube-proxy。它监听 Service 与 EndpointSlice 的变化,在节点的 iptables 里写规则。工作节点上 default/web 有 3 条跳转到端点的规则,依次按概率 0.33、0.50、其余全部选择——第一条以 1/3 的概率命中,没命中时第二条在剩下的里以 1/2 命中,最后一条兜底,三个端点各得 1/3。
这个选择发生在建立连接时。从客户端 Pod 发 300 次请求:
- 每次新建连接:3 个 Pod 分别收到 106、104、90 次;
- 复用一条长连接:300 次全部落到同一个 Pod。
连接一旦建立,后续请求都走同一个后端。使用连接池的客户端(HTTP/1.1 长连接、gRPC、数据库驱动),流量分布由连接数决定:扩容后新 Pod 可能很久接不到流量,缩容或发布时连接断开又集中重连。需要按请求均衡时,要在客户端或服务网格这一层做;给连接设置最大寿命,也能让分布逐渐均匀,与 DNS 切换后连接不更新是同一类问题,见 DNS 改了,为什么 Java 服务还在连旧地址。
二、两种配错,同样是连接被拒绝
| 配错 | EndpointSlice 端点数 | 请求结果 |
|---|---|---|
选择器写错(app: wbe) | 0 | 连接被拒绝,1ms |
targetPort 写错(9090,容器监听 8080) | 3 | 连接被拒绝,1ms |
没有端点的 Service,kube-proxy 会在节点的 filter 表里写一条拒绝规则,实验中看到的是 default/web-typo has no endpoints ... -j REJECT --reject-with icmp-port-unreachable,所以客户端立刻收到连接被拒绝,而不是超时。端口写错时端点一切正常——就绪探针检查的是容器,与 Service 的端口映射无关——包被转发到 Pod 上一个没人监听的端口,同样被拒绝。
两者靠 kubectl get endpointslice -l kubernetes.io/service-name=<服务名> 区分:没有端点,查选择器与 Pod 标签、Pod 是否就绪(见 Running 不等于可用);有端点,查 targetPort 与容器实际监听的端口。
三、DNS:search 列表与 ndots
Pod 的 /etc/resolv.conf 由 kubelet 写入:
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5所以在 default 命名空间里,web 会先被拼成 web.default.svc.cluster.local 去查,第一次就命中,解析出 ClusterIP。无头 Service(clusterIP: None)则直接解析出 3 个 Pod IP,由客户端自己选择连接哪一个。
ndots:5 的意思是:名字里的点少于 5 个,就先按 search 列表逐个拼接查询,都失败了才查原名。给 CoreDNS 打开 log 插件,数一次解析实际发出的查询:
| 解析的名字 | CoreDNS 收到的查询 | 结果 |
|---|---|---|
web | 2 | 第一个 search 域命中 |
example.com | 8 | 三个 search 域都是 NXDOMAIN,最后才查原名 |
example.com.(末尾带点) | 2 | 直接查原名 |
glibc 每一步同时查 A 和 AAAA,所以每个候选名字是两个查询。访问外部域名多的服务,每次解析都要多出 6 个必然失败的查询。CoreDNS 的缓存会让重复查询变快,但查询数量不变。可以在调用外部服务的地址末尾加点,或者在 Pod 的 dnsConfig 里调小 ndots;后者也会影响集群内短名字的解析,调整前要确认代码里用的都是什么形式的名字。
四、NetworkPolicy:丢弃,而不是拒绝
kindnet 实现了 NetworkPolicy。先在命名空间里加一条默认拒绝所有入站的策略:
- 客户端访问 web:超时,3002ms(客户端超时设为 3 秒)。策略的实现是丢弃数据包,客户端收不到任何回应,只能等到自己的超时。
再加一条策略,允许带 role: frontend 标签的 Pod 访问 app: web 的 8080 端口。策略是叠加的,放行规则取并集:
- 没有这个标签的
cli:仍然超时; - 带标签的
fe:HTTP 200。
然后给 fe 加一条出站策略,只允许访问 web 的 8080 端口:
- 按 ClusterIP 访问:HTTP 200;
- 按名字访问:解析失败(
Temporary failure in name resolution),耗时 20025ms。
出站策略一旦选中某个 Pod,没有明确放行的出站流量都被丢弃,包括发往 CoreDNS 的 53 端口。DNS 查询被丢弃后,解析器要按超时时间逐个重试,所以失败来得很慢。再加一条策略,放行到 kube-system 里 CoreDNS 的 UDP 与 TCP 53 端口,按名字访问恢复正常。
写出站策略时,第一条应该就是放行 DNS。反过来,排查时看到「解析要等很久才失败」,先检查 Pod 是否被某条出站策略选中。
五、排查顺序
按请求经过的顺序,每一层用一个命令验证:
- DNS:在客户端 Pod 里解析服务名。失败且很慢,查出站策略与 CoreDNS;外部域名慢,看
ndots带来的额外查询。 - Service 与端点:
kubectl get endpointslice -l kubernetes.io/service-name=<服务名>。没有端点查选择器与就绪;有端点查targetPort。 - Pod 网络:绕过 Service,直接访问 Pod IP 与端口。按 Pod IP 能通而按 ClusterIP 不通,查 kube-proxy;按 Pod IP 也不通,查 NetworkPolicy 与 CNI(跨节点问题见节点 NotReady)。
- 症状对照:连接被拒绝,通常是没有端点或端口不对;超时,通常是被策略丢弃或网络不通;解析失败,先看 DNS 流量是否被放行。
- 负载不均:先确认客户端是否用长连接。
配套实验
- codesphere-labs/kubernetes/service-request-path:kind 集群(Kubernetes 1.36.4,kindnet,kube-proxy iptables 模式)中的 EndpointSlice 与 iptables 规则、新建连接与长连接的请求分布、选择器与端口写错、search 与 ndots 下 CoreDNS 收到的查询、入站与出站 NetworkPolicy 以及被挡住的 DNS(验证记录)
参考资料