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

Repository Reading Site

第十三课:Service、EndpointSlice、kube-proxy、CoreDNS 与服务发现原理

上一课我们把 HPA 讲清楚了,知道了: 但平台真正要跑起来,还差一条非常硬的链路: 如果这一层不懂,你会在真实生产里反复踩坑: 1. Pod 都是 `Running`,为什么服务还是不通 2. DNS 明明解析成功了,为什么 `wget` 还是报错 3. 为什么有的 Service 没 Endpoints 4. 为什么 Endpoints 明明有,访问还是

Markdown13-第十三课-Service-EndpointSlice-kube-proxy-CoreDNS与服务发现原理.md2026年4月10日 06:09

第十三课:Service、EndpointSlice、kube-proxy、CoreDNS 与服务发现原理

为什么这一课必须接在 HPA 之后

上一课我们把 HPA 讲清楚了,知道了:

  • 什么时候该扩副本
  • 什么时候该缩副本
  • 指标从哪里来
  • 为什么 requests 会影响自动扩缩容

但平台真正要跑起来,还差一条非常硬的链路:

副本数变了之后,流量到底怎样稳定地找到这些 Pod。

如果这一层不懂,你会在真实生产里反复踩坑:

  1. Pod 都是 Running,为什么服务还是不通
  2. DNS 明明解析成功了,为什么 wget 还是报错
  3. 为什么有的 Service 没 Endpoints
  4. 为什么 Endpoints 明明有,访问还是 Connection refused
  5. Headless Service 和普通 Service 到底差在哪
  6. ClusterIP 为什么说是“虚拟 IP”

你以后想做到“架构搭建 + 问题排查 + 指导团队”,必须把这条服务发现链路吃透。


先建立一个核心认知:Service 解决的是“稳定入口”,不是“应用真的在监听”

Pod 天生不稳定:

  • 会重建
  • IP 会变
  • 会扩容缩容
  • 会跨节点调度

如果上游直接记 Pod IP,系统根本没法稳定运行。

所以 Kubernetes 需要一个抽象层:

给一组动态变化的 Pod,提供一个稳定名字和稳定访问入口。

这就是 Service 的本质。

但你要立刻补上一句非常重要的话:

Service 只负责把流量转到“它认为的后端”,不保证后端应用一定监听对了端口,也不保证应用逻辑一定健康。

这句话会直接决定你排障时的方向。


Service 到底由哪几个部件共同实现

很多新手会把 Service 理解成“一个对象”。

这不完整。

真正生效至少有四类角色在协作。

第一类:Service 对象

它描述:

  • 我要暴露哪个端口
  • 选择哪些 Pod 作为后端
  • 类型是 ClusterIPNodePortLoadBalancer 还是 Headless

第二类:EndpointSlice

它描述:

  • 这个 Service 当前到底有哪些后端地址
  • 每个后端对应什么 IP、什么端口
  • 是否 ready / serving / terminating

这相当于 Service 的“后端成员表”。

第三类:kube-proxy

它负责:

  • 监听 Service 和 EndpointSlice 变化
  • 在每个节点上写入数据平面规则
  • 把发往 ClusterIP 的流量转发到后端 Pod

在这套集群里,它实际运行在:

  • iptables 模式

第四类:CoreDNS

它负责:

  • web-svc.service-lab.svc.cluster.local 这类名字解析成 Service IP
  • 把 Headless Service 解析成一组 Pod IP
  • 把集群外域名继续转发给上游 DNS

所以你以后要把服务发现理解成两条并行链:

  • 名字解析链:Pod -> /etc/resolv.conf -> CoreDNS -> DNS 结果
  • 流量转发链:Pod -> ClusterIP -> kube-proxy 规则 -> Pod IP:Port

它们相关,但不是一回事。


先分清几个 Service 类型,不然后面会混

ClusterIP

默认类型。

作用:

只在集群内部提供一个稳定虚拟 IP。

这次实验里的 web-svcweb-no-endpointsweb-wrong-port 都是 ClusterIP

Headless Service

也就是:

  • clusterIP: None

作用:

不提供 VIP,直接通过 DNS 返回后端 Pod IP 列表。

这次实验里的 web-headless 就是这个类型。

NodePort

作用:

在每个节点上开一个固定端口,把外部请求转到对应 Service。

这属于“让集群外流量能进来”的一种基础办法,但它仍然主要是四层能力。

LoadBalancer

作用:

让云厂商或负载均衡器对外提供入口,再转到 Service。

Ingress / Gateway API

它们和 Service 不是一个层次。

它们做的是:

七层 HTTP/HTTPS 路由。

也就是说:

  • Service 更偏四层 TCP/UDP
  • Ingress / Gateway 更偏七层路径、域名、TLS

你要有 OSI 和分层意识:

  • DNS 是应用层协议,默认走 UDP/TCP 53
  • Service ClusterIP 更多体现为三层 IP + 四层端口的转发表
  • Ingress 是七层 HTTP 路由

这套真实集群里的服务发现底座长什么样

这一课我先核对了集群真实配置,而不是纸上谈兵。

1. Pod 默认用谁做 DNS

我在 service-lab/dns-client Pod 里看到:

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

这三行非常重要。

nameserver 10.96.0.10

说明 Pod 内默认 DNS 请求发给:

  • kube-dns Service

我进一步核对系统 Service,确实看到:

  • kube-dns 的 ClusterIP 是 10.96.0.10

说明你在 Pod 里只写短名时,系统会自动尝试拼接搜索域。

例如你执行:

  • nslookup web-svc

它可能依次尝试:

  • web-svc.service-lab.svc.cluster.local
  • web-svc.svc.cluster.local
  • web-svc.cluster.local

这也是为什么有时你会看到:

  • 一边已经解析成功
  • 一边仍然夹杂几行 NXDOMAIN

不是系统坏了,而是 DNS 搜索路径在工作。

options ndots:5

表示名字里点的数量不足 5 个时,会优先按搜索域扩展。

所以排障时如果你想要“干净证据”,最好直接查 FQDN。

2. CoreDNS 在这套集群里的配置

我检查了 kube-system/coredns ConfigMap,看到关键配置:

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

这表示:

  1. cluster.local 域名,CoreDNS 走 Kubernetes 插件逻辑
  2. 对集群外域名,CoreDNS 转发给宿主机上游 DNS
  3. 结果会缓存 30 秒
  4. 多个结果可以做一定的负载均衡

3. kube-proxy 的真实模式

kube-proxy ConfigMap 里 mode 是空字符串,看起来像“没有配”。

但日志明确显示:

  • Using iptables proxy
  • Using iptables Proxier

所以在这套集群里,Service 数据平面实际是:

  • kube-proxy 监听对象变化
  • 在每台节点写入 iptables 规则

这很关键,因为不同集群的数据平面实现可能不同:

  • iptables
  • IPVS
  • nftables
  • eBPF / Cilium

但是不管实现细节怎么变,Service 的核心语义不变:

提供稳定虚拟入口,把流量转到后端。


ClusterIP 为什么说是“虚拟 IP”

这一点是 Service 的灵魂。

我在 dns-client 所在节点 wk-1 上直接查网卡地址:

  • 10.108.57.14,没有
  • 10.97.187.150,没有
  • 10.98.244.141,没有

也就是说:

这些 Service IP 根本没有真正绑在宿主机网卡上。

那为什么还能访问?

因为请求并不是发到某个真实网卡进程上,而是被内核里的转发表规则拦住并改写了目的地址。

在这套集群里,这份“转发表”就是 kube-proxy 写下去的 iptables 规则。

所以你以后面试如果别人问:

Service ClusterIP 明明没有网卡绑定,为什么能通?

你就应该回答:

  • 因为它是内核数据平面的虚拟入口
  • 请求命中 kube-proxy 写的转发规则
  • 再被 DNAT 到真实 Pod IP 和端口

这才叫真正理解。


正常样本:web-svc 到底发生了什么

这次实验里我创建了一个最小但很有教学价值的样本:

  • Deployment:web
  • 副本数:2
  • 容器端口:8080
  • Service:web-svc
  • Service 端口:80
  • targetPort: 8080

两个 Pod 的真实信息例如:

  • 10.244.119.193,节点 us590068728056
  • 10.244.147.94,节点 wk-1

对应的 Service 是:

  • web-svc ClusterIP:10.108.57.14

对应的 Endpoints / EndpointSlice 是:

  • 10.244.119.193:8080
  • 10.244.147.94:8080

访问结果

我从 dns-client 里连续请求:

wget -qO- http://web-svc

返回内容交替出现:

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

这证明:

  • 请求先到 web-svc
  • 再被分发到两个不同后端 Pod

宿主机上对应的规则证据

wk-1 宿主机 iptables-save 里能看到:

  • 10.108.57.14 进入 KUBE-SVC-HCMWRNUQQOC4KVG2
  • 这个链再按概率 0.5
    • 转到 10.244.119.193:8080
    • 或转到 10.244.147.94:8080

这件事说明两个非常重要的原理。

第一:Service 的负载均衡在这套集群里是通过 iptables 规则实现的

不是用户态代理进程在一跳一跳转发。

第二:这更像“连接级分发”,不是“每个 HTTP 请求都单独负载均衡”

我们这次之所以多次 wget 能看到不同 Pod,是因为每次 wget 都新建了一个 TCP 连接。

如果你的客户端长期复用同一条连接,后端选择未必每次变化。

这就是为什么理解:

  • 四层连接
  • conntrack
  • 七层请求

之间的区别非常重要。


Headless Service:为什么 DNS 结果完全不一样

我同时创建了:

  • web-headless

它和 web-svc 选中的是同一组 Pod,但设置了:

  • clusterIP: None

查询结果显示:

  • web-svc.service-lab.svc.cluster.local -> 10.108.57.14
  • web-headless.service-lab.svc.cluster.local -> 10.244.147.94, 10.244.119.193

这就是 Headless Service 的核心差异:

  • 普通 Service:DNS 返回一个稳定 VIP
  • Headless Service:DNS 直接返回后端 Pod 列表

使用场景

Headless 特别适合:

  • StatefulSet
  • 主从发现
  • 需要客户端自己感知多个后端地址
  • 需要稳定网络身份的系统

这也是为什么你前面学 StatefulSet 时必须配 Headless Service。


第一个故障样本:web-no-endpoints

这是非常高频的一类真实事故。

我故意创建了一个 Service:

  • 名字:web-no-endpoints
  • ClusterIP:10.97.187.150
  • selector 多加了一个不存在的标签

结果是:

  • DNS 解析成功
  • Endpoints<none>
  • EndpointSlice 也是空的
  • wget http://web-no-endpoints 报:
    • Connection refused

这说明什么

说明问题根本不在 DNS。

因为:

  • 名字能解析
  • Service 对象也存在

真正出问题的是:

  • Service 找不到后端 Pod

宿主机上的证据更直接

iptables-save 里我看到了:

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

这句话的含义非常硬核:

kube-proxy 已经知道这个 Service 没后端,所以它直接下发了一条拒绝规则。

这就是为什么你会得到 Connection refused

以后你怎么排这类问题

如果你看到:

  • DNS 成功
  • 但 Service 不通
  • Endpoints 是空的

那优先查:

  1. Service selector 和 Pod labels 是否匹配
  2. Pod 是否 Ready
  3. Pod 是否被 Service 控制器剔除

这类问题本质上是:

  • 选择器 / 就绪状态 / 后端成员表问题

不是 DNS 故障。


第二个故障样本:web-wrong-port

这类问题比“无 Endpoints”更隐蔽。

我故意创建了另一个 Service:

  • 名字:web-wrong-port
  • ClusterIP:10.98.244.141
  • selector 正常
  • targetPort 故意写成了 9090

而 Pod 实际监听的端口是:

  • 8080

结果是什么

我看到:

  • DNS 解析成功
  • Endpoints 也不是空的
  • 但成员表里端口是:
    • 10.244.119.193:9090
    • 10.244.147.94:9090
  • wget http://web-wrong-port 仍然报:
    • Connection refused

为什么会这样

因为 Service 控制器并不会替你验证:

  • Pod 里这个端口上到底有没有进程在监听

它做的只是:

  • 按 selector 找 Pod
  • 按 Service 的 targetPort 生成后端地址

于是 kube-proxy 很老实地把流量 DNAT 到:

  • 10.244.119.193:9090
  • 10.244.147.94:9090

但容器里根本没人监听 9090,所以连接被对端拒绝。

这是非常典型的排障训练

你要把两类失败分开:

web-no-endpoints

  • DNS 成功
  • Endpoints 空
  • 根因在 selector / readiness

web-wrong-port

  • DNS 成功
  • Endpoints 有值
  • 根因在 targetPort / 容器监听端口

很多人只会说“Service 不通”,但讲不清故障层次。

真正的工程师必须把它拆到这个粒度。


为什么对象刚创建时,Endpoints 会先是空的

这一课里我在对象刚创建的瞬间就看到过:

  • web-svcEndpoints 先是 <none>

但过一会又恢复成两个后端。

这个现象很适合教学,因为它正好说明:

Service 后端不是“Pod 被创建了就算”,而是“Pod ready 了才算”。

这次 web Deployment 配了 readiness 探针。

所以控制器在构建 EndpointSlice 时,会考虑:

  • Pod 是否已经 ready

这也解释了为什么生产里有时会出现:

  • Pod 已经 Running
  • 但 Service 还暂时没有后端

原因可能不是 Pod 不存在,而是:

  • Pod 还没有通过 readiness

这条链你如果和第十课的探针结合起来看,就彻底打通了。


EndpointsEndpointSlice 到底是什么关系

你在很多教程里仍然会看到 Endpoints,但现代 Kubernetes 更推荐关注:

  • EndpointSlice

原因是:

  • Endpoints 是老对象
  • 大规模后端时扩展性较差
  • EndpointSlice 更适合分片、扩展和多协议场景

这次实验里你可以同时看到两者:

  • Endpoints/web-svc
  • EndpointSlice/web-svc-gmbrv

它们表达的是同一组后端事实,只是形式不同。

生产里很多组件,包括 kube-proxy,本质上都越来越依赖 EndpointSlice。

所以你以后排障时,建议优先看:

kubectl get endpointslices -n <ns>

DNS 成功,不代表服务一定通

这是这节课最重要的判断句之一。

我们已经用真实样本证明了三种不同状态:

状态 1:DNS 成功,Service 也通

样本:

  • web-svc

状态 2:DNS 成功,但 Service 无后端

样本:

  • web-no-endpoints

状态 3:DNS 成功,Service 也有后端,但端口错

样本:

  • web-wrong-port

所以你以后绝不能把:

  • “域名能解析”

等同于:

  • “服务一定正常”

DNS 只回答:

这个名字解析成什么地址。

它不回答:

  • 这个地址有没有后端
  • 后端端口对不对
  • 应用协议是否真的正常

作为专家,你必须知道的 Service 数据平面变体

这套集群里是 iptables,但你不能把 Service 理解成“永远就是 iptables”。

现实里常见几种实现:

iptables

特点:

  • 普遍
  • 简单
  • 规则量大时可读性和性能一般

IPVS

特点:

  • 更偏内核负载均衡
  • 大规模 Service 时更高效

nftables

特点:

  • 新一代 Linux 包过滤框架
  • 某些新版本环境会倾向它

eBPF / Cilium

特点:

  • 把 Service 转发做进更底层的数据平面
  • 可观测性和性能都可能更好
  • 但理解门槛更高

所以真正的专家口径应该是:

Service 语义稳定,但数据平面实现可以变化。


最后给你一条标准排障路径

以后遇到 Service / DNS 问题,尽量按这个顺序查。

第一步:先看 Pod 内 DNS 配置

kubectl exec -n <ns> <pod> -- cat /etc/resolv.conf

回答:

  • 用哪个 nameserver
  • 搜索域是什么
  • ndots 是多少

第二步:查名字能不能解析

kubectl exec -n <ns> <pod> -- nslookup <name>
kubectl exec -n <ns> <pod> -- nslookup <fqdn>

最好优先查 FQDN,减少搜索域带来的噪音。

第三步:查 Service 自己怎么定义

kubectl get svc -n <ns> <name> -o wide

重点看:

  • selector
  • port
  • targetPort
  • type
  • ClusterIP

第四步:查后端成员表

kubectl get endpoints,endpointslices -n <ns>

这里能直接把问题切成两半:

  • 后端为空
  • 后端存在但行为异常

第五步:查 Pod 是否 Ready、端口是否真在监听

kubectl get pod -n <ns> -o wide
kubectl logs -n <ns> <pod>
kubectl exec -n <ns> <pod> -- netstat -lnt

第六步:必要时下沉到节点

iptables-save
ipvsadm -Ln

具体查什么,取决于集群数据平面实现。

如果像这套集群一样是 iptables,你就应该去找:

  • KUBE-SERVICES
  • KUBE-SVC-*
  • KUBE-SEP-*

这节课你必须真正掌握的结论

  1. Service 解决的是给动态 Pod 集合提供稳定入口,不是保证应用一定健康。
  2. Service 生效依赖 Service 对象、EndpointSlice、kube-proxy、CoreDNS 多个组件协作。
  3. 普通 Service 返回一个 VIP,Headless Service 返回一组 Pod IP。
  4. ClusterIP 通常是虚拟 IP,没有绑在真实网卡上。
  5. DNS 成功不代表服务一定通,因为问题可能出在后端成员表或端口映射。
  6. Endpoints 空,优先查 selector 和 readiness。
  7. Endpoints 不空但仍然拒绝连接,优先查 targetPort 和容器监听端口。
  8. 这套集群里 kube-proxy 实际运行在 iptables 模式,但别把实现细节当成 Service 唯一语义。

你现在具备了什么能力

学完这一课,你已经能把下面这些高频生产问题分层看待:

  • DNS 是否正常
  • Service 对象是否正确
  • EndpointSlice 是否正确生成
  • kube-proxy 是否正确编程数据平面
  • 应用监听端口是否和 Service targetPort 一致

这就是从“会创建 Service”走向“会排 Service 事故”的分水岭。