如何在集群的负载均衡过程保留请求源IP

2024-05-30 16:23:49 浏览数 (1)

引言

应用部署不一定总是简单的安装运行, 有时候还需要考虑网络的问题. 本文将介绍如何在k8s集群中使服务能获取到请求的源IP.

应用提供服务一般依赖输入信息, 输入信息如果不依赖五元组(源 IP, 源端口, 目的 IP, 目的端口, 协议), 那么该服务和网络耦合性低, 不需要关心网络细节.

因此, 对多数人来说都没有阅读本文的必要, 如果你对网络感兴趣, 或者希望拓宽一点视野, 可以继续阅读下文, 了解更多的服务场景.

本文基于 k8s v1.29.4, 文中部分叙述混用了 pod 和 endpoint, 本文场景下可以视为等价.

如果有错误, 欢迎指正, 我会及时更正.

为什么源 IP 信息会丢失?

我们首先明确源 IP 是什么, 当 A 向 B 发送请求, B 将请求转发给 C, 虽然 C 看到的 IP 协议的源 IP 是 B 的 IP, 但本文把A的IP看作源 IP.

主要有两类行为会导致源信息丢失:

  1. 网络地址转换(NAT), 目的是节省公网 IPv4, 负载均衡等. 将导致服务端看到的源 IP 是 NAT 设备的 IP, 而不是真实的源 IP.
  2. 代理(Proxy), 反向代理(RP, Reverse Proxy)和负载均衡(LB, Load Balancer)都属于这一类, 下文统称代理服务器. 这类代理服务会将请求转发给后端服务, 但是会将源 IP 替换为自己的 IP.
  3. NAT 简单来说是以端口空间换IP空间, IPv4 地址有限, 一个 IP 地址可以映射 65535 个端口, 绝大多数时候这些端口没有用完, 因而可以多个子网 IP 共用一个公网 IP, 在端口上区分不同的服务. 其使用形式是: public IP:public port -> private IP_1:private port, 更多内容请自行参阅网络地址转换
  4. 代理服务是为了隐藏或暴露, 代理服务会将请求转发给后端服务, 同时将源 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

首先创建服务:

代码语言:yaml复制
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服务.

代码语言:yaml复制
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 只会将代理请求代理到本地端点,而不会将流量转发到其他节点。

代码语言:yaml复制
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 端口下.

代码语言:yaml复制
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.

代码语言:shell复制
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, 下图展示了二者区别.

代码语言:mermaid复制
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]
代码语言:mermaid复制
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的服务,

代码语言:shell复制
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-controllerconfigmap中的use-forwarded-headerstrue, 以便Ingress Controller能够识别X-Forwarded-ForX-REAL-IP字段.

代码语言:yaml复制
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-controllerdaemonset形式部署在所有loadbalancer角色 node 上的前提下, 设置externalTrafficPolicy: Local可以保留源 IP, 且保留负载均衡能力.

参考

  • Kubernetes 使用源 IP
  • Ingress-Nginx Controller:ConfigMap
  • Ingress Controller

0 人点赞