Como o APISIX Ingress Controller Suporta Milhares de Réplicas de Pods?

Xin Rong

October 21, 2022

Technology

1.000+ Pods no Kubernetes

Pods são os menores objetos implantáveis no Kubernetes. Usamos um pod para executar uma única instância de uma aplicação, portanto, atribuímos recursos limitados ao pod devido à escalabilidade. No entanto, se encontrarmos cenários de alto tráfego, usaremos o dimensionamento horizontal para lidar com eles.

Por exemplo, para varejistas online, a Black Friday causará um aumento rápido no tráfego. Para lidar com esse cenário, precisamos dimensionar automaticamente os serviços para lidar com mais tráfego. Implantaríamos mais réplicas com base na estratégia de dimensionamento automático, resultando em mais Pods.

Cada Pod tem um endereço IP dinâmico. A API Endpoints forneceu uma maneira direta de rastrear endpoints de rede no Kubernetes, para que possamos alcançar o balanceamento de carga monitorando as mudanças no IP do Pod. No entanto, à medida que os clusters e serviços do Kubernetes crescem para lidar com mais tráfego para mais Pods, por exemplo, o cenário da Black Friday mencionado acima, o número de Pods continua aumentando, e a API Endpoints se torna maior. Consequentemente, as limitações da API Endpoints se tornam mais visíveis e até se tornam o gargalo de desempenho.

Para resolver o problema de limitação da API Endpoints, o Kubernetes começa a suportar a API EndpointSlice na versão v1.21. A API EndpointSlice ajuda a resolver o problema de desempenho ao lidar com enormes endpoints de rede na API Endpoints e possui excelente escalabilidade e extensibilidade.

Podemos ver diretamente as diferenças entre eles no diagrama abaixo:

  • Mudanças nos Endpoints durante um pico de tráfego

Endpoints durante um pico de tráfego

  • Mudanças nos Endpointslices durante um pico de tráfego

Endpointslices durante um pico de tráfego

No Kubernetes, como as aplicações se comunicam entre si? Quais são as diferenças específicas entre Endpoints e EndpointSlice? Qual é a relação entre Pod e Endpoints/EndpointSlice? Como o APISIX suporta esses recursos e como instalá-los e usá-los? Focaremos nessas questões neste artigo.

Como acessar aplicações no Kubernetes

Via Service

Cada Pod tem seu próprio endereço IP no Kubernetes. Normalmente, o Service estabelece conexões com o Pod usando selector, fornece o mesmo subdomínio DNS e alcança o balanceamento de carga. Além disso, as aplicações dentro do cluster Kubernetes podem usar DNS para se comunicar entre si.

Quando um Service é criado, o Kubernetes conecta o Service a um recurso Endpoints. No entanto, se o Service não especificar nenhum seletor, o Kubernetes não criará automaticamente Endpoints para o Service.

O que são Endpoints e qual é a relação com o Pod

Endpoints é um objeto de recurso no Kubernetes, armazenado no etcd, e inclui referências a um conjunto de endereços de acesso de Pods que correspondem a um Service. Portanto, cada Service pode ter apenas um recurso Endpoints. O recurso Endpoints monitora os clusters de Pods e atualiza sincronamente quando qualquer Pod no Service muda.

  1. Implante 3 réplicas de httpbin e verifique o status dos Pods, incluindo informações de IP
$ kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS        AGE   IP            NODE             NOMINATED NODE   READINESS GATES
httpbin-deployment-fdd7d8dfb-8sxxq   1/1     Running   0               49m   10.1.36.133   docker-desktop   <none>           <none>
httpbin-deployment-fdd7d8dfb-bjw99   1/1     Running   4 (5h39m ago)   23d   10.1.36.125   docker-desktop   <none>           <none>
httpbin-deployment-fdd7d8dfb-r5nf9   1/1     Running   0               49m   10.1.36.131   docker-desktop   <none>           <none>
  • Crie o serviço httpbin e verifique a condição dos endpoints de Endpoints
$ kubectl get endpoints httpbin
NAME      ENDPOINTS                                      AGE
httpbin   10.1.36.125:80,10.1.36.131:80,10.1.36.133:80   23d

A partir dos dois exemplos acima, podemos ver que cada endpoint de rede do recurso httpbin em Endpoints corresponde a um endereço IP de Pod.

Desvantagens dos Endpoints

  1. Endpoints tem um limite de armazenamento; se qualquer recurso Endpoints tiver mais de 1000 endpoints, o controlador Endpoints truncará os endpoints para 1000.
  2. Um Service pode ter apenas um recurso Endpoints, o que significa que o recurso Endpoints precisa armazenar endereços IP e outras informações de rede para cada Pod que suporta o Service correspondente. Como resultado, o recurso da API Endpoints se torna enorme e precisa ser atualizado quando um único endpoint de rede no recurso muda. Quando o negócio exige mudanças frequentes de endpoints, um enorme recurso de API será enviado entre os componentes, o que afeta o desempenho dos componentes do Kubernetes.

O que são Endpointslices

Endpointslices é uma alternativa mais escalável e extensível aos Endpoints e ajuda a lidar com o problema de desempenho causado pelo processamento de enormes endpoints de rede. Ele também ajuda a fornecer uma plataforma extensível para recursos adicionais, como roteamento topológico. Esse recurso é suportado no Kubernetes v1.21.

A API EndpointSlice foi projetada para resolver esse problema com uma abordagem semelhante ao sharding. Em vez de rastrear todos os IPs de Pods para um Service com um único recurso Endpoints, nós os dividimos em vários EndpointSlices menores.

Por padrão, o plano de controle cria e gerencia EndpointSlices para ter não mais que 100 endpoints cada. Você pode configurar isso com o sinalizador --max-endpoints-per-slice do kube-controller-manager, até 1000.

Por que precisamos disso

Considere um cenário

Suponha que haja um Service suportado por 2000 Pods, o que poderia resultar em um recurso Endpoints de 1,0 MB. No ambiente de produção, se esse Service tiver atualizações contínuas ou migrações de endpoints, os recursos Endpoint serão atualizados com frequência. Pense em atualizações contínuas que causarão a substituição de todos os Pods devido ao limite máximo de tamanho de qualquer solicitação no etcd. O Kubernetes definiu um limite máximo de 1000 endpoints para Endpoints. Se houver mais de 1000 endpoints, o recurso Endpoints não terá referências a endpoints de rede adicionais.

Se o Service precisar de várias atualizações contínuas devido a algumas necessidades especiais, um enorme objeto de recurso de API será transferido entre os componentes do Kubernetes, o que afeta significativamente o desempenho dos componentes do Kubernetes.

E se usarmos EndpointSlice

Suponha que haja um Service suportado por 2000 Pods, e atribuímos 100 endpoints a cada Endpointslices na configuração, então terminaremos com 20 Endpointslices. Agora, quando um Pod é adicionado ou removido, apenas um pequeno EndpointSlice precisa ser atualizado. Obviamente, é uma melhoria notável em escalabilidade e extensibilidade de rede. Se cada Service tiver uma necessidade de dimensionamento automático, o Service implantará mais Pods, e o recurso Endpoints será atualizado com frequência para lidar com o aumento do tráfego durante um pico de tráfego, e a diferença se tornará mais perceptível. Mais importante, agora que todos os IPs de Pods para um Service não precisam ser armazenados em um único recurso, não precisamos nos preocupar com o limite de tamanho para objetos armazenados no etcd.

Conclusão de Endpoints VS EndpointSlice

Como o EndpointSlice é suportado desde o Kubernetes v1.21, todas as conclusões se referem ao Kubernetes v1.21.

Casos de uso de Endpoints:

  • Há necessidade de dimensionamento automático, mas o número de Pods é relativamente pequeno, e as transferências de recursos não causarão grande tráfego de rede e necessidades extras de manipulação.
  • Não há necessidade de dimensionamento automático, e o número de Pods não será grande. No entanto, mesmo que o número de Pods seja fixo, o Service não pode omitir atualizações contínuas e falhas.

Casos de uso de EndpointSlice:

  • Há necessidade de dimensionamento automático, e o número de Pods é grande (centenas de Pods).
  • O número de Pods é enorme (centenas de Pods) devido ao limite máximo de endpoints de Endpoints ser 1000; qualquer Pod que tenha mais de 1000 endpoints deve usar EndpointSlice.

Prática no APISIX Ingress Controller

O APISIX Ingress Controller alcança o balanceamento de carga e verificações de saúde observando as mudanças no recurso Endpoints ou EndpointSlice. Para suportar Kubernetes v1.16+, o APISIX Ingress Controller usará Endpoints por padrão durante a instalação.

Se a versão do seu cluster for Kubernetes v1.21+, então você precisa especificar o sinalizador watchEndpointSlice=true para suportar o recurso EndpointSlice durante a instalação do APISIX Ingress Controller.

Nota: Nos clusters com Kubernetes v1.21+, recomendamos usar o recurso Endpointslice, caso contrário, quando o número de Pods exceder o valor configurado no sinalizador --max-endpoints-per-slice, a configuração seria perdida, pois o APISIX Ingress Controller está observando o objeto de recurso Endpoints.

Criando um Service suportado por 20 réplicas de Pods

Configure o Service da aplicação httpbin no Kubernetes e crie 20 réplicas de Pods.

  • - kubectl apply -f httpbin-deploy.yaml
# htppbin-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin-deployment
spec:
  replicas: 20
  selector:
    matchLabels:
      app: httpbin-deployment
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: httpbin-deployment
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - livenessProbe:
            failureThreshold: 3
            initialDelaySeconds: 2
            periodSeconds: 5
            successThreshold: 1
            tcpSocket:
              port: 80
            timeoutSeconds: 2
          readinessProbe:
            failureThreshold: 3
            initialDelaySeconds: 2
            periodSeconds: 5
            successThreshold: 1
            tcpSocket:
              port: 80
            timeoutSeconds: 2
          image: "kennethreitz/httpbin:latest"
          imagePullPolicy: IfNotPresent
          name: httpbin-deployment
          ports:
            - containerPort: 80
              name: "http"
              protocol: "TCP"

---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  selector:
    app: httpbin-deployment
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
  type: ClusterIP

Proxy via APISIX Ingress

  • Use Helm para instalar o APISIX Ingress Controller

Use --set ingress-controller.config.kubernetes.watchEndpointSlice=true para habilitar o suporte ao recurso EndpointSlice.

helm repo add apisix https://charts.apiseven.com
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

kubectl create ns ingress-apisix
helm install apisix apisix/apisix \
  --set gateway.type=NodePort \
  --set ingress-controller.enabled=true \
  --namespace ingress-apisix \
  --set ingress-controller.config.apisix.serviceNamespace=ingress-apisix \
  --set ingress-controller.config.kubernetes.watchEndpointSlice=true
  • Use o recurso CRD para proxy

Os usuários não perceberão o suporte aos recursos Endpoints e EndpointSlice no APISIX Ingress Controller, e suas configurações são as mesmas.

apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
  name: httpbin-route
spec:
  http:
    - name: rule
      match:
        hosts:
          - httpbin.org
        paths:
          - /get
      backends:
        - serviceName: httpbin
          servicePort: 80
  • Verificando o Pod do APISIX, podemos ver que o campo nodes do objeto upstream do APISIX contém os endereços IP de 20 Pods.
kubectl exec -it ${Pod for APISIX} -n ingress-apisix -- curl "http://127.0.0.1:9180/apisix/admin/upstreams" -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
{
    "action": "get",
    "count": 1,
    "node": {
        "key": "\/apisix\/upstreams",
        "nodes": [
            {
                "value": {
                    "hash_on": "vars",
                    "desc": "Created by apisix-ingress-controller, DO NOT modify it manually",
                    "pass_host": "pass",
                    "nodes": [
                        {
                            "weight": 100,
                            "host": "10.1.36.100",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.101",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.102",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.103",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.104",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.109",
                            "priority": 0,
                            "port": 80
                        },
                        {
                            "weight": 100,
                            "host": "10.1.36.92",
                            "priority": 0,
                            "port": 80
                        }
                        ... // ignore 13 nodes below
                        // 10.1.36.118
                        // 10.1.36.115
                        // 10.1.36.116
                        // 10.1.36.106
                        // 10.1.36.113
                        // 10.1.36.111
                        // 10.1.36.108
                        // 10.1.36.114
                        // 10.1.36.107
                        // 10.1.36.110
                        // 10.1.36.105
                        // 10.1.36.112
                        // 10.1.36.117
                    ],
                    "labels": {
                        "managed-by": "apisix-ingress-controller"
                    },
                    "type": "roundrobin",
                    "name": "default_httpbin_80",
                    "scheme": "http"
                },
                "key": "\/apisix\/upstreams\/5ce57b8e"
            }
        ],
        "dir": true
    }
}
  • Correspondência com os endpoints de rede do EndpointSlice
addressType: IPv4
apiVersion: discovery.k8s.io/v1
endpoints:
- addresses:
  - 10.1.36.92
  ...
- addresses:
  - 10.1.36.100
  ...
- addresses:
  - 10.1.36.104
  ...
- addresses:
  - 10.1.36.102
  ...
- addresses:
  - 10.1.36.101
  ...
- addresses:
  - 10.1.36.103
  ...
- addresses:
  - 10.1.36.109
  ...
- addresses:
  - 10.1.36.118
  ...
- addresses:
  - 10.1.36.115
  ...
- addresses:
  - 10.1.36.116
  ...
- addresses:
  - 10.1.36.106
  ...
- addresses:
  - 10.1.36.113
  ...
- addresses:
  - 10.1.36.111
  ...
- addresses:
  - 10.1.36.108
  ...
- addresses:
  - 10.1.36.114
  ...
- addresses:
  - 10.1.36.107
  ...
- addresses:
  - 10.1.36.110
  ...
- addresses:
  - 10.1.36.105
  ...
- addresses:
  - 10.1.36.112
  ...
- addresses:
  - 10.1.36.117
  ...
kind: EndpointSlice
metadata:
  labels:
    endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
    kubernetes.io/service-name: httpbin
  name: httpbin-dkvtr
  namespace: default
ports:
- name: http
  port: 80
  protocol: TCP

Conclusão

Este artigo introduz cenários em que o Kubernetes precisa implantar um grande número de Pods e os problemas que encontramos. Também compara as diferenças entre Endpoints e EndpointSlice e introduz a maneira de habilitar o recurso EndpointSlice durante a instalação do APISIX Ingress Controller. Se a versão do seu cluster for Kubernetes v1.21+, recomendamos habilitar o recurso EndpointSlice durante a instalação do APISIX Ingress Controller. Portanto, isso pode evitar a perda de configuração e não precisamos nos preocupar com o valor configurado no sinalizador --max-endpoints-per-slice.

Tags: