使用 Source IP

    你必须拥有一个 Kubernetes 的集群,同时你的 Kubernetes 集群必须带有 kubectl 命令行工具。 如果你还没有集群,你可以通过 构建一 个你自己的集群,或者你可以使用下面任意一个 Kubernetes 工具构建:

    要获知版本信息,请输入 .

    术语表

    本文使用了下列术语:

    • NAT: 网络地址转换
    • : 替换数据包的源 IP, 通常为节点的 IP
    • Destination NAT: 替换数据包的目的 IP, 通常为 Pod 的 IP
    • : 一个虚拟 IP, 例如分配给每个 Kubernetes Service 的 IP
    • Kube-proxy: 一个网络守护程序,在每个节点上协调 Service VIP 管理

    准备工作

    你必须拥有一个正常工作的 Kubernetes 1.5 集群来运行此文档中的示例。该示例使用一个简单的 nginx webserver,通过一个HTTP消息头返回它接收到请求的源IP。你可以像下面这样创建它:

    输出结果为

    1. deployment.apps/source-ip-app created
    • 通过多种类型的 Services 暴露一个简单应用
    • 理解每种 Service 类型如何处理源 IP NAT
    • 理解保留源IP所涉及的折中

    Type=ClusterIP 类型 Services 的 Source IP

    如果你的 kube-proxy 运行在 下,从集群内部发送到 ClusterIP 的包永远不会进行源地址 NAT,这从 Kubernetes 1.2 开始是默认选项。Kube-proxy 通过一个 proxyMode endpoint 暴露它的模式。

    1. kubectl get nodes

    输出结果与以下结果类似:

    1. NAME STATUS ROLES AGE VERSION
    2. kubernetes-node-6jst Ready <none> 2h v1.13.0
    3. kubernetes-node-cx31 Ready <none> 2h v1.13.0
    4. kubernetes-node-jj1t Ready <none> 2h v1.13.0

    从其中一个节点中得到代理模式

    1. kubernetes-node-6jst $ curl localhost:10249/proxyMode

    输出结果为:

    1. iptables

    你可以通过在source IP应用上创建一个Service来测试源IP保留。

    1. kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

    输出结果为:

    1. service/clusterip exposed
    1. kubectl get svc clusterip

    输出结果与以下结果类似:

    1. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    2. clusterip ClusterIP 10.0.170.92 <none> 80/TCP 51s

    从相同集群中的一个 pod 访问这个 ClusterIP

    1. kubectl run busybox -it --image=busybox --restart=Never --rm

    输出结果与以下结果类似:

    1. Waiting for pod default/busybox to be running, status is Pending, pod ready: false
    2. If you don't see a command prompt, try pressing enter.
    3. # ip addr
    4. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    5. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    6. inet 127.0.0.1/8 scope host lo
    7. valid_lft forever preferred_lft forever
    8. valid_lft forever preferred_lft forever
    9. 3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    10. link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    11. inet 10.244.3.8/24 scope global eth0
    12. valid_lft forever preferred_lft forever
    13. inet6 fe80::188a:84ff:feb0:26a5/64 scope link
    14. valid_lft forever preferred_lft forever
    15. # wget -qO - 10.0.170.92
    16. CLIENT VALUES:
    17. client_address=10.244.3.8
    18. ...

    无论客户端 pod 和 服务端 pod 是否在相同的节点上,client_address 始终是客户端 pod 的 IP 地址。

    Type=NodePort 类型 Services 的 Source IP

    从 Kubernetes 1.5 开始,发送给类型为 Type=NodePort Services 的数据包默认进行源地址 NAT。你可以通过创建一个 NodePort Service 来进行测试:

    1. kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

    输出结果为:

    1. service/nodeport exposed
    1. for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

    输出结果与以下结果类似:

    1. client_address=10.180.1.1
    2. client_address=10.240.0.5
    3. client_address=10.240.0.3

    请注意,这些并不是正确的客户端 IP,它们是集群的内部 IP。这是所发生的事情:

    • 客户端发送数据包到 node2:nodePort
    • node2 使用它自己的 IP 地址替换数据包的源 IP 地址(SNAT)
    • node2 使用 pod IP 地址替换数据包的目的 IP 地址
    • 数据包被路由到 node 1,然后交给 endpoint
    • Pod 的回复被路由回 node2
    • Pod 的回复被发送回给客户端

    用图表示:

    1. client
    2. \ ^
    3. \ \
    4. v \
    5. node 1 <--- node 2
    6. | ^ SNAT
    7. | | --->
    8. v |
    9. endpoint

    为了防止这种情况发生,Kubernetes 提供了一个特性来保留客户端的源 IP 地址。设置 service.spec.externalTrafficPolicy 的值为 Local,请求就只会被代理到本地 endpoints 而不会被转发到其它节点。这样就保留了最初的源 IP 地址。如果没有本地 endpoints,发送到这个节点的数据包将会被丢弃。这样在应用到数据包的任何包处理规则下,你都能依赖这个正确的 source-ip 使数据包通过并到达 endpoint。

    设置 service.spec.externalTrafficPolicy 字段如下:

    1. kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

    输出结果为:

    1. service/nodeport patched

    现在,重新运行测试:

    1. for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

    输出结果为:

    1. client_address=104.132.1.79

    请注意,你只从 endpoint pod 运行的那个节点得到了一个回复,这个回复有正确的客户端 IP。

    这是发生的事情:

    • 客户端发送数据包到 node2:nodePort,它没有任何 endpoints
    • 数据包被丢弃
    • 客户端发送数据包到 node1:nodePort,它endpoints

    用图表示:

    1. client
    2. ^ / \
    3. / / \
    4. / v X
    5. node 1 node 2
    6. ^ |
    7. | v
    8. endpoint

    从Kubernetes1.5开始,发送给类型为 Type=LoadBalancer Services 的数据包默认进行源地址 NAT,这是因为所有处于 Ready 状态的可调度 Kubernetes 节点对于负载均衡的流量都是符合条件的。所以如果数据包到达一个没有 endpoint 的节点,系统将把这个包代理到 endpoint 的节点,并替换数据包的源 IP 为节点的 IP(如前面章节所述)。

    你可以通过在一个 loadbalancer 上暴露这个 source-ip-app 来进行测试。

    1. kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

    输出结果为:

    1. service/loadbalancer exposed

    打印Service的IPs:

    1. kubectl get svc loadbalancer

    输出结果与以下结果类似:

    1. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    2. loadbalancer LoadBalancer 10.0.65.118 104.198.149.140 80/TCP 5m
    1. curl 104.198.149.140

    输出结果与以下结果类似:

    用图表示:

    1. client
    2. |
    3. lb VIP
    4. / ^
    5. v /
    6. health check ---> node 1 node 2 <--- health check
    7. 200 <--- ^ | ---> 500
    8. | V
    9. endpoint

    你可以设置 annotation 来进行测试:

    1. kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

    你应该能够立即看到 Kubernetes 分配的 service.spec.healthCheckNodePort 字段:

    1. kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

    输出结果与以下结果类似:

    1. healthCheckNodePort: 32122

    service.spec.healthCheckNodePort 字段指向每个节点在 /healthz 路径上提供的用于健康检查的端口。你可以这样测试:

    1. kubectl get pod -o wide -l run=source-ip-app

    输出结果与以下结果类似:

    1. NAME READY STATUS RESTARTS AGE IP NODE
    2. source-ip-app-826191075-qehz4 1/1 Running 0 20h 10.180.1.136 kubernetes-node-6jst

    使用 curl 命令发送请求到每个节点的 /healthz 路径。

    1. kubernetes-node-6jst $ curl localhost:32122/healthz

    输出结果与以下结果类似:

    1. 1 Service Endpoints found
    1. kubernetes-node-jj1t $ curl localhost:32122/healthz

    输出结果与以下结果类似:

    1. No Service Endpoints Found

    主节点运行的 service 控制器负责分配 cloud loadbalancer。在这样做的同时,它也会分配指向每个节点的 HTTP 健康检查的 port/path。等待大约 10 秒钟之后,没有 endpoints 的两个节点的健康检查会失败,然后 curl 负载均衡器的 ip:

    1. curl 104.198.149.140

    输出结果与以下结果类似:

    1. CLIENT VALUES:
    2. client_address=104.132.1.79
    3. ...

    跨平台支持

    从 Kubernetes 1.5 开始,通过类型为 Type=LoadBalancer 的 Services 进行源 IP 保存的支持仅在一部分 cloudproviders 中实现(GCP and Azure)。你的集群运行的 cloudprovider 可能以某些不同的方式满足 loadbalancer 的要求:

    1. 使用一个代理终止客户端连接并打开一个到你的 nodes/endpoints 的新连接。在这种情况下,源 IP 地址将永远是云负载均衡器的地址而不是客户端的。

    2. 使用一个包转发器,因此从客户端发送到负载均衡器 VIP 的请求在拥有客户端源 IP 地址的节点终止,而不被中间代理。

    第一类负载均衡器必须使用一种它和后端之间约定的协议来和真实的客户端 IP 通信,例如 HTTP 头,或者 proxy 协议。 第二类负载均衡器可以通过简单的在保存于 Service 的 字段上创建一个 HTTP 健康检查点来使用上面描述的特性。

    清理现场

    删除服务:

    1. $ kubectl delete svc -l run=source-ip-app

    接下来

    • 学习更多关于