使用源 IP
运行在 Kubernetes 集群中的应用程序通过 Service 抽象找到彼此并进行通信,并与外部世界交互。本文档解释了发送到不同类型 Service 的数据包的源 IP 会发生什么变化,以及如何根据需要切换此行为。
开始之前
术语
本文档使用了以下术语
- NAT
- 网络地址转换
- 源 NAT
- 替换数据包中的源 IP;在本页面中,这通常意味着将其替换为 Node 的 IP 地址。
- 目标 NAT
- 替换数据包中的目标 IP;在本页面中,这通常意味着将其替换为 Pod 的 IP 地址。
- VIP
- 虚拟 IP 地址,例如分配给 Kubernetes 中每个 Service 的 VIP。
- kube-proxy
- 在每个 Node 上协调管理 Service VIP 的网络守护进程。
前提条件
你需要有一个 Kubernetes 集群,并且 kubectl 命令行工具必须配置为与你的集群通信。建议在至少有两个非控制平面主机的 Node 的集群上运行本教程。如果你还没有集群,可以使用 minikube 创建一个,或者使用以下 Kubernetes 演练场之一:
示例使用了一个小型 nginx Web 服务器,它通过 HTTP 头部将接收到的请求的源 IP 回显出来。你可以按如下方式创建它:
说明
以下命令中的镜像仅在 AMD64 架构上运行。kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10
输出如下:
deployment.apps/source-ip-app created
目标
- 通过不同类型的 Service 暴露一个简单的应用程序
- 理解每种 Service 类型如何处理源 IP NAT
- 理解保留源 IP 的权衡
Type=ClusterIP
的 Service 源 IP
如果你以 iptables 模式 运行 kube-proxy(默认模式),则从集群内部发送到 ClusterIP 的数据包永远不会被源 NAT。你可以在运行 kube-proxy 的节点上获取 http://localhost:10249/proxyMode
来查询 kube-proxy 的模式。
kubectl get nodes
输出类似于:
NAME STATUS ROLES AGE VERSION
kubernetes-node-6jst Ready <none> 2h v1.13.0
kubernetes-node-cx31 Ready <none> 2h v1.13.0
kubernetes-node-jj1t Ready <none> 2h v1.13.0
获取其中一个节点上的代理模式(kube-proxy 监听端口 10249)
# Run this in a shell on the node you want to query.
curl http://localhost:10249/proxyMode
输出如下:
iptables
你可以通过在源 IP 应用上创建一个 Service 来测试源 IP 保留。
kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080
输出如下:
service/clusterip exposed
kubectl get svc clusterip
输出类似于:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
clusterip ClusterIP 10.0.170.92 <none> 80/TCP 51s
并从同一集群中的 Pod 访问 ClusterIP
。
kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm
输出类似于:
Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.
然后你可以在该 Pod 内运行一个命令:
# Run this inside the terminal from "kubectl run"
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
inet 10.244.3.8/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::188a:84ff:feb0:26a5/64 scope link
valid_lft forever preferred_lft forever
…然后使用 wget
查询本地 Web 服务器。
# Replace "10.0.170.92" with the IPv4 address of the Service named "clusterip"
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...
client_address
始终是客户端 Pod 的 IP 地址,无论客户端 Pod 和服务器 Pod 是否在同一 Node 上。
Type=NodePort
的 Service 源 IP
发送到 Type=NodePort
的 Service 的数据包默认会被源 NAT。你可以通过创建 NodePort
Service 来测试这一点:
kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort
输出如下:
service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')
如果你在云厂商上运行,你可能需要为上面报告的 nodes:nodeport
打开防火墙规则。现在你可以尝试从集群外部通过上面分配的节点端口访问 Service。
for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done
输出类似于:
client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3
注意,这些不是正确的客户端 IP,它们是集群内部 IP。这是发生的情况:
- 客户端发送数据包到
node2:nodePort
node2
将数据包中的源 IP 地址(SNAT)替换为其自己的 IP 地址node2
将数据包中的目标 IP 替换为 Pod IP- 数据包被路由到 Node 1,然后到 Endpoint
- Pod 的回复被路由回 node2
- Pod 的回复被发送回客户端
图示
图. Type=NodePort 使用 SNAT 的源 IP
为了避免这种情况,Kubernetes 有一个 保留客户端源 IP 的特性。如果你将 service.spec.externalTrafficPolicy
设置为 Local
值,kube-proxy 将仅代理请求到本地 Endpoints,并且不会将流量转发到其他 Node。这种方法保留了原始源 IP 地址。如果没有本地 Endpoints,发送到节点的 数据包将被丢弃,因此你可以在应用到到达 Endpoint 的数据包的任何数据包处理规则中依赖正确的源 IP。
按如下方式设置 service.spec.externalTrafficPolicy
字段:
kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'
输出如下:
service/nodeport patched
现在,重新运行测试:
for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done
输出类似于:
client_address=198.51.100.79
注意,你只收到一个回复,其中包含 正确的 客户端 IP,来自于运行 Endpoint Pod 的那个节点。
这是发生的情况:
- 客户端发送数据包到
node2:nodePort
,该节点没有 Endpoints - 数据包被丢弃
- 客户端发送数据包到
node1:nodePort
,该节点 有 Endpoints - node1 将数据包路由到具有正确源 IP 的 Endpoint
图示
图. Type=NodePort 保留客户端源 IP 地址
Type=LoadBalancer
的 Service 源 IP
发送到 Type=LoadBalancer
的 Service 的数据包默认会被源 NAT,因为所有处于 Ready
状态的可调度 Kubernetes 节点都适合负载均衡流量。因此,如果数据包到达一个没有 Endpoint 的节点,系统会将其代理到 有 Endpoint 的节点,并将数据包中的源 IP 替换为该节点的 IP(如前一节所述)。
你可以通过负载均衡器暴露 source-ip-app 来测试这一点。
kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer
输出如下:
service/loadbalancer exposed
打印出 Service 的 IP 地址:
kubectl get svc loadbalancer
输出类似于:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
loadbalancer LoadBalancer 10.0.65.118 203.0.113.140 80/TCP 5m
接下来,向此 Service 的 external-ip 发送请求:
curl 203.0.113.140
输出类似于:
CLIENT VALUES:
client_address=10.240.0.5
...
然而,如果你在 Google Kubernetes Engine/GCE 上运行,将相同的 service.spec.externalTrafficPolicy
字段设置为 Local
会强制 没有 Service Endpoints 的节点通过故意使其健康检查失败来从符合负载均衡流量条件的节点列表中移除自己。
图示
你可以通过设置注解来测试这一点:
kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'
你应该立即看到 Kubernetes 分配的 service.spec.healthCheckNodePort
字段。
kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort
输出类似于:
healthCheckNodePort: 32122
service.spec.healthCheckNodePort
字段指向每个节点上提供 /healthz
健康检查的端口。你可以测试这一点:
kubectl get pod -o wide -l app=source-ip-app
输出类似于:
NAME READY STATUS RESTARTS AGE IP NODE
source-ip-app-826191075-qehz4 1/1 Running 0 20h 10.180.1.136 kubernetes-node-6jst
使用 curl
在各个节点上抓取 /healthz
Endpoint:
# Run this locally on a node you choose
curl localhost:32122/healthz
1 Service Endpoints found
在不同的节点上你可能会得到不同的结果:
# Run this locally on a node you choose
curl localhost:32122/healthz
No Service Endpoints Found
在 控制平面 上运行的控制器负责分配云负载均衡器。同一个控制器还会分配指向每个节点上此端口/路径的 HTTP 健康检查。等待大约 10 秒,直到没有 Endpoints 的 2 个节点健康检查失败,然后使用 curl
查询负载均衡器的 IPv4 地址:
curl 203.0.113.140
输出类似于:
CLIENT VALUES:
client_address=198.51.100.79
...
跨平台支持
只有部分云厂商支持通过 Type=LoadBalancer
的 Service 保留源 IP。你正在使用的云厂商可能会以几种不同的方式实现负载均衡器请求:
通过一个代理,该代理终止客户端连接并打开一个新连接到你的节点/Endpoints。在这种情况下,源 IP 始终是云 LB 的 IP,而不是客户端的 IP。
通过一个数据包转发器,这样客户端发送到负载均衡器 VIP 的请求最终会到达带有客户端源 IP 的节点,而不是中间代理。
第一类负载均衡器必须使用负载均衡器和后端之间商定的协议来传递真实的客户端 IP,例如 HTTP 的 Forwarded 或 X-FORWARDED-FOR 头部,或者 代理协议。第二类负载均衡器可以利用上述特性,通过创建一个 HTTP 健康检查指向 Service 的 service.spec.healthCheckNodePort
字段中存储的端口。
清理
删除 Service
kubectl delete svc -l app=source-ip-app
删除 Deployment、ReplicaSet 和 Pod
kubectl delete deployment source-ip-app
下一步
- 了解更多关于 通过 Service 连接应用 的信息。
- 阅读如何 创建外部负载均衡器。