如何在集群的负载均衡过程保留请求源IP
Categories:
引言
应用部署不一定总是简单的安装和运行, 有时候还需要考虑网络的问题. 本文将介绍如何在k8s集群中使服务能获取到请求的源IP.
应用提供服务一般依赖输入信息, 输入信息如果不依赖五元组(源 IP, 源端口, 目的 IP, 目的端口, 协议), 那么该服务和网络耦合性低, 不需要关心网络细节.
因此, 对多数人来说都没有阅读本文的必要, 如果你对网络感兴趣, 或者希望拓宽一点视野, 可以继续阅读下文, 了解更多的服务场景.
本文基于 k8s v1.29.4
, 文中部分叙述混用了 pod 和 endpoint, 本文场景下可以视为等价.
如果有错误, 欢迎指正, 我会及时更正.
为什么源 IP 信息会丢失?
我们首先明确源 IP 是什么, 当 A 向 B 发送请求, B 将请求转发给 C, 虽然 C 看到的 IP 协议的源 IP 是 B 的 IP, 但本文把A的IP看作源 IP.
主要有两类行为会导致源信息丢失:
- 网络地址转换(NAT), 目的是节省公网 IPv4, 负载均衡等. 将导致服务端看到的源 IP 是 NAT 设备的 IP, 而不是真实的源 IP.
- 代理(Proxy), 反向代理(RP, Reverse Proxy)和负载均衡(LB, Load Balancer)都属于这一类, 下文统称代理服务器. 这类代理服务会将请求转发给后端服务, 但是会将源 IP 替换为自己的 IP.
- NAT 简单来说是以端口空间换IP空间, IPv4 地址有限, 一个 IP 地址可以映射 65535 个端口, 绝大多数时候这些端口没有用完, 因而可以多个子网 IP 共用一个公网 IP, 在端口上区分不同的服务. 其使用形式是:
public IP:public port -> private IP_1:private port
, 更多内容请自行参阅网络地址转换 - 代理服务是为了隐藏或暴露, 代理服务会将请求转发给后端服务, 同时将源 IP 替换为自己的 IP, 以此来隐藏后端服务的真实 IP, 保护后端服务的安全. 代理服务的使用形式是:
client IP -> proxy IP -> server IP
, 更多内容请自行参阅代理
NAT和代理服务器都非常常见, 多数服务都无法获得请求的源 IP.
这是常见的两类修改源 IP 的途径, 如有其它欢迎补充.
如何保留源 IP?
以下是一个 HTTP 请求
的例子:
字段 | 长度(字节) | 位偏移 | 描述 |
---|---|---|---|
IP 首部 | |||
源 IP | 4 | 0-31 | 发送方的 IP 地址 |
目的 IP | 4 | 32-63 | 接收方的 IP 地址 |
TCP 首部 | |||
源端口 | 2 | 0-15 | 发送端口号 |
目的端口 | 2 | 16-31 | 接收端口号 |
序列号 | 4 | 32-63 | 用于标识发送方发送的数据的字节流 |
确认号 | 4 | 64-95 | 如果设置了 ACK 标志,则为下一个期望收到的序列号 |
数据偏移 | 4 | 96-103 | 数据起始位置相对于 TCP 首部的字节数 |
保留 | 4 | 104-111 | 保留字段,未使用,设置为 0 |
标志位 | 2 | 112-127 | 各种控制标志,如 SYN、ACK、FIN 等 |
窗口大小 | 2 | 128-143 | 接收方可以接收的数据量 |
检验和 | 2 | 144-159 | 用于检测数据是否在传输过程中发生了错误 |
紧急指针 | 2 | 160-175 | 发送方希望接收方尽快处理的紧急数据的位置 |
选项 | 可变 | 176-… | 可能包括时间戳、最大报文段长度等 |
HTTP 首部 | |||
请求行 | 可变 | …-… | 包括请求方法、URI 和 HTTP 版本 |
头部字段 | 可变 | …-… | 包含各种头部字段,如 Host、User-Agent 等 |
空行 | 2 | …-… | 用于分隔头部和主体部分 |
主体 | 可变 | …-… | 可选的请求或响应正文 |
浏览以上 HTTP 请求结构, 可以发现, 有TCP选项,请求行, 头部字段,主体是可变的, 其中TCP选项空间有限, 一般不会用来传递源 IP, 请求行携带信息固定不能扩展, HTTP主体加密后不能修改, 只有HTTP 头部字段
适合扩展传递源 IP.
HTTP header 中可以增加X-REAL-IP
字段, 用来传递源 IP, 这个操作通常放在代理服务器上, 然后代理服务器会将请求发送给后端服务, 后端服务就可以通过这个字段获取到源 IP 信息.
注意, 需要保证代理服务器在NAT设备之前, 这样才能获取到真实的请求的源 whoami. 我们可以在阿里云的产品中看到负载均衡器这个商品单独品类, 它在网络中的位置不同于普通的应用服务器.
K8S 操作指导
以whoami项目为例进行部署.
创建 Deployment
首先创建服务:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami-deployment
spec:
replicas: 3
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: docker.io/traefik/whoami:latest
ports:
- containerPort: 8080
这步会创建一个Deployment, 里面包含 3 个Pod, 每个 pod 包含一个容器, 该容器会运行whoami服务.
创建 Service
可以创建NodePort或者LoadBalancer类型的服务, 支持外部访问, 或者创建ClusterIP类型的服务, 仅支持集群内部访问, 再增加Ingress服务, 通过Ingress服务暴露外部访问.
NodePort既可以通过NodeIP:NodePort访问, 也可以通过Ingess服务访问, 方便测试, 本节使用NodePort服务.
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
创建服务后, 以curl whoami.example.com:30002
访问, 可以看到返回的 IP 是NodeIP, 而不是请求的源 whoami.
请注意,这并不是正确的客户端 IP,它们是集群的内部 IP。这是所发生的事情:
- 客户端发送数据包到 node2:nodePort
- node2 使用它自己的 IP 地址替换数据包的源 IP 地址(SNAT)
- node2 将数据包上的目标 IP 替换为 Pod IP
- 数据包被路由到 node1,然后到端点
- Pod 的回复被路由回 node2
- Pod 的回复被发送回给客户端
用图表示:
配置 externalTrafficPolicy: Local
为避免这种情况,Kubernetes 有一个特性可以保留客户端源 IP。 如果将 service.spec.externalTrafficPolicy 设置为 Local, kube-proxy 只会将代理请求代理到本地端点,而不会将流量转发到其他节点。
apiVersion: v1
kind: Service
metadata:
name: whoami-service
spec:
type: NodePort
externalTrafficPolicy: Local
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 8080
nodePort: 30002
使用curl whoami.example.com:30002
进行测试, 当whoami.example.com映射到集群多个 node 的 IP 时, 有一定比例的几率无法访问. 需要确认域名记录只含有 endpoint(pod)所在 node(节点)的 ip.
这个配置有其代价, 那就是失去了集群内的负载均衡能力, 客户端只有访问部署了 endpoint 的 node 才会得到响应.
当客户端访问 Node 2 时, 不会有响应.
创建 Ingress
多数服务提供给用户时使用 http/https, https://ip:port的形式可能让用户感到陌生. 一般会使用Ingress将上文创建的NodePort服务负载到一个域名的 80/443 端口下.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami-ingress
namespace: default
spec:
ingressClassName: external-lb-default
rules:
- host: whoami.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami-service
port:
number: 80
应用后, 使用curl whoami.example.com
访问测试, 可以看到 ClientIP 总是 endpoint 所在节点上的Ingress Controller
的 Pod IP.
root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...
root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7 1/1 Running 0 3d2h 10.42.1.10 k3s-agent-1 <none> <none>
使用Ingress反向代理NodePort服务, 也就是在 endpoint 前套了两层 service, 下图展示了二者区别.
graph LR A[Client] -->|whoami.example.com:80| B(Ingress) B -->|10.43.38.129:32123| C[Service] C -->|10.42.1.1:8080| D[Endpoint]
graph LR A[Client] -->|whoami.example.com:30001| B(Service) B -->|10.42.1.1:8080| C[Endpoint]
在路径 1 中, 外部访问 Ingress 时, 流量先到达的 endpoint 是Ingress Controller
, 然后再到达 endpoint whoami.
而Ingress Controller实质是一个LoadBalancer的服务,
kubectl -n ingress-nginx get svc
NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE
default echoip-ingress nginx ip.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 18h
default whoami-ingress nginx whoami.example.com 172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707 80 16h
因此, 可以通过将前文提到的externalTrafficPolicy
设置到 Ingress Controller 中来保留源 IP.
同时还需要设置ingress-nginx-controller
的configmap
中的use-forwarded-headers
为true
, 以便Ingress Controller能够识别X-Forwarded-For
或X-REAL-IP
字段.
apiVersion: v1
data:
allow-snippet-annotations: "false"
compute-full-forwarded-for: "true"
use-forwarded-headers: "true"
enable-real-ip: "true"
forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.10.1
name: ingress-nginx-controller
namespace: ingress-nginx
NodePort服务与ingress-nginx-controller服务的区别主要在于, NodePort的后端通常不部署在每台 node 上, 而ingress-nginx-controller的后端通常部署在每台对外暴露的 node 上.
与NodePort服务中设置externalTrafficPolicy
会导致跨 node 的请求无响应不同, Ingress可以将请求先设置 HEADER 之后再进行代理转发, 实现了保留源 IP和负载均衡的两种能力.
总结
- 地址转换(NAT), 代理(Proxy),反向代理(Reverse Proxy), **负载均衡(Load Balance)**会导致源 IP 丢失.
- 为防止源 IP 丢失, 可以代理服务器转发时将真实 IP 设置在 HTTP 头部字段
X-REAL-IP
中, 通过代理服务传递. 如果使用多层代理, 则可以使用X-Forwarded-For
字段, 该字段以栈的形式记录了源 IP 及代理路径的 IP list. - 集群NodePort服务设置
externalTrafficPolicy: Local
可以保留源 IP, 但会失去负载均衡能力. - ingress-nginx-controller以daemonset形式部署在所有loadbalancer角色 node 上的前提下, 设置
externalTrafficPolicy: Local
可以保留源 IP, 且保留负载均衡能力.