概述

在 Kubernetes 中获取客户端真实 IP 是一个常见需求,但由于 kube-proxy 的 SNAT 机制,往往获取到的是中间节点的 IP。externalTrafficPolicy 字段提供了解决方案。

核心知识:

  • 🔍 Service 外部流量策略
  • 📡 客户端 IP 保留机制
  • ⚖️ Cluster vs Local 模式
  • 🎯 负载均衡权衡

适用场景:

  • 需要记录真实客户端 IP
  • 访问控制和安全审计
  • 流量分析和用户画像
  • 地域识别和限流

externalTrafficPolicy 详解

什么是 externalTrafficPolicy

定义:

externalTrafficPolicy 是 Kubernetes Service 中控制外部流量转发策略的字段,决定流量如何路由到 Pod 以及是否保留客户端源 IP。

可选值:

说明默认
Cluster流量可转发到任意节点的 Pod✅ 是
Local流量仅转发到本节点的 Pod❌ 否

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer # 或 NodePort
externalTrafficPolicy: Local # 保留客户端 IP
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

适用类型:

1
2
3
4
5
6
7
# ✅ 支持 externalTrafficPolicy
type: LoadBalancer
type: NodePort

# ❌ 不支持(会报错)
type: ClusterIP
type: ExternalName

Cluster 模式详解

工作原理

流量转发:

1
2
3
4
5
6
7
客户端 (1.2.3.4)

LoadBalancer

Node 1 (10.0.1.1)
↓ [SNAT 替换源 IP]
Pod 在 Node 2 (看到源 IP: 10.0.1.1)

SNAT 机制:

为什么需要 SNAT?

1
2
3
4
5
6
7
8
问题:如果不做 SNAT
客户端 1.2.3.4 → Node 1 → Node 2 上的 Pod
Pod 回复直接发给 1.2.3.4(不经过 Node 1)
客户端收到:发给 Node 1,Node 2 回复 → 丢弃(非法响应)

解决:SNAT 确保响应原路返回
客户端 → Node 1 → Node 2 上的 Pod
Pod 回复 → Node 2 → Node 1 → 客户端 ✅

特点分析

优势:

优势说明
负载均衡好流量可分发到所有节点的 Pod
高可用性即使本节点没有 Pod 也能访问
配置简单默认模式,无需额外配置

劣势:

劣势说明
丢失原始 IP应用看到的是节点 IP
多一跳转发性能略有损失(纳秒级)
难以审计无法记录真实客户端 IP

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
externalTrafficPolicy: Cluster # 默认值,可省略
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

Local 模式详解

工作原理

流量转发:

1
2
3
4
5
6
7
客户端 (1.2.3.4)

LoadBalancer

Node 1 (10.0.1.1)
↓ [不做 SNAT,保留源 IP]
Pod 在 Node 1 (看到源 IP: 1.2.3.4) ✅

保留源 IP 机制:

为什么能保留源 IP?

1
2
3
4
5
6
7
流量只转发到本节点 Pod

不需要跨节点转发

不需要 SNAT

Pod 能看到真实客户端 IP ✅

特点分析

优势:

优势说明
保留原始 IP应用能看到真实客户端 IP
性能更好少一跳转发,延迟更低
便于审计可记录真实访问来源

劣势:

劣势说明
负载可能不均流量只发往有 Pod 的节点
可用性降低节点无 Pod 则无法访问
配置复杂需要配合反亲和性等

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 保留客户端 IP
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

负载不均问题

问题场景:

问题分析:

1
2
3
4
5
6
7
8
9
10
11
12
LoadBalancer 视角:
- 后端节点:Node 1, Node 2
- 无法感知每个节点上有几个 Pod

实际分布:
- Node 1:3 个 Pod
- Node 2:1 个 Pod

结果:
- Node 1 收 50% 流量 → 每个 Pod 16.7%
- Node 2 收 50% 流量 → 每个 Pod 50%
负载不均!

解决负载不均

方案:Pod 反亲和性

配置示例:

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 6
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
# Pod 反亲和性配置
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- my-app
topologyKey: kubernetes.io/hostname
containers:
- name: app
image: my-app:latest

反亲和性参数说明:

参数说明
preferredDuringSchedulingIgnoredDuringExecution软性约束,优先满足但不强制
requiredDuringSchedulingIgnoredDuringExecution硬性约束,必须满足
topologyKeytopology 域,kubernetes.io/hostname 代表节点
weight权重,越大优先级越高(1-100)

效果对比:

1
2
3
4
5
6
7
8
9
优化前:
Node 1: [Pod1, Pod2, Pod3]
Node 2: [Pod4]
Node 3: []

优化后:
Node 1: [Pod1, Pod2]
Node 2: [Pod3, Pod4]
Node 3: [Pod5, Pod6]

其他优化方案

1. 拓扑感知路由(Topology Aware Routing)

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
name: my-service
annotations:
service.kubernetes.io/topology-aware-hints: "auto"
spec:
type: LoadBalancer
externalTrafficPolicy: Local

2. 使用 DaemonSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 每个节点一个 Pod,天然负载均衡
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: my-app
spec:
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:latest

模式选择指南

决策流程

1
2
3
4
5
6
是否需要真实客户端 IP?
├─ 是 → Local 模式
│ ├─ 配置反亲和性
│ └─ 监控负载均衡
└─ 否 → Cluster 模式(默认)
└─ 无需额外配置

场景推荐

场景推荐模式原因
日志审计Local需要记录真实 IP
访问控制LocalIP 白名单等功能
地域识别Local基于 IP 的地域判断
高性能要求Local少一跳转发
高可用优先Cluster负载均衡更好
简单场景Cluster默认即可

配置对比

Cluster 模式:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
# externalTrafficPolicy: Cluster # 默认,可省略
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

Local 模式:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 必须显式指定
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

不同 Service 类型的客户端 IP

ClusterIP 类型

特点:

1
type: ClusterIP  # 集群内部服务

客户端 IP 情况:

1
2
3
4
5
Pod A (10.244.1.5)

ClusterIP Service (10.96.0.10)

Pod B (看到源 IP: 10.244.1.5) ✅

结论:

  • ✅ 集群内访问始终保留源 IP
  • ✅ 无需配置 externalTrafficPolicy
  • ❌ 不支持 externalTrafficPolicy(会报错)

验证:

1
2
3
4
5
# 在 Pod A 中访问 ClusterIP Service
kubectl exec -it pod-a -- curl http://service-b

# Pod B 日志显示
# remote_addr: 10.244.1.5 ✅ 真实的 Pod A IP

NodePort 类型

特点:

1
type: NodePort

Cluster 模式(默认):

1
2
3
4
5
客户端外部 (1.2.3.4)

Node 1:30080 (10.0.1.1)
↓ [SNAT]
Pod (看到源 IP: 10.0.1.1) ❌

Local 模式:

1
2
3
4
5
客户端外部 (1.2.3.4)

Node 1:30080 (10.0.1.1)
↓ [不 SNAT]
Pod 在 Node 1 (看到源 IP: 1.2.3.4) ✅

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
externalTrafficPolicy: Local # 保留客户端 IP
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
nodePort: 30080

验证:

1
2
3
4
5
6
# 外部访问 NodePort
curl http://node-ip:30080

# Pod 日志
# Cluster 模式:remote_addr: 10.0.1.1 (节点 IP)
# Local 模式: remote_addr: 1.2.3.4 (客户端 IP) ✅

LoadBalancer 类型

特点:

1
type: LoadBalancer

流量路径:

1
2
3
4
5
6
7
客户端 (1.2.3.4)

Cloud LoadBalancer (公网IP)

NodePort (各节点)

Pod

配置:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 推荐
selector:
app: my-app
ports:
- port: 80
targetPort: 8080

注意事项:

云厂商支持情况说明
AWS ELB✅ 完全支持ALB/NLB 都支持
GCP✅ 完全支持-
Azure✅ 完全支持-
阿里云 SLB✅ 完全支持需要特定配置
腾讯云 CLB⚠️ 部分支持见下文特殊说明

特殊场景:经过代理

问题描述

场景:

1
2
3
4
5
6
7
真实客户端 (1.2.3.4)

CLB/SLB (Load Balancer IP)

Kubernetes Service (externalTrafficPolicy: Local)

Pod (看到源 IP: CLB/SLB IP) ❌

即使配置了 Local 模式,Pod 看到的仍是 LB 的 IP!

解决方案

方案1:使用 X-Forwarded-For 头

1
2
3
4
5
6
7
8
9
10
11
# Nginx 配置
server {
listen 80;

location / {
# 设置真实 IP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://backend;
}
}

应用层获取:

1
2
3
4
5
6
7
8
9
10
11
# Python Flask 示例
from flask import request

@app.route('/')
def index():
# 优先从 X-Forwarded-For 获取
if 'X-Forwarded-For' in request.headers:
client_ip = request.headers['X-Forwarded-For'].split(',')[0].strip()
else:
client_ip = request.remote_addr
return f"Client IP: {client_ip}"

方案2:Proxy Protocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# MetalLB 配置示例
apiVersion: v1
kind: ConfigMap
metadata:
name: config
namespace: metallb-system
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- 192.168.1.240-192.168.1.250
bgp-communities:
proxy-protocol:
- lb.proxy-protocol=true

Nginx 启用 Proxy Protocol:

1
2
3
4
5
6
7
8
9
10
11
12
13
http {
server {
listen 80 proxy_protocol;

# 使用 Proxy Protocol 传递的真实 IP
set_real_ip_from 10.0.0.0/8;
real_ip_header proxy_protocol;

location / {
proxy_pass http://backend;
}
}
}

参考文档:

详见站内文章《Nginx - remote_addr 的一些认识》

验证和测试

查看 kube-proxy 模式

1
2
3
4
5
# 查看 kube-proxy 模式
curl localhost:10249/proxyMode

# 输出
iptables # 或 ipvs

测试客户端 IP

部署测试应用:

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-server
spec:
replicas: 3
selector:
matchLabels:
app: echo-server
template:
metadata:
labels:
app: echo-server
spec:
containers:
- name: echo
image: hashicorp/http-echo
args:
- "-text=Hello from $(hostname)"
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: echo-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: echo-server
ports:
- port: 80
targetPort: 5678

测试命令:

1
2
3
4
5
6
7
8
9
# 测试 ClusterIP
kubectl run -it --rm test --image=busybox --restart=Never -- \
wget -qO- http://echo-service

# 测试 NodePort
curl http://node-ip:30080

# 测试 LoadBalancer
curl http://lb-external-ip

查看 Pod 日志:

1
kubectl logs -f echo-server-xxx | grep "remote_addr"

最佳实践

生产环境推荐配置

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
apiVersion: v1
kind: Service
metadata:
name: production-service
annotations:
# 云厂商特定注解(示例:AWS)
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
spec:
type: LoadBalancer
externalTrafficPolicy: Local # 保留客户端 IP
selector:
app: production-app
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: production-app
spec:
replicas: 6
selector:
matchLabels:
app: production-app
template:
metadata:
labels:
app: production-app
spec:
# Pod 反亲和性
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- production-app
topologyKey: kubernetes.io/hostname
containers:
- name: app
image: production-app:latest
ports:
- containerPort: 8080

监控建议

关键指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Prometheus 监控规则
groups:
- name: service-health
rules:
# 监控负载不均
- alert: UnbalancedLoad
expr: |
max(rate(http_requests_total[5m])) by (pod) /
avg(rate(http_requests_total[5m])) by (pod) > 2
for: 10m

# 监控无 Pod 节点
- alert: NodeWithoutPods
expr: |
sum(kube_pod_status_phase{phase="Running"}) by (node) == 0
for: 5m

总结

核心要点:

方面ClusterLocal
客户端 IP❌ 丢失✅ 保留
负载均衡✅ 好⚠️ 需优化
性能✅ 好✅ 更好
配置复杂度✅ 简单⚠️ 复杂
推荐场景简单应用需真实IP

选择建议:

1
2
需要客户端真实 IP → Local + 反亲和性
不需要真实 IP → Cluster(默认)

注意事项:

  1. ✅ Local 模式必须配合 LoadBalancer 或 NodePort
  2. ✅ 建议配置 Pod 反亲和性优化负载
  3. ✅ 经过代理需要额外处理(X-Forwarded-For)
  4. ✅ 监控负载均衡情况

参考资料