Cómo conservar la IP de origen de las solicitudes después de la equilibración de carga en un clúster de K8s

Introducción

El despliegue de aplicaciones no siempre es simplemente instalar y ejecutar, a veces también es necesario considerar problemas de red. Este artículo explicará cómo hacer que los servicios en un clúster de k8s puedan obtener la IP de origen de la solicitud.

Las aplicaciones que proporcionan servicios generalmente dependen de la información de entrada. Si la información de entrada no depende de la quíntuple (IP de origen, puerto de origen, IP de destino, puerto de destino, protocolo), entonces el servicio tiene una baja acoplamiento con la red y no necesita preocuparse por los detalles de la red.

Por lo tanto, la mayoría de las personas no necesitan leer este artículo. Si estás interesado en la red o deseas ampliar tu visión, puedes continuar leyendo para conocer más escenarios de servicios.

Este artículo se basa en k8s v1.29.4. Algunas descripciones en el artículo mezclan pod y endpoint, pero en el escenario de este artículo se pueden considerar equivalentes.

Si hay errores, bienvenidas las correcciones, las actualizaré oportunamente.

¿Por qué se pierde la información de la IP de origen?

Primero aclaramos qué es la IP de origen. Cuando A envía una solicitud a B, y B la reenvía a C, aunque C ve la IP de origen del protocolo IP como la IP de B, en este artículo consideramos la IP de A como la IP de origen.

Hay principalmente dos tipos de comportamientos que causan la pérdida de la información de origen:

  1. Traducción de direcciones de red (NAT), cuyo propósito es ahorrar IPv4 públicas, equilibración de carga, etc. Esto hace que el servidor vea la IP del dispositivo NAT como la IP de origen, en lugar de la IP de origen real.
  2. Proxy, proxy inverso (RP, Reverse Proxy) y equilibrador de carga (LB, Load Balancer) pertenecen a esta categoría, denominados colectivamente servidores proxy a continuación. Estos servicios proxy reenvían las solicitudes al servicio backend, pero reemplazan la IP de origen con su propia IP.
  • NAT, en términos simples, es intercambiar espacio de puertos por espacio de IP. Las direcciones IPv4 son limitadas; una dirección IP puede mapear 65535 puertos. En la mayoría de los casos, estos puertos no se agotan, por lo que varias subredes IP pueden compartir una IP pública, distinguiendo servicios por puertos. Su forma de uso es: IP pública:puerto público -> IP privada_1:puerto privado. Para más información, consulta Traducción de direcciones de red.
  • Los servicios proxy son para ocultar o exponer. Los servicios proxy reenvían las solicitudes al servicio backend y reemplazan la IP de origen con su propia IP, ocultando así la IP real del servicio backend y protegiendo su seguridad. La forma de uso del servicio proxy es: IP del cliente -> IP del proxy -> IP del servidor. Para más información, consulta Proxy.

NAT y servidores proxy son muy comunes, y la mayoría de los servicios no pueden obtener la IP de origen de la solicitud.

Estas son las dos vías comunes para modificar la IP de origen. Si hay otras, bienvenidas las sugerencias.

¿Cómo conservar la IP de origen?

A continuación, un ejemplo de una solicitud HTTP:

CampoLongitud (bytes)Desplazamiento de bitsDescripción
Cabecera IP
IP de origen40-31Dirección IP del remitente
IP de destino432-63Dirección IP del receptor
Cabecera TCP
Puerto de origen20-15Número de puerto del remitente
Puerto de destino216-31Número de puerto del receptor
Número de secuencia432-63Para identificar el flujo de bytes enviado por el remitente
Número de confirmación464-95Si se establece la bandera ACK, es el siguiente número de secuencia esperado
Desplazamiento de datos496-103Número de bytes desde la posición inicial de los datos hasta la cabecera TCP
Reservado4104-111Campo reservado, no utilizado, establecido en 0
Bits de bandera2112-127Varios bits de control, como SYN, ACK, FIN, etc.
Tamaño de ventana2128-143Cantidad de datos que el receptor puede recibir
Suma de verificación2144-159Para detectar si los datos han cambiado durante la transmisión
Puntero urgente2160-175Posición de los datos urgentes que el remitente espera que el receptor procese lo antes posible
OpcionesVariable176-…Puede incluir marca de tiempo, longitud máxima del segmento de mensaje, etc.
Cabecera HTTP
Línea de solicitudVariable…-…Incluye método de solicitud, URI y versión HTTP
Campos de cabeceraVariable…-…Contiene varios campos de cabecera, como Host, User-Agent, etc.
Línea en blanco2…-…Para separar la cabecera y el cuerpo
CuerpoVariable…-…Cuerpo opcional de la solicitud o respuesta

Al examinar la estructura de la solicitud HTTP anterior, se puede ver que las opciones TCP, línea de solicitud, campos de cabecera y cuerpo son variables. El espacio de opciones TCP es limitado y generalmente no se usa para pasar la IP de origen. La línea de solicitud tiene información fija que no se puede expandir. El cuerpo HTTP no se puede modificar después del cifrado. Solo los campos de cabecera HTTP son adecuados para expandir y pasar la IP de origen.

Se puede agregar el campo X-REAL-IP en la cabecera HTTP para pasar la IP de origen. Esta operación generalmente se realiza en el servidor proxy, y luego el servidor proxy envía la solicitud al servicio backend, que puede obtener la información de la IP de origen a través de este campo.

Nota: Es necesario garantizar que el servidor proxy esté antes del dispositivo NAT, para poder obtener la IP de origen real de la solicitud whoami. En los productos de Alibaba Cloud, podemos ver el producto independiente de equilibrador de carga, cuya posición en la red es diferente a la de un servidor de aplicación común.

Guía de operaciones de K8S

Usando el proyecto whoami como ejemplo para el despliegue.

Crear Deployment

Primero crea el servicio:

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

Este paso creará un Deployment que contiene 3 Pods, cada pod contiene un contenedor que ejecuta el servicio whoami.

Crear Service

Puedes crear un servicio de tipo NodePort o LoadBalancer para acceso externo, o crear un servicio de tipo ClusterIP solo para acceso interno del clúster, y luego agregar un servicio Ingress para exponer el acceso externo.

NodePort se puede acceder tanto a través de NodeIP:NodePort como a través del servicio Ingress, lo que facilita las pruebas. Esta sección usa el servicio NodePort.

apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  type: NodePort
  selector:
    app: whoami
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30002

Después de crear el servicio, accede con curl whoami.example.com:30002 y verás que la IP devuelta es la NodeIP, no la IP de origen de la solicitud whoami.

Por favor, ten en cuenta que esta no es la IP del cliente correcta; son IPs internas del clúster. Esto es lo que sucede:

  • El cliente envía el paquete de datos a node2:nodePort
  • node2 reemplaza la IP de origen del paquete con su propia dirección IP (SNAT)
  • node2 reemplaza la IP de destino del paquete con la IP del Pod
  • El paquete se enruta a node1 y luego al endpoint
  • La respuesta del Pod se enruta de vuelta a node2
  • La respuesta del Pod se envía de vuelta al cliente

Representado en diagrama:

Configurar externalTrafficPolicy: Local

Para evitar esta situación, Kubernetes tiene una característica que conserva la IP de origen del cliente. Si configuras service.spec.externalTrafficPolicy como Local, kube-proxy solo proxyará las solicitudes a endpoints locales, sin reenviar el tráfico a otros nodos.

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

Prueba con curl whoami.example.com:30002. Cuando whoami.example.com se resuelve a las IPs de múltiples nodos del clúster, hay una cierta probabilidad de que no sea accesible. Necesitas confirmar que el registro DNS solo contenga la IP del nodo donde se encuentra el endpoint (pod).

Esta configuración tiene su costo: pierde la capacidad de equilibración de carga dentro del clúster. El cliente solo obtendrá respuesta si accede al nodo donde se despliegan los endpoints.

Restricción de ruta de acceso

Cuando el cliente accede al Nodo 2, no habrá respuesta.

Crear Ingress

La mayoría de los servicios se proporcionan a los usuarios mediante http/https. La forma https://ip:port puede resultar extraña para los usuarios. Generalmente, se usa Ingress para equilibrar la carga del servicio NodePort creado anteriormente a los puertos 80/443 de un dominio.

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

Después de aplicarlo, prueba accediendo con curl whoami.example.com. Verás que ClientIP siempre es la IP del Pod del Ingress Controller en el nodo donde se encuentra el endpoint.

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>

Usar Ingress como proxy inverso para el servicio NodePort significa agregar dos capas de servicio antes del endpoint. El diagrama siguiente muestra la diferencia entre ambos.

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]

En la ruta 1, cuando se accede externamente a Ingress, el primer endpoint que llega el tráfico es el Ingress Controller, y luego llega al endpoint whoami.
El Ingress Controller es en esencia un servicio 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

Por lo tanto, se puede conservar la IP de origen configurando externalTrafficPolicy en el Ingress Controller como se mencionó anteriormente.

Además, es necesario configurar use-forwarded-headers como true en el configmap de ingress-nginx-controller, para que el Ingress Controller pueda reconocer los campos X-Forwarded-For o 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

La diferencia principal entre el servicio NodePort y el servicio ingress-nginx-controller radica en que el backend de NodePort generalmente no se despliega en cada nodo, mientras que el backend de ingress-nginx-controller generalmente se despliega en cada nodo expuesto externamente.

A diferencia de configurar externalTrafficPolicy en el servicio NodePort, lo que causa que las solicitudes entre nodos no respondan, Ingress puede configurar primero el HEADER y luego proxyar y reenviar la solicitud, logrando así las capacidades de conservar la IP de origen y equilibración de carga.

Resumen

  • La traducción de direcciones (NAT), proxy, proxy inverso (Reverse Proxy) y equilibración de carga (Load Balance) causan la pérdida de la IP de origen.
  • Para evitar la pérdida de la IP de origen, el servidor proxy puede establecer la IP real en el campo de cabecera HTTP X-REAL-IP durante el reenvío, transmitiéndola a través del servicio proxy. Si se usan múltiples capas de proxy, se puede usar el campo X-Forwarded-For, que registra la lista de IPs de la IP de origen y la ruta del proxy en forma de pila.
  • Configurar externalTrafficPolicy: Local en el servicio NodePort del clúster puede conservar la IP de origen, pero pierde la capacidad de equilibración de carga.
  • Bajo la premisa de que ingress-nginx-controller se despliega en forma de daemonset en todos los nodos con rol loadbalancer, configurar externalTrafficPolicy: Local puede conservar la IP de origen y mantener la capacidad de equilibración de carga.

Referencias