本文以一个真实的生产案例为线索:https://prow-prod.mycloud.io/manual-trigger,完整还原一个外部请求从域名解析到命中 Pod 的全链路,并对比集群内访问(ClusterIP)与无头服务(Headless Service)的工作原理,帮助你建立完整的 K8s 网络脑图。
一、场景概述 我们在排查 CI 流水线时,需要调用 https://prow-prod.mycloud.io/manual-trigger 来手动触发 Prow Job。这个服务跑在 Kubernetes 集群(cluster 001)的 ci-system 命名空间里,Pod 名叫 manual-trigger。
整条链路如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 外部用户 │ ▼ DNS 解析 prow-prod.mycloud.io (CNAME → traefik.ci-system.svc.001.mycloud.io) │ ▼ A 记录 (kube2dns 自动注册) 10.49.32.12 ← CLB IPVS VIP │ ▼ 四层负载均衡 Traefik Pod :8443 (Ingress Controller) │ ▼ 七层路由 (/manual-trigger) manual-trigger Service (ClusterIP 192.168.134.249:80) │ ▼ kube-proxy iptables/ipvs manual-trigger Pod :8080
下面逐层拆解。
二、LoadBalancer Service:对外暴露的第一层 2.1 Service 类型回顾
类型
作用域
IP 来源
ClusterIP
仅集群内
集群内虚拟 IP 段
NodePort
节点端口
节点 IP + 随机端口
LoadBalancer
对外
外部负载均衡器分配 VIP
Headless
无 ClusterIP
DNS 直接指向 Pod IP
2.2 Traefik 的 LoadBalancer Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 apiVersion: v1 kind: Service metadata: name: traefik namespace: ci-system annotations: network.mycloud.io/lbprovider: clb-ipvs spec: type: LoadBalancer selector: app: traefik ports: - name: https port: 443 targetPort: 8443 - name: http port: 80 targetPort: 8081
关键注解 network.mycloud.io/lbprovider: clb-ipvs:告诉云平台的 LB Controller 使用 CLB(Cloud Load Balancer) 来分配一个 VIP。
CLB 工作原理 1 2 3 4 5 CLB Controller └── 监听 type=LoadBalancer 的 Service └── 从 VIP 池(如 10.49.32.0/24)分配一个空闲 IP └── 在物理 LVS/IPVS 节点上配置四层转发规则 └── 将 VIP 写回到 Service 的 status.loadBalancer.ingress[0].ip
执行后,Service 状态变为:
1 2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) traefik LoadBalancer 192.168.38.189 10.49.32.12 80,443,8080
10.49.32.12 就是 CLB 分配的对外 VIP。
三、DNS 链:域名如何解析到 VIP 3.1 kube2dns:集群内自动 DNS 注册 kube2dns 是云平台的一个控制器,运行在 kube-system 中,专门监听 type=LoadBalancer 的 Service,一旦 VIP 分配完成,就自动在 CorpDNS (企业内部 DNS 系统)中注册 A 记录:
1 2 3 4 # Service 上的注解(kube2dns 写入) network.mycloud.io/kube2dns: | traefik.ci-system.svc.001.mycloud.io. 3600 IN A 10.49.32.12 12.32.49.10.in-addr.arpa. 3600 IN PTR traefik.ci-system.svc.001.mycloud.io.
命名规则是固定的:<svc-name>.<namespace>.svc.<cluster-id>.mycloud.io
3.2 外部 CNAME:人工配置的友好域名 traefik.ci-system.svc.001.mycloud.io 这个地址太长,对外不友好。平台管理员在 CorpDNS 中手动创建了一条 CNAME:
1 prow-prod.mycloud.io. → CNAME → traefik.ci-system.svc.001.mycloud.io.
这样最终解析链为:
1 2 3 4 dig prow-prod.mycloud.io prow-prod.mycloud.io. 299 IN CNAME traefik.ci-system.svc.001.mycloud.io. traefik.ci-system.svc.001.mycloud.io. 299 IN A 10.49.32.12
谁负责什么 :
DNS 记录
管理方式
组件
traefik.ci-system.svc.001.mycloud.io → 10.49.32.12
自动
kube2dns
prow-prod.mycloud.io → CNAME → ...
手动
CorpDNS 管理平台
四、Ingress + Traefik:七层路由 流量到达 VIP 10.49.32.12:443 后,CLB 将其转发到 Traefik Pod 的 8443 端口。Traefik 作为 Ingress Controller ,读取集群中的 Ingress 资源做七层路由。
4.1 Ingress 资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ing namespace: ci-system annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.tls: "true" spec: rules: - http: paths: - path: /manual-trigger pathType: ImplementationSpecific backend: service: name: manual-trigger port: number: 80 - path: /hook backend: service: { name: hook , port: { number: 8888 } } - path: / backend: service: { name: deck , port: { number: 80 } }
4.2 Traefik 配置 Traefik 通过 ConfigMap 配置,指定使用 kubernetesIngress Provider:
1 2 [providers.kubernetesIngress] namespaces = ["ci-system" ]
Traefik 持续 watch ci-system 命名空间的 Ingress 对象,动态更新路由表。当请求路径为 /manual-trigger 时,路由到 manual-trigger Service 的 80 端口。
五、ClusterIP Service:集群内访问的核心 manual-trigger Service 是一个典型的 ClusterIP 类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: v1 kind: Service metadata: name: manual-trigger namespace: ci-system spec: type: ClusterIP clusterIP: 192.168 .134 .249 selector: app: manual-trigger ports: - name: http port: 80 targetPort: 8080
5.1 ClusterIP 的实现原理 ClusterIP 192.168.134.249 是一个虚拟 IP,不存在于任何网卡上 。它的实现依赖 kube-proxy :
1 2 3 4 kube-proxy(每个节点运行) └── watch API Server 中的 Service 和 Endpoints 变化 └── 在节点 iptables/ipvs 中写入转发规则: DNAT: 192.168.134.249:80 → Pod IP:8080 (轮询/随机)
集群内任意 Pod 访问 192.168.134.249:80,内核 netfilter 拦截并 DNAT 到真实 Pod IP。
5.2 集群内 DNS 解析 集群内的 Pod 通过 CoreDNS 解析 Service 名:
1 2 3 4 5 6 # 同命名空间内: curl http://manual-trigger/api # CoreDNS 解析:manual-trigger.ci-system.svc.cluster.local → 192.168.134.249 # 跨命名空间: curl http://manual-trigger.ci-system.svc.cluster.local/api
CoreDNS 的 A 记录指向的是 ClusterIP(虚拟 IP) ,而非 Pod IP。
1 2 3 4 5 6 7 8 9 10 集群内 Pod │ DNS 查询:manual-trigger.ci-system.svc.cluster.local ▼ CoreDNS (kube-dns) │ 返回 A 记录:192.168.134.249 ▼ kube-proxy iptables 拦截 │ DNAT: 192.168.134.249:80 → Pod:8080 ▼ manual-trigger Pod
六、Headless Service:直达 Pod IP 6.1 什么是 Headless Service 将 clusterIP: None 设置时,Service 变为 Headless ,不再分配虚拟 IP:
1 2 3 4 5 6 7 8 9 10 11 apiVersion: v1 kind: Service metadata: name: manual-trigger-headless namespace: ci-system spec: clusterIP: None selector: app: manual-trigger ports: - port: 8080
6.2 Headless Service DNS 解析行为 CoreDNS 对 Headless Service 的处理完全不同:
1 2 3 4 5 6 7 8 DNS 查询:manual-trigger-headless.ci-system.svc.cluster.local # 普通 ClusterIP:返回 1 条 A 记录(虚拟 IP) manual-trigger.ci-system.svc.cluster.local → 192.168.134.249 # Headless Service:直接返回所有 Pod IP manual-trigger-headless.ci-system.svc.cluster.local → 10.x.x.1 → 10.x.x.2 (多副本时)
同时,CoreDNS 还会为每个 Pod 生成独立的 DNS 记录(StatefulSet 场景尤为重要):
1 2 # Pod 级别的 DNS(格式:pod-ip.namespace.pod.cluster.local) 10-x-x-1.ci-system.pod.cluster.local → 10.x.x.1
6.3 Headless Service 的典型场景
场景
原因
StatefulSet (如 etcd、Kafka)
需要稳定的 Pod 身份标识(pod-0, pod-1…),客户端直连特定 Pod
自定义客户端负载均衡
gRPC 等长连接协议需要客户端感知所有 Pod IP
服务发现
应用自己实现故障切换,不依赖 kube-proxy
6.4 无头服务 vs 普通服务对比 1 2 3 4 5 普通 ClusterIP 访问流程: Client → DNS(返回VIP) → iptables/ipvs(DNAT) → Pod Headless Service 访问流程: Client → DNS(返回Pod IP列表) → 直连 Pod(无 kube-proxy 介入)
kube-proxy 完全不参与 Headless Service 的流量转发,减少了一次内核 NAT 开销。
七、完整对比:三种 Service 的 DNS 与流量链路 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 外部访问 (LoadBalancer) ======================== 浏览器/curl │ DNS: prow-prod.mycloud.io ▼ CorpDNS (CNAME → traefik.ci-system.svc.001.mycloud.io) │ kube2dns 注册的 A 记录 → 10.49.32.12 ▼ CLB IPVS (四层,VIP 10.49.32.12:443) ▼ Traefik Pod (七层,Ingress 路由 /manual-trigger) ▼ manual-trigger Service (ClusterIP 虚拟 IP) ▼ kube-proxy iptables DNAT ▼ manual-trigger Pod :8080 集群内访问 (ClusterIP) ====================== 集群内 Pod │ DNS: manual-trigger.ci-system.svc.cluster.local ▼ CoreDNS → 返回 ClusterIP 192.168.134.249 ▼ kube-proxy iptables DNAT ▼ manual-trigger Pod :8080 集群内访问 (Headless) ===================== 集群内 Pod │ DNS: manual-trigger-headless.ci-system.svc.cluster.local ▼ CoreDNS → 返回所有 Pod IP(如 10.x.x.1, 10.x.x.2) ▼ 客户端直连 Pod(无 kube-proxy) ▼ manual-trigger Pod :8080
八、关键组件职责总结
组件
职责
CLB / LB Controller
监听 LoadBalancer Service,从 VIP 池分配外部 IP,配置 IPVS 转发规则
kube2dns
监听 LoadBalancer Service,VIP 就绪后自动在 CorpDNS 注册 A/PTR 记录
CorpDNS
企业内部权威 DNS,存储 *.svc.<cluster>.mycloud.io 的 A 记录和手动 CNAME
CoreDNS
集群内 DNS,解析 *.svc.cluster.local;ClusterIP→虚拟IP,Headless→Pod IP
kube-proxy
每节点运行,维护 iptables/ipvs 规则,将 ClusterIP 流量 DNAT 到 Pod
Traefik
Ingress Controller,读取 Ingress 资源,做七层 HTTP 路由
Ingress 资源
声明 URL 路径到后端 Service 的映射规则(由 Traefik 消费)
九、排查 405 的经验教训 回到最初的问题:为什么浏览器访问 https://prow-prod.mycloud.io/manual-trigger 会得到 405?
整条链路是完全正常的 :
DNS 解析 ✓(CNAME → A 记录 → 10.49.32.12)
CLB 四层转发 ✓(到 Traefik)
Traefik 七层路由 ✓(/manual-trigger 规则存在)
Service 和 Pod 健康 ✓(Pod 日志显示正常心跳)
问题在于:/manual-trigger 是一个 POST-only 的 API 端点,浏览器发 GET 请求,服务器返回 405 Method Not Allowed。这是应用层行为,与 K8s 网络无关。
正确的调用方式:
1 2 3 curl -k -X POST https://prow-prod.mycloud.io/manual-trigger \ -H "Content-Type: application/json" \ -d '{"job": "my-job-name", "refs": {...}}'
十、总结
访问场景
DNS 解析者
流量路径
负载均衡
外部访问 LoadBalancer
CorpDNS + kube2dns
CLB → Ingress → Service → Pod
CLB(四层) + Ingress(七层)
集群内访问 ClusterIP
CoreDNS
kube-proxy DNAT → Pod
kube-proxy iptables/ipvs
集群内访问 Headless
CoreDNS
直连 Pod IP
客户端自行负载均衡
Kubernetes 的 Service 设计非常精妙:用统一的抽象屏蔽了底层 Pod 的变化 ,无论 Pod 漂移到哪个节点、IP 如何变化,Service 名始终有效。而 kube2dns 和 CLB 这样的平台扩展,则把这层抽象延伸到了集群外部,让外部域名与集群内 Service 无缝衔接。
深挖一:kube2dns 控制器内部机制
源码位于 controllers 仓库 pkg/controller/kube2udns/
控制器架构 kube2dns 是一个标准的 Kubernetes Controller ,基于 client-go 的 Informer + WorkQueue 模式构建:
1 2 3 4 5 6 7 8 9 10 11 12 13 API Server │ Watch Service / Endpoints / Pod 变化事件 ▼ Informer (cache + event handler) │ AddFunc / UpdateFunc / DeleteFunc ▼ WorkQueue (rate-limited) │ 去重 + 限速 ▼ reconcile loop(handler.Handle) │ 调用 go-corpDNS 客户端 ▼ CorpDNS REST API
控制器内部分为三种 Handler ,分别处理不同场景:
Handler
监听对象
触发条件
DNS 操作
lbHandler
Service (type=LoadBalancer)
status.loadBalancer.ingress 有 IP
A 记录 + PTR 记录
headlessHandler
Service (clusterIP=None) + Endpoints
Endpoints 的 Subsets 有 Pod IP
A 记录 + PTR 记录
podHandler
Pod
Pod Running,有 Pod IP
A 记录 + PTR 记录
LoadBalancer Handler 代码逻辑 以 Traefik 的 LoadBalancer Service 为例,lbHandler.updateLB 的核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 subdomain := buildDNSNameString(h.domain, "svc" , service.Namespace, service.Name) for _, ingress := range service.Status.LoadBalancer.Ingress { recA := rr.New(subdomain, ttl, rrtype.A, ingress.IP) recPTR := rr.New(rr.IPV4ToPTRDomain(ingress.IP), ttl, rrtype.PTR, subdomain) newRecs = append (newRecs, recA, recPTR) } h.udnsHelper.Update(ctx, oldRecs, newRecs) h.metaHelper.SetCachedRecs(ctx, service, newRecs)
状态管理 :kube2dns 将已注册的 DNS 记录序列化写入 Service 的 Annotation (network.mycloud.io/kube2dns),下次 reconcile 时先读取旧记录,与期望状态做 diff,再调用 CorpDNS API 增删。这避免了”先查后改”的竞态问题。
Finalizer 机制 :kube2dns 在处理 Service 时会加上 Finalizer,确保 Service 被删除前 DNS 记录已先清理,防止残留 A 记录。
Headless Handler 与 Endpoints Headless Service 的 DNS 不是注册 VIP,而是注册每一个 Endpoint(Pod IP) :
1 2 3 4 5 6 7 8 9 Service: manual-trigger-headless (clusterIP: None) └── Endpoints subsets: └── addresses: [{ip: 10.x.x.1}, {ip: 10.x.x.2}] kube2dns 生成: manual-trigger-headless.ci-system.svc.001.mycloud.io. → 10.x.x.1 manual-trigger-headless.ci-system.svc.001.mycloud.io. → 10.x.x.2 12.x.x.01.in-addr.arpa. → manual-trigger-headless.ci-system.svc.001.mycloud.io. 12.x.x.02.in-addr.arpa. → manual-trigger-headless.ci-system.svc.001.mycloud.io.
headlessHandler 同时监听 Service 和 Endpoints 两类对象(共享 namespace/name),任意一个变化都会触发 reconcile。
深挖二:go-corpDNS —— CorpDNS 的 Go 客户端
源码位于 go-udns 仓库
go-corpDNS 是与企业内部权威 DNS(CorpDNS)交互的 HTTP REST 客户端库 ,kube2dns 依赖它来完成实际的 DNS 记录写入。
CorpDNS 系统架构 1 2 3 4 5 6 7 8 9 10 11 12 13 外部请求 │ GTM (Global Traffic Manager) / \ SLC 负载均衡器 LVS 负载均衡器 / | \ / | \ UDNS-1 UDNS-2 UDNS-3 UDNS-4 UDNS-5 UDNS-6 \ | / \ | / ANSP-SLC ◄──── 同步 ────► ANSP-LVS │ Bind Master │ Zone 传输(~30s) Bind Slaves(各环境)
UDNS (CorpDNS 服务实例):每个区域 3 个实例,接收 REST API 请求
ANSP :权威 DNS 后端,SLC 与 LVS 互为 active/active,自动同步
Bind Slaves :负责将权威记录传播给生产/Corp/Dev 等各环境的解析服务器,传播延迟 ~30 秒
客户端接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type AClient interface { Update(ctx, name string , ttl uint32 , ips []string ) Result Get(ctx, name string ) GetAResult Delete(ctx, name, ip string ) Result List(ctx, zone string ) GetAllAResult } type PTRClient interface { Update(ctx, ip string , ttl uint32 , name string ) Result Get(ctx, ip string ) GetPTRResult Delete(ctx, ip, name string ) Result } type CNAMEClient interface { ... }type SRVClient interface { ... }
认证机制 go-corpDNS 支持两种认证方式:
方式
适用场景
原理
Keystone Token
人工/脚本
OpenStack Keystone /v2.0/tokens 获取临时 token,附加在 X-Auth-Token 请求头
TrustFabric
平台自动化(kube2dns)
基于服务身份证书,自动续期,无需人工操作
kube2dns 在集群内运行时使用 TrustFabric 认证,scope 为 udns,对应的 Keystone 权限角色为 CLOUD-ROLE-UnifiedDNS-USERS-WRITE。
记录写入时序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 kube2dns lbHandler.updateLB() │ ├─ 读取 Service annotation 获取 oldRecs(已注册的记录) │ ├─ 根据当前 VIP 构造 newRecs(A + PTR) │ ├─ udnsHelper.Update(ctx, oldRecs, newRecs) │ │ │ ├─ 对比 diff:需要删除的 / 需要新增的 │ │ │ ├─ go-corpDNS AClient.Update(name, ttl, ips) → POST /zones/<zone>/a/<name> │ │ │ └─ go-corpDNS PTRClient.Update(ip, ttl, name) → POST /zones/<zone>/ptr/<reverse-ip> │ └─ 将 newRecs 写回 Service annotation(状态缓存)
深挖三:CoreDNS 插件链与 cloud_ndots 优化
源码位于 coredns 仓库(定制版)
CoreDNS 插件链架构 CoreDNS 本质上是一个插件链(Plugin Chain) ,每个插件实现 Handler 接口:
1 2 3 4 5 6 7 type Handler interface { ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int , error ) Name() string }
云平台使用的 CoreDNS 典型插件链(Corefile 伪配置):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cluster.local { cloud_ndots 001.mycloud.io { # 1. ndots 问题优化(自研插件) domains ebay.com ebayc3.com } kubernetes cluster.local { # 2. 集群内 Service/Pod DNS pods verified fallthrough in-addr.arpa ip6.arpa } forward . /etc/resolv.conf { # 3. 兜底:转发给 CorpDNS except cluster.local } cache 30 # 4. 响应缓存 errors # 5. 错误日志 prometheus :9153 # 6. metrics 暴露 }
cloud_ndots 插件:解决”ndots 噩梦” Kubernetes 默认设置 Pod 的 /etc/resolv.conf ndots 为 5 ,这导致集群内 Pod 解析外部域名时会触发大量无效查询。
问题复现 :Pod 内执行 curl https://github.corp.mycloud.io,DNS 解析器会依次尝试:
1 2 3 4 5 # ndots=5,github.corp.mycloud.io 只有 3 个 dot,触发 search 补全 github.corp.mycloud.io.ci-system.svc.cluster.local. # 第1次(失败) github.corp.mycloud.io.svc.cluster.local. # 第2次(失败) github.corp.mycloud.io.cluster.local. # 第3次(失败) github.corp.mycloud.io. # 第4次(成功)
前 3 次都会打到 CoreDNS,CoreDNS 再 forward 给 CorpDNS,7 次 DNS 往返才能拿到结果 。
cloud_ndots 的优化 :该插件识别出 github.corp.mycloud.io.ci-system.svc.001.mycloud.io 这类”带了 search suffix 的外部域名”请求,直接重写查询 ,一次命中,并在响应中加入 CNAME 记录(与 autopath 类似):
1 2 3 4 5 6 7 8 9 10 # 实际查询(带 search 补全的垃圾请求) github.corp.mycloud.io.ci-system.svc.001.mycloud.io. IN A # cloud_ndots 检测到外部域名 + .svc.<cluster> 后缀 → 重写为 github.corp.mycloud.io. IN A # 响应(CNAME + 最终 A 记录,一次往返) github.corp.mycloud.io.ci-system.svc.001.mycloud.io. CNAME github.corp.mycloud.io. github.corp.mycloud.io. CNAME github-prod.glb.corp.mycloud.io. github-prod.glb.corp.mycloud.io. A 10.x.x.x
效果 :DNS 往返从 7 次减少到 1 次 ,外部域名解析延迟大幅下降。
kubernetes 插件:ClusterIP vs Headless kubernetes 插件连接 K8s API Server,对 *.svc.cluster.local 查询实现不同处理:
1 2 3 4 5 6 7 查询:manual-trigger.ci-system.svc.cluster.local (A) └── 插件查找 Service(namespace=ci-system, name=manual-trigger) └── 若 Service.Spec.ClusterIP != "None" → 返回 ClusterIP: 192.168.134.249 └── 若 Service.Spec.ClusterIP == "None"(Headless) → 查找 Endpoints,返回所有 Ready 的 Pod IP 列表 └── 若查不到 → fallthrough 到下一个插件(forward)
对于 Pod DNS (10-x-x-1.ci-system.pod.cluster.local),插件通过 pods verified 模式查找真实 Pod 并验证 IP 匹配后返回。
深挖四:cluster 001 manual-trigger 完整 DNS 数据流 以 https://prow-prod.mycloud.io/manual-trigger 为主线,完整追踪每一层 DNS 交互:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 ┌─────────────────────────────────────────────────────────────┐ │ 外部访问路径 │ └─────────────────────────────────────────────────────────────┘ ① 用户浏览器发起请求 DNS Query: prow-prod.mycloud.io (A) │ ▼ ② 企业内部递归 DNS(Corp Resolver) → 查询 CorpDNS 权威服务器 ← CNAME: prow-prod.mycloud.io → traefik.ci-system.svc.001.mycloud.io. → 继续查询 A 记录 ← A: traefik.ci-system.svc.001.mycloud.io → 10.49.32.12 │ │ (此 A 记录由 kube2dns 写入,流程见 ⑥) ▼ ③ HTTP 请求到达 CLB IPVS VIP: 10.49.32.12:443 四层转发 → Traefik Pod :8443 │ ▼ ④ Traefik 七层路由 path: /manual-trigger → Service: manual-trigger:80 │ ▼ ⑤ CoreDNS(Traefik 内部解析 manual-trigger Service) Query: manual-trigger.ci-system.svc.cluster.local kubernetes 插件 → ClusterIP: 192.168.134.249 kube-proxy DNAT → Pod IP:8080 │ ▼ manual-trigger Pod ┌─────────────────────────────────────────────────────────────┐ │ kube2dns 注册 DNS 记录(异步,VIP 就绪时触发) │ └─────────────────────────────────────────────────────────────┘ ⑥ CLB Controller 为 traefik Service 分配 VIP: 10.49.32.12 写入 service.status.loadBalancer.ingress[0].ip │ ▼ kube2dns lbHandler(watch Service 变化) 检测到 status.loadBalancer.ingress 就绪 │ ▼ buildDNSNameString(domain="001.mycloud.io", "svc", "ci-system", "traefik") → FQDN: "traefik.ci-system.svc.001.mycloud.io." │ ▼ go-corpDNS AClient.Update( name="traefik.ci-system.svc.001.mycloud.io.", ttl=3600, ips=["10.49.32.12"]) → POST https://corpDNS.vip.mycloud.io/zones/001.mycloud.io/a/... go-corpDNS PTRClient.Update( ip="10.49.32.12", ttl=3600, name="traefik.ci-system.svc.001.mycloud.io.") → POST https://corpDNS.vip.mycloud.io/zones/.../ptr/... │ ▼ CorpDNS ANSP 写入权威记录 → Bind Slaves 同步(~30s) → 全网解析生效 ┌─────────────────────────────────────────────────────────────┐ │ 集群内访问路径(ClusterIP) │ └─────────────────────────────────────────────────────────────┘ 集群内 Pod curl http://manual-trigger/api │ ▼ /etc/resolv.conf search 展开(ndots=5) → manual-trigger.ci-system.svc.cluster.local.(最终命中) │ ▼ CoreDNS(Pod 的 nameserver 指向 CoreDNS ClusterIP) cloud_ndots 插件:不是外部域名,pass through kubernetes 插件: Service ci-system/manual-trigger 存在 ClusterIP: 192.168.134.249(非 None) → 返回 A: 192.168.134.249 │ ▼ kube-proxy iptables DNAT 192.168.134.249:80 → Pod IP:8080(轮询)
DNS 记录生命周期 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Service 创建 └── CLB Controller 分配 VIP └── kube2dns 注册 CorpDNS A+PTR 记录 └── CoreDNS kubernetes 插件缓存 Service(TTL=5s) Service 更新(VIP 变更) └── kube2dns diff oldRecs vs newRecs └── 删除旧 A 记录,写入新 A 记录 └── Finalizer 确保顺序:先删 DNS,再删 Service Service 删除 └── Kubernetes 触发 Finalizer 流程 └── kube2dns deleteLB → go-corpDNS 删除 A + PTR └── 移除 Finalizer → Service 真正删除
十一、扩展总结:三层 DNS 体系 理解了源码之后,可以归纳出云平台 K8s 集群的三层 DNS 体系 :
1 2 3 4 5 6 7 8 9 10 11 12 ┌──────────────────────────────────────────────────────┐ │ 层次 │ 组件 │ 解析范围 │ ├──────────┼───────────────┼──────────────────────────── │ │ L1 │ CorpDNS │ 全局:*.mycloud.io 外部域名 │ │ (外部) │ (权威 DNS) │ 由 kube2dns 自动注册 │ ├──────────┼───────────────┼──────────────────────────── │ │ L2 │ CoreDNS │ 集群内:*.svc.cluster.local │ │ (集群) │ (kube-dns) │ kubernetes 插件实时查询 API │ ├──────────┼───────────────┼──────────────────────────── │ │ L3 │ cloud_ndots │ 跨层优化:减少 ndots 引起 │ │ (优化) │ (自研插件) │ 的无效 CorpDNS 转发请求 │ └──────────────────────────────────────────────────────┘
数据面总结 :
组件
职责
实现
go-corpDNS
CorpDNS REST 客户端
HTTP + Keystone/TrustFabric 认证
kube2dns
K8s → CorpDNS 桥接器
Informer + WorkQueue + Finalizer
CoreDNS kubernetes 插件
集群内 Service DNS
Watch API Server,ClusterIP/Headless 分支处理
cloud_ndots 插件
ndots 优化
Regexp 匹配 + 查询重写 + CNAME 注入
CorpDNS (UDNS)
企业权威 DNS
多区域 ANSP active/active + Bind Slaves 传播