Skip to content

一次请求在集群里经过了什么 ​

同样是「服务调不通」:一个 Service 的选择器少写了一个字母,请求 1 毫秒就返回连接被拒绝;另一个 Service 的端口写错了,端点列表里明明有 3 个 Pod,结果也是连接被拒绝;加了一条只放行业务端口的出站策略,按名字访问要等 20 秒才报解析失败;一条入站策略挡住的请求,则是等到客户端超时。症状不同,是因为请求在不同的层停下了。

在 Pod 里访问 http://web/,请求依次经过:DNS 把名字解析成 Service 的 ClusterIP;节点上的 kube-proxy 规则把 ClusterIP 换成某个后端 Pod 的 IP;后端列表来自 EndpointSlice;包到达目标 Pod 之前,还要经过 CNI 执行的 NetworkPolicy。

客户端 Pod按名字访问 webCoreDNSweb → ClusterIP连接发往 ClusterIP例如 10.96.x.x:80kube-proxy 的 iptables按 1/3、1/2、其余 选端点NetworkPolicyCNI 在目标侧执行web Pod 1web Pod 2EndpointSlice选择器匹配且就绪的 PodServiceselector + targetPort症状:DNS 被挡 → 解析等 20 秒后失败;选择器写错 → 没有端点,立即拒绝;端口写错 → 有端点,立即拒绝;策略不放行 → 超时
图 1 · 客户端按名字访问 web:先由 CoreDNS 把名字解析成 ClusterIP;连接发出时,节点上 kube-proxy 写好的 iptables 规则按概率选一个端点,把目标地址改成 Pod IP;端点列表来自 EndpointSlice,只包含选择器匹配且就绪的 Pod;包到达目标 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 写入:

text
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 插件,数一次解析实际发出的查询:

解析 example.com(1 个点,少于 5)example.com.default.svc.cluster.localA、AAAA → NXDOMAINexample.com.svc.cluster.localA、AAAA → NXDOMAINexample.com.cluster.localA、AAAA → NXDOMAINexample.com.A、AAAA → 得到地址集群内的名字web 在第一个 search 域就命中:2 个查询末尾带点的名字不走 search 列表:2 个查询search 域与 ndots 来自 kubelet 写入的 /etc/resolv.conf,可以在 Pod 的 dnsConfig 里覆盖
图 2 · Pod 的 resolv.conf 里 ndots 是 5:名字里的点少于 5 个,就先依次拼上三个 search 域查询,都是 NXDOMAIN 之后才查原名。glibc 每次同时查 A 和 AAAA,解析一次 example.com 共 8 个查询;写成 example.com.(末尾带点)只有 2 个
解析的名字CoreDNS 收到的查询结果
web2第一个 search 域命中
example.com8三个 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 是否被某条出站策略选中。

五、排查顺序 ​

按请求经过的顺序,每一层用一个命令验证:

  1. DNS:在客户端 Pod 里解析服务名。失败且很慢,查出站策略与 CoreDNS;外部域名慢,看 ndots 带来的额外查询。
  2. Service 与端点:kubectl get endpointslice -l kubernetes.io/service-name=<服务名>。没有端点查选择器与就绪;有端点查 targetPort。
  3. Pod 网络:绕过 Service,直接访问 Pod IP 与端口。按 Pod IP 能通而按 ClusterIP 不通,查 kube-proxy;按 Pod IP 也不通,查 NetworkPolicy 与 CNI(跨节点问题见节点 NotReady)。
  4. 症状对照:连接被拒绝,通常是没有端点或端口不对;超时,通常是被策略丢弃或网络不通;解析失败,先看 DNS 流量是否被放行。
  5. 负载不均:先确认客户端是否用长连接。

配套实验

  • codesphere-labs/kubernetes/service-request-path:kind 集群(Kubernetes 1.36.4,kindnet,kube-proxy iptables 模式)中的 EndpointSlice 与 iptables 规则、新建连接与长连接的请求分布、选择器与端口写错、search 与 ndots 下 CoreDNS 收到的查询、入站与出站 NetworkPolicy 以及被挡住的 DNS(验证记录)

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权