Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes
Este artigo ajudará você a entender como funciona o balanceamento de carga no Kubernetes, o que acontece ao dimensionar conexões de longa duração e por que você deve considerar o balanceamento do lado do cliente se usar HTTP/2, gRPC, RSockets, AMQP ou outros protocolos de longa duração . 

Um pouco sobre como o tráfego é redistribuído no Kubernetes 

O Kubernetes fornece duas abstrações convenientes para implantação de aplicativos: Serviços e Implantações.

As implantações descrevem como e quantas cópias do seu aplicativo devem estar em execução a qualquer momento. Cada aplicativo é implantado como um pod e recebe um endereço IP.

Os serviços têm função semelhante a um balanceador de carga. Eles são projetados para distribuir o tráfego entre vários pods.

Vamos ver como é.

  1. No diagrama abaixo você pode ver três instâncias do mesmo aplicativo e um balanceador de carga:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  2. O balanceador de carga é chamado de Serviço e recebe um endereço IP. Qualquer solicitação recebida é redirecionada para um dos pods:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  3. O cenário de implantação determina o número de instâncias do aplicativo. Você quase nunca terá que expandir diretamente em:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  4. Cada pod recebe seu próprio endereço IP:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

É útil pensar nos serviços como uma coleção de endereços IP. Cada vez que você acessa o serviço, um dos endereços IP é selecionado na lista e usado como endereço de destino.

Se parece com isso.

  1. Uma solicitação curl 10.96.45.152 é recebida no serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  2. O serviço seleciona um dos três endereços de pod como destino:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  3. O tráfego é redirecionado para um pod específico:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

Se o seu aplicativo consistir em um front-end e um back-end, você terá um serviço e uma implantação para cada um.

Quando o frontend faz uma solicitação ao backend, ele não precisa saber exatamente quantos pods o backend atende: pode ser um, dez ou cem.

Além disso, o front-end não sabe nada sobre os endereços dos pods que atendem ao back-end.

Quando o frontend faz uma solicitação ao backend, ele usa o endereço IP do serviço backend, que não muda.

Veja como parece.

  1. Abaixo de 1 solicita o componente de back-end interno. Em vez de selecionar um específico para o backend, ele faz uma solicitação ao serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  2. O serviço seleciona um dos pods de back-end como endereço de destino:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  3. O tráfego vai do Pod 1 ao Pod 5, selecionado pelo serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  4. Abaixo de 1 não sabe exatamente quantos pods como abaixo de 5 estão escondidos atrás do serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

Mas como exatamente o serviço distribui as solicitações? Parece que o balanceamento round-robin é usado? Vamos descobrir. 

Balanceamento em serviços Kubernetes

Os serviços Kubernetes não existem. Não há nenhum processo para o serviço ao qual foi atribuído um endereço IP e uma porta.

Você pode verificar isso fazendo login em qualquer nó do cluster e executando o comando netstat -ntlp.

Você nem conseguirá encontrar o endereço IP alocado para o serviço.

O endereço IP do serviço fica localizado na camada de controle, no controlador, e registrado no banco de dados - etcd. O mesmo endereço é usado por outro componente - kube-proxy.
O Kube-proxy recebe uma lista de endereços IP para todos os serviços e gera um conjunto de regras iptables em cada nó do cluster.

Essas regras dizem: “Se virmos o endereço IP do serviço, precisamos modificar o endereço de destino da solicitação e enviá-lo para um dos pods”.

O endereço IP do serviço é usado apenas como ponto de entrada e não é atendido por nenhum processo que escuta esse endereço IP e porta.

Vamos dar uma olhada nisso

  1. Considere um cluster de três nós. Cada nó possui pods:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  2. Vagens amarradas pintadas de bege fazem parte do serviço. Como o serviço não existe como processo, ele é mostrado em cinza:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  3. O primeiro pod solicita um serviço e deve ir para um dos pods associados:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  4. Mas o serviço não existe, o processo não existe. Como funciona?

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  5. Antes de a solicitação sair do nó, ela passa pelas regras do iptables:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  6. As regras do iptables sabem que o serviço não existe e substituem seu endereço IP por um dos endereços IP dos pods associados a esse serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  7. A solicitação recebe um endereço IP válido como endereço de destino e é processada normalmente:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  8. Dependendo da topologia da rede, a solicitação eventualmente chega ao pod:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

O iptables pode balancear a carga?

Não, o iptables é usado para filtragem e não foi projetado para balanceamento.

No entanto, é possível escrever um conjunto de regras que funcionem como pseudo-balanceador.

E é exatamente isso que é implementado no Kubernetes.

Se você tiver três pods, o kube-proxy escreverá as seguintes regras:

  1. Selecione o primeiro sub com uma probabilidade de 33%, caso contrário, vá para a próxima regra.
  2. Escolha a segunda com probabilidade de 50%, caso contrário passe para a próxima regra.
  3. Selecione o terceiro abaixo.

Este sistema resulta na seleção de cada grupo com uma probabilidade de 33%.

Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

E não há garantia de que o Pod 2 será escolhido em seguida ao Pod 1.

Nota: iptables usa um módulo estatístico com distribuição aleatória. Assim, o algoritmo de balanceamento é baseado na seleção aleatória.

Agora que você entende como os serviços funcionam, vejamos cenários de serviço mais interessantes.

Conexões de longa duração no Kubernetes não são escalonadas por padrão

Cada solicitação HTTP do frontend para o backend é atendida por uma conexão TCP separada, que é aberta e fechada.

Se o frontend enviar 100 solicitações por segundo para o backend, 100 conexões TCP diferentes serão abertas e fechadas.

Você pode reduzir o tempo de processamento e carregamento de solicitações abrindo uma conexão TCP e usando-a para todas as solicitações HTTP subsequentes.

O protocolo HTTP possui um recurso chamado HTTP keep-alive ou reutilização de conexão. Neste caso, uma única conexão TCP é usada para enviar e receber múltiplas solicitações e respostas HTTP:

Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

Este recurso não está habilitado por padrão: tanto o servidor quanto o cliente devem ser configurados adequadamente.

A configuração em si é simples e acessível para a maioria das linguagens e ambientes de programação.

Aqui estão alguns links para exemplos em diferentes idiomas:

O que acontece se usarmos keep-alive em um serviço Kubernetes?
Vamos supor que tanto o frontend quanto o backend suportam keep-alive.

Temos uma cópia do frontend e três cópias do backend. O frontend faz a primeira solicitação e abre uma conexão TCP com o backend. A solicitação chega ao serviço, um dos pods de back-end é selecionado como endereço de destino. O backend envia uma resposta e o frontend a recebe.

Ao contrário da situação usual em que a conexão TCP é fechada após receber uma resposta, ela agora é mantida aberta para futuras solicitações HTTP.

O que acontece se o frontend enviar mais solicitações ao backend?

Para encaminhar essas solicitações, será utilizada uma conexão TCP aberta, todas as solicitações irão para o mesmo backend de onde foi a primeira solicitação.

O iptables não deveria redistribuir o tráfego?

Não neste caso.

Quando uma conexão TCP é criada, ela passa pelas regras do iptables, que selecionam um backend específico para onde o tráfego irá.

Como todas as solicitações subsequentes estão em uma conexão TCP já aberta, as regras do iptables não são mais chamadas.

Vamos ver como é.

  1. O primeiro pod envia uma solicitação ao serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  2. Você já sabe o que acontecerá a seguir. O serviço não existe, mas existem regras de iptables que irão processar a solicitação:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  3. Um dos pods de back-end será selecionado como endereço de destino:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  4. A solicitação chega ao pod. Neste ponto, uma conexão TCP persistente entre os dois pods será estabelecida:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  5. Qualquer solicitação subsequente do primeiro pod passará pela conexão já estabelecida:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

O resultado é um tempo de resposta mais rápido e maior rendimento, mas você perde a capacidade de dimensionar o back-end.

Mesmo que você tenha dois pods no backend, com conexão constante, o tráfego sempre irá para um deles.

Isso pode ser corrigido?

Como o Kubernetes não sabe como equilibrar conexões persistentes, essa tarefa cabe a você.

Os serviços são uma coleção de endereços IP e portas chamados endpoints.

Seu aplicativo pode obter uma lista de endpoints do serviço e decidir como distribuir solicitações entre eles. Você pode abrir uma conexão persistente para cada pod e equilibrar solicitações entre essas conexões usando round-robin.

Ou aplique mais algoritmos de balanceamento complexos.

O código do lado do cliente responsável pelo balanceamento deve seguir esta lógica:

  1. Obtenha uma lista de endpoints do serviço.
  2. Abra uma conexão persistente para cada endpoint.
  3. Quando for necessário fazer uma solicitação, use uma das conexões abertas.
  4. Atualize regularmente a lista de endpoints, crie novos ou feche conexões persistentes antigas se a lista mudar.

É assim que vai parecer.

  1. Em vez de o primeiro pod enviar a solicitação ao serviço, você pode equilibrar as solicitações no lado do cliente:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  2. Você precisa escrever um código que pergunte quais pods fazem parte do serviço:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  3. Assim que tiver a lista, salve-a no lado do cliente e use-a para conectar-se aos pods:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

  4. Você é responsável pelo algoritmo de balanceamento de carga:

    Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

Agora surge a pergunta: esse problema se aplica apenas ao keep-alive HTTP?

Balanceamento de carga do lado do cliente

HTTP não é o único protocolo que pode usar conexões TCP persistentes.

Se a sua aplicação utiliza um banco de dados, uma conexão TCP não será aberta toda vez que você precisar fazer uma solicitação ou recuperar um documento do banco de dados. 

Em vez disso, uma conexão TCP persistente com o banco de dados é aberta e usada.

Se o seu banco de dados estiver implantado no Kubernetes e o acesso for fornecido como um serviço, você encontrará os mesmos problemas descritos na seção anterior.

Uma réplica do banco de dados será mais carregada que as outras. Kube-proxy e Kubernetes não ajudarão a equilibrar as conexões. Você deve tomar cuidado para equilibrar as consultas ao seu banco de dados.

Dependendo de qual biblioteca você usa para se conectar ao banco de dados, você pode ter diferentes opções para resolver esse problema.

Abaixo está um exemplo de acesso a um cluster de banco de dados MySQL do Node.js:

var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();

var endpoints = /* retrieve endpoints from the Service */

for (var [index, endpoint] of endpoints) {
  poolCluster.add(`mysql-replica-${index}`, endpoint);
}

// Make queries to the clustered MySQL database

Existem muitos outros protocolos que usam conexões TCP persistentes:

  • WebSockets e WebSockets seguros
  • HTTP / 2
  • gRPC
  • RSockets
  • AMQP

Você já deve estar familiarizado com a maioria desses protocolos.

Mas se esses protocolos são tão populares, por que não existe uma solução de balanceamento padronizada? Por que a lógica do cliente precisa mudar? Existe uma solução nativa do Kubernetes?

Kube-proxy e iptables são projetados para cobrir os casos de uso mais comuns durante a implantação no Kubernetes. Isto é por conveniência.

Se você estiver usando um serviço web que expõe uma API REST, você está com sorte - neste caso, conexões TCP persistentes não são usadas, você pode usar qualquer serviço Kubernetes.

Mas quando você começar a usar conexões TCP persistentes, terá que descobrir como distribuir uniformemente a carga entre os back-ends. O Kubernetes não contém soluções prontas para este caso.

No entanto, certamente existem opções que podem ajudar.

Equilibrando conexões de longa duração no Kubernetes

Existem quatro tipos de serviços no Kubernetes:

  1. ClusterIP
  2. Porta do nó
  3. Balanceador de carga
  4. Sem cabeça

Os três primeiros serviços operam com base em um endereço IP virtual, que é usado pelo kube-proxy para construir regras de iptables. Mas a base fundamental de todos os serviços é um serviço headless.

O serviço headless não possui nenhum endereço IP associado e apenas fornece um mecanismo para recuperar uma lista de endereços IP e portas dos pods (endpoints) associados a ele.

Todos os serviços são baseados no serviço headless.

O serviço ClusterIP é um serviço headless com algumas adições: 

  1. A camada de gerenciamento atribui um endereço IP.
  2. O Kube-proxy gera as regras de iptables necessárias.

Dessa forma, você pode ignorar o kube-proxy e usar diretamente a lista de endpoints obtidos do serviço headless para balancear a carga do seu aplicativo.

Mas como podemos adicionar lógica semelhante a todos os aplicativos implantados no cluster?

Se a sua aplicação já estiver implantada, esta tarefa pode parecer impossível. No entanto, existe uma opção alternativa.

Service Mesh irá ajudá-lo

Você provavelmente já percebeu que a estratégia de balanceamento de carga do lado do cliente é bastante padrão.

Quando o aplicativo é iniciado, ele:

  1. Obtém uma lista de endereços IP do serviço.
  2. Abre e mantém um pool de conexões.
  3. Atualiza periodicamente o pool adicionando ou removendo endpoints.

Assim que o aplicativo quiser fazer uma solicitação, ele:

  1. Seleciona uma conexão disponível usando alguma lógica (por exemplo, round-robin).
  2. Executa a solicitação.

Essas etapas funcionam para conexões WebSockets, gRPC e AMQP.

Você pode separar essa lógica em uma biblioteca separada e usá-la em seus aplicativos.

No entanto, você pode usar malhas de serviço como Istio ou Linkerd.

O Service Mesh amplia seu aplicativo com um processo que:

  1. Procura automaticamente endereços IP de serviço.
  2. Testa conexões como WebSockets e gRPC.
  3. Equilibra solicitações usando o protocolo correto.

O Service Mesh ajuda a gerenciar o tráfego dentro do cluster, mas consome muitos recursos. Outras opções são usar bibliotecas de terceiros, como Netflix Ribbon, ou proxies programáveis, como Envoy.

O que acontece se você ignorar os problemas de equilíbrio?

Você pode optar por não usar o balanceamento de carga e ainda assim não notar nenhuma alteração. Vejamos alguns cenários de trabalho.

Se você tiver mais clientes do que servidores, isso não será um grande problema.

Digamos que haja cinco clientes conectados a dois servidores. Mesmo que não haja balanceamento, ambos os servidores serão utilizados:

Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

As conexões podem não ser distribuídas uniformemente: talvez quatro clientes conectados ao mesmo servidor, mas há uma boa chance de que ambos os servidores sejam usados.

O que é mais problemático é o cenário oposto.

Se você tiver menos clientes e mais servidores, seus recursos poderão ser subutilizados e um potencial gargalo aparecerá.

Digamos que existam dois clientes e cinco servidores. Na melhor das hipóteses, haverá duas conexões permanentes com dois servidores em cada cinco.

Os servidores restantes ficarão ociosos:

Balanceamento de carga e dimensionamento de conexões de longa duração no Kubernetes

Se esses dois servidores não conseguirem lidar com as solicitações dos clientes, o dimensionamento horizontal não ajudará.

Conclusão

Os serviços Kubernetes são projetados para funcionar na maioria dos cenários de aplicativos web padrão.

No entanto, quando você começa a trabalhar com protocolos de aplicação que usam conexões TCP persistentes, como bancos de dados, gRPC ou WebSockets, os serviços não são mais adequados. O Kubernetes não fornece mecanismos internos para balancear conexões TCP persistentes.

Isso significa que você deve escrever aplicativos tendo em mente o equilíbrio do lado do cliente.

Tradução preparada pela equipe Kubernetes aaS de Mail.ru.

O que mais ler sobre o assunto:

  1. Três níveis de escalonamento automático no Kubernetes e como usá-los de maneira eficaz
  2. Kubernetes no espírito da pirataria com um modelo para implementação.
  3. Nosso canal Telegram sobre transformação digital.

Fonte: habr.com

Adicionar um comentário