Kubernetes 网络核心:kube-proxy 与 CoreDNS 深度解析
Kubernetes 网络核心:kube-proxy 与 CoreDNS 深度解析
引言:Kubernetes 网络面临的挑战
在 Kubernetes (K8s) 集群中,应用程序以 Pod 的形式运行。Pod 是短暂的,它们的 IP 地址会随着创建、销毁和调度而动态变化。这给服务间的通信带来了挑战:客户端如何才能可靠地发现并连接到提供特定服务的 Pod 集合?
为了解决这个问题,Kubernetes 引入了 Service
抽象。Service 提供了一个稳定的虚拟 IP (ClusterIP) 和端口,作为访问一组后端 Pod 的统一入口。然而,Service 本身只是一个 API 对象,需要有组件将这个抽象概念转化为实际的网络规则,实现服务发现和负载均衡。
本文将深入探讨 Kubernetes 网络体系中两个至关重要的组件:kube-proxy
和 CoreDNS
,它们分别负责实现 Service 的数据平面转发和基于名称的服务发现。同时,我们也将深入剖析 kube-proxy
iptables 模式所依赖的 Linux 内核 Netfilter
框架。
kube-proxy:Service 的智能流量调度器
kube-proxy
是运行在 Kubernetes 每个 Node 上的网络代理和负载均衡器,通常以 DaemonSet 形式部署。它的核心职责是监视(watch)API Server 上 Service
和 Endpoints
(或 EndpointSlice
) 对象的变化,并将这些变化转化为节点本地的网络规则,确保发送到 Service IP 的流量能够被正确地路由和负载均衡到后端健康的 Pod 上。
kube-proxy
的发展经历了几个模式的演进,本质上是不断追求更高性能、更低延迟和更好大规模集群适应性的过程。
演进之路:从 Userspace 到 IPVS
1. Userspace 模式(已废弃):初代方案的性能困局
最早期的模式。kube-proxy
在用户空间监听一个端口,通过 iptables
将 Service 流量重定向到这个端口。kube-proxy
进程内部维护 Pod 列表并进行轮询负载均衡,然后与选中的 Pod 建立新的连接。
- 工作流程: Client -> ServiceIP:Port -> (iptables DNAT) -> NodeIP:ProxyPort -> kube-proxy process -> (Round Robin) -> PodIP:TargetPort
- 核心缺陷:
- 内核态/用户态切换开销大: 数据包路径长 (内核 -> 用户 -> 内核)。
- 内存拷贝: 数据需要在内核和用户空间之间拷贝。
- 单点瓶颈: 所有流量经过单个
kube-proxy
进程。
- 性能影响: 吞吐量低,延迟高,仅适用于小型测试环境。
2. Iptables 模式:基于 Netfilter 的内核级转发
这是长期以来的默认模式,也是理解 Kubernetes 网络规则的关键。kube-proxy
不再直接处理数据包,而是将 Service 和 Endpoints 信息翻译成大量的 iptables
规则,利用 Linux 内核的 Netfilter 框架在内核空间直接处理流量转发和负载均衡。
- 工作原理概述: 通过在 Netfilter 的特定钩子点(Hooks)上创建
iptables
规则链(Chains),实现 DNAT (目标地址转换) 和简单的负载均衡 (通常是随机或轮询)。 - 核心优势: 相比 Userspace 模式,性能大幅提升,因为转发完全在内核态完成。
- 面临挑战:
- 规则数量爆炸: 每个 Service 和 Endpoint 都可能生成多条
iptables
规则。大规模集群(成千上万 Service/Endpoint)下,规则数量可达数万甚至数十万条。 - 性能随规模下降:
iptables
规则匹配是线性查找,规则越多,匹配延迟越高,CPU 消耗越大。 - 更新效率低: Service/Endpoint 变更时,
kube-proxy
需要更新iptables
规则。全量刷新大量规则可能非常耗时(分钟级),导致服务更新延迟。 - 连接跟踪 (Conntrack) 压力: NAT 操作依赖 conntrack 表记录连接状态,大量连接会消耗较多内存,极端情况可能导致 conntrack 表满而丢包。
- 规则数量爆炸: 每个 Service 和 Endpoint 都可能生成多条
为了深入理解 iptables 模式,我们必须先了解其底层的 Netfilter 框架。
[深度解析] Netfilter 框架:Linux 网络数据包处理核心

Netfilter 是 Linux 内核中一个强大而灵活的网络数据包处理框架。它不仅仅是防火墙的基础,更是许多高级网络功能(如 NAT、数据包修改、连接跟踪)的核心。理解 Netfilter 对于掌握 kube-proxy
的 iptables 模式至关重要。
Iptables 与 Netfilter 的关系:用户空间工具与内核框架

- Netfilter: 内核空间框架,在网络协议栈的关键路径上提供钩子 (Hooks)。内核模块可以在这些钩子上注册处理函数,对流经的数据包进行操作。
- iptables: 用户空间命令行工具,用于定义规则。这些规则被
iptables
工具加载到内核中,由 Netfilter 框架在相应的钩子上执行。
简单说:iptables
定义策略,Netfilter
提供执行机制。
Netfilter Hooks:内核网络栈的关键切入点

Netfilter 为 IPv4 定义了 5 个核心钩子点,分布在数据包处理流程的关键位置:
- NF_IP_PRE_ROUTING: 数据包进入网络栈后,路由决策之前。是执行 DNAT (目的地址转换,如 Service ClusterIP -> Pod IP) 的主要位置。
- NF_IP_LOCAL_IN: 路由决策确定数据包目的地是本机后,传递给上层协议 (TCP/UDP) 之前。用于过滤访问本机服务的数据包 (Filter 表 INPUT 链)。
- NF_IP_FORWARD: 路由决策确定数据包需要转发给其他主机时,实际转发之前。用于过滤转发的数据包 (Filter 表 FORWARD 链)。
- NF_IP_LOCAL_OUT: 本机进程发出的数据包,进入网络栈,路由决策之前。用于过滤本机发出的数据包 (Filter 表 OUTPUT 链),也是处理本机访问 Service ClusterIP 进行 DNAT 的地方。
- NF_IP_POST_ROUTING: 数据包即将离开本机发送到网络接口之前 (无论是本机发出还是转发)。是执行 SNAT (源地址转换,如 Pod IP -> Node IP / Masquerade) 的主要位置。

数据包流经不同 Hook 点的示意图
Hooks、Tables 与 Chains:规则的组织结构
iptables
使用 表 (Table) 和 链 (Chain) 来组织规则。
Tables (表): 代表不同的处理逻辑类型。主要有:
raw
: 优先级最高,用于标记数据包跳过连接跟踪 (NOTRACK)。mangle
: 用于修改 IP 头字段 (如 TOS, TTL, MARK)。nat
: 用于网络地址转换 (DNAT, SNAT)。kube-proxy iptables 模式的核心。filter
: 默认表,用于数据包过滤 (允许/拒绝)。security
: 用于与 SELinux 等安全模块集成。
Chains (链): 表内部规则的有序列表。
- 内建链 (Built-in Chains): 直接与 Netfilter Hooks 关联 (如
PREROUTING
,INPUT
,FORWARD
,OUTPUT
,POSTROUTING
)。 - 自定义链 (User-defined Chains): 用户创建,可以从内建链或其他自定义链跳转过来,用于组织复杂规则 (如
kube-proxy
创建的KUBE-SERVICES
,KUBE-SVC-XXX
等)。
- 内建链 (Built-in Chains): 直接与 Netfilter Hooks 关联 (如
数据包处理流程与 Table/Chain 关系:

图片展示了数据包流经不同 Hook 点时,会依次经过哪些 Table 的哪些 Chain。
- 入口 (PRE_ROUTING Hook):
raw
->mangle
->nat
(DNAT) - 本机接收 (LOCAL_IN Hook):
mangle
->filter
(INPUT) ->security
->nat
(少用) - 转发 (FORWARD Hook):
mangle
->filter
(FORWARD) ->security
- 本机发出 (LOCAL_OUT Hook):
raw
->mangle
->nat
(DNAT/SNAT) ->filter
(OUTPUT) ->security
- 出口 (POST_ROUTING Hook):
mangle
->nat
(SNAT)
从 Linux IP 协议栈深入理解 Netfilter

简化的 Linux IP 协议栈数据包接收路径示意图
数据包接收流程概述
硬件接收与中断: 当网卡(NIC)接收到一个目的 MAC 地址匹配本机或为广播/多播地址的以太网帧时,它会将数据通过 DMA (Direct Memory Access) 传输到内存中的预分配缓冲区(Ring Buffer)。传输完成后,网卡会向 CPU 发出一个硬件中断信号。
中断处理程序 (ISR - Interrupt Service Routine): CPU 响应该中断,暂停当前任务,跳转执行该网卡驱动注册的中断处理程序。ISR 的主要工作是快速响应硬件,通常会:
- 禁用网卡中断(防止中断风暴)。
- 分配一个内核数据结构
sk_buff
(Socket Buffer) 来表示这个数据包。sk_buff
是 Linux 网络栈中表示网络数据包的核心结构,包含了数据本身以及大量的元数据(如协议类型、接口信息、时间戳、路由结果等)。 - 调用网卡驱动的特定函数,将 DMA 缓冲区中的数据拷贝到
sk_buff
中,并更新 Ring Buffer 的状态。 - 调用与协议无关的网络设备接收函数
netif_rx()
或其变体(如napi_gro_receive()
)。
netif_rx()
与 NAPI:netif_rx()
将sk_buff
添加到 CPU 的 backlog 队列,并触发一个NET_RX_SOFTIRQ
软中断。为了提高性能并避免中断风暴,现代驱动普遍使用 NAPI (New API)。在 NAPI 模式下,ISR 只需禁用中断并触发软中断,实际的数据包处理(分配sk_buff
、拷贝数据)被推迟到软中断上下文中,由napi_poll()
函数批量处理。软中断处理 (
NET_RX_SOFTIRQ
): 内核调度器在适当的时候(通常是中断返回或内核线程调度时)会检查并执行挂起的软中断。ksoftirqd
内核线程也会在系统负载较高时帮助处理软中断。处理NET_RX_SOFTIRQ
的核心函数是net_rx_action()
。它会从 backlog 队列或 NAPI 的 poll 列表中取出sk_buff
,然后根据sk_buff->protocol
字段(由驱动根据以太网帧类型设置)将其分发给相应的 L3 协议处理函数。例如,IP 包会交给ip_rcv()
处理,ARP 包交给arp_rcv()
处理。
IPv4 数据包处理与 Netfilter Hooks

IP 层处理流程与 Netfilter Hooks 的嵌入点
ip_rcv()
- 初始处理与NF_IP_PRE_ROUTING
:ip_rcv()
函数是 IP 层处理接收到的数据包的入口。它首先会对 IP 头部进行基本的验证,如版本号、头部长度、总长度、校验和等。- 如果验证通过,紧接着,数据包会经过
NF_IP_PRE_ROUTING
钩子。所有注册在此钩子上的Netfilter
处理函数(来自raw
,mangle
,nat
表)会依次执行。这是进行 DNAT 或早期过滤/修改的关键点。 - 如果
Netfilter
函数返回NF_DROP
,数据包处理流程终止。如果返回NF_ACCEPT
,则继续。
ip_rcv_finish()
- 路由决策:- 通过
NF_IP_PRE_ROUTING
钩子后,数据包进入ip_rcv_finish()
。此函数的核心任务是进行路由查找,决定数据包的下一跳。它会调用ip_route_input_slow()
(或其快速路径缓存)来查询路由表。 - 路由查找的结果会填充到
skb->dst
(destination cache entry) 结构中,该结构包含了路由决策的全部信息,包括下一跳地址、输出设备、以及指向后续处理函数的指针(如dst->input
和dst->output
)。
- 通过
根据路由结果分发:
- 目的为本机 (
dst->input == ip_local_deliver
): 如果路由查找确定数据包的目的 IP 是本机的某个地址,dst_input(skb)
最终会调用ip_local_deliver()
。ip_local_deliver()
与NF_IP_LOCAL_IN
: 在将数据包传递给更上层的协议(如 TCP 的tcp_v4_rcv
或 UDP 的udp_rcv
)之前,ip_local_deliver()
会首先调用ip_local_deliver_finish()
,在这里会触发NF_IP_LOCAL_IN
钩子。注册在此钩子上的Netfilter
处理函数(来自mangle
,filter
,security
,nat
表)会被执行,主要用于对访问本机服务的数据包进行过滤。
- 需要转发 (
dst->input == ip_forward
): 如果路由查找确定数据包需要被转发到另一个网络接口,dst_input(skb)
最终会调用ip_forward()
。ip_forward()
与NF_IP_FORWARD
:ip_forward()
函数负责处理数据包的转发逻辑。在进行必要的检查(如 TTL 检查)之后,它会调用ip_forward_finish()
,在这里会触发NF_IP_FORWARD
钩子。注册在此钩子上的Netfilter
处理函数(来自mangle
,filter
,security
表)会被执行,用于对转发的数据包进行过滤和修改。- TTL 递减与 MTU 处理: 在
ip_forward()
过程中,IP 头部的 TTL 值会被减 1。如果 TTL 变为 0,数据包会被丢弃,并可能发送 ICMP Time Exceeded 消息。如果数据包大小超过了出口设备的 MTU 且允许分片,还会进行 IP 分片。
- 多播处理 (
dst->input == ip_mr_input
): 如果是多播数据包且本机配置了多播路由,会进入多播转发流程。
- 目的为本机 (
数据包发送路径与
NF_IP_LOCAL_OUT
和NF_IP_POST_ROUTING
:- 本地产生的数据包 (
ip_queue_xmit
): 当本地应用程序通过 Socket API 发送数据时,数据会逐层向下传递(例如 TCP -> IP)。在 IP 层,ip_queue_xmit()
或类似函数负责构建 IP 头部并准备发送。NF_IP_LOCAL_OUT
: 在ip_queue_xmit()
内部,构建完 IP 头并进行初步路由查找(确定源地址和出口设备等)之后,会触发NF_IP_LOCAL_OUT
钩子。注册在此钩子上的Netfilter
处理函数(来自raw
,mangle
,nat
,filter
,security
表)会被执行,用于对本地产生的数据包进行处理。
- 最终路由与
NF_IP_POST_ROUTING
: 无论是本地产生的包还是需要转发的包,在最终确定所有 IP 头部字段(特别是经过了可能的 NAT 修改后)、选定出口网络设备,即将调用邻居子系统(ARP 或 NDP)解析下一跳 MAC 地址并将数据包传递给设备驱动程序之前,都会通过ip_output()
(单播)或ip_mc_output()
(多播)等函数,最终触发NF_IP_POST_ROUTING
钩子。注册在此钩子上的Netfilter
处理函数(来自mangle
,nat
表)会被执行。这是执行 SNAT 或进行最后修改的理想位置。 dst_output()
: 最终,dst_output(skb)
函数会调用skb->dst->output
指针指向的函数(如ip_output
),它会调用ip_finish_output()
,后者将sk_buff
交给邻居子系统(neigh_output
)和网络设备驱动进行 L2 封装和物理发送。
- 本地产生的数据包 (

TCP 层发送路径示意图,显示了数据包向下传递至 IP 层的过程,最终也会触发 Netfilter 的 OUT/POSTROUTING 钩子。
Netfilter Hook 函数的注册与实现细节
Netfilter
框架的核心在于允许内核模块动态地注册和注销钩子处理函数。这使得内核的功能可以灵活扩展,而无需修改核心代码。
注册和注销 Netfilter Hook
注册一个钩子处理函数主要依赖于 struct nf_hook_ops
结构和 nf_register_net_hook()
/ nf_unregister_net_hook()
(或者针对特定网络命名空间的版本 nf_register_hook
/ nf_unregister_hook
) 函数。
struct nf_hook_ops
结构定义在 <linux/netfilter.h>
中,其关键成员如下:
1 |
|
list
:此成员由Netfilter框架内部管理,用于将注册的nf_hook_ops
组织成链表。对于在同一协议族(pf
)和同一挂接点(hooknum
)注册的多个hook函数,内核正是通过遍历这个链表来依次调用它们的。模块开发者在注册时无需关心此字段。hook
:这是一个函数指针,指向类型为nf_hookfn
的函数。这正是你的模块提供的核心处理逻辑,当匹配的网络数据包经过指定的hooknum
时,内核将调用此函数。nf_hookfn
的函数原型我们稍后会详细解析。owner
:通常设置为THIS_MODULE
宏,用于内核的模块引用计数管理。当模块被卸载时,内核可以通过这个指针自动注销其注册的hooks,防止出现悬挂指针导致系统崩溃。虽然示例代码中设置为NULL
,但在生产级代码中,正确设置owner
是保证系统稳定性的重要实践。pf
:指定此hook函数适用的协议族(Protocol Family)。常见的协议族定义在linux/socket.h
中,例如PF_INET
代表IPv4协议栈,PF_INET6
代表IPv6,PF_BRIDGE
用于网桥等。你的hook函数只会处理属于指定协议族的数据包。hooknum
:这明确了你的hook函数要挂载到Netfilter处理流程中的哪个具体位置。对于IPv4 (PF_INET
),这些挂接点(如NF_INET_PRE_ROUTING
,NF_INET_LOCAL_IN
,NF_INET_FORWARD
,NF_INET_LOCAL_OUT
,NF_INET_POST_ROUTING
)定义在linux/netfilter_ipv4.h
中,它们对应了数据包在内核中处理的不同阶段。priority
:定义了在同一挂接点(hooknum
)上注册的多个hook函数之间的执行优先级。优先级是一个整数,数值越小,优先级越高,越先被执行。内核提供了一系列预定义的优先级常量,例如NF_IP_PRI_FIRST
(最高优先级)、NF_IP_PRI_CONNTRACK
、NF_IP_PRI_NAT_DST
、NF_IP_PRI_FILTER
、NF_IP_PRI_NAT_SRC
、NF_IP_PRI_LAST
(最低优先级)等,定义在linux/netfilter_ipv4.h
的nf_ip_hook_priorities
枚举中。选择合适的优先级对于确保你的hook函数在正确的时机(例如,在NAT转换之前或之后)执行至关重要。
要将你定义的nf_hook_ops
结构体实例注册到Netfilter框架中,你需要调用nf_register_net_hook()
函数(或者在较新内核中推荐使用针对特定netns的nf_register_net_hook()
,如果你的模块需要感知网络命名空间的话;对于简单的全局hook,nf_register_hook()
是一个历史接口,现在通常封装了nf_register_net_hook(&init_net, ops)
)。此函数接受一个指向nf_hook_ops
结构体的指针作为参数。注册成功,你的hook函数就会成为内核网络处理流程的一部分。相应地,当你的内核模块卸载时,必须调用nf_unregister_net_hook()
(或nf_unregister_hook()
)并传入相同的nf_hook_ops
结构体指针,以将其从Netfilter框架中移除,释放资源并避免潜在的错误。
下面的示例代码演示了这一注册与注销过程。它注册了一个简单的hook函数,挂载在IPv4协议栈的NF_INET_LOCAL_OUT
挂接点(即本机进程发出的数据包在路由决策之后、发送到网络接口之前的位置),并赋予其最高优先级(NF_IP_PRI_FIRST
)。该hook函数的实现极为简单粗暴:直接返回NF_DROP
,这意味着所有经由此挂接点的IPv4数据包都将被丢弃。
1 |
|
(Note: The provided example code was slightly adjusted for better practice (logging, THIS_MODULE
, modern registration API), but kept the core logic as requested. The original prototype of the hook function is also mentioned for context.)
Hook函数的实现细节
理解nf_hookfn
的原型对于编写有效的Netfilter hook至关重要。虽然原型在不同内核版本中略有演变,但核心传递的信息保持一致。我们来看一个常见的(略旧但更易于解释基础概念的)原型,定义在linux/netfilter.h
:
1 |
|
(较新内核倾向于使用 nf_hookfn(void \*priv, struct sk_buff \*skb, const struct nf_hook_state \*state)
,其中state
结构体封装了hooknum
, in
, out
, sk
等信息,priv
通常指向注册时nf_hook_ops
结构体本身)
让我们解析这个原型中的参数:
hooknum
: 这个无符号整数明确告知hook函数当前是在哪个Netfilter挂接点被调用的(例如NF_INET_PRE_ROUTING
)。这使得一个函数可以服务于多个挂接点,并根据hooknum
执行不同的逻辑。skb
: 这是指向struct sk_buff
的指针,是Linux内核中表示网络数据包的核心数据结构。sk_buff
(Socket Buffer)不仅包含数据包的原始负载,还携带了大量的元数据,如协议头指针、路由信息、时间戳、关联的socket等。访问和操作skb
是Netfilter hook函数进行包检查和修改的基础。in
: 指向struct net_device
的指针,代表数据包进入系统的网络接口。这个参数仅在数据包是接收路径上的特定挂接点上(如NF_INET_PRE_ROUTING
,NF_INET_LOCAL_IN
)才有意义,此时它指向接收该数据包的物理或虚拟网络设备。在其他挂接点(如NF_INET_LOCAL_OUT
,NF_INET_POST_ROUTING
),in
通常为NULL
。out
: 同样是指向struct net_device
的指针,代表数据包计划离开系统的网络接口。这个参数仅在数据包是发送路径上的特定挂接点上(如NF_INET_LOCAL_OUT
,NF_INET_POST_ROUTING
)才有意义,指向数据包将被发送出去的网络设备。在接收相关的挂接点,out
通常为NULL
。理解in
和out
的有效性取决于hooknum
是至关重要的。okfn
: 这是一个函数指针,原型为int (*okfn)(struct sk_buff *)
。在某些复杂的Netfilter场景(尤其是在旧版本或特定子系统中),它指向下一个应该处理该数据包的函数或决策点。然而,在大多数现代的、简单的过滤场景下,hook函数的行为主要由其返回值(如NF_ACCEPT
,NF_DROP
)决定,okfn
的使用并不普遍,可以直接忽略。
Netfilter 报文过滤技术实现
Netfilter 构成了 Linux 内核网络栈的核心部分,它提供了一套强大的钩子(Hooks)机制,允许内核模块在数据包流经网络协议栈的关键路径点上注册回调函数,从而实现对数据包的检查、修改、丢弃或重新注入等操作。报文过滤是 Netfilter 最为经典和广泛的应用之一,诸如 iptables、nftables 等用户空间工具正是基于 Netfilter 框架来实现防火墙功能的。接下来,我们将深入探讨几种基于 Netfilter 实现报文过滤的具体技术途径。
基于网络接口的过滤
在网络数据包的处理流程中,识别数据包的来源或目的地网络接口是一项基本的过滤需求。Linux 内核中使用 struct net_device
结构来抽象表示一个网络接口(如 eth0, lo 等)。每个流经网络栈的数据包都由一个 struct sk_buff
(skb) 结构体表示,该结构体内部包含了指向相关网络设备的指针,通常是 skb->dev
,它指向接收或发送该数据包的网络设备。通过在 Netfilter 钩子函数中访问这个 skb->dev
指针,并进一步读取其 name
成员(包含接口名称),就可以实现基于接口的过滤策略。例如,如果我们的策略是阻止所有进入 “eth0” 接口的数据包,那么在钩子函数中,当检测到 skb->dev->name
与 “eth0” 匹配时,函数直接返回 NF_DROP
即可。这个返回值会通知 Netfilter 框架立即丢弃该数据包,并释放相关的 sk_buff
资源,数据包将不再继续在网络栈中传递。
基于 IP 地址的过滤
基于源或目的 IP 地址进行过滤是防火墙最核心的功能之一。Netfilter 同样为此提供了便捷的实现方式。sk_buff
结构体中包含了指向各层协议头部的指针。网络层(IP层)的头部指针可以通过 skb->network_header
访问,或者更常用的方式是使用内核提供的辅助宏 ip_hdr(skb)
来获取指向 struct iphdr
(定义于 <linux/ip.h>
)的指针。这个结构体详细定义了 IPv4 报头的所有字段,包括源 IP 地址 (saddr
) 和目的 IP 地址 (daddr
)。需要注意的是,这些地址在内存中是以网络字节序(Big Endian)存储的。在 Netfilter 钩子函数中,我们可以提取出这些地址字段,并将其与过滤规则中定义的特定 IP 地址或地址范围进行比较(比较时通常需要使用 ntohl()
等函数将网络字节序转换为主机字节序,或者直接以网络字节序进行比较)。如果数据包的源地址或目的地址满足了我们设定的丢弃条件(例如,阻止来自某个特定恶意 IP 的所有连接请求),钩子函数便返回 NF_DROP
,从而实现对该数据包的精确过滤。
基于 TCP 端口的过滤
当需要进行更细粒度的控制,例如阻止对特定服务端口的访问时,就需要深入到传输层进行过滤。对于 TCP 协议而言,这意味着我们需要检查 TCP 头部中的源端口和目的端口。在 Netfilter 钩子函数中,这通常发生在确认了数据包是 IP 包(通过检查 iphdr
)并且其协议字段 (iph->protocol
) 指示为 IPPROTO_TCP
之后。要获取 TCP 头部的指针,我们需要知道 IP 头部的实际长度,因为 IP 头部可能包含选项,长度并非固定。struct iphdr
中的 ihl
(Internet Header Length) 字段表示 IP 头部的长度,但其单位是 4 字节(32位字)。因此,TCP 头部的起始位置可以通过将 IP 头部指针(iph
)加上 IP 头部的字节长度(iph->ihl * 4
)来计算得到。获取到指向 struct tcphdr
(定义于 <linux/tcp.h>
)的指针后,我们就可以访问其成员,如源端口 (source
) 和目的端口 (dest
)。这些端口号同样是以网络字节序存储的。下面的代码片段展示了一个简单的示例,演示了如何在 Netfilter 钩子函数中检查 TCP 目的端口,并在端口号为 25 (SMTP 服务的默认端口) 时丢弃数据包:
1 |
|
kube-proxy iptables 模式工作原理详解

kube-proxy
在 iptables 模式下,通过在 nat
表和 filter
表(较少)中创建一系列自定义链和规则,巧妙地利用了 Netfilter 的钩子点来实现 Service 功能。
核心 iptables 链 (位于 nat
表):
KUBE-SERVICES
: 所有 Service 流量的总入口。规则根据目标 IP (ClusterIP) 或目标端口 (NodePort) 跳转到相应的 Service 处理链。挂载在PREROUTING
(外部/跨节点流量) 和OUTPUT
(节点内部流量) 链上。KUBE-NODEPORTS
: 处理 NodePort 类型服务的入口。规则根据目标端口跳转到对应的 Service 处理链,并通常先跳转到KUBE-MARK-MASQ
。挂载在PREROUTING
和OUTPUT
链上(通常通过KUBE-SERVICES
跳转过来)。KUBE-SVC-<hash>
: 每个 Service 对应一个此链。负责负载均衡。包含多条规则,每条规则对应一个健康的 Endpoint (Pod)。使用statistic
模块(--mode random --probability P
或--mode nth
) 实现 Pod 选择,然后跳转到对应的 Endpoint 处理链。KUBE-SEP-<hash>
: 每个 Endpoint (Service EndPoint) 对应一个此链。负责执行 DNAT。包含一条-j DNAT --to-destination <pod-ip>:<target-port>
规则,将数据包目标地址修改为实际 Pod IP 和端口。通常也会先跳转到KUBE-MARK-MASQ
。KUBE-MARK-MASQ
: 给需要进行源地址伪装 (SNAT/Masquerade) 的数据包打上一个 Netfilter 标记 (通常是0x4000
)。例如,从 NodePort 进来的流量,或者externalTrafficPolicy: Cluster
时跨节点转发的流量。KUBE-POSTROUTING
: 挂载在POSTROUTING
链上。检查数据包是否有KUBE-MARK-MASQ
设置的标记,如果有,则执行-j MASQUERADE
,将源 IP 修改为出接口的 IP 地址,确保响应流量能正确返回。
流量路径示例 (外部访问 NodePort):
- 外部请求 -> NodeIP:NodePort
- 数据包到达 Node,进入
PREROUTING
Hook。 nat
表PREROUTING
链 ->KUBE-SERVICES
链。KUBE-SERVICES
链匹配 NodePort ->KUBE-NODEPORTS
链。KUBE-NODEPORTS
链匹配端口 ->KUBE-MARK-MASQ
(打标记0x4000
) ->KUBE-SVC-<hash>
链。KUBE-SVC-<hash>
链根据负载均衡策略 (e.g., random) ->KUBE-SEP-<hash>
链。KUBE-SEP-<hash>
链 ->KUBE-MARK-MASQ
(可能重复打标,无害) -> 执行DNAT --to-destination <pod-ip>:<target-port>
。- 数据包目标地址变为 Pod IP,内核重新路由,可能在本机或转发到其他 Node。
- 数据包离开 Node,进入
POSTROUTING
Hook。 nat
表POSTROUTING
链 ->KUBE-POSTROUTING
链。KUBE-POSTROUTING
链匹配标记0x4000
-> 执行MASQUERADE
(源 IP 变为 Node IP)。- 数据包发送给目标 Pod。
流量路径示例 (Pod 访问 ClusterIP):
- Pod A (Client) -> ClusterIP:Port
- 数据包在本机产生,进入
OUTPUT
Hook。 nat
表OUTPUT
链 ->KUBE-SERVICES
链。KUBE-SERVICES
链匹配 ClusterIP ->KUBE-SVC-<hash>
链。KUBE-SVC-<hash>
链 ->KUBE-SEP-<hash>
链 (选择目标 Pod B)。KUBE-SEP-<hash>
链 -> 执行DNAT --to-destination <podB-ip>:<target-port>
。- 数据包目标地址变为 Pod B IP,内核重新路由。
- 如果 Pod B 在同一节点,数据包直接转发给 Pod B。
- 如果 Pod B 在不同节点,数据包进入
POSTROUTING
Hook。 nat
表POSTROUTING
链 ->KUBE-POSTROUTING
链。- 是否 MASQUERADE 取决于 CNI 和网络策略。 如果 Pod 网络 (如 Calico BGP 模式) 可以直接路由,则可能不需要 Masquerade。如果需要隐藏 Pod IP 或网络策略要求,
KUBE-SEP
链中可能已包含KUBE-MARK-MASQ
跳转,此时会执行 Masquerade。 - 数据包发送给目标 Pod B。
iptables 模式总结: 利用 Netfilter/iptables 在内核中实现了 Service 的转发和负载均衡,性能优于 userspace,但受限于 iptables 自身的可扩展性瓶颈。
3. IPVS 模式:面向大规模集群的高性能方案

为了解决 iptables 模式在大规模集群下的性能问题,Kubernetes 引入了 IPVS (IP Virtual Server) 模式。IPVS 是 Linux 内核内置的高性能 L4 负载均衡模块,最初为 LVS (Linux Virtual Server) 项目开发。
核心优势:
- 高效查找: IPVS 使用哈希表存储 Service (Virtual Server) 到 Endpoint (Real Server) 的映射,查找效率接近 O(1),不受规则数量影响。
- 性能优越: 在大量 Service/Endpoint 场景下,转发性能远超
iptables
,CPU 消耗和延迟更低。 - 增量更新: Service/Endpoint 变更时,只需通过
netlink
接口增量更新 IPVS 规则,速度快 (毫秒级)。 - 丰富调度算法: IPVS 内核模块原生支持多种成熟的负载均衡算法,为 Service 流量分发提供了更多选择和优化空间,例如可以根据后端 Pod 的连接数进行负载均衡(Least Connection)。
- 连接保持优化: 支持更好的会话保持机制。
工作原理:
- 监听 API Server: 同 iptables 模式。
- 调用 IPVS 内核接口:
kube-proxy
通过netlink
与内核 IPVS 模块通信。 - 创建 IPVS 规则:
- 为每个 Kubernetes Service(目前主要支持 ClusterIP 和 NodePort 类型的 Service),
kube-proxy
会在宿主机内核中创建一个对应的 IPVS 虚拟服务器(Virtual Server)。这个虚拟服务器的地址和端口就是 Service 的 ClusterIP 和 Port(或者 NodePort 的监听地址和端口)。 - 对于该 Service 关联的每一个健康的 Endpoint(即 Pod 的 IP 和 Port),
kube-proxy
会为对应的 IPVS 虚拟服务器添加一个 真实服务器(Real Server)。 kube-proxy
还会根据 Service 的配置(例如sessionAffinity
)和kube-proxy
的负载均衡策略(图中显示为--mode random
)来决定如何选择 Endpoint。
- 为每个 Kubernetes Service(目前主要支持 ClusterIP 和 NodePort 类型的 Service),
- 创建虚拟接口:
kube-proxy
通常会创建一个虚拟接口 (如kube-ipvs0
) 并将所有 ClusterIP 配置在该接口上,确保发往 ClusterIP 的流量能被本地 IP 栈捕获,进而触发 IPVS 处理。 - 流量转发: 当一个数据包到达宿主机,其目标地址和端口匹配某个 IPVS 虚拟服务器时,内核的 IPVS 模块会接管这个数据包。它会根据选定的调度算法,从该虚拟服务器关联的真实服务器列表中选择一个健康的 Pod。然后,IPVS 执行目标网络地址转换(DNAT),将数据包的目标 IP 和端口修改为选定 Pod 的 IP 和端口,并将数据包转发出去。
- SNAT/Masquerade 仍依赖 iptables: 需要注意的是,虽然 IPVS 负责主要的负载均衡和 DNAT,但它通常不直接处理源网络地址转换(SNAT)。当 Pod 响应请求时,为了保证响应数据包能够正确返回给原始客户端(而不是直接返回,导致客户端收到一个来自 Pod IP 的非预期响应),通常还是需要进行 SNAT,将源 IP 地址改为节点的 IP 地址。这部分功能,
kube-proxy
在 IPVS 模式下,仍然依赖iptables
(通常是MASQUERADE
规则)来完成。kube-proxy
会配置简单的iptables
规则来标记需要进行 SNAT 的数据包(例如,对从 IPVS 转发出去的、源 IP 是 Pod IP 但目标 IP 不在本地 Pod CIDR 范围内的包进行标记),然后在POSTROUTING
链中对这些标记的包执行MASQUERADE
。
关键技术实现 (Go 代码片段示意):
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// pkg/proxy/ipvs/proxier.go (简化示意)
// ...
// syncProxyRules() -> syncService()
// ...
// Create or Update IPVS Service (Virtual Server)
service := &ipvs.Service{
Address: net.ParseIP(clusterIP),
Port: uint16(port),
Protocol: ipvs.Protocol(protocol),
Scheduler: scheduler, // e.g., "rr", "lc"
Flags: serviceFlags,
Timeout: timeout,
}
if err := p.ipvs.AddService(service); err != nil { ... }
// ...
// syncEndpoint()
// ...
// Add IPVS Destination (Real Server)
dest := &ipvs.Destination{
Address: net.ParseIP(endpointIP),
Port: uint16(targetPort),
Weight: 1, // Or based on annotations
Flags: destinationFlags,
}
if err := p.ipvs.AddDestination(service, dest); err != nil { ... }
// ...
// Setup iptables rules for masquerading, hairpin, etc.
p.syncMasqueradeMarkRule()
p.syncHairpinIptablesRules()
// ...
IPVS 模式总结: 通过利用内核 IPVS 模块,显著提升了大规模集群下的服务转发性能和可扩展性,是当前生产环境推荐的选择。但仍需 iptables
配合完成 SNAT 等功能。
生产环境选型与实践建议
性能基准对比 (示意)
模式 | 万规则更新时延 | 并发连接能力 | CPU 消耗 (高负载) | 内存占用 (规则) |
---|---|---|---|---|
Userspace | N/A | ~1k | 非常高 | 高 |
Iptables | 分钟级 | ~10k-50k | 中高 | 高 (规则+conntrack) |
IPVS | 毫秒级 | 100k+ | 低 | 低 (规则) + 中 (conntrack) |
实践建议
- 模式选择: 对于新集群或有性能需求的集群,强烈推荐使用 IPVS 模式。如果内核版本过低或有特殊原因无法使用 IPVS,iptables 模式仍是稳定选择。
- 内核要求 (IPVS): 确保 Linux 内核版本 ≥ 4.1 (推荐 4.19+ 以获得更好性能和特性),并已加载
ip_vs
,ip_vs_rr
,ip_vs_wrr
,ip_vs_sh
,nf_conntrack
(或nf_conntrack_ipv4
) 等必要内核模块。 - 切换模式:
- 编辑
kube-proxy
的 ConfigMap (kubectl edit configmap kube-proxy -n kube-system
)。 - 修改
mode
字段为"ipvs"
。 - 重启
kube-proxy
Pods (kubectl delete pod -l k8s-app=kube-proxy -n kube-system
)。 - 验证 IPVS 规则:
sudo ipvsadm -Ln
。
- 编辑
- 调度算法选择 (IPVS):
- 默认
rr
(Round Robin): 简单轮询,适用于无状态服务。 lc
(Least Connection): 将请求发往当前活动连接数最少的 Pod,适用于长连接或连接数不均的服务。sh
(Source Hashing): 基于源 IP 哈希,可实现简单的会话保持 (同一客户端总是访问同一 Pod),适用于需要会话保持的有状态服务。
- 默认
- 配合 CNI: 确保使用的 CNI 插件与所选
kube-proxy
模式兼容。例如,某些 CNI 可能需要特定配置才能与 IPVS 协同工作。 - 监控: 关注关键指标:
- iptables 模式:
iptables
规则数量、iptables-restore
耗时、conntrack
表项数量 (conntrack -L | wc -l
)、CPU 使用率。 - IPVS 模式:
ipvsadm -Ln --stats
查看连接数和速率、ipvs_sync_daemon
相关指标 (如果使用)、conntrack
表项数量、CPU 使用率。 - 通用: 网络延迟、丢包率、
kube-proxy
自身资源消耗和错误日志。
- iptables 模式:
- SNAT 端口耗尽: 无论哪种模式,如果大量 Pod 需要访问集群外部,都可能遇到 SNAT 源端口耗尽的问题。考虑使用 CNI 的 IPAM 功能分配更多 IP、配置 Egress Gateway 或使用 IPv6。
架构演进启示
kube-proxy
从 userspace 到 iptables 再到 IPVS 的演进,清晰地展示了 Kubernetes 网络模型在性能、可扩展性和生产可用性方面的持续优化。IPVS 模式通过利用成熟的内核负载均衡技术,有效解决了大规模微服务场景下的 L4 负载均衡挑战。未来,基于 eBPF 的方案 (如 Cilium 的 Service 实现) 提供了绕过 kube-proxy
和 iptables/IPVS
的可能性,有望带来进一步的性能提升和灵活性,但这仍在发展中,IPVS 模式是当前广泛验证的生产级选择。
CoreDNS:集群服务发现的基石
虽然 kube-proxy
解决了将 Service IP 流量转发到后端 Pod 的问题,但还有一个关键问题:应用程序如何知道要访问哪个 Service IP? 特别是当 Service 重建导致 ClusterIP 变化时。
答案是 DNS。Kubernetes 集群内部署了 DNS 服务,为 Service 和 Pod 自动创建 DNS 记录。应用程序只需要使用稳定、可读的服务名称,DNS 服务就能将其解析为当前有效的 ClusterIP 或 Pod IP。
CoreDNS
是 Kubernetes 当前默认且推荐的集群 DNS 服务器。它是一个灵活、可扩展、基于插件的 DNS 服务器,使用 Go 语言编写。
CoreDNS 的核心职责与架构
- 核心职责: 响应集群内部对 Service 和 Pod 名称的 DNS 查询请求,将其解析为对应的 IP 地址。
- 部署形式: 通常作为 Deployment 部署在
kube-system
命名空间,并通过一个名为kube-dns
的 Service (ClusterIP 类型) 暴露给集群内的其他 Pod。 - 工作模式 (控制器模式): CoreDNS (通过其
kubernetes
插件) 扮演着 Kubernetes 控制器的角色。它监听 (Watch) API Server 上的Service
和EndpointSlice
(或Endpoints
) 资源的变化。 - 内存态 DNS: 当资源发生变化时,
kubernetes
插件会实时更新其内存中的 DNS 记录。查询时直接从内存查找,速度快,无需读写磁盘 Zone 文件。 - 插件化架构: CoreDNS 的核心非常轻量,功能由插件链提供。常用插件包括:
kubernetes
: 核心插件,与 K8s API 交互,解析集群内部域名。cache
: 缓存 DNS 记录,提高性能,降低上游负载。forward
: 将无法本地解析的查询 (如外部域名) 转发给上游 DNS 服务器。prometheus
: 暴露监控指标。health
,ready
: 提供健康检查和就绪检查端点。reload
: 支持配置热加载。loop
: 检测并阻止 DNS 转发循环。
CoreDNS 工作原理解析

- Pod 发起查询: Pod 内应用需要访问
my-svc
服务 -> 调用 DNS 解析库 (如 glibcgetaddrinfo
) -> 根据/etc/resolv.conf
配置,向kube-dns
Service 的 ClusterIP (即 CoreDNS) 发送 DNS 查询请求 (e.g., 查询my-svc.my-namespace.svc.cluster.local
的 A 记录)。 - CoreDNS 处理:
- 请求到达 CoreDNS Pod。
- CoreDNS 根据
Corefile
配置,将请求交给插件链处理。 kubernetes
插件检查域名是否匹配其负责的集群域 (e.g.,cluster.local
)。- 匹配集群域:
- 解析出 Service 名称 (
my-svc
) 和 Namespace (my-namespace
)。 - 在内存缓存中查找对应的 Service 信息。
- 根据 Service 类型返回记录:
- 普通 Service (ClusterIP/NodePort/LoadBalancer): 返回 Service 的 ClusterIP 作为 A/AAAA 记录。
- Headless Service (
clusterIP: None
): 返回所有**就绪 (Ready)**状态的后端 Pod 的 IP 地址列表 作为多个 A/AAAA 记录。 - ExternalName Service: 返回一个 CNAME 记录,指向
.spec.externalName
定义的外部域名。
- (可选) 返回 SRV 记录 (用于命名端口) 或 PTR 记录 (用于反向查询)。
- 解析出 Service 名称 (
- 不匹配集群域 (e.g.,
www.google.com
):kubernetes
插件配置了fallthrough
,将请求传递给下一个插件。cache
插件检查缓存。forward
插件将请求转发给上游 DNS 服务器 (通常是 Node 的 DNS 或公共 DNS)。
- 返回响应: CoreDNS 将解析结果 (或错误) 返回给 Pod。
Pod 的 DNS 配置 (dnsPolicy
, /etc/resolv.conf
, dnsConfig
)
kubelet
负责为每个 Pod 配置 DNS 解析环境。
dnsPolicy
: Pod Spec 中的字段,决定 DNS 配置策略。ClusterFirst
(默认):- Pod 内
/etc/resolv.conf
由 Kubelet 生成。 nameserver
指向 CoreDNS Service IP。search
包含 Pod 命名空间、svc 域、集群域以及 Node 的搜索域。- 查询短名称 (点数 <
ndots
) 时,会优先尝试附加search
域进行解析。 - 无法解析的查询会通过 CoreDNS 转发给上游 DNS。
- Pod 内
Default
: Pod 直接继承 Node 节点的/etc/resolv.conf
配置。Pod 将无法解析集群内部 Service 名称,除非 Node 的 DNS 配置了指向 CoreDNS。None
: Kubelet 不管理 Pod 的/etc/resolv.conf
。Pod 使用镜像自带的/etc/resolv.conf
。通常需要配合dnsConfig
字段使用。ClusterFirstWithHostNet
: 用于hostNetwork: true
的 Pod,行为类似ClusterFirst
,但会考虑宿主机网络环境。
/etc/resolv.conf
(由 Kubelet 生成示例 -ClusterFirst
):1
2
3
4
5
6# search <namespace>.svc.<cluster.local> svc.<cluster.local> <cluster.local> [node's search domains]
search default.svc.cluster.local svc.cluster.local cluster.local mycorp.com
# nameserver 指向 CoreDNS Service IP
nameserver 10.96.0.10
# options ndots:5 (关键选项)
options ndots:5 timeout:1 attempts:2search
: 域名搜索路径。查询my-svc
时会尝试my-svc.default.svc.cluster.local
,my-svc.svc.cluster.local
, …nameserver
: DNS 服务器地址。options ndots:5
: 如果查询的域名包含的点< 5
,优先尝试附加search
域;如果>= 5
,则视为 FQDN 直接查询。
dnsConfig
: Pod Spec 中的字段,仅在dnsPolicy: "None"
时或需要覆盖默认策略时使用。允许用户显式指定nameservers
,searches
,options
。1
2
3
4
5
6
7
8
9
10
11spec:
dnsPolicy: "None" # Kubelet 不自动配置
dnsConfig: # 用户提供完整配置
nameservers:
- "1.1.1.1"
- "8.8.8.8"
searches:
- my.custom.domain
options:
- name: ndots
value: "1"注意: 如果使用
dnsPolicy: None
但仍需解析集群服务,必须在dnsConfig.nameservers
中手动加入 CoreDNS 的 IP,并在dnsConfig.searches
中加入集群搜索域。环境变量方式 (已不推荐): 早期 K8s 会将 Service 信息注入环境变量 (
<SERVICE_NAME>_SERVICE_HOST
,<SERVICE_NAME>_SERVICE_PORT
)。这种方式是静态的,无法感知 Service 或 IP 变化,且污染环境,强烈建议使用 DNS 进行服务发现。
CoreDNS 配置示例 (Corefile
)
CoreDNS 的行为由 Corefile
控制,通常存储在 coredns
ConfigMap 中。
1 |
|
配置解读:
.:53 { ... }
: 定义了一个处理所有 DNS 查询(.
代表根区域),监听标准 DNS 端口 53。errors
: 捕获并记录处理过程中的错误。health { lameduck 5s }
: 在:8080/health
(默认端口) 提供健康检查接口。lameduck
确保在 CoreDNS 准备关闭时,会先保持 5 秒的运行状态以完成正在处理的请求,然后才报告不健康。ready
: 在:8181/ready
(默认端口) 提供就绪检查接口,只有当 CoreDNS 完全准备好处理请求时才返回成功。kubernetes cluster.local in-addr.arpa ip6.arpa { ... }
: 这是核心。cluster.local
: 指定 CoreDNS 负责解析的 Kubernetes 集群域。所有*.cluster.local
的查询将由这个插件处理。in-addr.arpa
和ip6.arpa
: 使插件能够处理 PTR 反向查询,将 IP 地址解析回对应的 Kubernetes 服务或 Pod 名称。pods insecure
: 允许解析 Pod 的 IP 地址到 DNS 名称的 A 记录。insecure
表示即使 Pod 未处于 Ready 状态也可能被解析(通常还支持verified
和disabled
选项)。upstream
: 对于指向外部名称 (ExternalName) 的 Service,如果需要解析,会使用上游解析器。fallthrough in-addr.arpa ip6.arpa
: 如果查询的名称不匹配cluster.local
域,或者不匹配反向查询域,则将请求传递给插件链中的下一个插件(在这里是forward
)。
prometheus :9153
: 在 9153 端口暴露/metrics
端点,供 Prometheus 抓取监控数据。forward . /etc/resolv.conf
: 对于不由kubernetes
插件处理的查询(由于fallthrough
),使用此插件转发。.
表示转发所有域。/etc/resolv.conf
表示使用 CoreDNS Pod 所在 Node 的 DNS 配置作为上游解析器。也可以直接指定 IP 地址,如forward . 8.8.8.8 1.1.1.1
。cache 30
: 对 DNS 记录进行缓存,默认缓存时间 30 秒。这能显著提高性能并减少对上游 DNS 的依赖。loop
: 防止 DNS 查询在转发过程中形成循环。reload 5s
: CoreDNS 会定期检查Corefile
的修改时间。如果文件发生变化,它会在不中断服务的情况下平滑地重新加载配置。loadbalance
: 对 DNS 响应中的多个 A/AAAA/MX 记录进行轮询分发,提供简单的客户端负载均衡。
通过这套机制,CoreDNS 为 Kubernetes 集群提供了一个健壮、高效且可配置的内部 DNS 解析服务,完美解决了因 Service IP 动态变化而导致的服务发现难题,是构建稳定可靠微服务架构的关键基础设施之一。理解 CoreDNS 的工作原理和配置方式,对于深入掌握 Kubernetes 网络和服务发现至关重要。
CoreDNS 的核心架构:内存态 DNS 与控制器模式
你提到的 “内存态 DNS” 和 “控制器” 是理解 CoreDNS 工作方式的关键。
内存态 DNS:CoreDNS 本身可以配置为权威 DNS 服务器或转发器/缓存。在 Kubernetes 环境中,通过其 kubernetes
插件,CoreDNS 能够动态地维护集群内部服务的 DNS 记录。这些记录主要存储在内存中,以便快速响应查询请求。这意味着 CoreDNS 不需要依赖传统的、基于文件的区域(Zone)文件来存储 K8s 服务的记录。这种内存态存储对于应对 K8s 中 Service 和 Pod 的频繁变化至关重要,可以提供低延迟的查询响应。当然,CoreDNS 也包含 cache
插件,用于缓存内部和外部查询结果,进一步提高性能并减少对上游 DNS 服务器的负载。
控制器模式:这是 CoreDNS 与 Kubernetes 集成运作的核心机制。CoreDNS (通过其 kubernetes
插件) 扮演着一个标准的 Kubernetes 控制器角色。它通过 Kubernetes API 监听(Watch) Service
和 EndpointSlice
(或者旧版本的 Endpoints
)资源的变化。
- 当一个新的
Service
被创建时,控制器会收到通知,并根据 Service 的类型和配置,在内存中生成相应的 DNS 记录(A、AAAA、SRV、PTR 等)。 - 当一个
Service
关联的 Pod 实例发生变化(例如,Pod 启动就绪、Pod 被删除、Pod IP 改变)时,对应的EndpointSlice
会更新。控制器监听到EndpointSlice
的变化,并相应地更新内存中的 DNS 记录(特别是对于 Headless Service 的 A/AAAA 记录)。 - 当
Service
被删除时,控制器也会移除相关的 DNS 记录。
这个过程是持续、自动的。CoreDNS 不需要手动配置每个服务的 DNS 条目,它通过与 Kubernetes API 的实时交互,动态地维护了一份准确反映集群当前状态的 DNS 视图。这种机制与其他的 Kubernetes 控制器(如 Deployment Controller、ReplicaSet Controller)的工作原理类似,都是通过 Watch API -> 对比期望状态与实际状态 -> 执行调整动作 的循环来确保系统达到期望的状态。
从 Linux 内核或网络角度看,当一个 Pod 内的进程发起 DNS 查询时(通常是调用 libc 的 gethostbyname
或 getaddrinfo
等函数),最终会根据 /etc/resolv.conf
的配置,将 UDP 或 TCP 请求发送到 CoreDNS Pod 的 IP 地址和 53 端口。CoreDNS Pod 接收到请求后,其内部的插件链开始处理。kubernetes
插件会检查请求的域名是否匹配集群内部的域(如 cluster.local
),如果匹配,则在内存数据中查找相应的记录并返回;如果不匹配,则可能由 forward
或 proxy
插件将请求转发给上游 DNS 服务器(例如节点宿主机的 DNS 或公共 DNS)。
不同类型服务的 DNS 记录详解
CoreDNS 如何为不同类型的 Kubernetes Service 生成 DNS 记录是服务发现的核心细节。
普通 Service (ClusterIP, NodePort, LoadBalancer)
这类 Service 都有一个由 Kubernetes API Server 分配的、稳定的虚拟 IP,即 ClusterIP。这个 IP 并不是绑定在某个具体的网络设备上,而是由 kube-proxy(或等效的网络组件如 Cilium eBPF)在数据平面上实现负载均衡和转发。
- A/AAAA 记录:CoreDNS 会为这类 Service 创建一个 FQDN(完全限定域名),格式通常是
<service-name>.<namespace-name>.svc.<cluster-domain>
(例如my-service.default.svc.cluster.local
)。这个 FQDN 解析为一个 A 记录(IPv4)或 AAAA 记录(IPv6),其值就是该 Service 的 ClusterIP。客户端 Pod 查询这个域名时,会得到 ClusterIP,然后发往这个 IP 的请求会被 kube-proxy 拦截并转发到该 Service 背后某个健康的 Pod IP 上。 - SRV 记录:如果 Service 定义了端口(Ports),CoreDNS 还会为每个命名端口(Named Port)创建 SRV 记录。格式通常是
_<port-name>._<protocol>.<service-name>.<namespace-name>.svc.<cluster-domain>
(例如_http._tcp.my-service.default.svc.cluster.local
)。SRV 记录包含了端口号、优先级和权重,允许客户端发现服务提供的具体端口信息,而不仅仅是 IP 地址。这对于需要知道特定服务端口的应用(如 gRPC、LDAP)非常有用。 - PTR 记录:用于反向 DNS 查询,将 IP 地址解析回对应的 Kubernetes 服务或 Pod 名称。
Headless Service
直接指向所有 Ready 的 Pod IP。
当 Service 的 .spec.clusterIP
字段被显式设置为 None
时,就创建了一个 Headless Service。顾名思义,它没有 ClusterIP。API Server 不会为其分配虚拟 IP,kube-proxy 也不会处理到这个 Service 的流量。
- A/AAAA 记录 (多条):对于 Headless Service,当查询其 FQDN
<service-name>.<namespace-name>.svc.<cluster-domain>
时,CoreDNS 不会返回 ClusterIP(因为它不存在)。相反,它会返回多个 A/AAAA 记录,每个记录对应一个**当前就绪(Ready)**状态的、属于该 Service 的 Pod 的 IP 地址。**这意味着客户端直接解析到后端 Pod 的真实 IP 列表。**这对于需要直接与 Pod 通信的场景(如 StatefulSet 的 Pod 间发现)或者希望自己实现客户端负载均衡策略的场景非常有用。 - Pod FQDN 记录:对于与 Headless Service 关联的每个 Pod(特别是那些由 StatefulSet 创建的、具有稳定网络标识符的 Pod),CoreDNS 还会创建一个特定的 FQDN,格式通常是
<pod-hostname>.<service-name>.<namespace-name>.svc.<cluster-domain>
(如果 Service 指定了subdomain
字段,格式会是<pod-hostname>.<subdomain>.<namespace-name>.svc.<cluster-domain>
) 或者对于普通 Pod 可能简化为类似<pod-ip-dashed>.<namespace-name>.pod.<cluster-domain>
。这个记录直接解析为该 Pod 的 IP 地址。这允许直接寻址到某个具体的 Pod 实例。
ExternalName Service
这种类型的 Service 比较特殊,它不涉及 Pod 选择器或 IP 地址分配。它的目的是在集群内部为外部的一个 DNS 域名创建一个别名。
- CNAME 记录:CoreDNS 会为 ExternalName Service 的 FQDN
<service-name>.<namespace-name>.svc.<cluster-domain>
创建一个 CNAME 记录。这个 CNAME 记录的值是 Service 定义中.spec.externalName
字段指定的外部域名。当集群内部的 Pod 查询这个 Service FQDN 时,CoreDNS 会返回 CNAME 记录,指示客户端应该去查询externalName
指定的那个域名。这相当于在集群 DNS 内部做了一个“符号链接”或“别名”,将内部服务名映射到集群外部的某个实际服务上,而无需关心外部服务的 IP 是否变化。
Pod 的 DNS 配置 (dnsPolicy
, /etc/resolv.conf
, dnsConfig
)
Kubernetes 通过 kubelet
组件管理每个 Pod 的 DNS 配置,确保 Pod 能够正确地使用 CoreDNS 进行服务发现。
dnsPolicy
: Pod Spec 中的字段,决定 DNS 配置策略。ClusterFirst
(默认):- Pod 内
/etc/resolv.conf
由 Kubelet 生成。 nameserver
指向 CoreDNS Service IP。search
包含 Pod 命名空间、svc 域、集群域以及 Node 的搜索域。- 查询短名称 (点数 <
ndots
) 时,会优先尝试附加search
域进行解析。 - 无法解析的查询会通过 CoreDNS 转发给上游 DNS。
- Pod 内
Default
: Pod 直接继承 Node 节点的/etc/resolv.conf
配置。Pod 将无法解析集群内部 Service 名称,除非 Node 的 DNS 配置了指向 CoreDNS。None
: Kubelet 不管理 Pod 的/etc/resolv.conf
。Pod 使用镜像自带的/etc/resolv.conf
。通常需要配合dnsConfig
字段使用。ClusterFirstWithHostNet
: 用于hostNetwork: true
的 Pod,行为类似ClusterFirst
,但会考虑宿主机网络环境。
/etc/resolv.conf
(由 Kubelet 生成示例 -ClusterFirst
):1
2
3
4
5
6# search <namespace>.svc.<cluster.local> svc.<cluster.local> <cluster.local> [node's search domains]
search default.svc.cluster.local svc.cluster.local cluster.local mycorp.com
# nameserver 指向 CoreDNS Service IP
nameserver 10.96.0.10
# options ndots:5 (关键选项)
options ndots:5 timeout:1 attempts:2search
: 域名搜索路径。查询my-svc
时会尝试my-svc.default.svc.cluster.local
,my-svc.svc.cluster.local
, …nameserver
: DNS 服务器地址。options ndots:5
: 如果查询的域名包含的点< 5
,优先尝试附加search
域;如果>= 5
,则视为 FQDN 直接查询。
dnsConfig
: Pod Spec 中的字段,仅在dnsPolicy: "None"
时或需要覆盖默认策略时使用。允许用户显式指定nameservers
,searches
,options
。1
2
3
4
5
6
7
8
9
10
11spec:
dnsPolicy: "None" # Kubelet 不自动配置
dnsConfig: # 用户提供完整配置
nameservers:
- "1.1.1.1"
- "8.8.8.8"
searches:
- my.custom.domain
options:
- name: ndots
value: "1"注意: 如果使用
dnsPolicy: None
但仍需解析集群服务,必须在dnsConfig.nameservers
中手动加入 CoreDNS 的 IP,并在dnsConfig.searches
中加入集群搜索域。环境变量方式 (已不推荐): 早期 K8s 会将 Service 信息注入环境变量 (
<SERVICE_NAME>_SERVICE_HOST
,<SERVICE_NAME>_SERVICE_PORT
)。这种方式是静态的,无法感知 Service 或 IP 变化,且污染环境,强烈建议使用 DNS 进行服务发现。
CoreDNS 配置示例 (Corefile
)
CoreDNS 的行为由 Corefile
控制,通常存储在 coredns
ConfigMap 中。
1 |
|
配置解读:
.:53 { ... }
: 定义了一个处理所有 DNS 查询(.
代表根区域),监听标准 DNS 端口 53。errors
: 捕获并记录处理过程中的错误。health { lameduck 5s }
: 在:8080/health
(默认端口) 提供健康检查接口。lameduck
确保在 CoreDNS 准备关闭时,会先保持 5 秒的运行状态以完成正在处理的请求,然后才报告不健康。ready
: 在:8181/ready
(默认端口) 提供就绪检查接口,只有当 CoreDNS 完全准备好处理请求时才返回成功。kubernetes cluster.local in-addr.arpa ip6.arpa { ... }
: 这是核心。cluster.local
: 指定 CoreDNS 负责解析的 Kubernetes 集群域。所有*.cluster.local
的查询将由这个插件处理。in-addr.arpa
和ip6.arpa
: 使插件能够处理 PTR 反向查询,将 IP 地址解析回对应的 Kubernetes 服务或 Pod 名称。pods insecure
: 允许解析 Pod 的 IP 地址到 DNS 名称的 A 记录。insecure
表示即使 Pod 未处于 Ready 状态也可能被解析(通常还支持verified
和disabled
选项)。upstream
: 对于指向外部名称 (ExternalName) 的 Service,如果需要解析,会使用上游解析器。fallthrough in-addr.arpa ip6.arpa
: 如果查询的名称不匹配cluster.local
域,或者不匹配反向查询域,则将请求传递给插件链中的下一个插件(在这里是forward
)。
prometheus :9153
: 在 9153 端口暴露/metrics
端点,供 Prometheus 抓取监控数据。forward . /etc/resolv.conf
: 对于不由kubernetes
插件处理的查询(由于fallthrough
),使用此插件转发。.
表示转发所有域。/etc/resolv.conf
表示使用 CoreDNS Pod 所在 Node 的 DNS 配置作为上游解析器。也可以直接指定 IP 地址,如forward . 8.8.8.8 1.1.1.1
。cache 30
: 对 DNS 记录进行缓存,默认缓存时间 30 秒。这能显著提高性能并减少对上游 DNS 的依赖。loop
: 防止 DNS 查询在转发过程中形成循环。reload 5s
: CoreDNS 会定期检查Corefile
的修改时间。如果文件发生变化,它会在不中断服务的情况下平滑地重新加载配置。loadbalance
: 对 DNS 响应中的多个 A/AAAA/MX 记录进行轮询分发,提供简单的客户端负载均衡。
通过这套机制,CoreDNS 为 Kubernetes 集群提供了一个健壮、高效且可配置的内部 DNS 解析服务,完美解决了因 Service IP 动态变化而导致的服务发现难题,是构建稳定可靠微服务架构的关键基础设施之一。理解 CoreDNS 的工作原理和配置方式,对于深入掌握 Kubernetes 网络和服务发现至关重要。
CoreDNS 的核心架构:内存态 DNS 与控制器模式
你提到的 “内存态 DNS” 和 “控制器” 是理解 CoreDNS 工作方式的关键。
内存态 DNS:CoreDNS 本身可以配置为权威 DNS 服务器或转发器/缓存。在 Kubernetes 环境中,通过其 kubernetes
插件,CoreDNS 能够动态地维护集群内部服务的 DNS 记录。这些记录主要存储在内存中,以便快速响应查询请求。这意味着 CoreDNS 不需要依赖传统的、基于文件的区域(Zone)文件来存储 K8s 服务的记录。这种内存态存储对于应对 K8s 中 Service 和 Pod 的频繁变化至关重要,可以提供低延迟的查询响应。当然,CoreDNS 也包含 cache
插件,用于缓存内部和外部查询结果,进一步提高性能并减少对上游 DNS 服务器的负载。
控制器模式:这是 CoreDNS 与 Kubernetes 集成运作的核心机制。CoreDNS (通过其 kubernetes
插件) 扮演着一个标准的 Kubernetes 控制器角色。它通过 Kubernetes API 监听(Watch) Service
和 EndpointSlice
(或者旧版本的 Endpoints
)资源的变化。
- 当一个新的
Service
被创建时,控制器会收到通知,并根据 Service 的类型和配置,在内存中生成相应的 DNS 记录(A、AAAA、SRV、PTR 等)。 - 当一个
Service
关联的 Pod 实例发生变化(例如,Pod 启动就绪、Pod 被删除、Pod IP 改变)时,对应的EndpointSlice
会更新。控制器监听到EndpointSlice
的变化,并相应地更新内存中的 DNS 记录(特别是对于 Headless Service 的 A/AAAA 记录)。 - 当
Service
被删除时,控制器也会移除相关的 DNS 记录。
这个过程是持续、自动的。CoreDNS 不需要手动配置每个服务的 DNS 条目,它通过与 Kubernetes API 的实时交互,动态地维护了一份准确反映集群当前状态的 DNS 视图。这种机制与其他的 Kubernetes 控制器(如 Deployment Controller、ReplicaSet Controller)的工作原理类似,都是通过 Watch API -> 对比期望状态与实际状态 -> 执行调整动作 的循环来确保系统达到期望的状态。
从 Linux 内核或网络角度看,当一个 Pod 内的进程发起 DNS 查询时(通常是调用 libc 的 gethostbyname
或 getaddrinfo
等函数),最终会根据 /etc/resolv.conf
的配置,将 UDP 或 TCP 请求发送到 CoreDNS Pod 的 IP 地址和 53 端口。CoreDNS Pod 接收到请求后,其内部的插件链开始处理。kubernetes
插件会检查请求的域名是否匹配集群内部的域(如 cluster.local
),如果匹配,则在内存数据中查找相应的记录并返回;如果不匹配,则可能由 forward
或 proxy
插件将请求转发给上游 DNS 服务器(例如节点宿主机的 DNS 或公共 DNS)。
不同类型服务的 DNS 记录详解
CoreDNS 如何为不同类型的 Kubernetes Service 生成 DNS 记录是服务发现的核心细节。
普通 Service (ClusterIP, NodePort, LoadBalancer)
这类 Service 都有一个由 Kubernetes API Server 分配的、稳定的虚拟 IP,即 ClusterIP。这个 IP 并不是绑定在某个具体的网络设备上,而是由 kube-proxy(或等效的网络组件如 Cilium eBPF)在数据平面上实现负载均衡和转发。
- A/AAAA 记录:CoreDNS 会为这类 Service 创建一个 FQDN(完全限定域名),格式通常是
<service-name>.<namespace-name>.svc.<cluster-domain>
(例如my-service.default.svc.cluster.local
)。这个 FQDN 解析为一个 A 记录(IPv4)或 AAAA 记录(IPv6),其值就是该 Service 的 ClusterIP。客户端 Pod 查询这个域名时,会得到 ClusterIP,然后发往这个 IP 的请求会被 kube-proxy 拦截并转发到该 Service 背后某个健康的 Pod IP 上。 - SRV 记录:如果 Service 定义了端口(Ports),CoreDNS 还会为每个命名端口(Named Port)创建 SRV 记录。格式通常是
_<port-name>._<protocol>.<service-name>.<namespace-name>.svc.<cluster-domain>
(例如_http._tcp.my-service.default.svc.cluster.local
)。SRV 记录包含了端口号、优先级和权重,允许客户端发现服务提供的具体端口信息,而不仅仅是 IP 地址。这对于需要知道特定服务端口的应用(如 gRPC、LDAP)非常有用。 - PTR 记录:用于反向 DNS 查询,将 IP 地址解析回对应的 Kubernetes 服务或 Pod 名称。
Headless Service
直接指向所有 Ready 的 Pod IP。
当 Service 的 .spec.clusterIP
字段被显式设置为 None
时,就创建了一个 Headless Service。顾名思义,它没有 ClusterIP。API Server 不会为其分配虚拟 IP,kube-proxy 也不会处理到这个 Service 的流量。
- A/AAAA 记录 (多条):对于 Headless Service,当查询其 FQDN
<service-name>.<namespace-name>.svc.<cluster-domain>
时,CoreDNS 不会返回 ClusterIP(因为它不存在)。相反,它会返回多个 A/AAAA 记录,每个记录对应一个**当前就绪(Ready)**状态的、属于该 Service 的 Pod 的 IP 地址。**这意味着客户端直接解析到后端 Pod 的真实 IP 列表。**这对于需要直接与 Pod 通信的场景(如 StatefulSet 的 Pod 间发现)或者希望自己实现客户端负载均衡策略的场景非常有用。 - Pod FQDN 记录:对于与 Headless Service 关联的每个 Pod(特别是那些由 StatefulSet 创建的、具有稳定网络标识符的 Pod),CoreDNS 还会创建一个特定的 FQDN,格式通常是
<pod-hostname>.<service-name>.<namespace-name>.svc.<cluster-domain>
(如果 Service 指定了subdomain
字段,格式会是<pod-hostname>.<subdomain>.<namespace-name>.svc.<cluster-domain>
) 或者对于普通 Pod 可能简化为类似<pod-ip-dashed>.<namespace-name>.pod.<cluster-domain>
。这个记录直接解析为该 Pod 的 IP 地址。这允许直接寻址到某个具体的 Pod 实例。
ExternalName Service
这种类型的 Service 比较特殊,它不涉及 Pod 选择器或 IP 地址分配。它的目的是在集群内部为外部的一个 DNS 域名创建一个别名。
- CNAME 记录:CoreDNS 会为 ExternalName Service 的 FQDN
<service-name>.<namespace-name>.svc.<cluster-domain>
创建一个 CNAME 记录。这个 CNAME 记录的值是 Service 定义中.spec.externalName
字段指定的外部域名。当集群内部的 Pod 查询这个 Service FQDN 时,CoreDNS 会返回 CNAME 记录,指示客户端应该去查询externalName
指定的那个域名。这相当于在集群 DNS 内部做了一个“符号链接”或“别名”,将内部服务名映射到集群外部的某个实际服务上,而无需关心外部服务的 IP 是否变化。
Pod 的 DNS 配置 (dnsPolicy
, /etc/resolv.conf
, dnsConfig
)
Kubernetes 通过 kubelet
组件管理每个 Pod 的 DNS 配置,确保 Pod 能够正确地使用 CoreDNS 进行服务发现。
dnsPolicy
: Pod Spec 中的字段,决定 DNS 配置策略。ClusterFirst
(默认):- Pod 内
/etc/resolv.conf
由 Kubelet 生成。 nameserver
指向 CoreDNS Service IP。search
包含 Pod 命名空间、svc 域、集群域以及 Node 的搜索域。- 查询短名称 (点数 <
ndots
) 时,会优先尝试附加search
域进行解析。 - 无法解析的查询会通过 CoreDNS 转发给上游 DNS。
- Pod 内
Default
: Pod 直接继承 Node 节点的/etc/resolv.conf
配置。Pod 将无法解析集群内部 Service 名称,除非 Node 的 DNS 配置了指向 CoreDNS。None
: Kubelet 不管理 Pod 的/etc/resolv.conf
。Pod 使用镜像自带的/etc/resolv.conf
。通常需要配合dnsConfig
字段使用。ClusterFirstWithHostNet
: 用于hostNetwork: true
的 Pod,行为类似ClusterFirst
,但会考虑宿主机网络环境。
/etc/resolv.conf
(由 Kubelet 生成示例 -ClusterFirst
):1
2
3
4
5
6# search <namespace>.svc.<cluster.local> svc.<cluster.local> <cluster.local> [node's search domains]
search default.svc.cluster.local svc.cluster.local cluster.local mycorp.com
# nameserver 指向 CoreDNS Service IP
nameserver 10.96.0.10
# options ndots:5 (关键选项)
options ndots:5 timeout:1 attempts:2search
: 域名搜索路径。查询my-svc
时会尝试my-svc.default.svc.cluster.local
,my-svc.svc.cluster.local
, …nameserver
: DNS 服务器地址。options ndots:5
: 如果查询的域名包含的点< 5
,优先尝试附加search
域;如果>= 5
,则视为 FQDN 直接查询。
dnsConfig
: Pod Spec 中的字段,仅在dnsPolicy: "None"
时或需要覆盖默认策略时使用。允许用户显式指定nameservers
,searches
,options
。1
2
3
4
5
6
7
8
9
10
11spec:
dnsPolicy: "None" # Kubelet 不自动配置
dnsConfig: # 用户提供完整配置
nameservers:
- "1.1.1.1"
- "8.8.8.8"
searches:
- my.custom.domain
options:
- name: ndots
value: "1"注意: 如果使用
dnsPolicy: None
但仍需解析集群服务,必须在dnsConfig.nameservers
中手动加入 CoreDNS 的 IP,并在dnsConfig.searches
中加入集群搜索域。环境变量方式 (已不推荐): 早期 K8s 会将 Service 信息注入环境变量 (
<SERVICE_NAME>_SERVICE_HOST
,<SERVICE_NAME>_SERVICE_PORT
)。这种方式是静态的,无法感知 Service 或 IP 变化,且污染环境,强烈建议使用 DNS 进行服务发现。
CoreDNS 配置示例 (Corefile
)
CoreDNS 的行为由 Corefile
控制,通常存储在 coredns
ConfigMap 中。
1 |
|
配置解读:
.:53 { ... }
: 定义了一个处理所有 DNS 查询(.
代表根区域),监听标准 DNS 端口 53。errors
: 捕获并记录处理过程中的错误。health { lameduck 5s }
: 在:8080/health
(默认端口) 提供健康检查接口。lameduck
确保在 CoreDNS 准备关闭时,会先保持 5 秒的运行状态以完成正在处理的请求,然后才报告不健康。ready
: 在:8181/ready
(默认端口) 提供就绪检查接口,只有当 CoreDNS 完全准备好处理请求时才返回成功。kubernetes cluster.local in-addr.arpa ip6.arpa { ... }
: 这是核心。cluster.local
: 指定 CoreDNS 负责解析的 Kubernetes 集群域。所有*.cluster.local
的查询将由这个插件处理。in-addr.arpa
和ip6.arpa
: 使插件能够处理 PTR 反向查询,将 IP 地址解析回对应的 Kubernetes 服务或 Pod 名称。pods insecure
: 允许解析 Pod 的 IP 地址到 DNS 名称的 A 记录。insecure
表示即使 Pod 未处于 Ready 状态也可能被解析(通常还支持verified
和disabled
选项)。upstream
: 对于指向外部名称 (ExternalName) 的 Service,如果需要解析,会使用上游解析器。fallthrough in-addr.arpa ip6.arpa
: 如果查询的名称不匹配cluster.local
域,或者不匹配反向查询域,则将请求传递给插件链中的下一个插件(在这里是forward
)。
prometheus :9153
: 在 9153 端口暴露/metrics
端点,供 Prometheus 抓取监控数据。forward . /etc/resolv.conf
: 对于不由kubernetes
插件处理的查询(由于fallthrough
),使用此插件转发。.
表示转发所有域。/etc/resolv.conf
表示使用 CoreDNS Pod 所在 Node 的 DNS 配置作为上游解析器。也可以直接指定 IP 地址,如forward . 8.8.8.8 1.1.1.1
。cache 30
: 对 DNS 记录进行缓存,默认缓存时间 30 秒。这能显著提高性能并减少对上游 DNS 的依赖。loop
: 防止 DNS 查询在转发过程中形成循环。reload 5s
: CoreDNS 会定期检查Corefile
的修改时间。如果文件发生变化,它会在不中断服务的情况下平滑地重新加载配置。loadbalance
: 对 DNS 响应中的多个 A/AAAA/MX 记录进行轮询分发,提供简单的客户端负载均衡。
通过这套机制,CoreDNS 为 Kubernetes 集群提供了一个健壮、高效且可配置的内部 DNS 解析服务,完美解决了因 Service IP 动态变化而导致的服务发现难题,是构建稳定可靠微服务架构的关键基础设施之一。理解 CoreDNS 的工作原理和配置方式,对于深入掌握 Kubernetes 网络和服务发现至关重要。
DNS 的落地实践:与企业 DNS 集成 (ExternalDNS)
为了让集群外部也能通过 DNS 访问 K8s 服务 (通常是 LoadBalancer 或 Ingress),可以使用 ExternalDNS 控制器。
- 工作原理: ExternalDNS 监听 K8s
Service
和Ingress
资源,根据注解或配置,自动在外部 DNS 提供商 (如 AWS Route 53, Google Cloud DNS, Azure DNS, 或企业内部 BIND) 中创建/更新/删除 DNS 记录,将服务域名指向 LoadBalancer IP 或 Ingress IP。 - 目的: 实现服务在集群内外使用统一的域名访问。
- Headless Service 注意事项: 将 Headless Service 的所有 Pod IP 发布到外部 DNS 需谨慎,可能导致 DNS 记录频繁变动和管理复杂性。通常只按需发布特定 Pod 的记录,并确保 Pod IP 在外部可路由。
配置示例 (ExternalDNS 部署片段 - 示意):
部署 ExternalDNS 时,需要配置它连接到哪个 K8s 集群、监听哪些资源、使用哪个 DNS 提供商以及如何认证。
1 |
|
实战分析:理解 Service 访问
让我们分析一下文章末尾提供的实战场景:
1 |
|
分析:
DNS 解析成功:
ping nginx-basic.default.svc.cluster.local
首先触发 DNS 查询。根据/etc/resolv.conf
,查询被发送到10.96.0.10
(CoreDNS)。CoreDNS 成功将nginx-basic.default.svc.cluster.local
解析为其 ClusterIP10.98.3.144
。这证明 CoreDNS 服务发现工作正常。Ping (ICMP) 失败:
ping
命令使用的是 ICMP Echo Request 协议。然而,Kubernetes Service (ClusterIP) 是一个虚拟 IP,它本身并不监听任何端口或协议,而是依赖kube-proxy
创建的网络规则来转发特定端口的 TCP/UDP 流量。nginx-basic
Service 定义了端口80/TCP
。当 ICMP Echo Request 到达10.98.3.144
时,没有kube-proxy
规则或进程直接处理 ICMP,因此内核网络栈最终返回 “Destination Port Unreachable” (更准确地说是 Protocol Unreachable 或类似 ICMP 错误,具体取决于内核实现)。如何正确测试: 要测试 Service 是否工作,需要使用 Service 定义的协议和端口。对于
nginx-basic
(端口 80/TCP),应该使用 HTTP 客户端 (如curl
) 或 TCP 连接工具 (如telnet
):1
2
3
4
5
6
7
8
9
10
11
12
13
14# 在 centos Pod 内部执行
# 使用 curl 测试 HTTP 服务
[root@centos-686b8db5c7-krgbb /]# curl http://nginx-basic.default.svc.cluster.local
# 或者直接用 ClusterIP
[root@centos-686b8db5c7-krgbb /]# curl http://10.98.3.144
# 预期会看到 Nginx 的欢迎页面
# 使用 telnet 测试 TCP 端口连通性
[root@centos-686b8db5c7-krgbb /]# telnet nginx-basic.default.svc.cluster.local 80
Trying 10.98.3.144...
Connected to nginx-basic.default.svc.cluster.local.
Escape character is '^]'.
# (连接成功,可以输入 HTTP 命令或按 Ctrl+] 退出)如果
curl
或telnet
成功,说明kube-proxy
的规则正确地将 TCP 端口 80 的流量转发到了后端的 Nginx Pod。
结论: ping
Service ClusterIP 失败是预期行为,并不代表 Service 或 kube-proxy
有问题。测试 Service 需要使用其暴露的协议和端口。
总结
Kubernetes 网络是一个复杂但设计精良的系统。kube-proxy
通过不断演进的模式 (iptables, IPVS),利用 Linux 内核的网络能力 (Netfilter, IPVS),实现了 Service 这一核心抽象的数据平面转发和负载均衡。CoreDNS
则作为集群的 DNS 服务,提供了基于稳定名称的服务发现机制,解耦了客户端与动态变化的 Pod IP。理解 kube-proxy
的工作原理、Netfilter 的基础以及 CoreDNS
的服务发现机制,对于诊断网络问题、优化集群性能和构建可靠的云原生应用至关重要。随着 eBPF 等新技术的应用,Kubernetes 网络未来将继续向着更高性能、更灵活和更可编程的方向发展。