External Traffic Policy
简介
在 Kubernetes 中,externalTrafficPolicy
是 Service
(尤其是类型为 LoadBalancer
或 NodePort
的服务)的一项设置,用于控制来自集群外部的流量如何被路由到后端 Pod。
它有两个可选值:
Cluster
: 默认
- 行为:外部流量会先到达任意节点(例如通过负载均衡器或 NodePort 接收到请求),然后由 kube-proxy 在集群内转发到任意一个后端 Pod(可能在其他节点)。
- 优点:
- 更好的负载均衡,因为可以使用所有后端 Pod。
- 缺点:
- 客户端的真实 IP 会丢失,因为请求是从另一个节点转发过来的。
- 容器看到的是转发节点的 IP,不是真正的客户端。
Local
- 行为:仅将流量发送给本节点上的后端 Pod,如果当前节点没有匹配的 Pod,则请求会被丢弃。
- 优点:
- 可以保留客户端的原始 IP 地址。
- 更适合用在需要客户端真实 IP 的场景,例如日志分析、防火墙规则等。
- 缺点:
- 节点必须有对应的 Pod 才能接收请求,否则会丢失流量。
- 负载均衡可能不均匀,因为不是每个节点都有后端 Pod。
使用示例
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
实际应用建议
- 若你使用的是云提供商(如 AWS、GCP)的 LoadBalancer,并且希望保留客户端 IP,设置为
Local
是常见做法。 - 若你更关心服务的可用性和负载均衡,使用默认的
Cluster
即可。
需要注意的是,当设置为 Local
时,你可能还需要通过 DaemonSet 的方式确保每个节点上都有一个 Pod 来避免部分节点丢流量。
节点对应的 Pod
这句话里的 “对应的 Pod” 是指 被 Service 选中的后端 Pod,即符合该 Service 的 selector
条件的 Pod。
比如你定义了一个 Service 如下:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
externalTrafficPolicy: Local
这就意味着:
- 这个 Service 会选择所有带有
app: my-app
标签的 Pod 作为后端。 - 如果某个节点上没有任何带
app: my-app
的 Pod,即使外部请求到了这个节点,也不会被转发,最终会丢失。
举个例子
假设你有 3 个节点:
节点 | 是否运行有 app=my-app 的 Pod |
---|---|
node-a | ✅ 是 |
node-b | ❌ 否 |
node-c | ✅ 是 |
- 如果你使用的是
externalTrafficPolicy: Cluster
,那么即使请求打到了 node-b,它也会被 kube-proxy 转发到 node-a 或 node-c。 - 如果你使用的是
externalTrafficPolicy: Local
,而请求打到了 node-b,那因为 node-b 上没有目标 Pod,请求就会直接被丢弃。
CoreDNS
在 Kubernetes 内部访问某个 Service 时,通常是通过 CoreDNS 拿到 Service 的 ClusterIP
, 然后由 kube-proxy 或 IPVS 负责把流量转发到实际的后端 Pod。这种场景下:
- kube-proxy 会根据 Service 对应的 Endpoint 列表 做负载均衡;
- 并不会把请求发给 “没有后端 Pod 的节点”。
但我们之前讨论的是 externalTrafficPolicy: Local
,这个选项只影响 来自集群外部的流量(比如:
- LoadBalancer 类型 Service 后面的云负载均衡器,
- 或通过 NodePort 从集群外访问的请求)
这类流量通常 是先被发到某个节点的 IP 或某个节点的端口上,然后由这个节点上的 kube-proxy 决定如何处理。
外部流量
它不会走 CoreDNS:
- DNS(CoreDNS)只在 集群内部 Pod -> Service 的场景生效;
- 外部访问 LoadBalancer IP 或 NodePort 是直连节点的,不通过 DNS,也不管哪个节点有没有对应的 Pod。
例子说明
场景一:集群内部访问
curl http://my-service.default.svc.cluster.local
- 走 CoreDNS,解析出 ClusterIP;
- kube-proxy 根据 Endpoint 随机选择后端 Pod;
- 不管在哪个节点上的 Pod,都可以命中。
场景二:集群外访问(如访问某云 LoadBalancer IP)
curl http://<LoadBalancer-IP>
- 流量打到负载均衡器;
- LB 可能均匀地把请求转发到每个节点;
- 如果某个节点没有本地 Pod,而你设置了
externalTrafficPolicy: Local
,这个请求在该节点上就会被 kube-proxy 丢弃。
ClusterIP 的作用
ClusterIP 是 Service 在集群内部的虚拟 IP,所有内部 Pod 访问 Service 时,都会通过这个 IP 进入集群服务的负载均衡流程。
处理流程
- “走 CoreDNS,解析出 ClusterIP”
→ Pod 中通过 DNS 查询my-service.default.svc.cluster.local
,CoreDNS 返回对应 Service 的 ClusterIP,如10.96.0.1
。 - “kube-proxy 根据 Endpoint 随机选择后端 Pod”
→ kube-proxy 监控所有 Service 的ClusterIP:Port
,本地拦截对这些 IP 的访问,然后根据 Service 的 Endpoint 信息,把请求转发给合适的 Pod(不论在哪个节点)。 - “不管在哪个节点上的 Pod,都可以命中”
→ 因为 kube-proxy 转发能力 + 网络插件(如 Calico、Cilium)支持跨节点通信,所以只要有 Pod,在哪都可以命中。
ClusterIP 是什么?
- 是 Kubernetes 为每个
ClusterIP
类型的 Service 分配的集群内部虚拟 IP; - 所有集群内部通信通过它进行访问;
- 它不是某台节点的 IP,而是 kube-proxy 在所有节点上创建的 NAT 或 IPVS 规则对它进行监听。
举个例子
假设你有一个 Service:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ClusterIP
clusterIP: 10.96.0.1
selector:
app: my-app
ports:
- port: 80
然后你有 3 个 Pod:
Pod 名字 | 所在节点 | IP |
---|---|---|
pod-a | node1 | 10.1.1.2 |
pod-b | node2 | 10.1.2.3 |
pod-c | node3 | 10.1.3.4 |
此时:
- 某个 Pod 执行
curl http://my-service.default.svc.cluster.local
- CoreDNS → 返回
10.96.0.1
(ClusterIP) - 访问
10.96.0.1:80
被 kube-proxy 拦截 - kube-proxy 从 Endpoint 中挑一个,如
10.1.2.3:80
(pod-b) - 然后流量就转发过去了,不管它在哪个节点上
服务名 VS ClusterIP
kube-proxy 使用 ClusterIP 而不是服务名,是因为 IP 更快、更稳定、更适合底层转发逻辑;而服务名是给人和应用层用的,最终也会被解析为 IP。
深度解析
服务名是逻辑标识,ClusterIP 是实际地址
- Service 名字(如
my-service.default.svc
) 是 Kubernetes 的逻辑名称空间的一部分,属于应用层的抽象; - ClusterIP(如
10.96.0.1
) 是网络层的地址,是 kube-proxy 转发的关键依据,属于底层通信的标准格式。
网络通信最终必须通过 IP,不能通过“名字”直接建立连接,名字必须先被解析成 IP(靠 DNS)。
性能角度
- kube-proxy 本质是在所有节点上注册一批 iptables / IPVS 规则,监听 ClusterIP 端口。
- iptables 和 IPVS 只能工作在 IP 层,它们根本不理解 “服务名”。
- 如果 kube-proxy 使用服务名,就意味着它每次都要反复解析 DNS → 这太慢,也容易出错。
内核和网络栈
写代码的时候用服务名,比如:
http.Get("http://my-service.default.svc:80")
这其实走的是 DNS,CoreDNS 会返回 10.96.0.1
。 实际上 kube-proxy 和 Linux 内核工作的是这一层:
iptables -t nat -A PREROUTING -d 10.96.0.1 --dport 80 -j DNAT ...
安全性 & 可维护性
- 使用 ClusterIP 做网络路由目标,避免 kube-proxy 依赖 DNS 系统;
- 如果 kube-proxy 依赖服务名,它就必须内嵌一个 DNS 客户端系统,这会大大提高复杂度和出错点。
内部映射
- 每个 Service 在创建时,都会在 etcd 中记录一个固定的
clusterIP
(除非你用Headless
服务); - 所以只要 service 不被删,clusterIP 就不会变。
Endpoint
简要流程
Kubernetes 会通过控制器自动将符合 Service selector
的 Pod 列为该 Service 的 Endpoint。
具体流程
创建 Service 时,定义了 selector
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
selector = app: my-app
,是关键字段。
Endpoint Controller
- 它是 kube-controller-manager 中的一个控制器;
- 每当有新的 Pod、Service、或 Pod 状态变动时,它会自动:
- 查找所有符合 Service
selector
的 Pod; - 读取这些 Pod 的 IP 和 port;
- 生成对应的 Endpoints 或 EndpointSlice 对象;
- 存储在 etcd 里,并同步给 kube-proxy 等组件。
- 查找所有符合 Service
注意
- 只有当 Pod 是
Ready
状态时,它的 IP 才会被加入 Endpoint。 - 如果你创建 Service 时没有写 selector,那么不会自动生成 Endpoints;
- 你需要手动创建
Endpoints
或使用ExternalName
类型。
- 你需要手动创建
Kube Proxy
访问 http://foo.monitor.svc.cluster.local:8080
时,kube-proxy 完全不需要知道服务名, 只靠 ClusterIP 和端口 就能找到对应的 Endpoint,完成请求转发。
第一步:CoreDNS 解析服务名为 ClusterIP
- 主体:Pod 内部应用 → DNS 查询 → CoreDNS;
- CoreDNS 负责将
foo.monitor.svc.cluster.local
解析为ClusterIP
,比如10.96.123.45
; - 返回后,应用继续向
10.96.123.45:8080
发起请求。
第二步:请求通过 kube-proxy 拦截并转发
- 主体:节点上的 kube-proxy(基于 iptables 或 IPVS);
- kube-proxy 拦截访问
ClusterIP
的流量(通过内核),
iptables -t nat -A PREROUTING -d 10.96.123.45 --dport 8080 -j KUBE-SVC-xxxx
KUBE-SVC-xxxx
是 kube-proxy 根据 Service 对象构建的规则链(在启动或监听变更时生成);- 接着,它会查找 Endpoint(或 EndpointSlice),选择一个后端 Pod 的 IP+Port 进行 DNAT;
DNAT to 10.1.2.34:8080
第三步:流量到达 Pod
- DNAT 后,网络栈自动将流量路由至目标 Pod;
- Pod 监听端口接收到流量,返回响应。
流量整体流程
前提假设
我们有如下环境:
- 一个 K8s 集群部署在云上(比如 AWS / GCP / Azure);
- 有一个 Service 类型是
LoadBalancer
; - 用户从集群外部请求服务;
- 外部用户访问 IP:
<LoadBalancer-IP>:80
整体流程图(高层)
[Client]
↓
[Cloud Provider LoadBalancer]
↓
[Node (kube-proxy)]
↓
[Pod (via CNI + iptables/IPVS)]
详细说明
外部请求到达云 LoadBalancer
- 操作主体:客户端用户 + 云厂商的负载均衡器(如 AWS ELB)
- 触发事件:
- 用户访问
<LoadBalancer IP>:80
- 云厂商把请求分发到后端节点池中一台节点(Node)的某个端口上
- 用户访问
- 依赖对象:
- Kubernetes 中的
Service
类型为LoadBalancer
- kube-controller-manager 中的
service_controller.go
调用 cloud provider 接口创建负载均衡器
- Kubernetes 中的
staging/src/k8s.io/cloud-provider/controllers/service/controller.go
云 LB 直接把请求打到某个 Node 上
- 操作主体:云厂商负载均衡器
- 目标地址:
- Node IP:NodePort(即使是 LoadBalancer,本质是通过 NodePort 实现访问)
- 例子:
- 云厂商将请求转发至
192.168.1.10:30080
(Node IP + NodePort)
- 云厂商将请求转发至
Node 上的 kube-proxy 监听 NodePort 端口并拦截请求
- 操作主体:Node 上的
kube-proxy
- 关键操作:
- kube-proxy 在本地设置 iptables 或 IPVS 规则;
- 拦截所有到
30080
的流量,重定向到 Service 的ClusterIP
对应的规则集合 - 根据
externalTrafficPolicy
决定是转发所有 Pod 还是只本地 Pod(详见前文)
- 关键源码位置:
cmd/kube-proxy/app/server.go
:主入口pkg/proxy/iptables/proxier.go
:iptables 模式pkg/proxy/ipvs/proxier.go
:IPVS 模式- 特别是:
syncProxyRules()
函数会定期创建和更新这些转发规则
// 监听 NodePort → 转发到某个 Pod 的 IP:Port
-A KUBE-NODEPORTS -p tcp --dport 30080 -j KUBE-SVC-XXXX
-A KUBE-SVC-XXXX -j KUBE-SEP-1234 // 每个 SEP 是一个 Pod 的后端地址
kube-proxy 选择一个 Pod 后端 IP 进行转发
- 操作主体:iptables / IPVS + kube-proxy 控制规则
- 选择来源:Service 的 Endpoints 或 EndpointSlice(通过 controller 维护)
- 转发方式:
- 如果使用 iptables:使用 DNAT 改写目标地址为 Pod 的 IP
- 如果使用 IPVS:构建虚拟服务和真实服务器的映射
流量进入 Pod 网卡(CNI 插件)
- 操作主体:Pod 网络插件(CNI,如 Calico、Cilium、Flannel)
- 关键点:
- Pod 的 IP 来自于 CNI 插件创建的虚拟网络;
- 流量进入 Pod 的网卡接口(通常是
eth0
),被容器内服务监听接收
- 源码位置(部分 CNI 插件):
Pod 中的容器进程处理请求
- 操作主体:Pod 内的容器(通常是一个 Web 服务,如 Nginx、Go 服务等)
- 监听端口:比如 8080,对应于 Service 的 targetPort
- 响应返回:响应会沿着原路返回(Pod → Node → LB → Client)
补充细节:EndpointSlice 是怎么起作用的?
- kube-controller-manager 会监听 Service 和 Pod 的变更;
- 使用
endpointslice-controller
维护对应的EndpointSlice
; - kube-proxy 监听这些 EndpointSlice 更新,将后端 IP 地址同步到转发规则中。