AI时代的Kubernetes教程36招-第12招-Service、EndpointSlice和CoreDNS怎么让服务稳定互相发现?
Kubernetes Service 稳定入口:ClusterIP、EndpointSlice 与 CoreDNS 拆解
Pod 会死,入口不能死:Service 的稳到底落在哪一层?
[读者] 正在把服务搬上 Kubernetes 的后端工程师
[痛点] Pod 重建后 IP 全变,上游配置跟着改,改一次崩一次
[现在读] 需要把稳定入口从口头约定变成可验证的集群内契约
[读完] 能独立写出 Service 清单,看懂 EndpointSlice,用 CoreDNS 自证解析链路
[旧方案] 客户端直连 Pod IP,靠人工同步地址
|
v
[新需求] Pod 随时重建、扩缩容、滚动更新
|
v
[冲突] IP 是易失资源,却被当成长期标识
|
v
[后果] 每次发布都要改配置,故障窗口和发布窗口完全重合
我是老李,带一个做订单查询的后端小组,清单统一放在 deploy/base/services/ 下。半年前上 Kubernetes 时图省事,让上游服务直接连 Pod IP。
一开始确实顺:三个 Pod、三个 IP,写死在配置文件里,谁也没觉得有问题。
直到第一次滚动更新,Pod 名字后缀全变,IP 也全变。凌晨两点,上游同事在群里问:你们服务地址是不是改了?
Service 的稳定入口到底稳定在哪里?为什么 Pod 一直在换,客户端却可以不改配置?
EndpointSlice 是 Service 的子对象还是独立资源?它凭什么是自动更新的?
CoreDNS 解析成功,就代表 Service 真的可用吗?Headless Service 又为什么不能直接当负载均衡用?
01、故事
小组负责的订单查询服务叫 order-api,deploy/base/services/ 下有三份 YAML:一份 Deployment,一份给上游调用的 Service,一份给内部对等发现用的 Headless Service。
这次的任务是:把上游调用方从 Pod IP 改成集群内域名,并且在不改上游任何一行配置的前提下,扛住一次滚动更新和一次扩容。
当时的现状是这样的:
- Deployment order-api,3 个副本,容器端口 80;
- Service order-api,默认 ClusterIP 类型,对外端口 8080,转发到 80;
- 上游 order-gateway,通过环境变量读取订单服务地址。
第一版改造只做了一步:写一个带 selector 的 Service。上游配置里出现的是一串固定域名,不再是会变的 IP。
第一次验证很成功:域名能解析,请求能通,滚动更新之后上游配置一个字没改。
但这份安心只维持了三天。第三天凌晨,扩容到 5 副本的当天,上游开始报连接超时。也是从那次开始,我们才意识到:Service 稳的是契约,而契约能不能兑现,取决于后端集合里到底有谁。
先立一个名
再理一串址
滚动过三回
上游未曾改
02、问题
旧方案失效的地方很具体:上游配置里写的是域名,但域名的解析结果和真实后端之间,存在一条我们完全没有观测的链路。
旧方案:上游直连 Pod IP,或者只看一眼域名解析结果就认为服务可用。
业务影响:每次发布、扩容、节点漂移,都可能出现一次全量连接超时,而这段时间恰好是发布窗口,值班同学默认的怀疑对象总是 DNS。
技术表现分三层:
- 名字层:CoreDNS 能解析出 ClusterIP,看起来一切正常;
- 集合层:EndpointSlice 里可能是空的,也可能混进了尚未就绪的 Pod;
- 数据面:节点上的转发规则按集合层的内容生成,集合错,转发就错。
可验证的完成标准,我们最后定成四条:
- 上游只用订单服务的集群内域名,滚动更新期间配置零变更;
- Pod 副本数变化后,对应 EndpointSlice 的 endpoints 数量同步变化;
- 在 Pod 内用 CoreDNS 解析集群内域名,能拿到 ClusterIP;
- Headless Service 解析返回的是 Ready Pod 的地址列表,而不是一个虚拟 IP。
旧法随址动
新令要名稳
无法可验者
皆是空口言
03、原理
Service 只做三件事,但每一件都容易被误解。
名字:Service 拿到一个 ClusterIP,同时 CoreDNS 为它写入
集合:EndpointSlice 存的是这个 Service 现在该转发给谁。它由集群控制器根据 Service 的 selector 和 Pod 的 Ready 状态自动维护,Pod 增删、标签变化、就绪状态翻转,都会反映到这里。
数据面:节点上的 kube-proxy 或等价组件监听 Service 与 EndpointSlice 的变化,把它们翻译成本地转发规则。
反直觉判断:ClusterIP ping 不通是正常的。它没有绑定在任何网卡上,也没有进程在它上面响应 ICMP,ping 失败不构成故障证据。
反直觉判断:DNS 解析成功和 Service 可用,是两件事。只要 Service 对象存在,A 记录就在;哪怕后端 Pod 一个都没有,解析照样能返回 ClusterIP。
反直觉判断:Pod Ready 不等于 Pod 可用。没有 readinessProbe 的容器,进程一跑起来就算 Ready,会被立刻写进 EndpointSlice。
反直觉判断:EndpointSlice 不是 Service 的附属字段,而是独立 API 资源。Service 负责描述我要谁,另一个控制器负责描述现在有谁。这个解耦是好事,但它同时意味着:selector 写错时,Service 本身不会报错,只是集合悄悄变空。
理解这三层之后,后面的故障就好解释了:标签漂移,集合层直接空;没有就绪探针,集合层混入未就绪实例;Headless 场景,客户端拿到地址列表之后,缓存和重试都由它自己负责。
虚址不落网
真身在切片
就绪方入列
解析非可用
04、架构
[输入] 客户端请求 order-api.default.svc.cluster.local:8080
|
v
[模块] Service(order-api):ClusterIP + selector + 端口映射
|
v
[数据/状态] EndpointSlice:Ready Pod 的 IP:Port 列表
|
v
[处理] 节点数据面按规则转发;Headless 场景直接返回 A 记录列表
|
v
[输出] 订单查询响应;或在 Headless 下输出后端地址列表
边界:Service 只解决入口稳定和后端集合选择,不解决应用层超时、重试、熔断;ClusterIP 只保证集群内可达,集群外访问需要额外入口;Headless 不提供虚拟 IP,负载均衡交给客户端。
收益:Pod 的生命周期与访问入口彻底解耦,发布不再需要改上游配置。
代价:多了一条控制器链路和一个虚拟转发层,排查时要把名字层、集合层、数据面分开看。
适用条件:集群内东西向流量,且 Pod 能被 readinessProbe 准确描述。反过来,如果应用没有可信的健康端点,Service 只能退化成进程存活判断,集合层的可信度就没了。
一器择其后
一节省其间
边界划清楚
收益方长久
05、实战一次
环境:一个可用集群(v1.21 及以上,EndpointSlice 属于 discovery.k8s.io/v1),kubectl 已指向该集群。
依赖:后端用官方 nginx 镜像,DNS 调试用 busybox 镜像,不引入私有仓库。
编写 deploy/base/services/order-api.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
spec:
containers:
- name: api
image: nginx:1.25-alpine
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 2
periodSeconds: 3
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: order-api
namespace: default
spec:
type: ClusterIP
selector:
app: order-api
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: order-api-headless
namespace: default
spec:
clusterIP: None
selector:
app: order-api
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
启动与验证:
kubectl apply -f deploy/base/services/order-api.yaml
kubectl get svc order-api order-api-headless
kubectl get endpointslice -l kubernetes.io/service-name=order-api
未在当前环境实测,以下为预期结果:
$ kubectl get svc order-api order-api-headless
NAME TYPE CLUSTER-IP PORT(S) AGE
order-api ClusterIP 10.96.132.77 8080/TCP 40s
order-api-headless ClusterIP None 8080/TCP 40s
$ kubectl get endpointslice -l kubernetes.io/service-name=order-api
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
order-api-2x9kq IPv4 80 10.244.1.7,10.244.2.4,10.244.3.11 40s
再用 CoreDNS 做一次自证:
kubectl run dnstest --rm -it --restart=Never --image=busybox:1.36 -- nslookup order-api.default.svc.cluster.local
示例输出:
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: order-api.default.svc.cluster.local
Address 1: 10.96.132.77 order-api.default.svc.cluster.local
Headless 的解析结果应当是一串 Pod 地址:
kubectl run dnstest2 --rm -it --restart=Never --image=busybox:1.36 -- nslookup order-api-headless.default.svc.cluster.local
示例输出:
Name: order-api-headless.default.svc.cluster.local
Address 1: 10.244.1.7
Address 2: 10.244.2.4
Address 3: 10.244.3.11
最后观察自动更新:
kubectl scale deployment order-api --replicas=5
kubectl get endpointslice -l kubernetes.io/service-name=order-api -w
示例输出中 endpoints 会从 3 个增加到 5 个,整个过程不需要动 Service 对象。
V1 到这里就跑通了,但它留下两个问题:selector 一旦和 Pod 标签对不上会发生什么?滚动更新时上游会不会出现短时错误?
清单先落地
命令再验证
最小闭环成
隐患留后章
06、排查
链一:域名能解析,请求却超时。
现象:在 Pod 内 curl order-api.default.svc.cluster.local:8080 超时,但 nslookup 能返回 ClusterIP。
怀疑:先怀疑 CoreDNS 抖动,再怀疑节点上的转发规则丢了。
检查:
kubectl get endpointslice -l kubernetes.io/service-name=order-api
kubectl get pods --show-labels -l app=order-api
证据:EndpointSlice 查询返回 No resources found,而 Pod 是健康的,标签是 app=order-api-v2。
错误尝试: 先重启 CoreDNS 的 Pod,再逐节点排查转发规则。为什么错:nslookup 已经成功,说明名字链路是好的;问题出在后端集合,重启 DNS 只会把影响面从一个服务扩大到整个集群。
根因:Deployment 模板的标签被改成了 app=order-api-v2,Service 的 selector 还停在 app=order-api。selector 匹配不到任何 Pod,控制器就不会为它生成 EndpointSlice。
修复:把标签和 selector 收敛到同一处定义,并在清单仓库里加一条静态检查规则,禁止 selector 与 Pod 模板标签分头维护。
链二:滚动更新期间上游偶发 502。
现象:滚动更新开始后,新 Pod 刚 Running 的几秒里,上游出现少量 502,随后自动恢复。
怀疑:第一反应是节点数据面更新规则慢。
检查:
kubectl get endpointslice -l kubernetes.io/service-name=order-api -o yaml
kubectl get pods -l app=order-api -w
证据:EndpointSlice 里已经出现新 Pod 的地址,同时 kubectl 的 READY 列显示 1/1,但容器日志显示应用此时还没有完成监听端口初始化。
根因:这个 Deployment 当时没有 readinessProbe。没有就绪探针时,容器 Running 就被视为 Ready,Pod 会立刻进入后端集合,而应用其实还没准备好接流量。这不是数据面慢,是就绪定义缺失。
修复:补上 readinessProbe,并设置合理的 initialDelaySeconds 与 failureThreshold,让就绪状态由应用真实健康状态决定。
链三:Headless 域名解析到已终止的 Pod。
现象:客户端用 Headless 域名做直连,扩容后仍然把请求发往已经终止的 Pod 地址。
怀疑:DNS 记录没更新。
检查:在 Pod 内 nslookup 得到的地址列表是正确的,但客户端进程内部仍在使用旧地址。
证据:解析结果与当前 Ready Pod 一致,说明服务端记录没问题;旧地址存在于客户端进程的连接缓存与 DNS 缓存里。
根因:Headless 返回的是地址列表,地址列表一旦被客户端缓存,更新就不会自动传导。
修复:对这类客户端,要么改回 ClusterIP Service,要么明确缩短应用层 DNS 缓存时间并启用带健康检查的负载策略。这条修复的成本不在集群侧,在客户端侧。
现象先钉死
怀疑逐条排
证据不撒谎
根因自浮现
07、优化
三个根因,三次修改,全部对应第 06 章拿到的证据。
根因一,标签与 selector 分头维护。修改:把标签收敛成团队统一的键值约定,Service 与 Pod 模板引用同一处定义。原因:selector 落空不会产生任何报错,只能靠约束提前拦住。新行为:标签漂移在提交阶段就暴露,而不是在凌晨的告警里。验证:改一次标签,确认 EndpointSlice 随标签同步变化。
根因二,缺少就绪探针。修改:为容器补上 readinessProbe,指向应用真实的健康端点,并设定 initialDelaySeconds、periodSeconds、failureThreshold。原因:Kubernetes 只能按你给的信号判断就绪,不给信号它只看进程存活。新行为:后端集合里只包含真正接受流量的 Pod。验证:滚动更新时用 kubectl get pods -w 与 EndpointSlice 对照,观察地址是在 READY 变为 1/1 之后才出现。
根因三,Headless 的地址列表被客户端缓存。修改:只在需要成员互认或直连特定实例的场景保留 Headless,其余场景统一走 ClusterIP Service;无法改造的客户端,明确其 DNS 缓存与连接池策略。原因:Headless 把负载均衡和故障摘除的责任交给了调用方。新行为:常规调用不再依赖客户端对地址变化的敏感度。验证:扩容后观察上游连接目标是否跟着变化。
需要说明的是,V2 的验证只给出方法与观察点,具体数值需要在各自环境按上述命令采集确认,不同集群的数据面实现和 DNS 配置可能带来差异。
改在根因处
不在表象层
行为可复现
取舍要写清
08、演进
[同一输入] 上游请求 order-api.default.svc.cluster.local:8080
|
+--[V1] Service + selector(app=order-api) + 无就绪探针
| 行为: 标签一致时可达
| 代价: 标签漂移 => 后端集合为空, 表现为全量超时
| 无就绪探针 => 未就绪 Pod 提前入列, 表现为发布期抖动
|
+--[V2] Service + 统一标签约定 + readinessProbe + Headless 边界说明
| 行为: 后端集合只含真正就绪的 Pod
| 代价: 就绪判定变严, 发布要多等一个探针周期; 探针参数需要维护
|
[Trade-off]
得到: 入口与 Pod 生命周期解耦, 且集合内容可信
失去: 容器起来就算能服务的侥幸, 发布节奏略慢
适用边界: 集群内东西向访问; 应用必须提供可信健康端点
按维度比较:
- 正确性:V1 依赖标签长期不漂移这一假设;V2 把正确性建立在可探测的就绪状态上。
- 稳定性:V1 在滚动更新窗口存在已知的抖动来源;V2 把这个窗口交给了就绪探针控制。
- 复杂度:V1 清单更短;V2 多一个探针和一条标签约束,清单复杂度上升,但排查路径更清晰。
- 成本:V2 的额外成本是发布等待时间和探针维护,不需要新增组件。
- 适用范围:V1 只适合实验环境;V2 适合有稳定健康端点的常规无状态服务。
- 遗留问题:Headless 场景下客户端的缓存与负载策略仍未完全解决,需要按调用方逐个确认。
同入不同出
新旧各有价
得失算明白
边界定去留
09、洞见
9.1 稳定入口稳定的是契约,不是地址
反直觉判断:Service 里最不重要的就是那个 ClusterIP 的具体数值,它会变,客户端也不需要知道。真正稳定的是
9.2 解析成功只证明名字存在,可用性要看集合
反直觉判断:nslookup 返回了地址,不能证明服务可用。A 记录由 Service 对象决定,EndpointSlice 才由 Pod 的就绪状态决定。这两件事在时间上不同步,所以排查 Service 类故障时,第一步应该看 EndpointSlice 里的端点,而不是先怀疑 DNS。
9.3 没有就绪探针时,Kubernetes 只能按进程存活的粒度理解可用
第 06 章第二条链已经证明:缺 readinessProbe 时,Pod 会在应用真正可服务之前进入后端集合。这不是数据面更新慢,而是可用性的定义被降级了。就绪探针的价值不在于探针本身,而在于它把应用对“我准备好了”的判断,翻译成了集群能理解的信号。
9.4 Headless 不是更高级的 Service,而是把复杂度转移给了调用方
Headless 返回地址列表,看起来更直接,但负载均衡、连接池、缓存失效、故障摘除全部落到客户端。只有当对等成员需要互相发现,或者业务确实需要直连特定实例时,这份复杂度才值得付。默认选择仍然是 ClusterIP Service。
稳的不是址
稳的是契约
通的不一定
可得才算通
10、系统落地
原来有什么:上游配置里写死 Pod IP,或者写一个没人验证过的域名,发生故障时只能逐台机器看。
本篇新增了什么:一套最小可验证的 Service 清单,包含 ClusterIP Service、Headless Service 与带就绪探针的 Deployment;一条自证链路,用 EndpointSlice 看集合、用 CoreDNS 看名字;以及一条标签约束,防止 selector 与 Pod 模板分头漂移。
现在能做什么:滚动更新和扩缩容期间,上游配置零变更;任意时刻都能回答两个问题——这个名字解析成什么,这个 Service 现在转发给谁。
还缺什么:集群外入口、超时与重试策略、把 EndpointSlice 纳入日常观测的习惯、Headless 客户端的缓存治理。
下一步如何演进:把 EndpointSlice 的关键状态纳入巡检与告警(例如 ready 端点总数为 0 时立即告警);把标签约定写进清单仓库的静态检查;对仍然使用 Headless 的调用方,逐个确认其 DNS 缓存与健康检查策略。
旧有散地址
新增稳定名
能查能自证
边界仍须知
11、小结
Q1 → Service 用不随 Pod 变化的 ClusterIP 与集群内域名提供稳定契约,把 Pod 的增删收敛到 EndpointSlice,所以客户端不必感知 Pod 重建
Q2 → EndpointSlice 是独立 API 资源,由控制器根据 Service 的 selector 与 Pod 的就绪状态自动维护,Pod 变化时自动更新
Q3 → 解析成功只说明名字存在,可用性要看 EndpointSlice 中 ready 的端点;Headless 返回地址列表,负载均衡与缓存治理由客户端负责
状态 → 一套可跑通的 Service 清单 + 可自证的解析与集合观察链路
三问皆有答
一链已闭环
状态可交付
余下待演进
12、作业
12.1 理解题:ClusterIP 无法 ping 通,能否据此判断 Service 故障?
参考答案:不能。ClusterIP 是转发规则匹配用的虚拟地址,不绑定在任何网卡上,也不响应 ICMP。判断 Service 是否可用,要看 EndpointSlice 中是否存在 ready 的端点,再看数据面转发是否正常。
12.2 实战题:不改上游配置,验证一次扩容不会影响访问。
参考答案:先确认上游使用的是
12.3 排障题:Service 的域名能解析,但请求全部超时,先查什么?
参考答案:先查 EndpointSlice 是否存在且非空,再核对 Service 的 selector 与 Pod 标签是否一致,随后确认 Pod 是否处于 Ready。只有在集合层确认无误之后,才去排查节点数据面与 DNS 配置,避免从最不可能出错的环节开始。
12.4 架构判断题:一个客户端框架自带 DNS 缓存与连接池,是否应该用 Headless Service 做负载均衡?
参考答案:需要谨慎。Headless 返回地址列表,地址变化要经过客户端缓存与连接池才能生效,容易在扩缩容与滚动更新时留下过期连接。除非业务依赖对等成员互认或直连特定实例,否则优先使用 ClusterIP Service,把地址变化交给集群侧处理;若必须使用 Headless,则要明确缓存时间与健康检查策略。
理解须动脑
实战须动手
排障须动证
判断须动心
13、思考
回到最初那个冲突:Pod 是易失的,入口却要求稳定。Service 给出的答案不是把地址变成永久,而是把地址从契约里拿掉——向上游承诺名字,向下用 selector 和就绪状态挑选后端。
这个模式可以带走的判断有三条。第一,任何稳定标识背后都必须有一个维护映射关系的组件,Service 稳定的是名字,EndpointSlice 维护的是实时映射,两者职责不能混。第二,可用性的定义一旦被降级,所有自动化都会按降级后的定义执行,没有就绪探针就等于告诉集群“进程活着就算能服务”。第三,当平台把复杂度从服务端转移给客户端时,要先确认客户端是否具备承接这份复杂度的能力。
下一次遇到类似的稳定入口问题,先问清楚三件事:契约是什么,谁在维护映射,可用性由谁定义。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号