Black lives matter.
We stand in solidarity with the Black community.
Racism is unacceptable.
It conflicts with the core values of the Kubernetes project and our community does not tolerate it.
We stand in solidarity with the Black community.
Racism is unacceptable.
It conflicts with the core values of the Kubernetes project and our community does not tolerate it.
Kubernetes 集群中运行的应用通过 Service 抽象来互相查找、通信和与外部世界沟通。本文介绍被发送到不同类型 Services 的数据包源 IP 的变化过程,你可以根据你的需求改变这些行为。
你必须拥有一个 Kubernetes 的集群,同时你的 Kubernetes 集群必须带有 kubectl 命令行工具。 如果你还没有集群,你可以通过 Minikube 构建一 个你自己的集群,或者你可以使用下面任意一个 Kubernetes 工具构建:
要获知版本信息,请输入kubectl version
.
本文使用了下列术语:
你必须拥有一个正常工作的 Kubernetes 1.5 集群来运行此文档中的示例。该示例使用一个简单的 nginx webserver,通过一个HTTP消息头返回它接收到请求的源IP。你可以像下面这样创建它:
kubectl run source-ip-app --image=k8s.gcr.io/echoserver:1.4
输出结果为
deployment.apps/source-ip-app created
如果你的 kube-proxy 运行在 iptables 模式下,从集群内部发送到 ClusterIP 的包永远不会进行源地址 NAT,这从 Kubernetes 1.2 开始是默认选项。Kube-proxy 通过一个 proxyMode
endpoint 暴露它的模式。
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
从其中一个节点中得到代理模式
kubernetes-node-6jst $ curl localhost:10249/proxyMode
输出结果为:
iptables
你可以通过在source 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 --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.
# 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 -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...
无论客户端 pod 和 服务端 pod 是否在相同的节点上,client_address 始终是客户端 pod 的 IP 地址。
从 Kubernetes 1.5 开始,发送给类型为 Type=NodePort Services 的数据包默认进行源地址 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=="ExternalIP")].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 地址替换数据包的源 IP 地址(SNAT)node2
使用 pod IP 地址替换数据包的目的 IP 地址用图表示:
client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint
为了防止这种情况发生,Kubernetes 提供了一个特性来保留客户端的源 IP 地址(点击此处查看可用特性)。设置 service.spec.externalTrafficPolicy
的值为 Local
,请求就只会被代理到本地 endpoints 而不会被转发到其它节点。这样就保留了最初的源 IP 地址。如果没有本地 endpoints,发送到这个节点的数据包将会被丢弃。这样在应用到数据包的任何包处理规则下,你都能依赖这个正确的 source-ip 使数据包通过并到达 endpoint。
设置 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=104.132.1.79
请注意,你只从 endpoint pod 运行的那个节点得到了一个回复,这个回复有正确的客户端 IP。
这是发生的事情:
node2:nodePort
,它没有任何 endpointsnode1:nodePort
,它有endpoints用图表示:
client
^ / \
/ / \
/ v X
node 1 node 2
^ |
| |
| v
endpoint
从Kubernetes1.5开始,发送给类型为 Type=LoadBalancer Services 的数据包默认进行源地址 NAT,这是因为所有处于 Ready
状态的可调度 Kubernetes 节点对于负载均衡的流量都是符合条件的。所以如果数据包到达一个没有 endpoint 的节点,系统将把这个包代理到有 endpoint 的节点,并替换数据包的源 IP 为节点的 IP(如前面章节所述)。
你可以通过在一个 loadbalancer 上暴露这个 source-ip-app 来进行测试。
kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer
输出结果为:
service/loadbalancer exposed
打印Service的IPs:
kubectl get svc loadbalancer
输出结果与以下结果类似:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
loadbalancer LoadBalancer 10.0.65.118 104.198.149.140 80/TCP 5m
curl 104.198.149.140
输出结果与以下结果类似:
CLIENT VALUES:
client_address=10.240.0.5
...
然而,如果你的集群运行在 Google Kubernetes Engine/GCE 上,可以通过设置 service.spec.externalTrafficPolicy 字段值为 Local ,故意导致健康检查失败来强制使没有 endpoints 的节点把自己从负载均衡流量的可选节点列表中删除。
用图表示:
client
|
lb VIP
/ ^
v /
health check ---> node 1 node 2 <--- health check
200 <--- ^ | ---> 500
| V
endpoint
你可以设置 annotation 来进行测试:
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 run=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
路径。
kubernetes-node-6jst $ curl localhost:32122/healthz
输出结果与以下结果类似:
1 Service Endpoints found
kubernetes-node-jj1t $ curl localhost:32122/healthz
输出结果与以下结果类似:
No Service Endpoints Found
主节点运行的 service 控制器负责分配 cloud loadbalancer。在这样做的同时,它也会分配指向每个节点的 HTTP 健康检查的 port/path。等待大约 10 秒钟之后,没有 endpoints 的两个节点的健康检查会失败,然后 curl 负载均衡器的 ip:
curl 104.198.149.140
输出结果与以下结果类似:
CLIENT VALUES:
client_address=104.132.1.79
...
跨平台支持
从 Kubernetes 1.5 开始,通过类型为 Type=LoadBalancer 的 Services 进行源 IP 保存的支持仅在一部分 cloudproviders 中实现(GCP and Azure)。你的集群运行的 cloudprovider 可能以某些不同的方式满足 loadbalancer 的要求:
使用一个代理终止客户端连接并打开一个到你的 nodes/endpoints 的新连接。在这种情况下,源 IP 地址将永远是云负载均衡器的地址而不是客户端的。
使用一个包转发器,因此从客户端发送到负载均衡器 VIP 的请求在拥有客户端源 IP 地址的节点终止,而不被中间代理。
第一类负载均衡器必须使用一种它和后端之间约定的协议来和真实的客户端 IP 通信,例如 HTTP X-FORWARDED-FOR 头,或者 proxy 协议。
第二类负载均衡器可以通过简单的在保存于 Service 的 service.spec.healthCheckNodePort
字段上创建一个 HTTP 健康检查点来使用上面描述的特性。
删除服务:
$ kubectl delete svc -l run=source-ip-app
删除 Deployment、ReplicaSet 和 Pod:
$ kubectl delete deployment source-ip-app