Kubernetes DNS 与 Service 流量链路深度剖析——从真实案例看外部访问与集群内访问

本文以一个真实的生产案例为线索: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 # 声明使用 CLB IPVS 分配 VIP
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 # 关键:无 ClusterIP
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?

整条链路是完全正常的

  1. DNS 解析 ✓(CNAME → A 记录 → 10.49.32.12)
  2. CLB 四层转发 ✓(到 Traefik)
  3. Traefik 七层路由 ✓(/manual-trigger 规则存在)
  4. 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 无缝衔接。