Como preservar o IP de origem da solicitação após o balanceamento de carga em um cluster K8s

Introdução

O implantação de aplicativos nem sempre é simplesmente instalar e executar, às vezes também é necessário considerar problemas de rede. Este artigo introduz como fazer com que os serviços em um cluster k8s possam obter o IP de origem da solicitação.

Os aplicativos que fornecem serviços geralmente dependem de informações de entrada. Se as informações de entrada não dependem do quintuplo (IP de origem, porta de origem, IP de destino, porta de destino, protocolo), então o serviço tem baixa acoplamento com a rede e não precisa se preocupar com detalhes de rede.

Portanto, a maioria das pessoas não precisa ler este artigo. Se você estiver interessado em redes ou quiser expandir um pouco sua visão, pode continuar lendo para entender mais cenários de serviço.

Este artigo é baseado no k8s v1.29.4. Algumas descrições no artigo misturam pod e endpoint; neste cenário, eles podem ser considerados equivalentes.

Se houver erros, bem-vindo para corrigir, eu corrigirei prontamente.

Por que a informação do IP de origem é perdida?

Primeiro, esclarecemos o que é o IP de origem. Quando A envia uma solicitação para B, e B encaminha a solicitação para C, embora C veja o IP de origem do protocolo IP como o IP de B, este artigo considera o IP de A como o IP de origem.

Existem principalmente duas classes de comportamentos que levam à perda da informação de origem:

  1. Conversão de Endereço de Rede (NAT), o objetivo é economizar IPv4 público, balanceamento de carga etc. Isso fará com que o servidor veja o IP do dispositivo NAT como o IP de origem, não o IP de origem real.
  2. Proxy, Proxy Reverso (RP, Reverse Proxy) e Balanceador de Carga (LB, Load Balancer) pertencem a esta classe, abaixo denominados coletivamente servidor proxy. Este tipo de serviço proxy encaminhará a solicitação para o serviço backend, mas substituirá o IP de origem pelo seu próprio IP.
  • NAT, em termos simples, é trocar espaço de porta por espaço de IP. Os endereços IPv4 são limitados. Um endereço IP pode mapear 65535 portas, e na maioria das vezes essas portas não são esgotadas, portanto, várias sub-redes IP podem compartilhar um IP público, distinguindo diferentes serviços pelas portas. Sua forma de uso é: IP público:porta pública -> IP privado_1:porta privada. Para mais conteúdo, consulte Conversão de Endereço de Rede
  • O serviço proxy é para ocultar ou expor. O serviço proxy encaminhará a solicitação para o serviço backend e substituirá o IP de origem pelo seu próprio IP, ocultando assim o IP real do serviço backend e protegendo a segurança do serviço backend. A forma de uso do serviço proxy é: IP do cliente -> IP do proxy -> IP do servidor. Para mais conteúdo, consulte Proxy

NAT e servidor proxy são muito comuns, e a maioria dos serviços não consegue obter o IP de origem da solicitação.

Estas são as duas principais vias comuns para modificar o IP de origem; bem-vindo para suplementar outras.

Como preservar o IP de origem?

A seguir está um exemplo de solicitação HTTP:

Campo Comprimento (bytes) Deslocamento de bits Descrição
Cabeçalho IP
IP de origem 4 0-31 Endereço IP do remetente
IP de destino 4 32-63 Endereço IP do destinatário
Cabeçalho TCP
Porta de origem 2 0-15 Número da porta de envio
Porta de destino 2 16-31 Número da porta de recebimento
Número de sequência 4 32-63 Usado para identificar o fluxo de bytes enviado pelo remetente
Número de confirmação 4 64-95 Se o sinal ACK estiver definido, é o próximo número de sequência esperado
Deslocamento de dados 4 96-103 Número de bytes da posição inicial dos dados em relação ao cabeçalho TCP
Reservado 4 104-111 Campo reservado, não utilizado, definido como 0
Bits de sinal 2 112-127 Vários sinais de controle, como SYN, ACK, FIN etc.
Tamanho da janela 2 128-143 Quantidade de dados que o receptor pode receber
Soma de verificação 2 144-159 Usado para detectar se os dados foram corrompidos durante a transmissão
Ponteiro urgente 2 160-175 Posição dos dados urgentes que o remetente espera que o receptor processe o mais rápido possível
Opções Variável 176-… Pode incluir timestamp, comprimento máximo do segmento de mensagem etc.
Cabeçalho HTTP
Linha de solicitação Variável …-… Inclui método de solicitação, URI e versão HTTP
Campos de cabeçalho Variável …-… Contém vários campos de cabeçalho, como Host, User-Agent etc.
Linha vazia 2 …-… Usada para separar cabeçalho e corpo
Corpo Variável …-… Corpo opcional da solicitação ou resposta

Examinando a estrutura da solicitação HTTP acima, pode-se ver que opções TCP, linha de solicitação, campos de cabeçalho, corpo são variáveis. Entre elas, o espaço de opções TCP é limitado e geralmente não é usado para transmitir IP de origem. A linha de solicitação carrega informações fixas que não podem ser expandidas. O corpo HTTP não pode ser modificado após criptografado. Apenas os campos de cabeçalho HTTP são adequados para expansão e transmissão de IP de origem.

No cabeçalho HTTP, pode-se adicionar o campo X-REAL-IP para transmitir o IP de origem. Essa operação geralmente é colocada no servidor proxy, e então o servidor proxy enviará a solicitação para o serviço backend. O serviço backend pode obter a informação do IP de origem por meio desse campo.

Note que é necessário garantir que o servidor proxy esteja antes do dispositivo NAT, para que possa obter o whoami da origem real da solicitação. Podemos ver o produto balanceador de carga da Alibaba Cloud como uma categoria de produto separada, cuja posição na rede é diferente de um servidor de aplicativo comum.

Guia de Operação K8S

Usando o projeto whoami como exemplo para implantação.

Criar Deployment

Primeiro, crie o serviço:

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

Esta etapa criará um Deployment contendo 3 Pods, cada pod contém um contêiner que executará o serviço whoami.

Criar Service

Pode-se criar serviços do tipo NodePort ou LoadBalancer para acesso externo, ou criar serviços do tipo ClusterIP para acesso apenas interno ao cluster, e adicionar serviços Ingress para expor acesso externo.

NodePort pode ser acessado tanto por NodeIP:NodePort quanto por serviços Ingress, conveniente para testes. Esta seção usa o serviço NodePort.

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

Após criar o serviço, acesse com curl whoami.example.com:30002 e verá que o IP retornado é o NodeIP, não o whoami da origem da solicitação.

Por favor, note que este não é o IP do cliente correto, eles são IPs internos do cluster. Eis o que acontece:

  • O cliente envia o pacote para node2:nodePort
  • node2 substitui o IP de origem do pacote pelo seu próprio endereço IP (SNAT)
  • node2 substitui o IP de destino do pacote pelo IP do Pod
  • O pacote é roteado para node1 e depois para o endpoint
  • A resposta do Pod é roteada de volta para node2
  • A resposta do Pod é enviada de volta para o cliente

Representado em diagrama:

Configurar externalTrafficPolicy: Local

Para evitar isso, o Kubernetes tem uma funcionalidade que preserva o IP de origem do cliente. Se service.spec.externalTrafficPolicy for definido como Local, o kube-proxy só proxyará as solicitações para endpoints locais, sem encaminhar o tráfego para outros nós.

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

Use curl whoami.example.com:30002 para testar. Quando whoami.example.com mapeia para IPs de múltiplos nodes do cluster, há uma certa probabilidade de falha no acesso. É necessário confirmar que o registro DNS contém apenas o IP do node onde está o endpoint (pod).

Essa configuração tem seu custo, que é a perda da capacidade de balanceamento de carga no cluster. O cliente só receberá resposta ao acessar o node onde o endpoint está implantado.

Restrição de caminho de acesso

Quando o cliente acessa o Node 2, não haverá resposta.

Criar Ingress

A maioria dos serviços fornecidos aos usuários usa http/https. A forma https://ip:port pode parecer estranha para os usuários. Geralmente, usa-se Ingress para balancear o serviço NodePort criado acima para a porta 80/443 de um domínio.

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

Após aplicar, use curl whoami.example.com para testar o acesso e verá que o ClientIP é sempre o IP do Pod do Ingress Controller no node onde está o 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 reverso para o serviço NodePort significa adicionar duas camadas de service antes do endpoint. O diagrama abaixo mostra a diferença entre eles.

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]

No caminho 1, ao acessar o Ingress externamente, o tráfego primeiro chega ao endpoint Ingress Controller e depois ao endpoint whoami.
O Ingress Controller é essencialmente um serviço 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

Portanto, pode-se preservar o IP de origem configurando externalTrafficPolicy no Ingress Controller, como mencionado anteriormente.

Ao mesmo tempo, é necessário definir use-forwarded-headers como true no configmap do ingress-nginx-controller, para que o Ingress Controller possa reconhecer os campos X-Forwarded-For ou 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

A diferença principal entre o serviço NodePort e o serviço ingress-nginx-controller é que o backend do NodePort geralmente não é implantado em cada node, enquanto o backend do ingress-nginx-controller geralmente é implantado em cada node exposto externamente.

Diferente de definir externalTrafficPolicy no serviço NodePort, o que causa falha de resposta para solicitações entre nodes, o Ingress pode primeiro definir o HEADER na solicitação e depois proxyá-la, realizando as capacidades de preservar IP de origem e balanceamento de carga.

Resumo

  • Conversão de Endereço (NAT), Proxy, Proxy Reverso, Balanceamento de Carga levarão à perda do IP de origem.
  • Para evitar a perda do IP de origem, ao encaminhar no servidor proxy, defina o IP real no campo de cabeçalho HTTP X-REAL-IP e transmita pelo serviço proxy. Se usar múltiplas camadas de proxy, pode-se usar o campo X-Forwarded-For, que registra a lista de IPs do IP de origem e o caminho do proxy em forma de pilha.
  • Definir externalTrafficPolicy: Local no serviço NodePort do cluster pode preservar o IP de origem, mas perderá a capacidade de balanceamento de carga.
  • Sob a premissa de que o ingress-nginx-controller é implantado em forma de daemonset em todos os nodes com papel loadbalancer, definir externalTrafficPolicy: Local pode preservar o IP de origem e manter a capacidade de balanceamento de carga.

Referências