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


Este artigo ajudará você a entender como o balanceamento de carga no Kubernetes funciona, o que acontece ao dimensionar conexões de longa duração e por que você deve considerar o balanceamento no lado do cliente se você usa 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 a 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 em (Pod) e recebe um endereço IP.

Os serviços de recursos são semelhantes a um balanceador de carga. Eles são projetados para distribuir o tráfego entre várias lareiras.

Vamos ver como fica .

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

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

  3. O script de implantação determina o número de instâncias do aplicativo. Você quase nunca precisará implantar diretamente em:

  4. Cada pod possui um endereço IP próprio:



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

Isto é o seguinte .

  1. Há uma solicitação de ondulação 10.96.45.152 para o serviço:

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

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



Se 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 front-end atende à solicitação ao back-end, ele não precisa saber exatamente quantos heaps o back-end serve: pode haver um, dez ou cem.

Além disso, o frontend não sabe nada sobre os endereços dos lares que atendem ao backend.

Quando o front-end faz uma solicitação ao back-end, ele usa o endereço IP do serviço de back-end, que não muda.

Aqui está como fica .

  1. Em 1 solicita o componente interno de back-end. Em vez de escolher um específico para o back-end, ele executa uma solicitação de serviço:

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

  3. O tráfego vai da lareira 1 à lareira 5 selecionada pelo serviço:

  4. Em 1, ele não sabe exatamente quantos lares menores de 5 estão ocultos atrás do serviço:



Mas como exatamente o serviço distribui solicitações? O balanceamento de rodízio parece ser usado? Vamos acertar. 

Balanceamento nos serviços Kubernetes


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

Você pode verificar isso acessando qualquer nó no cluster e executando o comando netstat -ntlp.

Você nem consegue encontrar o endereço IP alocado ao serviço.

O endereço IP do serviço está 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 proxy Kube recebe uma lista de endereços IP para todos os serviços e forma um conjunto de regras de tabelas de ip 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 é servido por nenhum processo que esteja ouvindo esse endereço IP e porta.

Vamos olhar para isso

  1. Considere um cluster de três nós. Existem pods em cada nó:

  2. Lareiras tricotadas em bege fazem parte do serviço. Como o serviço não existe como um processo, fica acinzentado:

  3. O primeiro solicita o serviço e deve recair em um dos lares relacionados:

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

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

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

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

  8. Dependendo da topologia da rede, a solicitação finalmente chega ao coração:



O iptables é capaz de equilibrar 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 funcionam como um pseudo-balanceador .

E é exatamente isso que o Kubernetes faz.

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

  1. Escolha o primeiro com uma probabilidade de 33%, caso contrário, vá para a próxima regra.
  2. Escolha o segundo com uma probabilidade de 50%, caso contrário, vá para a próxima regra.
  3. Escolha o terceiro em.

Esse sistema leva ao fato de que cada sub é selecionado com uma probabilidade de 33%.



E não há garantia de que menos de 2 seja selecionado a seguir após o arquivo 1.

Nota : iptables usa um módulo estatístico de 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 trabalho mais interessantes.

As conexões de longa duração no Kubernetes não são dimensionadas por padrão


Cada solicitação HTTP do front-end ao back-end é atendida por uma conexão TCP separada, que abre e fecha.

Se o front-end enviar 100 solicitações por segundo para o back-end, 100 conexões TCP diferentes serão abertas e fechadas.

Você pode reduzir o tempo de processamento da solicitação e reduzir a carga se abrir uma conexão TCP e usá-la para todas as solicitações HTTP subsequentes.

O protocolo HTTP contém um recurso chamado HTTP keep-alive ou reutilização da conexão. Nesse caso, uma conexão TCP é usada para enviar e receber muitos pedidos e respostas HTTP:



Esse recurso não está ativado por padrão: o servidor e o cliente devem ser configurados de acordo.

A instalaçã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 o keep-alive no Kubernetes?
Vamos supor que o suporte de front-end e back-end se mantenha ativo.

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

Diferente da situação usual, quando a conexão TCP é fechada após o recebimento da resposta, ela é mantida aberta para as seguintes solicitações HTTP.

O que acontece se o front-end envia mais solicitações de back-end?

Para encaminhar essas solicitações, uma conexão TCP aberta será usada, todas as solicitações serão enviadas para a mesma sob o back-end, onde a primeira solicitação foi recebida.

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

Não neste caso.

Quando uma conexão TCP é criada, ela segue as regras do iptables, que selecionam uma específica para o back-end para onde o tráfego irá.

Como todos os pedidos a seguir passam por uma conexão TCP já aberta, as regras do iptables não são mais chamadas.

Vamos ver como fica .

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

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

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

  4. A solicitação chega à lareira. Nesse ponto, uma conexão TCP permanente entre os dois pods será estabelecida:

  5. Qualquer próxima solicitação do primeiro pod passará por uma conexão já estabelecida:



Como resultado, você obteve uma resposta mais rápida e maior largura de banda, mas perdeu a capacidade de dimensionar o back-end.

Mesmo se você tiver dois pods no back-end, com uma conexão constante, o tráfego sempre será direcionado para um deles.

Isso pode ser corrigido?

Como o Kubernetes não sabe como equilibrar conexões persistentes, essa tarefa é de sua responsabilidade.

Serviços são um conjunto de endereços IP e portas chamados de terminais.

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

Ou aplique algoritmos de balanceamento mais sofisticados .

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

  1. Obtenha a lista de pontos de extremidade do serviço.
  2. Para cada nó de extremidade, abra uma conexão persistente.
  3. Quando você precisar fazer uma solicitação, use uma das conexões abertas.
  4. Atualize regularmente a lista de terminais, crie novos ou feche as conexões persistentes antigas, se a lista mudar.

Aqui está como será .

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

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

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

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



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

Balanceamento de carga no lado do cliente


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

Se seu aplicativo usar um banco de dados, a conexão TCP não abrirá toda vez que você precisar executar uma solicitação ou obter um documento no banco de dados. 

Em vez disso, uma conexão TCP permanente 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á carregada mais que o restante. O Kube-proxy e o Kubernetes não ajudarão a equilibrar as conexões. Você deve cuidar de equilibrar as consultas no seu banco de dados.

Dependendo da biblioteca que você usa para se conectar ao banco de dados, você pode ter várias opções para resolver esse problema.

A seguir, é apresentado um exemplo de acesso a um cluster de banco de dados MySQL no 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 protegidos
  • 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 é necessária uma alteração na lógica do cliente? Existe uma solução nativa do Kubernetes?

O proxy e o iptables do Kube são projetados para fechar a maioria dos cenários de implantação padrão do Kubernetes. Isto é por conveniência.

Se você usa um serviço da web que fornece uma API REST, está com sorte - nesse caso, conexões TCP permanentes não são usadas, você pode usar qualquer serviço Kubernetes.

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

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

Balanceamento de conexões de longa duração em Kubernetes


O Kubernetes possui quatro tipos de serviços:

  1. Clusterip
  2. NodePort
  3. Balanceador de carga
  4. Sem cabeça

Os três primeiros serviços são baseados no endereço IP virtual, usado pelo kube-proxy para criar regras do iptables. Mas a base fundamental de todos os serviços é um serviço do tipo sem cabeça.

Nenhum endereço IP está associado ao serviço decapitado e fornece apenas um mecanismo para obter uma lista de endereços IP e portas das lareiras associadas (pontos de extremidade).

Todos os serviços são baseados no serviço sem cabeça.

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

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

Portanto, você pode ignorar o kube-proxy e usar diretamente a lista de pontos de extremidade recebidos do serviço sem cabeçalho para equilibrar a carga em seu aplicativo.

Mas como adicionar lógica semelhante a todos os aplicativos implantados em um cluster?

Se seu aplicativo já estiver implantado, essa tarefa poderá parecer impossível. No entanto, existe uma alternativa.

O Service Mesh o ajudará


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 conjunto de conexões.
  3. Atualiza periodicamente o pool, adicionando ou removendo pontos de extremidade.

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

  1. Seleciona uma conexão disponível usando algum tipo de lógica (por exemplo, round-robin).
  2. Atende à solicitação.

Essas etapas funcionam para WebSockets, gRPC e AMQP.

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

No entanto, grades de serviço como Istio ou Linkerd podem ser usadas.

O Service Mesh complementa seu aplicativo com um processo que:

  1. Pesquisa automaticamente endereços IP de serviços.
  2. Verifica 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 bastante recursos. Outras opções estão usando bibliotecas de terceiros, como o Netflix Ribbon, ou proxies programáveis, como o Envoy.

O que acontece se você ignorar problemas de balanceamento?


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

Se você tem mais clientes que servidores, esse não é um problema tão grande.

Suponha que haja cinco clientes que se conectem a dois servidores. Mesmo se não houver balanceamento, os dois servidores serão usados:



As conexões podem ser distribuídas de maneira desigual: talvez quatro clientes estejam conectados ao mesmo servidor, mas há uma boa chance de os dois servidores serem usados.

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

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

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

Outros servidores ficarão ociosos:



Se esses dois servidores não puderem processar o processamento de solicitações do cliente, a escala horizontal não ajudará.

Conclusão


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

No entanto, assim que você começa a trabalhar com protocolos de aplicativos 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 com a possibilidade de balancear no lado do cliente.

Tradução preparado por uma equipe Kubernetes AAS a partir de Mail.ru .

O que mais se pode ler sobre o tópico :

  1. Três níveis de dimensionamento automático no Kubernetes e como usá-los efetivamente
  2. Kubernetes no espírito de pirataria com um modelo de implementação .
  3. Kubernetes .

All Articles