[Tradução] Modelo de threading Envoy

Tradução do artigo: Modelo de threading Envoy - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Achei este artigo bastante interessante e, como o Envoy é mais frequentemente usado como parte do “istio” ou simplesmente como o “controlador de ingresso” do kubernetes, a maioria das pessoas não tem a mesma interação direta com ele como, por exemplo, com típico Instalações Nginx ou Haproxy. Porém, se algo quebrar, seria bom entender como funciona por dentro. Tentei traduzir o máximo possível do texto para o russo, incluindo palavras especiais; para aqueles que acham doloroso olhar para isso, deixei os originais entre parênteses. Bem-vindo ao gato.

A documentação técnica de baixo nível para a base de código do Envoy é atualmente bastante escassa. Para remediar isso, pretendo fazer uma série de postagens no blog sobre os vários subsistemas do Envoy. Como este é o primeiro artigo, deixe-me saber o que você pensa e o que pode lhe interessar em artigos futuros.

Uma das perguntas técnicas mais comuns que recebo sobre o Envoy é solicitar uma descrição de baixo nível do modelo de threading que ele usa. Neste post, descreverei como o Envoy mapeia conexões para threads, bem como o sistema Thread Local Storage que ele usa internamente para tornar o código mais paralelo e de alto desempenho.

Visão geral do encadeamento

[Tradução] Modelo de threading Envoy

O Envoy usa três tipos diferentes de fluxos:

  • Principal: Este thread controla a inicialização e encerramento do processo, todo o processamento da API XDS (xDiscovery Service), incluindo DNS, verificação de integridade, cluster geral e gerenciamento de tempo de execução, redefinição de estatísticas, administração e gerenciamento geral de processos - sinais do Linux, reinicialização a quente, etc. o que acontece neste thread é assíncrono e "sem bloqueio". Em geral, o thread principal coordena todos os processos de funcionalidade crítica que não requerem uma grande quantidade de CPU para serem executados. Isso permite que a maior parte do código de controle seja escrito como se fosse de thread único.
  • Trabalhador: Por padrão, o Envoy cria um thread de trabalho para cada thread de hardware no sistema, isso pode ser controlado usando a opção --concurrency. Cada thread de trabalho executa um loop de eventos “sem bloqueio”, que é responsável por ouvir cada ouvinte; no momento da escrita (29 de julho de 2017) não há fragmentação do ouvinte, aceitando novas conexões, instanciando uma pilha de filtros para a conexão e processar todas as operações de entrada/saída (IO) durante a vida útil da conexão. Novamente, isso permite que a maior parte do código de manipulação de conexões seja escrita como se fosse de thread único.
  • Liberador de arquivos: Cada arquivo que o Envoy grava, principalmente logs de acesso, possui atualmente um thread de bloqueio independente. Isso se deve ao fato de que a gravação em arquivos armazenados em cache pelo sistema de arquivos, mesmo ao usar O_NONBLOCK às vezes pode ficar bloqueado (suspiro). Quando os threads de trabalho precisam gravar em um arquivo, os dados são, na verdade, movidos para um buffer na memória, onde são eventualmente liberados pelo thread. descarga de arquivo. Esta é uma área do código onde tecnicamente todos os threads de trabalho podem bloquear o mesmo bloqueio enquanto tentam preencher um buffer de memória.

Manipulação de conexão

Conforme discutido brevemente acima, todos os threads de trabalho escutam todos os ouvintes sem qualquer fragmentação. Assim, o kernel é usado para enviar normalmente soquetes aceitos para threads de trabalho. Kernels modernos geralmente são muito bons nisso, eles usam recursos como aumento de prioridade de entrada/saída (IO) para tentar preencher um thread com trabalho antes de começarem a usar outros threads que também estão escutando no mesmo soquete, e também não usam round robin bloqueio (Spinlock) para processar cada solicitação.
Depois que uma conexão é aceita em um thread de trabalho, ela nunca sai desse thread. Todo o processamento adicional da conexão é tratado inteiramente no thread de trabalho, incluindo qualquer comportamento de encaminhamento.

Isto tem várias consequências importantes:

  • Todos os pools de conexões no Envoy são atribuídos a um thread de trabalho. Portanto, embora os pools de conexões HTTP/2 façam apenas uma conexão com cada host upstream por vez, se houver quatro threads de trabalho, haverá quatro conexões HTTP/2 por host upstream em um estado estável.
  • A razão pela qual o Envoy funciona dessa maneira é que, ao manter tudo em um único thread de trabalho, quase todo o código pode ser escrito sem bloqueio e como se fosse um thread único. Esse design facilita a gravação de muitos códigos e é incrivelmente bem dimensionado para um número quase ilimitado de threads de trabalho.
  • No entanto, uma das principais conclusões é que, do ponto de vista do pool de memória e da eficiência da conexão, é realmente muito importante configurar o --concurrency. Ter mais threads de trabalho do que o necessário desperdiçará memória, criará mais conexões ociosas e reduzirá a taxa de pooling de conexões. Na Lyft, nossos contêineres secundários enviados funcionam com simultaneidade muito baixa, para que o desempenho corresponda aproximadamente aos serviços ao lado deles. Executamos o Envoy como um proxy de borda apenas com simultaneidade máxima.

O que significa não bloqueio?

O termo "sem bloqueio" foi usado diversas vezes até agora ao discutir como funcionam os threads principais e de trabalho. Todo o código é escrito partindo do pressuposto de que nada será bloqueado. No entanto, isso não é inteiramente verdade (o que não é inteiramente verdade?).

O Envoy usa vários bloqueios de processo longos:

  • Conforme discutido, ao gravar logs de acesso, todos os threads de trabalho adquirem o mesmo bloqueio antes que o buffer de log na memória seja preenchido. O tempo de retenção do bloqueio deve ser muito baixo, mas é possível que o bloqueio seja contestado em alta simultaneidade e alto rendimento.
  • O Envoy usa um sistema muito complexo para lidar com estatísticas locais do thread. Este será o tema de uma postagem separada. No entanto, mencionarei brevemente que, como parte do processamento local de estatísticas de thread, às vezes é necessário adquirir um bloqueio em um "armazenamento de estatísticas" central. Este bloqueio nunca deve ser necessário.
  • O thread principal precisa periodicamente se coordenar com todos os threads de trabalho. Isso é feito "publicando" do thread principal para os threads de trabalho e, às vezes, dos threads de trabalho de volta para o thread principal. O envio requer um bloqueio para que a mensagem publicada possa ser colocada na fila para entrega posterior. Esses bloqueios nunca devem ser seriamente contestados, mas ainda podem ser tecnicamente bloqueados.
  • Quando o Envoy grava um log no fluxo de erros do sistema (erro padrão), ele bloqueia todo o processo. Em geral, o registro local do Envoy é considerado péssimo do ponto de vista de desempenho, portanto não tem sido dada muita atenção para melhorá-lo.
  • Existem alguns outros bloqueios aleatórios, mas nenhum deles é crítico para o desempenho e nunca deve ser desafiado.

Armazenamento local de thread

Devido à maneira como o Envoy separa as responsabilidades do thread principal das responsabilidades do thread de trabalho, há um requisito de que o processamento complexo possa ser feito no thread principal e, em seguida, fornecido a cada thread de trabalho de maneira altamente simultânea. Esta seção descreve o Envoy Thread Local Storage (TLS) em alto nível. Na próxima seção descreverei como ele é usado para gerenciar um cluster.
[Tradução] Modelo de threading Envoy

Conforme já descrito, o thread principal lida com praticamente todas as funcionalidades do plano de gerenciamento e controle no processo Envoy. O plano de controle está um pouco sobrecarregado aqui, mas quando você olha para ele dentro do próprio processo Envoy e o compara com o encaminhamento que os threads de trabalho fazem, faz sentido. A regra geral é que o processo do thread principal realiza algum trabalho e, em seguida, precisa atualizar cada thread de trabalho de acordo com o resultado desse trabalho. neste caso, o thread de trabalho não precisa adquirir um bloqueio em cada acesso.

O sistema TLS (Thread local storage) do Envoy funciona da seguinte forma:

  • O código em execução no thread principal pode alocar um slot TLS para todo o processo. Embora seja abstraído, na prática é um índice em um vetor, fornecendo acesso O(1).
  • O thread principal pode instalar dados arbitrários em seu slot. Quando isso é feito, os dados são publicados em cada thread de trabalho como um evento normal de loop de eventos.
  • Threads de trabalho podem ler de seu slot TLS e recuperar quaisquer dados locais de thread disponíveis lá.

Embora seja um paradigma muito simples e incrivelmente poderoso, é muito semelhante ao conceito de bloqueio RCU (Read-Copy-Update). Essencialmente, os threads de trabalho nunca veem nenhuma alteração de dados nos slots TLS enquanto o trabalho está em execução. A mudança ocorre apenas durante o período de descanso entre os eventos de trabalho.

O Envoy usa isso de duas maneiras diferentes:

  • Ao armazenar dados diferentes em cada thread de trabalho, os dados podem ser acessados ​​sem qualquer bloqueio.
  • Mantendo um ponteiro compartilhado para dados globais em modo somente leitura em cada thread de trabalho. Assim, cada thread de trabalho possui uma contagem de referência de dados que não pode ser decrementada enquanto o trabalho está em execução. Somente quando todos os trabalhadores se acalmarem e fizerem upload de novos dados compartilhados é que os dados antigos serão destruídos. Isso é idêntico ao RCU.

Threading de atualização de cluster

Nesta seção, descreverei como o TLS (armazenamento local de thread) é usado para gerenciar um cluster. O gerenciamento de cluster inclui API xDS e/ou processamento de DNS, bem como verificação de integridade.
[Tradução] Modelo de threading Envoy

O gerenciamento de fluxo de cluster inclui os seguintes componentes e etapas:

  1. O Cluster Manager é um componente do Envoy que gerencia todos os upstreams de cluster conhecidos, a API Cluster Discovery Service (CDS), as APIs Secret Discovery Service (SDS) e Endpoint Discovery Service (EDS), DNS e verificações externas ativas. Ele é responsável por criar uma visão “eventualmente consistente” de cada cluster upstream, que inclui hosts descobertos, bem como o status de integridade.
  2. O verificador de funcionamento executa uma verificação de funcionamento ativa e relata alterações de status de funcionamento ao gerenciador do cluster.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS são executados para determinar a associação ao cluster. A mudança de estado é retornada ao gerenciador do cluster.
  4. Cada thread de trabalho executa continuamente um loop de eventos.
  5. Quando o gerenciador de cluster determina que o estado de um cluster foi alterado, ele cria um novo instantâneo somente leitura do estado do cluster e o envia para cada thread de trabalho.
  6. Durante o próximo período de silêncio, o thread de trabalho atualizará o instantâneo no slot TLS alocado.
  7. Durante um evento de E/S que deve determinar o host para balanceamento de carga, o balanceador de carga solicitará um slot TLS (Thread local storage) para obter informações sobre o host. Isso não requer bloqueios. Observe também que o TLS também pode acionar eventos de atualização para que balanceadores de carga e outros componentes possam recalcular caches, estruturas de dados, etc. Isso está além do escopo desta postagem, mas é usado em vários locais do código.

Usando o procedimento acima, o Envoy pode processar todas as solicitações sem qualquer bloqueio (exceto conforme descrito anteriormente). Além da complexidade do próprio código TLS, a maior parte do código não precisa entender como funciona o multithreading e pode ser escrito em thread único. Isso torna a maior parte do código mais fácil de escrever, além de um desempenho superior.

Outros subsistemas que fazem uso de TLS

TLS (armazenamento local de thread) e RCU (atualização de leitura e cópia) são amplamente usados ​​no Envoy.

Exemplos de uso:

  • Mecanismo para alterar funcionalidade durante a execução: A lista atual de funcionalidades habilitadas é calculada no thread principal. Cada thread de trabalho recebe um instantâneo somente leitura usando a semântica RCU.
  • Substituindo tabelas de rotas: Para tabelas de rotas fornecidas pelo RDS (Route Discovery Service), as tabelas de rotas são criadas no thread principal. O instantâneo somente leitura será posteriormente fornecido a cada thread de trabalho usando a semântica RCU (Read Copy Update). Isso torna a alteração das tabelas de rotas atomicamente eficiente.
  • Cache de cabeçalho HTTP: Acontece que calcular o cabeçalho HTTP para cada solicitação (durante a execução de aproximadamente 25 mil + RPS por núcleo) é bastante caro. O Envoy calcula centralmente o cabeçalho aproximadamente a cada meio segundo e o fornece a cada trabalhador via TLS e RCU.

Existem outros casos, mas os exemplos anteriores devem fornecer uma boa compreensão de para que é usado o TLS.

Armadilhas de desempenho conhecidas

Embora o Envoy tenha um desempenho geral muito bom, existem algumas áreas notáveis ​​que requerem atenção quando ele é usado com simultaneidade e taxa de transferência muito altas:

  • Conforme descrito neste artigo, atualmente todos os threads de trabalho adquirem um bloqueio ao gravar no buffer de memória do log de acesso. Em alta simultaneidade e alto rendimento, você precisará agrupar os logs de acesso para cada thread de trabalho às custas da entrega fora de ordem ao gravar no arquivo final. Alternativamente, você pode criar um log de acesso separado para cada thread de trabalho.
  • Embora as estatísticas sejam altamente otimizadas, com simultaneidade e produtividade muito altas, provavelmente haverá contenção atômica nas estatísticas individuais. A solução para este problema são contadores por thread de trabalho com reinicialização periódica dos contadores centrais. Isso será discutido em uma postagem subsequente.
  • A arquitetura atual não funcionará bem se o Envoy for implantado em um cenário onde haja poucas conexões que exijam recursos de processamento significativos. Não há garantia de que as conexões serão distribuídas uniformemente entre os threads de trabalho. Isso pode ser resolvido implementando o balanceamento de conexões de trabalho, que permitirá a troca de conexões entre threads de trabalho.

Conclusão

O modelo de threading do Envoy foi projetado para fornecer facilidade de programação e paralelismo massivo às custas de memória e conexões potencialmente desperdiçadas se não for configurado corretamente. Este modelo permite um desempenho muito bom com contagens de threads e taxas de transferência muito altas.
Como mencionei brevemente no Twitter, o design também pode ser executado em uma pilha de rede completa no modo de usuário, como DPDK (Data Plane Development Kit), o que pode resultar em servidores convencionais lidando com milhões de solicitações por segundo com processamento L7 completo. Será muito interessante ver o que será construído nos próximos anos.
Um último comentário rápido: muitas vezes me perguntaram por que escolhemos C++ para Envoy. A razão é que ainda é a única linguagem de nível industrial amplamente utilizada na qual a arquitetura descrita neste post pode ser construída. C++ definitivamente não é adequado para todos ou mesmo para muitos projetos, mas para certos casos de uso ainda é a única ferramenta para realizar o trabalho.

Links para código

Links para arquivos com interfaces e implementações de cabeçalho discutidos nesta postagem:

Fonte: habr.com

Adicionar um comentário