K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:Service、EndpointSlice、CoreDNS 与服务发现排障实验

这一轮我要把下面几件最核心的事做成真实证据: 1. Pod 内 DNS 请求到底发给谁 2. 普通 Service 和 Headless Service 的解析结果为什么不同 3. `Endpoints` / `EndpointSlice` 是怎样生成的 4. kube-proxy 怎样把 ClusterIP 转成 Pod IP 5. 为什么“DNS 成功但

Markdown13-操作记录-Service-EndpointSlice-CoreDNS排障实验.md2026年4月10日 06:08

本轮操作记录:Service、EndpointSlice、CoreDNS 与服务发现排障实验

本轮目标

这一轮我要把下面几件最核心的事做成真实证据:

  1. Pod 内 DNS 请求到底发给谁
  2. 普通 Service 和 Headless Service 的解析结果为什么不同
  3. Endpoints / EndpointSlice 是怎样生成的
  4. kube-proxy 怎样把 ClusterIP 转成 Pod IP
  5. 为什么“DNS 成功但服务仍然不通”
  6. 为什么“无 Endpoints”和“targetPort 错误”是两类完全不同的故障

Step 1:先核对这套集群的服务发现底座

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n kube-system get svc kube-dns -o wide

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n kube-system get deploy coredns -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get cm coredns -o yaml | sed -n '1,220p'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system get cm kube-proxy -o yaml | sed -n '1,260p'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n kube-system logs ds/kube-proxy --tail=120

为什么要先查这一层

Service / DNS 是否正常,不是只看业务对象就够了。

你至少要先确认:

  • kube-dns 这个 Service 在不在
  • CoreDNS Pod 是否健康
  • CoreDNS 的 Corefile 怎么写
  • kube-proxy 到底用什么数据平面实现

我看到的结果

kube-dns Service

  • ClusterIP:10.96.0.10
  • 端口:53/UDP53/TCP

CoreDNS

  • Deployment 2/2 ready
  • 两个 Pod 都运行在控制面节点 us480851516617a

CoreDNS Corefile 关键项

  • kubernetes cluster.local in-addr.arpa ip6.arpa
  • forward . /etc/resolv.conf
  • cache 30
  • loadbalance

kube-proxy

ConfigMap 里 mode 是空字符串,但日志明确显示:

  • Using iptables proxy
  • Using iptables Proxier

原理解释

这一步建立三条关键认知:

  1. Pod 内默认 DNS 并不是“直接去公网 DNS”,而是先打给集群里的 kube-dns Service。
  2. CoreDNS 对 cluster.local 域名走 Kubernetes 插件逻辑,对其他域名再 forward。
  3. 这套集群里 Service 数据平面由 kube-proxy 用 iptables 实现。

Step 2:创建实验命名空间和四类对象

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/13-service-dns

本轮创建了什么

命名空间

  • service-lab

正常工作负载

  • Deployment web
  • 2 个副本
  • 容器监听 8080
  • readiness 探针命中 /

正常 Service

  • web-svc
  • port: 80
  • targetPort: 8080

Headless Service

  • web-headless
  • clusterIP: None

故障 Service 1:无 Endpoints

  • web-no-endpoints
  • selector 故意多了不存在的 role: missing

故障 Service 2:端口错误

  • web-wrong-port
  • selector 正常
  • targetPort 故意写成 9090

调试客户端

  • Pod dns-client
  • 固定调度到 wk-1

为什么这样设计实验

因为我要同时构造三类非常常见、但容易混淆的真实现象:

  1. Service 正常转发
  2. DNS 正常,但 Service 没有后端
  3. DNS 正常,Service 也有后端,但端口映射错了

如果这三类现象你能分清,生产里排 Service 问题就已经过了一大关。


Step 3:观察对象刚创建时的瞬时状态

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab get all,endpoints,endpointslices

我看到的结果

刚创建完的瞬间,web-svc 显示:

  • endpoints/web-svc <none>

为什么这个瞬间很重要

因为它能直接说明:

Service 后端不是“Pod 存在就算”,而是“Pod ready 了才会进入成员表”。

这次 web Deployment 配了 readiness 探针,所以刚起容器时短暂没有 Endpoints 是正常现象。

原理解释

这一点和第十课的探针必须联动理解:

  • Pod Running
  • 不等于 Pod Ready
  • Pod 不 Ready
  • Service 就可能暂时没有后端

很多新手看到 <none> 就以为 selector 写错了,其实有时只是 Pod 还没通过 readiness。


Step 4:等待工作负载就绪,再看成员表

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab rollout status deployment/web --timeout=180s

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab wait --for=condition=Ready pod/dns-client --timeout=180s

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab get pod,svc,endpoints,endpointslices -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab get endpointslices -o yaml | sed -n '1,260p'

我看到的关键结果

Pod

  • dns-client10.244.147.95,节点 wk-1
  • web-76d9b6dc8-2w9lt10.244.147.94,节点 wk-1
  • web-76d9b6dc8-dc6p810.244.119.193,节点 us590068728056

Service

  • web-svc:ClusterIP 10.108.57.14
  • web-headless:ClusterIP None
  • web-no-endpoints:ClusterIP 10.97.187.150
  • web-wrong-port:ClusterIP 10.98.244.141

Endpoints

  • web-svc -> 10.244.119.193:8080,10.244.147.94:8080
  • web-headless -> 10.244.119.193:8080,10.244.147.94:8080
  • web-no-endpoints -> <none>
  • web-wrong-port -> 10.244.119.193:9090,10.244.147.94:9090

原理解释

这一步非常关键,因为它把故障切成了两层。

web-no-endpoints

说明:

  • selector 没选到后端
  • 或者后端都不 ready

web-wrong-port

说明:

  • selector 选中了后端
  • 但 Service 生成出来的目标端口是错的

这两种现象在用户眼里可能都叫“Service 不通”,但工程上根因完全不同。


Step 5:从 Pod 内看 DNS 配置

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- cat /etc/resolv.conf

我看到的结果

search service-lab.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

命令解释

这个命令看的是容器里的真实 DNS 客户端配置,不是宿主机配置。

它能直接回答:

  • 当前 Pod 请求发给谁
  • 搜索域是什么
  • 短域名会怎样补全

原理解释

这一结果说明:

  1. dns-client 的 DNS 请求先发给 10.96.0.10
  2. 10.96.0.10 正是 kube-dns Service
  3. 查询短名时会先按 service-lab.svc.cluster.local 等搜索域补全
  4. ndots:5 会让短名查询夹杂一些搜索失败的噪音

所以以后你在 Pod 里执行 nslookup web-svc,如果看到几行 NXDOMAIN,不要慌。

先区分:

  • 最终目标名字是否成功解析
  • 噪音是不是搜索域扩展造成的

Step 6:验证正常 Service 与 Headless Service 的解析差异

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- nslookup web-svc.service-lab.svc.cluster.local

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- nslookup web-headless.service-lab.svc.cluster.local

我看到的结果

web-svc

Name: web-svc.service-lab.svc.cluster.local
Address: 10.108.57.14

web-headless

Name: web-headless.service-lab.svc.cluster.local
Address: 10.244.147.94
Name: web-headless.service-lab.svc.cluster.local
Address: 10.244.119.193

原理解释

这一步把普通 Service 和 Headless Service 的差异钉死了。

普通 Service

DNS 返回:

  • 一个稳定的虚拟 IP

Headless Service

DNS 返回:

  • 一组真实 Pod IP

这就是为什么 Headless Service 更适合:

  • StatefulSet
  • 客户端自己感知多个后端
  • 需要稳定发现每个成员的场景

Step 7:验证正常 Service 的负载分发

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- \
  sh -c 'for i in 1 2 3 4 5 6; do echo "--- request-$i"; wget -qO- http://web-svc; done'

我看到的结果

返回内容交替来自两个 Pod:

  • pod=web-76d9b6dc8-2w9lt
  • pod=web-76d9b6dc8-dc6p8

同时还能看到:

  • 一个 Pod 在 wk-1
  • 一个 Pod 在 us590068728056

命令解释

这里用循环多次 wget 的目的不是图省事,而是为了验证:

  • Service 是否真的把流量分发给多个后端

每次 wget 都会新建一个 TCP 连接,所以更容易看到连接级别的随机分发。

原理解释

这一步证明:

  • Service 不只是“能解析”
  • 它确实在做四层转发

但要注意,负载分发发生在连接层,不是 HTTP 语义层。

如果应用长连接很多,观察到的分布可能和你直觉不一样。


Step 8:验证“DNS 成功但无 Endpoints”的故障

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- \
  nslookup web-no-endpoints.service-lab.svc.cluster.local

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- \
  sh -c 'wget -T 2 -O- http://web-no-endpoints'

我看到的结果

DNS 查询

Name: web-no-endpoints.service-lab.svc.cluster.local
Address: 10.97.187.150

访问结果

Connecting to web-no-endpoints (10.97.187.150:80)
wget: can't connect to remote host (10.97.187.150): Connection refused

原理解释

这一步是最容易让人误判的。

因为你已经看到:

  • 名字能解析
  • Service 也有 ClusterIP

但仍然连接失败。

真正原因是:

  • web-no-endpoints 的成员表为空

这证明:

DNS 层没有问题,问题在 Service 没有后端。

以后你遇到这类现象,第一排查动作应该是:

kubectl get endpoints,endpointslices -n <ns>

而不是先去改 CoreDNS。


Step 9:验证“Endpoints 有值但 targetPort 错误”的故障

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- \
  nslookup web-wrong-port.service-lab.svc.cluster.local

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n service-lab exec dns-client -- \
  sh -c 'wget -T 2 -O- http://web-wrong-port'

我看到的结果

DNS 查询

Name: web-wrong-port.service-lab.svc.cluster.local
Address: 10.98.244.141

访问结果

Connecting to web-wrong-port (10.98.244.141:80)
wget: can't connect to remote host (10.98.244.141): Connection refused

而成员表里又明确是:

  • 10.244.119.193:9090
  • 10.244.147.94:9090

原理解释

这说明:

  • 不是 selector 问题
  • 不是 DNS 问题
  • 而是 Service 把流量转发到了错误端口

也就是:

  • Pod 真实监听 8080
  • Service 却把它写成 9090

这类问题本质上是:

  • targetPort
  • 容器监听端口
  • 应用启动参数

之间不一致。


Step 10:下沉到节点,看 kube-proxy 实际写下去的 iptables

实际命令

ssh root@154.219.104.66 \
  'sudo iptables-save | grep -n -C 2 -E "service-lab/web-svc|service-lab/web-no-endpoints|service-lab/web-wrong-port"'

ssh root@154.219.104.66 \
  'sudo iptables-save | grep -n -C 2 10.108.57.14'

ssh root@154.219.104.66 \
  'sudo iptables-save | grep -n -C 2 10.97.187.150'

ssh root@154.219.104.66 \
  'sudo iptables-save | grep -n -C 2 10.98.244.141'

为什么要下沉到宿主机

因为到这一步,我们不想再停留在“应该是这样”的层面,而是要直接看:

  • 内核规则到底怎么写的

这一步是很多高级排障和面试答题的分水岭。

我看到的关键结果

web-svc

可以看到:

  • 10.108.57.14 命中 KUBE-SVC-HCMWRNUQQOC4KVG2
  • 然后按 0.5 概率转发到:
    • 10.244.119.193:8080
    • 10.244.147.94:8080

web-no-endpoints

直接看到:

"service-lab/web-no-endpoints:http has no endpoints" -j REJECT

web-wrong-port

可以看到:

  • 10.98.244.141 命中 KUBE-SVC-P4EMT4RMUMWL2DCM
  • 再转发到:
    • 10.244.119.193:9090
    • 10.244.147.94:9090

原理解释

这一组规则把三种状态讲得非常透:

  1. 正常 Service:ClusterIP -> Service chain -> Endpoint chain -> PodIP:正确端口
  2. 无后端 Service:ClusterIP -> 直接 REJECT
  3. 错端口 Service:ClusterIP -> 正常 DNAT,但 DNAT 到错误端口

这就是为什么“同样都是连接拒绝”,底层原因可以完全不同。


Step 11:证明 ClusterIP 没有绑在网卡上

实际命令

ssh root@154.219.104.66 'ip addr | grep 10.108.57.14 || true'
ssh root@154.219.104.66 'ip addr | grep 10.97.187.150 || true'
ssh root@154.219.104.66 'ip addr | grep 10.98.244.141 || true'

我看到的结果

三个命令都没有任何输出。

原理解释

这就是最直接的证据:

Service ClusterIP 并不是一张真实网卡上的地址。

它之所以能工作,是因为请求被内核规则拦截并改写,不是因为某个进程真正 bind 了这个 IP。

以后如果有人问你:

ClusterIP 明明不存在于网卡上,为什么还能访问?

你就知道要从:

  • 虚拟 IP
  • kube-proxy
  • DNAT
  • 内核转发表

这条线回答。


Step 12:整理本轮最重要的结论

结论 1:DNS 和 Service 转发是两条链

DNS 负责:

  • 名字 -> 地址

Service 数据平面负责:

  • 地址 -> 后端 Pod

所以 DNS 成功不等于 Service 一定正常。

结论 2:Headless Service 和普通 Service 的核心差别是 DNS 返回内容

  • 普通 Service 返回一个 VIP
  • Headless 返回多个 Pod IP

结论 3:Endpoints 空和 targetPort 错是两类不同故障

前者重点查:

  • selector
  • labels
  • readiness

后者重点查:

  • targetPort
  • 容器监听端口
  • 应用启动参数

结论 4:这套集群的 Service 数据平面是 iptables

而且我们已经拿到了真实规则,不再只是停留在教材概念。


本轮最值得记住的命令与用途

kubectl exec -- cat /etc/resolv.conf

看 Pod 内真实 DNS 配置。

kubectl exec -- nslookup <name>

看名字是否能解析。

kubectl get svc,endpoints,endpointslices

看 Service 定义和后端成员表。

kubectl exec -- wget http://<svc>

看解析之后,真实连接是否成功。

iptables-save

看 kube-proxy 最终如何在节点上实现转发。

ip addr

验证 ClusterIP 是否真的绑定在网卡上。


这一课给你的专家能力提升点

如果你把这一轮实验真正吃透,你已经具备了下面这些生产级基本功:

  • 能把 DNS 问题和 Service 转发问题拆开
  • 能读 EndpointSlice 判断后端成员是否正确
  • 能区分“没后端”和“后端端口错”这两类事故
  • 能下沉到节点看 kube-proxy 规则,不再只停留在 kubectl get 层面

这就是从“会用 Service”进入“会排 Service / DNS 事故”的关键一步。