1. 项目概述:为什么服务发现是k8s里最常被低估的“隐形骨架”
你刚部署完一个Pod,又起了一个Service,curl一下IP能通,心里一松——“集群网络搞定了”。但过两天业务量上来,后端Pod扩到20个,前端调用开始随机503;再过一阵,某次滚动更新后,部分请求卡在半路超时;又或者,你明明在Service里写了sessionAffinity: ClientIP,却还是发现用户登录态频繁丢失。这些问题,表面看是网络不通、配置写错、负载不均,根子上,几乎都卡在服务发现这一步没理清楚。
Kubernetes的服务发现机制,不是某个命令或配置项,而是一整套贯穿DNS、Endpoint、kube-proxy、CNI插件、Ingress控制器的协同体系。它决定了Pod怎么找到另一个Pod,Service怎么把流量分发给后端,外部请求如何穿透层层网络抵达内部服务——它不显山露水,但一旦出问题,所有上层应用都会“失联”。我带过的几个团队,有超过60%的线上网络类故障,最终定位到服务发现链路上的某个环节:比如CoreDNS缓存未刷新、EndpointSlice没同步、iptables规则残留、或者CNI插件对Headless Service的SRV记录解析支持不全。
这篇内容专讲k8s的服务发现机制,不堆概念,不画大饼,只拆解真实生产环境里你每天都在用、却可能从未真正看清的四个核心层级:DNS层怎么把mysql.default.svc.cluster.local变成IP;Endpoint层如何动态维护后端Pod列表;kube-proxy如何把Service抽象落地为实际转发规则;以及当DNS和Endpoint都正常,为什么你的gRPC客户端还是连不上——那大概率是服务发现协议本身(如gRPC的DNS解析器)和k8s DNS返回格式不兼容。我会用实测命令一步步验证每个环节,告诉你哪些参数必须调、哪些日志必须盯、哪些配置看似可选实则致命。适合刚跑通第一个Deployment的新人,也适合正在排查“明明Service状态正常却调不通”的运维和开发同学。
2. 服务发现的四层架构:从域名解析到数据包转发
k8s的服务发现不是单点技术,而是由四个逻辑层紧密咬合构成的流水线。跳过任一层去排查问题,就像修车时不查油路只换火花塞——症状压住了,病根还在。下面这张表先给你一个全局视图,后面每层都会展开实操验证:
| 层级 | 核心组件 | 关键作用 | 典型故障现象 | 快速验证命令 |
|---|---|---|---|---|
| DNS层 | CoreDNS / kube-dns | 将服务名(如redis.default.svc.cluster.local)解析为ClusterIP或Pod IP | nslookup redis返回空或错误IP;dig看到NXDOMAIN | kubectl exec -it busybox -- nslookup redis.default.svc.cluster.local |
| Endpoint层 | Endpoint / EndpointSlice Controller | 监听Pod变化,动态更新Service关联的后端Pod列表 | kubectl get endpoints redis显示<none>;新Pod启动后Endpoint迟迟不更新 | kubectl get endpointslice -l kubernetes.io/service-name=redis |
| 代理层 | kube-proxy (iptables/ipvs) | 在每个Node上生成实际转发规则,将ClusterIP流量导向后端Pod | curl <ClusterIP>:6379超时;iptables -t nat -L KUBE-SERVICES找不到对应链 | kubectl get configmap -n kube-system kube-proxy -o yaml | grep mode |
| 客户端层 | 应用内DNS解析器、SDK内置服务发现逻辑 | 应用代码如何消费DNS结果,是否支持SRV记录、健康检查、重试 | gRPC客户端连接失败;Spring Cloud Kubernetes无法感知Pod剔除 | kubectl exec -it app-pod -- cat /etc/resolv.conf |
2.1 DNS层:服务名到IP的第一次“翻译”,也是最容易被忽略的起点
很多人以为“Service创建了,DNS就自动好了”,其实不然。k8s的DNS服务(默认CoreDNS)需要满足三个硬性前提才能正确解析服务名:
域名格式必须完整:
<service-name>.<namespace>.svc.cluster.local是标准格式。省略.svc.cluster.local(如只写redis)依赖于/etc/resolv.conf里的search域配置,而这个配置在不同镜像里差异极大。Alpine镜像默认不带search,BusyBox镜像甚至可能没有resolv.conf。我见过最典型的案例:一个Python Flask应用用requests.get("http://redis:6379")本地测试全通,一上k8s就报Name or service not known——因为它的基础镜像是Alpine,/etc/resolv.conf里压根没写search default.svc.cluster.local svc.cluster.local cluster.local。CoreDNS必须健康且配置正确:CoreDNS不是开箱即用的“黑盒”。它的ConfigMap(
corednsinkube-system)里forward插件指向的上游DNS服务器,如果配置成公司内网DNS但该DNS无法解析cluster.local域,就会导致所有服务名解析失败。更隐蔽的是cache插件:默认开启,TTL设为30秒,这意味着你刚删掉一个Pod,Endpoint更新了,但DNS缓存还没过期,客户端仍会拿到已销毁Pod的旧IP,造成连接拒绝。Pod的DNS策略必须匹配场景:
dnsPolicy有四种取值:ClusterFirst(默认):优先走CoreDNS,查不到再转上游;Default:直接继承Node的/etc/resolv.conf,完全绕过CoreDNS;ClusterFirstWithHostNet:用于hostNetwork模式的Pod;None:完全自定义DNS配置。
提示:如果你的应用Pod用了
hostNetwork: true但dnsPolicy没显式设为ClusterFirstWithHostNet,它会默认走ClusterFirst,而hostNetwork Pod的DNS请求会直接发到Node的8053端口(CoreDNS监听端口),但Node上可能没开这个端口或防火墙拦截,导致解析超时。这是生产环境高频踩坑点。
实操验证DNS是否真正常?别只信kubectl get service显示的CLUSTER-IP。进一个Pod里手动测:
# 启动一个带dig工具的临时Pod kubectl run dns-test --image=infoblox/dnstools:latest -it --rm --restart=Never -- sh # 在容器内执行 # 1. 查看DNS配置 cat /etc/resolv.conf # 2. 解析服务名(必须带全路径!) nslookup redis.default.svc.cluster.local # 3. 深度查看DNS响应细节(看TTL、权威回答) dig redis.default.svc.cluster.local +short # 4. 强制绕过缓存(加@指定CoreDNS IP) dig @10.96.0.10 redis.default.svc.cluster.local +short如果dig返回空,但nslookup能返回IP,说明/etc/resolv.conf里的search域生效了,但DNS服务器本身对全路径查询支持不全——这往往指向CoreDNS ConfigMap里kubernetes插件的pods insecure或fallthrough配置有问题。
2.2 Endpoint层:服务发现的“活体清单”,动态性才是它的灵魂
如果说DNS是服务发现的“门牌号”,Endpoint就是实时更新的“住户名单”。Service对象本身不存任何后端信息,它只是一个抽象标识;真正的后端Pod列表,由独立的Endpoint(或EndpointSlice)对象维护。这个设计是k8s服务发现高可用的核心:即使API Server短暂不可用,Endpoint Controller停止同步,只要已有Endpoint存在,kube-proxy就能继续转发流量。
Endpoint的生成逻辑非常明确:Controller通过Label Selector匹配Pod,把匹配到的Pod IP和Port写入Endpoint对象。关键在于“匹配”二字。常见误区是认为Service的selector和Pod的labels只要“看起来一样”就行。实际上,k8s的Label匹配是严格字符串相等,且区分大小写。比如Service写app: Redis,而Pod标签是app: redis,Endpoint就永远为空。
更隐蔽的是Topology Aware Hints(拓扑感知提示)。从k8s 1.21起,EndpointSlice Controller会尝试在EndpointSlice里添加topology.kubernetes.io/zone等标签,提示客户端优先选择同可用区的后端。但这需要两个条件同时满足:1)Node打了topology.kubernetes.io/zone=us-west-2a这类标签;2)Service的topologyMode字段设为Auto或BestEffort。如果只打标签不设topologyMode,EndpointSlice里就不会出现拓扑信息,跨AZ流量无法优化。
验证Endpoint是否健康,不能只看kubectl get endpoints:
# 查看Endpoint详情(注意AGE列,新创建的Service可能要等几秒才生成) kubectl get endpoints redis -o wide # 查看底层EndpointSlice(k8s 1.21+默认启用,比老Endpoint更高效) kubectl get endpointslice -l kubernetes.io/service-name=redis # 检查EndpointSlice里的endpoints字段,确认podNames和ip是否匹配你预期的Pod kubectl get endpointslice redis-xxxxx -o yaml \| grep -A 5 "endpoints:" # 如果发现endpoint数量远少于Pod数量,立刻检查Pod的label和Service selector是否完全一致 kubectl get pods -l app=redis --show-labels kubectl get service redis -o yaml \| grep -A 5 selector注意:当你用
kubectl scale扩缩容Pod时,Endpoint Controller的反应速度取决于--min-resync-period参数(默认12小时),但实际增量更新是秒级的。不过,如果遇到Endpoint长时间不更新,第一反应不是等,而是检查Controller Manager日志:kubectl logs -n kube-system kube-controller-manager | grep -i "endpoint" | tail -20,常能看到failed to update endpoint这类错误,根源往往是RBAC权限不足或etcd写入失败。
2.3 代理层:kube-proxy如何把“虚”的Service变成“实”的iptables规则
Service的ClusterIP是一个虚拟IP,它本身不绑定任何网卡,也不响应ARP请求。真正让这个IP“活”起来的,是运行在每个Node上的kube-proxy。它监听Service和Endpoint变化,然后在本机生成具体的网络规则。目前主流模式是iptables和ipvs,userspace模式已被弃用。
iptables模式原理简单粗暴:为每个Service生成一条KUBE-SERVICES链规则,匹配目标IP+Port后,跳转到KUBE-SVC-XXXXX链;再为每个后端Endpoint生成KUBE-SEP-YYYYY链,做DNAT(目标地址转换)把流量导向真实Pod IP。整个过程全是iptables规则,好处是兼容性好,坏处是规则数量随Service和Endpoint线性增长,节点上Service超2000个时,iptables-restore耗时可能达分钟级,导致服务变更延迟。
ipvs模式更先进:它把Service看作一个虚拟服务器(Virtual Server),后端Pod是Real Server,用Linux内核的IPVS模块做负载均衡。优势明显:1)性能更高,连接跟踪开销小;2)支持更多调度算法(rr, wrr, lc, dh等);3)健康检查更主动(可配置TCP探针)。但代价是:1)需要Node内核支持IPVS模块(lsmod | grep ip_vs);2)ipset工具必须存在;3)某些CNI插件(如早期Flannel)与IPVS的masquerade行为有冲突。
如何确认你的集群用的是哪种模式?看kube-proxy ConfigMap:
kubectl get configmap -n kube-system kube-proxy -o yaml \| grep mode # 输出 mode: iptables 或 mode: ipvs # 如果是ipvs,再看scheduler(调度算法) kubectl get configmap -n kube-system kube-proxy -o yaml \| grep scheduler实操中,iptables模式下最常遇到的问题是规则残留。比如你删了一个Service,但kube-proxy进程卡住没及时清理规则,旧的KUBE-SVC-XXXXX链还在,导致新创建同名Service时规则冲突。此时curl <old-ClusterIP>可能意外通向一个早已不存在的Pod。解决方法很直接:重启kube-proxy:
# 找到kube-proxy的Pod kubectl get pods -n kube-system \| grep kube-proxy # 删除它,DaemonSet会自动拉起新实例 kubectl delete pod -n kube-system kube-proxy-xxxxx新实例启动后,会全量重建iptables规则,彻底清理历史垃圾。
2.4 客户端层:应用代码里的“最后一公里”,SDK决定成败
前面三层都是k8s平台的事,但服务发现的终点,永远落在应用代码里。很多开发者以为“只要DNS能解析,我的HTTP客户端就能连”,忽略了不同协议、不同语言SDK对DNS解析结果的处理逻辑天差地别。
HTTP/1.1客户端(如curl、Python requests、Java OkHttp)通常只关心A记录(IPv4地址)。它们拿到DNS返回的ClusterIP,就直接发起TCP连接。这种最简单,但也最“笨”——它完全不知道后端有多个Pod,也不知道哪个Pod健康。负载均衡全靠kube-proxy的iptables规则轮询,应用层无感知。
gRPC客户端则复杂得多。它原生支持服务发现,但默认行为是:1)解析服务名得到A记录(ClusterIP);2)不使用SRV记录(k8s CoreDNS对Headless Service才返回SRV);3)连接建立后,不主动探测后端健康状态。这就导致一个问题:如果kube-proxy把流量轮到了一个刚启动、还没ready的Pod,gRPC连接会失败,但客户端不会自动重试或切换到其他后端,除非你显式配置round_robin负载均衡器并开启健康检查。
Spring Cloud Kubernetes这类框架更进一步,它会主动监听k8s API,获取Endpoint变化,实现客户端负载均衡。但它有个致命依赖:必须能访问k8s API Server(通过ServiceAccount Token),且Token要有endpoints的list/watch权限。如果权限不足,它会静默降级为只读本地缓存,导致Pod扩缩容后,客户端长期感知不到新实例。
验证客户端行为,最有效的是抓包:
# 在客户端Pod里安装tcpdump kubectl exec -it client-pod -- apk add tcpdump # 抓取DNS查询(看是否发往CoreDNS 10.96.0.10) kubectl exec -it client-pod -- tcpdump -i any port 53 -w /tmp/dns.pcap # 抓取TCP连接(看目标IP是ClusterIP还是Pod IP) kubectl exec -it client-pod -- tcpdump -i any host <redis-cluster-ip> -w /tmp/redis.pcap用Wireshark打开pcap文件,一眼就能看出:DNS请求是否命中CoreDNS?返回的IP是ClusterIP还是Endpoint IP?TCP SYN包的目标IP是什么?这比看日志快十倍。
3. 实操:从零构建一个可验证的服务发现链路
纸上谈兵不如动手一试。下面我带你用最简步骤,搭建一个能逐层验证的服务发现环境。全程不用任何第三方工具,只用k8s原生命令,确保你在任何合规集群都能复现。
3.1 准备工作:部署一个带健康检查的Redis服务
我们选Redis,因为它是典型无状态服务,且自带INFO命令可验证连通性。创建redis-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: redis spec: replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7.0-alpine ports: - containerPort: 6379 livenessProbe: exec: command: ["redis-cli", "ping"] initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: exec: command: ["redis-cli", "ping"] initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: redis spec: selector: app: redis ports: - port: 6379 targetPort: 6379 # 关键:启用拓扑感知,为后续验证铺路 topologyKeys: ["topology.kubernetes.io/zone"]执行部署:
kubectl apply -f redis-deployment.yaml # 等待Pod全部Running且Ready kubectl get pods -l app=redis -w # 确认Service已分配ClusterIP kubectl get service redis # 记下输出的CLUSTER-IP,比如 10.96.123.453.2 验证DNS层:亲手看到域名如何变成IP
启动一个调试Pod:
kubectl run debug-pod --image=busybox:1.35 --rm -it --restart=Never -- sh在容器内执行:
# 1. 查看DNS配置(重点关注search域) / # cat /etc/resolv.conf nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local # 2. 解析全路径域名(必须!) / # nslookup redis.default.svc.cluster.local Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: redis.default.svc.cluster.local Address 1: 10.96.123.45 redis.default.svc.cluster.local # 3. 解析短域名(依赖search域) / # nslookup redis Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: redis Address 1: 10.96.123.45 redis.default.svc.cluster.local # 4. 强制查A记录(排除CNAME干扰) / # dig redis.default.svc.cluster.local A +short 10.96.123.45如果第2步失败但第3步成功,说明search域生效,但CoreDNS对全路径解析有缺陷;如果第4步返回空,基本确定CoreDNS ConfigMap里kubernetes插件配置错误。
3.3 验证Endpoint层:确认后端Pod列表实时准确
新开一个终端,持续观察Endpoint:
# 实时监控Endpoint变化 kubectl get endpoints redis -w # 在另一个终端,手动删除一个redis Pod,触发重建 kubectl delete pod -l app=redis --field-selector=status.phase=Running --limit=1 # 观察Endpoint输出:应该先显示<none>(短暂窗口),几秒后恢复3个IP再查EndpointSlice:
# 获取当前redis的EndpointSlice名字 kubectl get endpointslice -l kubernetes.io/service-name=redis # 查看详情,确认endpoints字段包含3个podNames和对应IP kubectl get endpointslice redis-xxxxx -o yaml | grep -A 10 "endpoints:"你应该看到类似:
endpoints: - addresses: - "10.244.1.15" conditions: ready: true hostname: "" topology: kubernetes.io/hostname: node-1 topology.kubernetes.io/zone: us-west-2a - addresses: - "10.244.2.22" conditions: ready: true hostname: "" topology: kubernetes.io/hostname: node-2 topology.kubernetes.io/zone: us-west-2b如果topology字段为空,检查Node标签和Service的topologyKeys是否匹配。
3.4 验证代理层:亲眼看到iptables规则如何工作
登录任意一个运行redis Pod的Node(假设是node-1):
# 查看kube-proxy模式 kubectl get configmap -n kube-system kube-proxy -o yaml | grep mode # 假设是iptables,查看对应Service的规则链 iptables -t nat -L KUBE-SERVICES | grep redis # 应该看到类似:KUBE-SVC-XXXXX tcp -- anywhere anywhere /* default/redis:6379 */ tcp dpt:6379 # 进入KUBE-SVC链,看后端分发 iptables -t nat -L KUBE-SVC-XXXXX # 应该看到3条KUBE-SEP-YYYYY规则,每条带--probability 0.33333333349 # 查看其中一个KUBE-SEP链,确认DNAT目标 iptables -t nat -L KUBE-SEP-YYYYY # 应该看到:DNAT tcp -- anywhere anywhere /* default/redis:6379 -> 10.244.1.15:6379 */ to:10.244.1.15:6379现在,在debug-pod里curlClusterIP:
/ # curl -v telnet://10.96.123.45:6379 # 理论上会返回Redis的"Connection refused"(因为telnet协议不对),但TCP连接能建通就证明iptables规则生效如果curl卡住或超时,立刻检查Node上的iptables规则是否存在、kube-proxyPod是否Running、Node间网络是否通畅(telnet 10.96.123.45 6379从其他Node测试)。
3.5 验证客户端层:用gRPC客户端暴露隐藏问题
我们用一个极简gRPC客户端(Go语言)来演示。创建client.go:
package main import ( "context" "log" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "google.golang.org/grpc/examples/helloworld/helloworld" ) func main() { // 关键:这里用服务名,不是IP conn, err := grpc.Dial("redis.default.svc.cluster.local:6379", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), // 同步阻塞连接 grpc.WithTimeout(5*time.Second), ) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() // 尝试发送一个PING(虽然协议不匹配,但能触发连接建立) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // 此处会失败,但我们要看失败原因 _, err = conn.Invoke(ctx, "/redis.Ping", nil, nil) if err != nil { log.Printf("connection failed: %v", err) // 这里会打印具体错误 } }编译并部署到集群:
# 构建镜像(Dockerfile略) docker build -t grpc-client . docker push grpc-client # 部署 kubectl run grpc-client --image=grpc-client --rm -it --restart=Never你会看到类似错误:connection refused。这不是bug,而是gRPC试图用gRPC协议连Redis,自然失败。但重点是:它成功解析了域名,并尝试连接了ClusterIP。如果这里报name resolution failed,说明DNS层断了;如果报connection timeout,说明iptables规则没生效或Pod没监听。
4. 常见问题与排查技巧实录:那些文档里不会写的真相
服务发现的问题,90%都藏在“看似正常”的表象下。下面是我过去三年在十几个生产集群里,亲手记录、反复验证的典型问题及独家排查法。这些不是理论,是血泪教训。
4.1 “DNS能解析,但curl不通”:八成是iptables规则没生效
现象:nslookup redis返回正确IP,但curl redis:6379超时或拒绝连接。
错误排查路径:
- 第一反应是查Pod日志?错。先查
kube-proxy状态。 kubectl get pods -n kube-system | grep kube-proxy—— 如果是CrashLoopBackOff,立刻kubectl logs -n kube-system kube-proxy-xxxxx,90%是bind: address already in use(端口被占)或failed to list *v1.Service(RBAC权限不足)。
正确排查路径:
- 登录客户端Pod所在Node,确认
kube-proxy进程在运行:ps aux | grep kube-proxy - 查看
kube-proxy日志最后10行:journalctl -u kube-proxy | tail -10(systemd)或kubectl logs -n kube-system kube-proxy-xxxxx | tail -10 - 最关键的一步:在客户端Pod里
cat /proc/1/cgroup,确认它是否在/kubepodscgroup下。如果不是(比如在/system.slice),说明它没被k8s CNI接管,网络完全隔离,DNS和iptables都无效。
实操心得:我曾在一个客户集群里耗时两天排查此问题。最终发现他们用
hostPath挂载了/var/run/docker.sock到Pod,导致容器运行时被Docker接管,绕过了k8s CNI。解决方案是改用emptyDir或明确指定runtimeClassName: containerd。
4.2 “Endpoint一直 ”:Label Selector的三个致命陷阱
现象:kubectl get endpoints redis始终显示<none>,但kubectl get pods -l app=redis能看到Pod。
陷阱一:Selector的key-value必须完全一致,包括空格和大小写selector: {app: "Redis"}和labels: {app: "redis"}不匹配。用kubectl get service redis -o yaml和kubectl get pods --show-labels逐字对比。
陷阱二:Pod的status.phase不是RunningEndpoint Controller只同步phase=Running且conditions[0].type=Ready为True的Pod。如果Pod卡在ContainerCreating(镜像拉取失败)或Pending(资源不足),它不会被加入Endpoint。kubectl describe pod <pod-name>看Events。
陷阱三:Service的type是ExternalNameExternalName类型的Service不生成Endpoint,它只是DNS CNAME记录。kubectl get service redis -o yaml检查spec.type。
注意:
kubectl get endpoints输出的AGE列很重要。如果AGE是<invalid>,说明Endpoint对象根本没创建,问题在Controller层面;如果AGE是5m但Pod已运行10分钟,说明Controller卡住,立刻查kube-controller-manager日志。
4.3 “新Pod上线,老客户端连不上”:DNS缓存的黑暗森林
现象:滚动更新后,部分客户端Pod仍连接旧Pod IP,导致503。
真相:不是k8s的问题,是客户端应用的DNS缓存。Java应用默认DNS缓存60秒(networkaddress.cache.ttl),Node.js的dns.lookup()默认不缓存但dns.resolve()会缓存,Golang的net.Resolver默认缓存0秒(无缓存)但很多SDK自己实现了缓存。
排查法:
- 在客户端Pod里执行
cat /etc/resolv.conf,确认options ndots:5(影响search域查找次数)和timeout:1(DNS超时1秒)是否合理。 - 用
strace抓系统调用:strace -e trace=connect,sendto,recvfrom -p $(pgrep -f "your-app") 2>&1 | grep -E "(10\.|redis)",看应用实际连接的IP是什么。 - 终极方案:在应用启动脚本里强制刷新DNS缓存。例如Java应用加JVM参数:
-Dnetworkaddress.cache.ttl=5 -Dnetworkaddress.cache.negative.ttl=1。
4.4 “Headless Service的SRV记录不返回”:CoreDNS配置的隐性开关
现象:为StatefulSet创建Headless Service(clusterIP: None),期望DNS返回SRV记录供gRPC客户端使用,但dig srv _redis._tcp.redis.default.svc.cluster.local返回空。
原因:CoreDNS的kubernetes插件默认不返回SRV记录,需显式开启。编辑corednsConfigMap:
apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health { lameduck 5s } ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa # 关键:添加这一行 srv } prometheus :9153 forward . /etc/resolv.conf cache 30 loop reload loadbalance }kubectl apply -f coredns-cm.yaml后,kubectl rollout restart deployment -n kube-system coredns。
提示:
srv指令开启后,Headless Service的DNS查询会返回SRV记录,格式为0 100 6379 redis-0.redis.default.svc.cluster.local.,其中0是priority,100是weight,6379是port。gRPC客户端需配置grpclb或dns:///前缀才能识别。
4.5 “跨命名空间调用失败”:Service的FQDN和网络策略的双重枷锁
现象:default命名空间的Pod无法访问monitoring命名空间的prometheus服务,nslookup prometheus.monitoring.svc.cluster.local能解析,但curl超时。
第一重检查:FQDN是否正确prometheus.monitoring.svc.cluster.local是对的,prometheus.monitoring是错的(缺少svc.cluster.local后缀)。
第二重检查:NetworkPolicy是否拦截
即使DNS和Endpoint都正常,NetworkPolicy可能禁止跨命名空间流量。检查:
# 查看monitoring命名空间是否有出口限制 kubectl get networkpolicy -n monitoring # 查看default命名空间是否有入口限制 kubectl get networkpolicy -n default # 临时禁用所有NetworkPolicy测试(生产慎用) kubectl delete networkpolicy -A如果禁用后通了,说明是NetworkPolicy规则问题。典型错误是podSelector写成了matchLabels: {}(匹配所有Pod),但ingress.from.namespaceSelector没配,导致默认拒绝。
5. 进阶思考:服务发现的边界在哪里?当k8s不够用时
k8s的服务发现强大,但并非万能。理解它的边界,才能在架构选型时不做无谓挣扎。
5.1 多集群场景:k8s原生服务发现的天然短板
一个Service的DNS域名redis.default.svc.cluster.local,只在本集群内有效。跨集群调用,你需要:
- 方案1:服务网格(Istio/Linkerd):通过控制平面统一管理多集群ServiceEntry,客户端用
redis.global这样的域名,Sidecar代理负责路由。优点是透明,缺点是复杂度陡增。 - 方案2:DNS联邦(ExternalDNS + 自建DNS):用ExternalDNS把Service的ClusterIP同步到公司内网DNS,配置泛域名
*.global.company.com指向各集群Ingress。简单直接,但失去健康检查能力。 - 方案3:API网关统一入口:所有跨集群调用走API网关,网关后端配置不同集群的Ingress地址。牺牲了直连性能,但管控力最强。
我的建议:中小团队首选方案2,用脚本定期同步;大型平台必须上方案1,否则运维成本指数级上升。
5.2 服务网格的“二次发现”:Sidecar如何覆盖k8s的盲区
Istio的Pilot组件会监听k8s API,把Service、Endpoint、DestinationRule等资源转换为xDS协议下发给Envoy Sidecar。这带来两个关键增强:
- 细粒度流量控制:基于Header、Query参数的路由,k8s Service做不到。
- 主动健康检查:Envoy对每个上游Endpoint做TCP/HTTP探针,失败立即从负载均衡池剔除,比k8s的
readinessProbe更及时。
但代价是:所有流量必须经过Sidecar,延迟增加0.5~2ms,且调试链路变长(client -> Sidecar-in -> app -> Sidecar-out -> upstream)。
5.3 无服务发现的未来:eBPF和内核级服务网格
Cilium等基于eBPF的CNI,正在把服务发现下沉到内核。它不依赖iptables,直接在Socket层做服务发现和负载均衡,性能提升30%,且能感知TLS SNI、HTTP Host头。但目前成熟度还不够,大规模生产环境仍以Istio+Envoy为主流。
最后分享一个小技巧:在调试服务发现时,永远先问自己三个问题:1)DNS解析的IP是什么?2)这个IP对应的iptables规则是否存在?3)规则指向的Pod IP,那个Pod是否真的在监听端口?按这个顺序查,95%的问题能在5分钟内定位。不要一上来就怀疑“是不是k8s bug”,绝大多数时候,是某个环节的配置没对齐。