Como arquitetar um aplicativo Web baseado em Websockets, pesado em tempo real?

17

No processo de desenvolvimento de um aplicativo de página única em tempo real, adotei progressivamente websockets para capacitar meus usuários com dados atualizados. Durante essa fase, fiquei triste ao perceber que estava destruindo muito a estrutura do meu aplicativo e não consegui encontrar uma solução para esse fenômeno.

Antes de entrar em detalhes, apenas um pouco de contexto:

  • O webapp é um SPA em tempo real;
  • O back-end está em Ruby on Rails. Os eventos em tempo real são enviados por Ruby a uma chave Redis, e um servidor de micro nós puxa isso para trás e o envia para Socket.Io;
  • O Frontend está no AngularJS e se conecta diretamente ao servidor socket.io no Nó.

No lado do servidor, antes do tempo real, eu tinha uma clara separação de recursos baseada em controlador / modelo, com o processamento anexado a cada um. Esse design clássico do MVC foi completamente fragmentado, ou pelo menos ignorado, logo quando comecei a enviar coisas via websockets para meus usuários. Agora tenho um único canal no qual todo o meu aplicativo flui para dados mais ou menos estruturados . E acho estressante.

No front-end, a principal preocupação é a duplicação da lógica de negócios. Quando o usuário carrega a página, tenho que carregar meus modelos através de chamadas clássicas de AJAX. Mas também preciso lidar com a inundação de dados em tempo real e me pego duplicando grande parte da minha lógica de negócios do lado do cliente para manter a consistência dos meus modelos do lado do cliente.

Após algumas pesquisas, não consigo encontrar bons posts, artigos, livros ou o que quer que dê conselhos sobre como se pode e deve projetar a arquitetura de um webapp moderno com alguns tópicos específicos em mente:

  • Como estruturar os dados que são enviados do servidor para o usuário?
    • Devo enviar apenas eventos como "este recurso foi atualizado e você deve recarregá-lo por meio de uma chamada AJAX" ou enviar por push os dados atualizados e substituir os dados anteriores carregados pelas chamadas AJAX iniciais?
    • Como definir um esqueleto coerente e escalável para os dados enviados? é uma mensagem de atualização de modelo ou "houve um erro com blahblahblah"
  • Como não enviar dados sobre tudo de qualquer lugar do back-end?
  • Como reduzir a duplicação da lógica de negócios no servidor e no cliente?
Philippe Durix
fonte
Você tem certeza que o Rails é a melhor opção para o seu SPA? O Rails é ótimo, mas é voltado para o aplicativo monolítico ... você pode querer um back-end modular com separação de preocupações ... Dependendo de suas necessidades, consideraria estruturas Ruby alternativas para um SPA em tempo real.
Myst,
1
Não tenho certeza se o Rails é a melhor opção, mas estou muito satisfeito com a pilha instalada pelo menos no back-end (provavelmente porque sou bom com essa estrutura específica). Minha preocupação aqui é mais sobre como projetar o SPA do ponto de vista agnóstico da tecnologia. E também não quero multiplicar o número de linguagens, estruturas e bibliotecas em um único projeto.
Philippe Durix
O benchmark vinculado pode ter problemas, mas expõe uma fraqueza na implementação atual do ActiveRecord. Eu posso estar inclinado a respeito do plezi.io , mas, como apontado nos resultados posteriores do benchmark , ele fornece melhorias significativas de desempenho, mesmo antes do clustering e do Redis (que não foram testados). Eu acho que teve um desempenho melhor que o node.js ... Até que as coisas mudem, eu usaria o plezi.io.
Myst

Respostas:

10

Como estruturar os dados que são enviados do servidor para o usuário?

Use o padrão de mensagens . Bem, você já está usando um protocolo de mensagens, mas quero dizer estruturar as alterações como mensagens ... especificamente eventos. Quando o lado do servidor muda, isso resulta em eventos de negócios. No seu cenário, as visualizações do seu cliente estão interessadas nesses eventos. Os eventos devem conter todos os dados relevantes para essa alteração (nem sempre todos os dados da visualização). A página do cliente deve atualizar as partes da exibição que está mantendo com os dados do evento.

Por exemplo, se você estava atualizando um ticker de ações e a AAPL foi alterada, não desejaria reduzir todos os preços das ações ou mesmo todos os dados sobre a AAPL (nome, descrição, etc.). Você apenas enviaria AAPL, delta e novo preço. No cliente, você atualizaria apenas o preço das ações na exibição.

Devo enviar apenas eventos como "este recurso foi atualizado e você deve recarregá-lo por meio de uma chamada AJAX" ou enviar por push os dados atualizados e substituir os dados anteriores carregados pelas chamadas AJAX iniciais?

Eu também não diria. Se você estiver enviando o evento, vá em frente e envie dados relevantes com ele (não os dados do objeto inteiro). Dê um nome para o tipo de evento que é. (A nomeação e quais dados são relevantes para esse evento estão além do escopo do funcionamento mecânico do sistema. Isso tem mais a ver com a modelagem da lógica de negócios.) Seus atualizadores de exibição precisam saber como converter cada evento específico em uma alteração precisa da visualização (ou seja, atualize apenas o que mudou).

Como definir um esqueleto coerente e escalável para os dados enviados? é uma mensagem de atualização de modelo ou "houve um erro com blahblahblah"

Eu diria que essa é uma pergunta grande e aberta que deve ser dividida em várias outras perguntas e postada separadamente.

Em geral, porém, seu sistema de back-end deve criar e despachar eventos para acontecimentos importantes para o seu negócio. Eles podem vir de feeds externos ou de atividades no próprio backend.

Como não enviar dados sobre tudo de qualquer lugar do back-end?

Use o padrão de publicação / assinatura . Quando o seu SPA carrega uma nova página que está interessada em receber atualizações em tempo real, a página deve se inscrever apenas nos eventos que pode usar e chamar a lógica de atualização de exibição à medida que esses eventos chegarem. Você provavelmente precisará de lógica pub / sub em o servidor para reduzir a carga da rede. Existem bibliotecas para Websocket pub / sub, mas não tenho certeza do que elas estão no ecossistema Rails.

Como reduzir a duplicação da lógica de negócios no servidor e no cliente?

Parece que você está tendo que atualizar os dados da visualização no cliente e no servidor. Meu palpite é que você precisa dos dados da exibição do servidor para ter um instantâneo para iniciar o cliente em tempo real. Sendo que existem duas linguagens / plataformas envolvidas (Ruby e Javascript), a lógica de atualização da visualização precisará ser escrita em ambas. Além de transpilar (que tem seus próprios problemas), não vejo uma maneira de contornar isso.

Ponto técnico: a manipulação de dados (atualização de exibição) não é lógica de negócios. Se você quer dizer validação de caso de uso, isso parece inevitável, pois as validações do cliente são necessárias para uma boa experiência do usuário, mas, no final das contas, o servidor não pode confiar nele.


Aqui está como eu vejo uma coisa assim estruturada bem.

Visualizações do cliente:

  • Solicita um instantâneo da visualização e o número do último evento visto da visualização
    • Isso pré-preencherá a visualização para que o cliente não precise construir do zero.
    • Poderia ser sobre HTTP GET para simplificar
  • Estabelece uma conexão com o websocket e assina eventos específicos, iniciando no último número de evento da exibição.
  • Recebe eventos por websocket e atualiza sua exibição com base no tipo / dados do evento.

Comandos do cliente:

  • Solicitar alteração de dados (HTTP PUT / POST / DELETE)
    • A resposta é apenas sucesso ou falha + erro
    • (O (s) evento (s) gerado (s) pela mudança virá pelo websocket e acionará uma atualização de visualização.

O lado do servidor pode realmente ser dividido em vários componentes com responsabilidades limitadas. Um que apenas processa as solicitações recebidas e cria eventos. Outro poderia gerenciar assinaturas de clientes, ouvir eventos (por exemplo, em processo) e encaminhar eventos apropriados aos assinantes. Você pode ter um terço que ouça eventos e atualize as visualizações do servidor - talvez isso ocorra antes que os assinantes recebam os eventos.

O que descrevi é uma forma de CQRS + Messaging e uma estratégia típica para resolver os tipos de problemas que você está enfrentando.

Não trouxe o Event Sourcing para essa descrição, pois não tenho certeza se é algo que você deseja assumir ou se precisa necessariamente. Mas é um padrão relacionado.

Kasey Speakman
fonte
Eu progredi bastante no tópico, e os indicadores que você deu foram muito úteis. Aceitei a resposta porque usei muitos conselhos, mesmo que não os tenha usado. Vou descrever o caminho que segui em outra resposta.
Philippe Durix 18/01/19
4

Depois de alguns meses de trabalho principalmente no back-end, pude usar alguns dos conselhos aqui para resolver os problemas que a plataforma estava enfrentando.

O principal objetivo ao repensar o back-end era manter o máximo possível o CRUD. Todas as ações, mensagens e solicitações espalhadas por várias rotas foram reagrupadas em recursos criados, atualizados, lidos ou excluídos . Parece óbvio agora, mas essa tem sido uma maneira muito difícil de pensar para aplicar com cuidado.

Depois que tudo foi organizado em recursos, eu pude anexar mensagens em tempo real aos modelos.

  • A criação dispara uma mensagem com um novo recurso;
  • A atualização aciona uma mensagem apenas com os atributos atualizados (mais o UUID);
  • A exclusão aciona uma mensagem de exclusão.

Na API Rest, todos os métodos de criação, atualização e exclusão geram uma resposta exclusiva, o código HTTP informando o sucesso ou a falha e os dados reais sendo enviados pelos soquetes da web.

No front-end, cada recurso é tratado por um componente específico que os carrega através do HTTP na inicialização, depois assina atualizações e mantém seu estado ao longo do tempo. As visualizações são vinculadas a esses componentes para exibir recursos e executar ações nesses recursos através dos mesmos componentes.


Achei as leituras do CQRS + Messaging e Event Sourcing muito interessantes, mas achei que era um pouco complicado demais para o meu problema e talvez seja mais adaptado a aplicativos intensivos em que o comprometimento de dados em um banco de dados centralizado é um luxo caro. Mas definitivamente vou ter em mente essa abordagem.

Nesse caso, o aplicativo terá poucos clientes simultâneos e assumi a responsabilidade de confiar muito no banco de dados. Os modelos mais alterados são armazenados no Redis, que eu confio para lidar com algumas centenas de atualizações por segundo.

Philippe Durix
fonte