Blocos de construção de aplicativos distribuídos. Segunda aproximação

Anúncio

Colegas, em meados do verão pretendo lançar outra série de artigos sobre o projeto de sistemas de filas: “The VTrade Experiment” - uma tentativa de escrever uma estrutura para sistemas de negociação. A série examinará a teoria e a prática da construção de uma bolsa, leilão e loja. Ao final do artigo, convido você a votar nos temas que mais lhe interessam.

Blocos de construção de aplicativos distribuídos. Segunda aproximação

Este é o artigo final da série sobre aplicações reativas distribuídas em Erlang/Elixir. EM primeiro artigo você pode encontrar os fundamentos teóricos da arquitetura reativa. Segundo artigo ilustra os padrões e mecanismos básicos para a construção de tais sistemas.

Hoje levantaremos questões de desenvolvimento da base de código e projetos em geral.

Organização de serviços

Na vida real, ao desenvolver um serviço, muitas vezes é necessário combinar vários padrões de interação em um controlador. Por exemplo, o serviço de usuários, que resolve o problema de gerenciamento de perfis de usuários do projeto, deve responder às solicitações req-resp e relatar atualizações de perfil via pub-sub. Este caso é bastante simples: por trás das mensagens existe um controlador que implementa a lógica do serviço e publica atualizações.

A situação fica mais complicada quando precisamos implementar um serviço distribuído tolerante a falhas. Vamos imaginar que os requisitos para os usuários mudaram:

  1. agora o serviço deve processar solicitações em 5 nós do cluster,
  2. ser capaz de realizar tarefas de processamento em segundo plano,
  3. e também poder gerenciar dinamicamente listas de assinaturas para atualizações de perfil.

Nota: Não consideramos a questão do armazenamento consistente e da replicação de dados. Vamos supor que esses problemas foram resolvidos anteriormente e que o sistema já possui uma camada de armazenamento confiável e escalável, e os manipuladores possuem mecanismos para interagir com ela.

A descrição formal do serviço aos usuários tornou-se mais complicada. Do ponto de vista do programador, as mudanças são mínimas devido ao uso de mensagens. Para satisfazer o primeiro requisito, precisamos configurar o balanceamento no ponto de troca req-resp.

A necessidade de processar tarefas em segundo plano ocorre com frequência. Nos usuários, isso pode ser a verificação de documentos do usuário, o processamento de multimídia baixada ou a sincronização de dados com mídias sociais. redes. Essas tarefas precisam ser distribuídas de alguma forma dentro do cluster e o progresso da execução monitorado. Portanto, temos duas opções de solução: usar o modelo de distribuição de tarefas do artigo anterior ou, se não for adequado, escrever um agendador de tarefas personalizado que irá gerenciar o pool de processadores da maneira que precisamos.

O ponto 3 requer a extensão do modelo pub-sub. E para implementação, após criar um ponto de troca pub-sub, precisamos lançar adicionalmente o controlador deste ponto dentro do nosso serviço. Assim, é como se estivéssemos movendo a lógica de processamento de assinaturas e cancelamentos da camada de mensagens para a implementação de usuários.

Como resultado, a decomposição do problema mostrou que para atender aos requisitos, precisamos lançar 5 instâncias do serviço em nós diferentes e criar uma entidade adicional - um controlador pub-sub, responsável pela assinatura.
Para executar 5 manipuladores, não é necessário alterar o código de serviço. A única ação adicional é estabelecer regras de equilíbrio no ponto de câmbio, das quais falaremos um pouco mais tarde.
Há também uma complexidade adicional: o controlador pub-sub e o agendador de tarefas personalizado devem funcionar em uma única cópia. Novamente, o serviço de mensagens, como fundamental, deve fornecer um mecanismo para selecionar um líder.

Seleção de líder

Em sistemas distribuídos, a eleição do líder é o procedimento para nomear um único processo responsável por escalonar o processamento distribuído de alguma carga.

Em sistemas que não são propensos à centralização, são utilizados algoritmos universais e baseados em consenso, como paxos ou raft.
Como o sistema de mensagens é um intermediário e um elemento central, ele conhece todos os controladores de serviço – candidatos a líderes. As mensagens podem nomear um líder sem votação.

Após iniciar e conectar-se ao ponto de troca, todos os serviços recebem uma mensagem do sistema #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Se LeaderPid coincide com pid processo atual, ele é apontado como líder, e a lista Servers inclui todos os nós e seus parâmetros.
No momento em que um novo aparece e um nó de cluster funcional é desconectado, todos os controladores de serviço recebem #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} respectivamente.

Dessa forma, todos os componentes estão cientes de todas as alterações e é garantido que o cluster terá um líder a qualquer momento.

Mediadores

Para implementar processos complexos de processamento distribuído, bem como em problemas de otimização de uma arquitetura existente, é conveniente utilizar intermediários.
Para não alterar o código do serviço e resolver, por exemplo, problemas de processamento adicional, roteamento ou registro de mensagens, você pode habilitar um manipulador de proxy antes do serviço, que realizará todo o trabalho adicional.

Um exemplo clássico de otimização pub-sub é um aplicativo distribuído com um núcleo de negócios que gera eventos de atualização, como mudanças de preços no mercado, e uma camada de acesso - N servidores que fornecem uma API de websocket para clientes web.
Se você decidir de frente, o atendimento ao cliente será assim:

  • o cliente estabelece conexões com a plataforma. No lado do servidor que finaliza o tráfego, é lançado um processo para atender essa conexão.
  • No contexto do processo de serviço, ocorrem autorização e assinatura de atualizações. O processo chama o método subscribe para tópicos.
  • Depois que um evento é gerado no kernel, ele é entregue aos processos que atendem as conexões.

Vamos imaginar que temos 50000 assinantes do tópico “notícias”. Os assinantes são distribuídos uniformemente em 5 servidores. Com isso, cada atualização, que chega ao ponto de troca, será replicada 50000 mil vezes: 10000 mil vezes em cada servidor, de acordo com o número de assinantes nele. Não é um esquema muito eficaz, certo?
Para melhorar a situação, vamos apresentar um proxy que tenha o mesmo nome do ponto de troca. O registrador de nomes global deve ser capaz de retornar o processo mais próximo por nome, isso é importante.

Vamos lançar esse proxy nos servidores da camada de acesso, e todos os nossos processos que atendem à API do websocket se inscreverão nele, e não no ponto de troca pub-sub original no kernel. O Proxy assina o núcleo apenas no caso de uma assinatura única e replica a mensagem recebida para todos os seus assinantes.
Como resultado, 5 mensagens serão enviadas entre o kernel e os servidores de acesso, em vez de 50000.

Roteamento e balanceamento

Req-Resp

Na implementação atual de mensagens, existem 7 estratégias de distribuição de solicitações:

  • default. A solicitação é enviada a todos os controladores.
  • round-robin. As solicitações são enumeradas e distribuídas ciclicamente entre os controladores.
  • consensus. Os controladores que atendem o serviço são divididos em líderes e escravos. As solicitações são enviadas apenas ao líder.
  • consensus & round-robin. O grupo tem um líder, mas os pedidos são distribuídos entre todos os membros.
  • sticky. A função hash é calculada e atribuída a um manipulador específico. As solicitações subsequentes com esta assinatura vão para o mesmo manipulador.
  • sticky-fun. Ao inicializar o ponto de troca, a função de cálculo de hash para sticky equilíbrio.
  • fun. Semelhante ao sticky-fun, só você pode redirecioná-lo, rejeitá-lo ou pré-processá-lo adicionalmente.

A estratégia de distribuição é definida quando o ponto de troca é inicializado.

Além do equilíbrio, as mensagens permitem marcar entidades. Vejamos os tipos de tags no sistema:

  • Etiqueta de conexão. Permite entender por qual conexão os eventos ocorreram. Usado quando um processo controlador se conecta ao mesmo ponto de troca, mas com chaves de roteamento diferentes.
  • Etiqueta de serviço. Permite combinar manipuladores em grupos para um serviço e expandir os recursos de roteamento e balanceamento. Para o padrão req-resp, o roteamento é linear. Enviamos uma solicitação para o ponto de troca e ele repassa para o serviço. Mas se precisarmos dividir os manipuladores em grupos lógicos, a divisão será feita usando tags. Ao especificar um tag, a solicitação será enviada para um grupo específico de controladores.
  • Solicitar etiqueta. Permite distinguir entre respostas. Como nosso sistema é assíncrono, para processar respostas de serviço precisamos especificar uma RequestTag ao enviar uma solicitação. A partir dele poderemos entender a resposta a qual solicitação nos chegou.

Pub-sub

Para pub-sub tudo é um pouco mais simples. Temos um ponto de troca onde as mensagens são publicadas. O ponto de troca distribui mensagens entre os assinantes que assinaram as chaves de roteamento de que necessitam (podemos dizer que isso é análogo aos tópicos).

Escalabilidade e tolerância a falhas

A escalabilidade do sistema como um todo depende do grau de escalabilidade das camadas e componentes do sistema:

  • Os serviços são dimensionados adicionando nós adicionais ao cluster com manipuladores para este serviço. Durante a operação experimental, você pode escolher a política de balanceamento ideal.
  • O próprio serviço de mensagens dentro de um cluster separado é geralmente dimensionado movendo pontos de troca particularmente carregados para nós de cluster separados ou adicionando processos de proxy a áreas particularmente carregadas do cluster.
  • A escalabilidade de todo o sistema como característica depende da flexibilidade da arquitetura e da capacidade de combinar clusters individuais em uma entidade lógica comum.

O sucesso de um projeto geralmente depende da simplicidade e da velocidade de expansão. O Messaging em sua versão atual cresce junto com o aplicativo. Mesmo que não tenhamos um cluster de 50 a 60 máquinas, podemos recorrer à federação. Infelizmente, o tema da federação está além do escopo deste artigo.

Reserva

Ao analisar o balanceamento de carga, já discutimos a redundância dos controladores de serviço. No entanto, as mensagens também devem ser reservadas. No caso de falha de um nó ou máquina, as mensagens deverão ser recuperadas automaticamente e no menor tempo possível.

Em meus projetos utilizo nós adicionais que captam a carga em caso de queda. Erlang possui uma implementação de modo distribuído padrão para aplicativos OTP. O modo distribuído executa a recuperação em caso de falha, iniciando o aplicativo com falha em outro nó iniciado anteriormente. O processo é transparente; após uma falha, o aplicativo passa automaticamente para o nó de failover. Você pode ler mais sobre esta funcionalidade aqui.

Desempenho

Vamos tentar comparar pelo menos aproximadamente o desempenho do RabbitMQ e de nossas mensagens personalizadas.
eu encontrei resultados oficiais testes RabbitMQ da equipe openstack.

No parágrafo 6.14.1.2.1.2.2. O documento original mostra o resultado do RPC CAST:
Blocos de construção de aplicativos distribuídos. Segunda aproximação

Não faremos nenhuma configuração adicional no kernel do sistema operacional ou na VM erlang com antecedência. Condições para teste:

  • erl opta: +A1 +sbtu.
  • O teste dentro de um único nó erlang é executado em um laptop com um antigo i7 na versão móvel.
  • Os testes de cluster são realizados em servidores com rede 10G.
  • O código é executado em contêineres docker. Rede em modo NAT.

Código de teste:

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

Script 1: O teste é executado em um laptop com uma versão móvel i7 antiga. O teste, as mensagens e o serviço são executados em um nó em um contêiner Docker:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

Script 2: 3 nós rodando em máquinas diferentes no docker (NAT).

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

Em todos os casos, a utilização da CPU não excedeu 250%

Resultados de

Espero que este ciclo não pareça um despejo mental e que minha experiência seja de benefício real tanto para pesquisadores de sistemas distribuídos quanto para profissionais que estão no início da construção de arquiteturas distribuídas para seus sistemas de negócios e estão olhando para Erlang/Elixir com interesse , mas tenho dúvidas se vale a pena...

foto @chuttersnap

Apenas usuários registrados podem participar da pesquisa. Entrarpor favor

Quais tópicos devo abordar com mais detalhes como parte da série de experimentos VTrade?

  • Teoria: Mercados, ordens e seu timing: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Livro de pedidos. Teoria e prática de implementação de um livro com agrupamentos

  • Visualização de negociação: ticks, barras, resoluções. Como guardar e como colar

  • Backoffice. Planejamento e desenvolvimento. Monitoramento de funcionários e investigação de incidentes

  • API. Vamos descobrir quais interfaces são necessárias e como implementá-las

  • Armazenamento de informações: PostgreSQL, Timescale, Tarantool em sistemas de negociação

  • Reatividade em sistemas de negociação

  • Outro. vou escrever nos comentários

6 usuários votaram. 4 usuários se abstiveram.

Fonte: habr.com

Adicionar um comentário