Skip to content

External Traffic Policy

简介

在 Kubernetes 中,externalTrafficPolicyService(尤其是类型为 LoadBalancerNodePort 的服务)的一项设置,用于控制来自集群外部的流量如何被路由到后端 Pod。

它有两个可选值:

Cluster: 默认

  • 行为:外部流量会先到达任意节点(例如通过负载均衡器或 NodePort 接收到请求),然后由 kube-proxy 在集群内转发到任意一个后端 Pod(可能在其他节点)。
  • 优点
    • 更好的负载均衡,因为可以使用所有后端 Pod。
  • 缺点
    • 客户端的真实 IP 会丢失,因为请求是从另一个节点转发过来的。
    • 容器看到的是转发节点的 IP,不是真正的客户端。

Local

  • 行为:仅将流量发送给本节点上的后端 Pod,如果当前节点没有匹配的 Pod,则请求会被丢弃。
  • 优点
    • 可以保留客户端的原始 IP 地址。
    • 更适合用在需要客户端真实 IP 的场景,例如日志分析、防火墙规则等。
  • 缺点
    • 节点必须有对应的 Pod 才能接收请求,否则会丢失流量。
    • 负载均衡可能不均匀,因为不是每个节点都有后端 Pod。

使用示例

yaml
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 如下:

yaml
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。

例子说明

场景一:集群内部访问

bash
curl http://my-service.default.svc.cluster.local
  • 走 CoreDNS,解析出 ClusterIP;
  • kube-proxy 根据 Endpoint 随机选择后端 Pod;
  • 不管在哪个节点上的 Pod,都可以命中。

场景二:集群外访问(如访问某云 LoadBalancer IP)

bash
curl http://<LoadBalancer-IP>
  • 流量打到负载均衡器;
  • LB 可能均匀地把请求转发到每个节点;
  • 如果某个节点没有本地 Pod,而你设置了 externalTrafficPolicy: Local,这个请求在该节点上就会被 kube-proxy 丢弃。

ClusterIP 的作用

ClusterIP 是 Service 在集群内部的虚拟 IP,所有内部 Pod 访问 Service 时,都会通过这个 IP 进入集群服务的负载均衡流程。

处理流程

  1. “走 CoreDNS,解析出 ClusterIP”
    → Pod 中通过 DNS 查询 my-service.default.svc.cluster.local,CoreDNS 返回对应 Service 的 ClusterIP,如 10.96.0.1
  2. “kube-proxy 根据 Endpoint 随机选择后端 Pod”
    → kube-proxy 监控所有 Service 的 ClusterIP:Port,本地拦截对这些 IP 的访问,然后根据 Service 的 Endpoint 信息,把请求转发给合适的 Pod(不论在哪个节点)。
  3. “不管在哪个节点上的 Pod,都可以命中”
    → 因为 kube-proxy 转发能力 + 网络插件(如 Calico、Cilium)支持跨节点通信,所以只要有 Pod,在哪都可以命中。

ClusterIP 是什么?

  • 是 Kubernetes 为每个 ClusterIP 类型的 Service 分配的集群内部虚拟 IP
  • 所有集群内部通信通过它进行访问;
  • 它不是某台节点的 IP,而是 kube-proxy 在所有节点上创建的 NAT 或 IPVS 规则对它进行监听。

举个例子

假设你有一个 Service:

yaml
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-anode110.1.1.2
pod-bnode210.1.2.3
pod-cnode310.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 → 这太慢,也容易出错。

内核和网络栈

写代码的时候用服务名,比如:

go
http.Get("http://my-service.default.svc:80")

这其实走的是 DNS,CoreDNS 会返回 10.96.0.1。 实际上 kube-proxy 和 Linux 内核工作的是这一层:

bash
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

yaml
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;
    • 生成对应的 EndpointsEndpointSlice 对象;
    • 存储在 etcd 里,并同步给 kube-proxy 等组件。

注意

  • 只有当 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 的流量(通过内核),
bash
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;
bash
DNAT to 10.1.2.34:8080

第三步:流量到达 Pod

  • DNAT 后,网络栈自动将流量路由至目标 Pod;
  • Pod 监听端口接收到流量,返回响应。

流量整体流程

前提假设

我们有如下环境:

  • 一个 K8s 集群部署在云上(比如 AWS / GCP / Azure);
  • 有一个 Service 类型是 LoadBalancer
  • 用户从集群外部请求服务;
  • 外部用户访问 IP:<LoadBalancer-IP>:80

整体流程图(高层)

text
[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 接口创建负载均衡器
源码位置
bash
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() 函数会定期创建和更新这些转发规则
核心逻辑
go
// 监听 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 中的容器进程处理请求

  • 操作主体:Pod 内的容器(通常是一个 Web 服务,如 Nginx、Go 服务等)
  • 监听端口:比如 8080,对应于 Service 的 targetPort
  • 响应返回:响应会沿着原路返回(Pod → Node → LB → Client)

补充细节:EndpointSlice 是怎么起作用的?

  • kube-controller-manager 会监听 Service 和 Pod 的变更;
  • 使用 endpointslice-controller 维护对应的 EndpointSlice
  • kube-proxy 监听这些 EndpointSlice 更新,将后端 IP 地址同步到转发规则中。