Tudo o que você queria saber sobre o processamento de consultas, mas tinha vergonha de perguntar

O que é um serviço de rede? Este é um programa que aceita solicitações de entrada pela rede e as processa, possivelmente retornando respostas.


Existem muitos aspectos nos quais os serviços de rede diferem entre si. Neste artigo, eu me concentro em como lidar com solicitações recebidas.


A escolha de um método de processamento de solicitação tem consequências de longo alcance. Como criar um serviço de bate-papo suportando 100.000 conexões simultâneas? Qual abordagem a ser adotada para extrair dados de um fluxo de arquivos mal estruturados? A escolha errada levará a um desperdício de tempo e energia.


O artigo discute abordagens como um conjunto de processos / threads, processamento orientado a eventos, padrão de meia sincronização / meia assíncrona e muitos outros. Inúmeros exemplos são dados, os prós e os contras das abordagens, seus recursos e aplicações são considerados.


1. Introdução


O tópico dos métodos de processamento de consultas não é novo, consulte, por exemplo: um , dois . No entanto, a maioria dos artigos considera apenas parcialmente. Este artigo pretende preencher as lacunas e fornecer uma apresentação consistente do problema.


As seguintes abordagens serão consideradas:


  • processamento seqüencial
  • processo de solicitação
  • fluxo de solicitação
  • conjunto de processos / encadeamentos
  • processamento orientado a eventos (padrão do reator)
  • padrão de meia sincronização / meia assíncrona
  • processamento de transportadores

Note-se que um serviço que processa solicitações não é necessariamente um serviço de rede. Este pode ser um serviço que recebe novas tarefas do banco de dados ou fila de tarefas. Neste artigo, os serviços de rede são destinados, mas você precisa entender que as abordagens em consideração têm um escopo mais amplo.


TL; DR


No final do artigo, há uma lista com uma breve descrição de cada abordagem.


Processamento sequencial


Um aplicativo consiste em um único encadeamento em um único processo. Todas as solicitações são processadas apenas sequencialmente. Não há paralelismo. Se várias solicitações chegarem ao serviço ao mesmo tempo, uma delas será processada e as demais na fila.


Além disso, essa abordagem é fácil de implementar. Não há bloqueios e competição por recursos. O menos óbvio é a incapacidade de escalar com um grande número de clientes.


Processo de solicitação


Um aplicativo consiste em um processo principal que aceita solicitações e fluxos de trabalho recebidos. Para cada nova solicitação, o processo principal cria um fluxo de trabalho que processa a solicitação. A escala pelo número de solicitações é simples: cada solicitação obtém seu próprio processo.


Não há nada complicado nessa arquitetura, mas ela tem os problemas limitações :


  • O processo consome muitos recursos.
    Tente criar 10.000 conexões simultâneas com o PostgreSQL RDBMS e observe o resultado.
  • Os processos não têm memória compartilhada (padrão). Se você precisar acessar dados compartilhados ou um cache compartilhado, precisará mapear a memória compartilhada (chamando linux mmap, munmap) ou usar armazenamento externo (memcahed, redis)

Esses problemas não estão parando. A seguir, mostramos como eles são gerenciados no PostgeSQL RDBMS.


Prós desta arquitetura:


  • A queda de um dos processos não afetará os outros. Por exemplo, um erro raro de processamento de caso não descarta o aplicativo inteiro, apenas a solicitação processada sofrerá
  • Diferenciação de direitos de acesso no nível do sistema operacional. Como o processo é a essência do sistema operacional, você pode usar seus mecanismos padrão para delimitar os direitos de acesso aos recursos do sistema operacional.
  • Você pode alterar o processo de execução em tempo real. Por exemplo, se um script separado for usado para processar uma solicitação e, em seguida, para substituir o algoritmo de processamento, basta alterar o script. Um exemplo será considerado abaixo.
  • Máquinas multicore eficientemente usadas

Exemplos:


  • O PostgreSQL RDBMS cria um novo processo para cada nova conexão. A memória compartilhada é usada para trabalhar com dados gerais. O PostgreSQL pode lidar com o alto consumo de recursos de processos de várias maneiras diferentes. Se houver poucos clientes (um suporte dedicado para analistas), não haverá esse problema. Se houver um único aplicativo que acessa o banco de dados, você pode criar um pool de conexão com o banco de dados no nível do aplicativo. Se houver muitos aplicativos, você pode usar o pgbouncer
  • O sshd escuta solicitações de entrada na porta 22 e bifurca-se em todas as conexões. Cada conexão ssh é uma bifurcação do daemon sshd que recebe e executa comandos do usuário em sequência. Graças a essa arquitetura, os recursos do próprio sistema operacional são usados ​​para diferenciar direitos de acesso
  • Um exemplo de nossa própria prática. Existe um fluxo de arquivos não estruturados dos quais você precisa obter metadados. O principal processo de serviço distribui arquivos entre os processos do manipulador. Cada processo do manipulador é um script que utiliza um caminho de arquivo como parâmetro. O processamento do arquivo ocorre em um processo separado; portanto, devido a um erro de processamento, todo o serviço não falha. Para atualizar o algoritmo de processamento, basta alterar os scripts de processamento sem interromper o serviço.

Em geral, devo dizer que essa abordagem tem suas vantagens, que determinam seu escopo, mas a escalabilidade é muito limitada.


Solicitar fluxo


Essa abordagem é muito parecida com a anterior. A diferença é que os threads são usados ​​em vez de processos. Isso permite que você use a memória compartilhada imediatamente. No entanto, as outras vantagens da abordagem anterior não podem mais ser usadas, enquanto o consumo de recursos também será alto.


Prós:


  • Memória compartilhada pronta para uso
  • Facilidade de implementação
  • Uso eficiente de CPUs multi-core

Contras:


  • Um fluxo consome muitos recursos. Em sistemas operacionais do tipo unix, um encadeamento consome quase tantos recursos quanto um processo

Um exemplo de uso é o MySQL. Mas deve-se notar que o MySQL usa uma abordagem mista, portanto este exemplo será discutido na próxima seção.


Conjunto de processos / encadeamentos


Os fluxos (processos) criam caros e longos. Para não desperdiçar recursos, você pode usar o mesmo thread repetidamente. Tendo limitado adicionalmente o número máximo de threads, obtemos um pool de threads (processos). Agora, o encadeamento principal aceita solicitações recebidas e as coloca em uma fila. Os fluxos de trabalho recebem solicitações da fila e as processam. Essa abordagem pode ser adotada como o escalonamento natural do processamento seqüencial de solicitações: cada encadeamento de trabalho pode processar fluxos apenas sequencialmente, agrupando-os permite processar solicitações em paralelo. Se cada fluxo puder suportar 1000 rps, cinco fluxos lidarão com a carga perto de 5000 rps (sujeito a uma concorrência mínima por recursos compartilhados).


O pool pode ser criado com antecedência no início do serviço ou formado gradualmente. O uso de um conjunto de encadeamentos é mais comum como permite aplicar memória compartilhada.


O tamanho do conjunto de encadeamentos não precisa ser limitado. Um serviço pode usar threads gratuitos do pool e, se não houver, crie um novo thread. Após o processamento da solicitação, o encadeamento se junta ao pool e aguarda a próxima solicitação. Essa opção é uma combinação de uma abordagem de encadeamento sob solicitação e um conjunto de encadeamentos. Um exemplo será dado abaixo.


Prós:


  • o uso de muitos núcleos de CPU
  • redução de custos para criar um encadeamento / processo

Contras:


  • Escalabilidade limitada no número de clientes simultâneos. O uso do pool nos permite reutilizar o mesmo encadeamento várias vezes sem custos adicionais de recursos, no entanto, ele não resolve o problema fundamental de um grande número de recursos consumidos pelo encadeamento / processo. A criação de um serviço de bate-papo que possa suportar 100.000 conexões simultâneas usando essa abordagem falhará.
  • A escalabilidade é limitada por recursos compartilhados, por exemplo, se os threads usam memória compartilhada ajustando o acesso a ela usando semáforos / mutexes. Essa é uma limitação de todas as abordagens que usam recursos compartilhados.

Exemplos:


  1. Aplicativo Python em execução com uWSGI e nginx. O processo principal do uWSGI recebe solicitações de entrada do nginx e as distribui entre os processos Python do interpretador que processam as solicitações. O aplicativo pode ser escrito em qualquer estrutura compatível com uWSGI - Django, Flask, etc.
  2. O MySQL usa um pool de threads: cada nova conexão é processada por um dos threads livres do pool. Se não houver threads livres, o MySQL criará um novo thread. O tamanho do conjunto de threads livres e o número máximo de threads (conexões) são limitados pelas configurações.

Talvez essa seja uma das abordagens mais comuns para a criação de serviços de rede, se não a mais comum. Permite escalar bem, atingindo grandes rps. A principal limitação da abordagem é o número de conexões de rede processadas simultaneamente. De fato, essa abordagem funciona bem apenas se as solicitações forem curtas ou houver poucos clientes.


Processamento orientado a eventos (padrão do reator)


Dois paradigmas - síncronos e assíncronos - são eternos concorrentes um do outro. Até o momento, apenas abordagens síncronas foram discutidas, mas seria errado ignorar a abordagem assíncrona. O processamento de solicitação reativo ou orientado a eventos é uma abordagem na qual cada operação de E / S é executada de forma assíncrona e, no final da operação, um manipulador é chamado. Como regra, o processamento de cada solicitação consiste em muitas chamadas assíncronas seguidas pela execução de manipuladores. A qualquer momento, um aplicativo de thread único executa o código de apenas um manipulador, mas a execução dos manipuladores de várias solicitações se alterna entre si, o que permite processar simultaneamente (pseudo-paralelamente) muitas solicitações simultâneas.


Uma discussão completa dessa abordagem está além do escopo deste artigo. Para uma visão mais profunda, você pode recomendar o Reator (Reator) : Qual é o segredo da velocidade do NodeJS? , Dentro do NGINX . Aqui nos limitamos a considerar os prós e os contras dessa abordagem.


Prós:


  • Escala eficaz por rps e o número de conexões simultâneas. Um serviço reativo pode processar simultaneamente um grande número de conexões (dezenas de milhares) se a maioria das conexões estiver aguardando a conclusão da E / S

Contras:


  • A complexidade do desenvolvimento. Programar no estilo assíncrono é mais difícil do que no síncrono. A lógica do processamento de solicitações é mais complexa, a depuração também é mais difícil do que no código síncrono.
  • Erros que levam ao bloqueio de todo o serviço. Se o idioma ou o tempo de execução não tiver sido projetado originalmente para processamento assíncrono, uma única operação síncrona poderá bloquear todo o serviço, negando a possibilidade de dimensionamento.
  • Difícil de dimensionar nos núcleos da CPU. Essa abordagem assume um único encadeamento em um único processo, portanto, você não pode usar vários núcleos de CPU ao mesmo tempo. Deve-se notar que existem maneiras de contornar essa limitação.
  • Corolário do parágrafo anterior: essa abordagem não é adequada para solicitações que exigem CPU. O número de rps para essa abordagem é inversamente proporcional ao número de operações da CPU necessárias para processar cada solicitação. Exigir solicitações de CPU nega as vantagens dessa abordagem.

Exemplos:


  1. O Node.js usa o padrão de reator pronto para uso. Para mais detalhes, consulte Qual é o segredo da velocidade do NodeJS?
  2. nginx: os processos de trabalho do nginx usam o padrão do reator para processar solicitações em paralelo. Consulte Inside NGINX para obter mais detalhes.
  3. Programa C / C ++ que usa diretamente ferramentas do SO (epoll no linux, IOCP no windows, kqueue no FreeBSD) ou usa a estrutura (libev, libevent, libuv, etc.).

Meia sincronização / metade assíncrona


O nome é obtido em POSA: Padrões para objetos simultâneos e em rede . No original, esse padrão é interpretado de maneira muito ampla, mas, para os propósitos deste artigo, entenderei esse padrão de maneira um pouco mais restrita. Half sync / half async é uma abordagem de processamento de solicitação que usa um fluxo de controle leve (thread verde) para cada solicitação. Um programa consiste em um ou mais threads no nível do sistema operacional; no entanto, o sistema de execução do programa suporta threads verdes que o SO não vê e não pode controlar.


Alguns exemplos para tornar a consideração mais específica:


  1. Serviço no idioma Go. A linguagem Go suporta muitos threads de execução leves - goroutine. O programa usa um ou mais encadeamentos do sistema operacional, mas o programador opera com goroutines, que são distribuídas de forma transparente entre os encadeamentos do sistema operacional, a fim de usar CPUs com vários núcleos
  2. Serviço Python com biblioteca gevent. A biblioteca gevent permite que o programador use threads verdes no nível da biblioteca. O programa inteiro é executado em um único thread do sistema operacional.

Em essência, essa abordagem foi projetada para combinar o alto desempenho da abordagem assíncrona com a simplicidade da programação de código síncrono.


Usando essa abordagem, apesar da ilusão de sincronismo, o programa funcionará de forma assíncrona: o sistema de execução do programa controlará o loop de eventos e cada operação "síncrona" será realmente assíncrona. Quando essa operação é chamada, o sistema de execução chama a operação assíncrona usando as ferramentas do SO e registra o manipulador da conclusão da operação. Quando a operação assíncrona estiver concluída, o sistema de execução chamará o manipulador registrado anteriormente, que continuará a executar o programa no ponto de chamada da operação "síncrona".


Como resultado, a abordagem half sync / half async contém algumas vantagens e algumas desvantagens da abordagem assíncrona. O volume do artigo não nos permite considerar essa abordagem em detalhes. Para os interessados, aconselho a ler o capítulo com o mesmo nome no livro POSA: Padrões para objetos simultâneos e em rede .


A abordagem half sync / half async apresenta uma nova entidade de “fluxo verde” - um fluxo de controle leve no nível do programa ou sistema de execução de biblioteca. O que fazer com linhas verdes é a escolha de um programador. Ele pode usar um conjunto de threads verdes, pode criar um novo thread verde para cada nova solicitação. A diferença em relação aos processos / threads do SO é que os threads verdes são muito mais baratos: consomem muito menos RAM e são criados muito mais rapidamente. Isso permite que você crie um grande número de threads verdes, por exemplo, centenas de milhares no idioma Go. Uma quantidade tão grande justifica o uso da abordagem verde de fluxo sob solicitação.


Prós:


  • Escala bem em rps e o número de conexões simultâneas
  • O código é mais fácil de escrever e depurar em comparação com a abordagem assíncrona

Contras:


  • Como a execução das operações é realmente assíncrona, são possíveis erros de programação quando uma única operação síncrona bloqueia todo o processo. Isso é especialmente sentido nas linguagens em que essa abordagem é implementada por meio de uma biblioteca, por exemplo, Python.
  • A opacidade do programa. Ao usar threads ou processos do SO, o algoritmo de execução do programa é claro: cada thread / processo executa operações na sequência em que são escritas no código. Usando a abordagem half sync / half async, as operações gravadas seqüencialmente no código podem alternar imprevisivelmente com as operações que processam solicitações simultâneas.
  • Inadequado para sistemas em tempo real. O processamento assíncrono de solicitações complica muito o fornecimento de garantias para o tempo de processamento de cada solicitação individual. Isso é uma consequência do parágrafo anterior.

Dependendo da implementação, essa abordagem se adapta bem aos núcleos da CPU (Golang) ou não é escalável (Python).
Essa abordagem, além de assíncrona, permite lidar com um grande número de conexões simultâneas. Mas programar um serviço usando essa abordagem é mais fácil, porque o código é escrito em um estilo síncrono.


Processamento de transportadores


Como o nome indica, nessa abordagem, as solicitações são processadas por pipeline. O processo de processamento consiste em vários threads do SO organizados em uma cadeia. Cada encadeamento é um link na cadeia; ele executa um determinado subconjunto das operações necessárias para processar a solicitação. Cada solicitação passa seqüencialmente por todos os links da cadeia, e diferentes links a cada momento processam solicitações diferentes.


Prós:


  • Essa abordagem escala bem em rps. Quanto mais links na cadeia, mais solicitações são processadas por segundo.
  • O uso de vários threads permite escalar bem nos núcleos da CPU.

Contras:


  • Nem todas as categorias de consulta são adequadas para essa abordagem. Por exemplo, organizar pesquisas longas usando essa abordagem será difícil e inconveniente.
  • A complexidade da implementação e depuração. Bata o processamento seqüencial para que a produtividade seja alta pode ser difícil. Depurar um programa no qual cada solicitação é processada seqüencialmente em vários threads paralelos é mais difícil que o processamento seqüencial.

Exemplos:


  1. Um exemplo interessante de processamento de transportadores foi descrito no relatório highload 2018 A evolução da arquitetura do sistema de negociação e compensação da Bolsa de Moscou

O pipelining é amplamente usado, mas na maioria das vezes os links são componentes individuais em processos independentes que trocam mensagens, por exemplo, por meio de uma fila de mensagens ou banco de dados.


Sumário


Um breve resumo das abordagens consideradas:


  • Processamento síncrono.
    Uma abordagem simples, mas muito limitada em escalabilidade, tanto em rps quanto no número de conexões simultâneas. Não permite o uso de vários núcleos de CPU ao mesmo tempo.
  • Um novo processo para cada solicitação.
    . , . . ( , ).
  • .
    , , . , .
  • /.
    /. . rps . . .
  • - (reactor ).
    rps . - , . CPU
  • Half sync/half async.
    rps . CPU (Golang) (Python). , () . reactor , , reactor .
  • .
    , . (, long polling ).

, .


: ? , ?


Referências


  1. :
  2. - :
  3. :
  4. Half sync/half async:
  5. :
  6. :

Source: https://habr.com/ru/post/pt432630/


All Articles