Como projetar um aplicativo da web ajax multiusuário para ser simultaneamente seguro


95

Tenho uma página da web que mostra uma grande quantidade de dados do servidor. A comunicação é feita via ajax.

Cada vez que o usuário interage e altera esses dados (digamos que o usuário A renomeie algo), ele instrui o servidor a fazer a ação e o servidor retorna os novos dados alterados.

Se o usuário B acessar a página ao mesmo tempo e criar um novo objeto de dados, ele informará novamente ao servidor via ajax e o servidor retornará com o novo objeto para o usuário.

Na página de A, temos os dados com um objeto renomeado. E na página de B temos os dados com um novo objeto. No servidor, os dados têm um objeto renomeado e um novo objeto.

Quais são minhas opções para manter a página em sincronia com o servidor quando vários usuários a estão usando ao mesmo tempo?

Opções como bloquear a página inteira ou despejar todo o estado para o usuário a cada alteração são evitadas.

Se ajudar, neste exemplo específico, a página da web chama um método da web estático que executa um procedimento armazenado no banco de dados. O procedimento armazenado retornará todos os dados alterados e nada mais. O webmethod estático então encaminha o retorno do procedimento armazenado para o cliente.

Edição Bounty:

Como você projeta um aplicativo da web multiusuário que usa Ajax para se comunicar com o servidor, mas evita problemas de simultaneidade?

Ou seja, acesso simultâneo à funcionalidade e aos dados em um banco de dados sem nenhum risco de corrupção de dados ou estado


não tenho tanta certeza, mas você pode ter uma página como o Facebook onde o navegador envia uma solicitação ajax constantemente buscando alterações no banco de dados do servidor e atualizando-as no navegador
Santosh Linkha

Serializar o estado do cliente e, em seguida, informar ao servidor via ajax aqui está o meu estado, o que eu preciso atualizar é uma opção. Mas requer que o cliente saiba como atualizar toda e qualquer informação em um só lugar.
Raynos

1
A melhor solução para a simultaneidade do usuário final não é simplesmente uma das variantes de push? Websockets, cometa, etc.
davin

@davin pode muito bem ser. Mas eu não estou familiarizado com o cometa e os websockets não estão lá para suporte cross-browser.
Raynos de

2
existem bons pacotes para suporte cruzado de navegadores, especificamente eu recomendo socket.io, embora haja também jWebSocket e muitos outros. Se você seguir o caminho do socket.io, poderá incorporar todos os tipos de guloseimas do node.js, como frameworks e mecanismos de modelagem (do lado do cliente) etc.
davin

Respostas:


157

Visão geral:

  • Introdução
  • Arquitetura do servidor
  • Arquitetura do cliente
  • Atualizar caso
  • Confirmar caso
  • Caso de conflito
  • Desempenho e escalabilidade

Oi Raynos,

Não vou discutir nenhum produto em particular aqui. O que outros mencionaram é um bom conjunto de ferramentas para já dar uma olhada (talvez adicionar node.js a essa lista).

Do ponto de vista arquitetônico, você parece ter o mesmo problema que pode ser visto no software de controle de versão. Um usuário faz check-in de uma alteração em um objeto, outro usuário deseja alterar o mesmo objeto de outra forma => conflito. Você deve integrar as alterações dos usuários aos objetos e, ao mesmo tempo, ser capaz de fornecer atualizações oportunas e eficientes, detectando e resolvendo conflitos como o acima.

Se eu estivesse no seu lugar, desenvolveria algo assim:

1. Lado do servidor:

  • Determine um nível razoável no qual você definiria o que eu chamaria de "artefatos atômicos" (a página? Objetos na página? Valores dentro dos objetos?). Isso dependerá de seus servidores web, banco de dados e hardware de cache, número de usuário, número de objetos, etc. Não é uma decisão fácil de tomar.

  • Para cada artefato atômico, tenha:

    • uma identificação única em todo o aplicativo
    • um ID de versão incrementado
    • um mecanismo de bloqueio para acesso de gravação (mutex talvez)
    • um pequeno histórico ou "changelog" dentro de um ringbuffer (a memória compartilhada funciona bem para aqueles). Um único par de valores-chave também pode ser adequado, embora menos extensível. consulte http://en.wikipedia.org/wiki/Circular_buffer
  • Um servidor ou componente de pseudo-servidor que é capaz de entregar registros de mudanças relevantes a um usuário conectado de forma eficiente. Observer-Pattern é seu amigo para isso.

2. Lado do cliente:

  • Um cliente javascript que é capaz de ter uma conexão HTTP de longa duração com o servidor mencionado acima, ou usa polling leve.

  • Um componente atualizador de artefato javascript que atualiza o conteúdo dos sites quando o cliente javascript conectado notifica sobre as mudanças no histórico de artefatos observados. (novamente um padrão de observador pode ser uma boa escolha)

  • Um componente de confirmação de artefato javascript que pode solicitar a alteração de um artefato atômico, tentando adquirir o bloqueio mutex. Ele detectará se o estado do artefato foi alterado por outro usuário apenas alguns segundos antes (latância do cliente javascript e fatores de processo de confirmação), comparando o artefato-versão-id do cliente conhecido e o artefato-versão-id atual do servidor.

  • Um solucionador de conflitos javascript que permite uma decisão humana qual-mudança-é-a-certa. Você pode não querer apenas dizer ao usuário "Alguém foi mais rápido do que você. Excluí sua alteração. Vá chorar.". Muitas opções de diferenças bastante técnicas ou soluções mais amigáveis ​​ao usuário parecem possíveis.

Então, como seria ...

Caso 1: diagrama tipo-sequência para atualização:

  • O navegador renderiza a página
  • javascript "vê" artefatos em que cada um tem pelo menos um campo de valor, único e um id de versão
  • cliente javascript é iniciado, solicitando "assistir" o histórico de artefatos encontrados a partir de suas versões encontradas (alterações mais antigas não são interessantes)
  • O processo do servidor registra a solicitação e verifica e / ou envia continuamente o histórico
  • As entradas do histórico podem conter notificações simples "artefato x foi alterado, cliente pls solicitação de dados" permitindo ao cliente pesquisar independentemente ou conjuntos de dados completos "artefato x mudou para o valor foo"
  • javascript artifact-updater faz o que pode para buscar novos valores assim que se sabe que foram atualizados. Ele executa novas solicitações de ajax ou é alimentado pelo cliente javascript.
  • O conteúdo das páginas DOM é atualizado, o usuário é opcionalmente notificado. A observação da história continua.

Caso 2: agora para comprometer:

  • o artifact-committer sabe o novo valor desejado a partir da entrada do usuário e envia uma solicitação de mudança para o servidor
  • mutex do servidor é adquirido
  • O servidor recebe "Ei, eu conheço o estado do artefato x da versão 123, deixe-me defini-lo para o valor foo pls."
  • Se a versão do lado do servidor do artefato x for igual (não pode ser menor) que 123, o novo valor é aceito, um novo id de versão 124 é gerado.
  • As novas informações de estado "atualizadas para a versão 124" e, opcionalmente, o novo valor foo são colocados no início do ringbuffer do artefato x (changelog / histórico)
  • serveride mutex é lançado
  • O committer do artefato solicitante fica feliz em receber uma confirmação de commit junto com o novo id.
  • enquanto isso, o componente do servidor do lado do servidor mantém polling / empurrando os ringbuffers para clientes conectados. Todos os clientes observando o buffer do artefato x obterão as novas informações de estado e valor dentro de sua latência normal (consulte o caso 1.)

Caso 3: para conflitos:

  • O committer do artefato sabe o novo valor desejado a partir da entrada do usuário e envia uma solicitação de mudança ao servidor
  • entretanto, outro usuário atualizou o mesmo artefato com sucesso (consulte o caso 2), mas devido a várias latências, isso ainda é desconhecido para nosso outro usuário.
  • Assim, um mutex do lado do servidor é adquirido (ou aguardado até que o usuário "mais rápido" confirme sua alteração)
  • O servidor recebe "Ei, eu conheço o estado do artefato x da versão 123, deixe-me defini-lo com o valor foo."
  • No lado do servidor, a versão do artefato x agora já é 124. O cliente solicitante não pode saber o valor que ele estaria substituindo.
  • Obviamente, o servidor tem que rejeitar a solicitação de mudança (sem contar nas prioridades de substituição de intervenção divina), libera o mutex e é gentil o suficiente para enviar de volta o novo id de versão e o novo valor diretamente para o cliente.
  • confrontado com uma solicitação de confirmação rejeitada e um valor que o usuário solicitante de mudança ainda não sabia, o committer de artefato javascript se refere ao resolvedor de conflito que exibe e explica o problema para o usuário.
  • O usuário, sendo apresentado a algumas opções pelo resolvedor de conflito inteligente JS, tem permissão para outra tentativa de alterar o valor.
  • Depois que o usuário seleciona um valor que considera correto, o processo começa do caso 2 (ou caso 3 se outra pessoa foi mais rápida, novamente)

Algumas palavras sobre desempenho e escalabilidade

Sondagem HTTP vs. "envio" de HTTP

  • A pesquisa cria solicitações, uma por segundo, 5 por segundo, o que quer que você considere uma latência aceitável. Isso pode ser bastante cruel para sua infraestrutura se você não configurar seu (Apache?) E (php?) Bem o suficiente para serem iniciantes "leves". É desejável otimizar a solicitação de sondagem no lado do servidor para que seja executada por muito menos tempo do que a duração do intervalo de sondagem. Dividir esse tempo de execução pela metade pode significar reduzir a carga de todo o sistema em até 50%,
  • Enviar push via HTTP (assumindo que os webworkers estão muito distantes para suportá-los) exigirá que você tenha um processo apache / lighthttpd disponível para cada usuário o tempo todo . A memória residente reservada para cada um desses processos e a memória total de seus sistemas será um limite de escala muito certo que você encontrará. Será necessário reduzir o consumo de memória da conexão, bem como limitar a quantidade contínua de CPU e trabalho de E / S feito em cada um deles (você quer muito tempo de repouso / inatividade)

escala de backend

  • Esqueça o banco de dados e o sistema de arquivos, você precisará de algum tipo de back-end baseado em memória compartilhada para a pesquisa frequente (se o cliente não pesquisar diretamente, então cada processo do servidor em execução irá)
  • se você optar pelo memcache, poderá escalar melhor, mas ainda é caro
  • O mutex para commits deve funcionar globalmente, mesmo se você quiser ter vários servidores front-end para balancear a carga.

dimensionamento de front-end

  • independentemente se você está pesquisando ou recebendo "push", tente obter informações de todos os artefatos observados em uma única etapa.

ajustes "criativos"

  • Se os clientes estão pesquisando e muitos usuários tendem a assistir os mesmos artefatos, você pode tentar publicar o histórico desses artefatos como um arquivo estático, permitindo que o apache o armazene em cache, mas atualizando-o no lado do servidor quando os artefatos forem alterados. Isso tira o PHP / memcache do jogo para algumas solicitações. Lighthttpd é muito eficiente em servir arquivos estáticos.
  • use uma rede de distribuição de conteúdo como cotendo.com para enviar o histórico de artefatos para lá. A latência de push será maior, mas a escalabilidade é um sonho
  • escrever um servidor real (não usando HTTP) ao qual os usuários se conectam usando java ou flash (?). Você tem que lidar com o atendimento de muitos usuários em um thread de servidor. Percorrer soquetes abertos, fazendo (ou delegando) o trabalho necessário. Pode escalar por meio de processos de bifurcação ou inicializando mais servidores. No entanto, as mutexes devem permanecer exclusivas globalmente.
  • Dependendo dos cenários de carga, agrupe seus servidores front-end e back-end por intervalos de id de artefato. Isso permitirá um melhor uso da memória persistente (nenhum banco de dados possui todos os dados) e torna possível dimensionar o mutexing. Seu javascript tem que manter conexões com vários servidores ao mesmo tempo.

Bem, espero que isso possa ser um começo para suas próprias ideias. Tenho certeza de que existem muitas outras possibilidades. Estou mais do que bem-vindo qualquer crítica ou aprimoramento a este post, o wiki está habilitado.

Christoph Strasen


1
@ChristophStrasen Observe servidores com eventos como node.js, que não dependem de um thread por usuário. Isso permite que a técnica de push seja tratada com menor consumo de memória. Acho que um servidor node.js e confiar em TCP WebSockets realmente ajuda com o dimensionamento. No entanto, isso destrói completamente a compatibilidade cross-browser.
Raynos

Concordo totalmente e espero que meu artigo não incentive a reinvenção da roda! Embora algumas rodas sejam meio novas, estão apenas começando a se tornar conhecidas e não são bem explicadas para que arquitetos de software de nível intermediário possam julgar sua aplicação para uma ideia específica. NA MINHA HUMILDE OPINIÃO. Node.js meio que merece um livro "para manequins";). Eu certamente compraria.
Christoph Strasen

2
+500 Você desafiadoramente um este. É uma ótima resposta.
Raynos de

1
@luqmaan, esta resposta é de fevereiro de 2011. Websockets ainda eram uma novidade e só foram lançados sem prefixo no Chrome por volta de agosto. Comet e socket.io funcionaram bem, eu acho que foi apenas uma sugestão para uma abordagem de maior desempenho.
Ricardo Tomasi

1
E se o Node.js estiver um pouco fora da sua zona de conforto (ou da zona de conforto da equipe de Operações, mas com certeza sobre o contexto de negócios da questão), você também pode usar Socket.io com um servidor baseado em Java. Tanto o Tomcat quanto o Jetty suportam conexões sem encadeamento para configurações do tipo server-push (veja por exemplo: wiki.eclipse.org/Jetty/Feature/Continuations )
Tomas

13

Eu sei que esta é uma pergunta antiga, mas pensei em apenas interromper.

OT (transformações operacionais) parece ser uma boa opção para seus requisitos de edição multiusuário simultânea e consistente. É uma técnica usada no Google Docs (e também foi usada no Google Wave):

Há uma biblioteca baseada em JS para usar Operational Transforms - ShareJS ( http://sharejs.org/ ), escrita por um membro da equipe do Google Wave.

E se você quiser, existe uma estrutura web MVC completa - DerbyJS ( http://derbyjs.com/ ) construída no ShareJS que faz tudo para você.

Ele usa o BrowserChannel para comunicação entre o servidor e os clientes (e acredito que o suporte a WebSockets deve estar em andamento - estava lá anteriormente via Socket.IO, mas foi retirado devido a problemas do desenvolvedor com Socket.io). Documentos para iniciantes são um um pouco esparso no momento, no entanto.


5

Eu consideraria adicionar um carimbo modificado baseado em tempo para cada conjunto de dados. Portanto, se você estiver atualizando tabelas de banco de dados, deverá alterar o carimbo de data / hora modificado de acordo. Usando AJAX, você pode comparar o timestamp modificado do cliente com o timestamp da fonte de dados - se o usuário estiver atrasado, atualize a exibição. Semelhante a como este site verifica uma pergunta periodicamente para ver se alguém respondeu enquanto você digita uma resposta.


Esse é um ponto útil. Também me ajuda a entender os campos "LastEdited" em nosso banco de dados mais de um ponto de design.
Raynos de

Exatamente. Este site usa um "heartbeat", o que significa que a cada x quantidade de tempo ele envia uma solicitação AJAX para o servidor e passa a ID dos dados a serem verificados, bem como o timestamp modificado para esses dados. Então, digamos que estejamos na pergunta nº 1029. Cada solicitação AJAX, o servidor só olha para o carimbo de data / hora modificado para a pergunta # 1029. Se ele descobrir que o cliente tem uma versão mais antiga dos dados, ele responde à pulsação com uma nova cópia. O cliente pode então recarregar a página (atualizar) ou exibir algum tipo de mensagem para o usuário avisando sobre novos dados.
Chris Baker de

carimbos modificados são muito mais legais do que hash de nossos "dados" atuais e compará-los com um hash do outro lado.
Raynos de

1
Lembre-se de que o cliente e o servidor devem ter acesso ao mesmo tempo para evitar inconsistências.
prayerslayer

3

Você precisa usar técnicas de push (também conhecidas como Comet ou Ajax reverso) para propagar as alterações para o usuário assim que forem feitas no banco de dados. A melhor técnica atualmente disponível para isso parece ser a sondagem longa do Ajax, mas não é suportada por todos os navegadores, então você precisa de alternativas. Felizmente, já existem soluções que tratam disso para você. Entre eles estão: orbited.org e o já citado socket.io.

No futuro, haverá uma maneira mais fácil de fazer isso, que é chamada de WebSockets, mas ainda não se sabe quando esse padrão estará pronto para o horário nobre, pois há questões de segurança sobre o estado atual do padrão.

Não deve haver problemas de simultaneidade no banco de dados com novos objetos. Mas quando um usuário edita um objeto, o servidor precisa ter alguma lógica que verifique se o objeto foi editado ou excluído nesse meio tempo. Se o objeto foi excluído, a solução é, novamente, simples: basta descartar a edição.

Mas o problema mais difícil aparece, quando vários usuários estão editando o mesmo objeto ao mesmo tempo. Se os usuários 1 e 2 começarem a editar um objeto ao mesmo tempo, os dois farão suas edições nos mesmos dados. Digamos que as alterações feitas pelo usuário 1 sejam enviadas ao servidor primeiro, enquanto o usuário 2 ainda está editando os dados. Você tem então duas opções: Você pode tentar mesclar as alterações do Usuário 1 nos dados do Usuário 2 ou pode informar ao Usuário 2 que seus dados estão desatualizados e exibir uma mensagem de erro assim que seus dados forem enviados ao servidor. A última opção não é muito amigável aqui, mas a primeira é muito difícil de implementar.

Uma das poucas implementações que realmente acertou pela primeira vez foi o EtherPad , que foi adquirido pelo Google. Acredito que eles usaram algumas das tecnologias EtherPad no Google Docs e Google Wave, mas não posso dizer com certeza. O Google também abre o EtherPad com recursos, então talvez valha a pena dar uma olhada, dependendo do que você está tentando fazer.

Realmente não é fácil fazer isso editando coisas simultaneamente, porque não é possível fazer operações atômicas na web por causa da latência. Talvez este artigo ajude você a aprender mais sobre o assunto.


2

Tentar escrever tudo isso sozinho é um trabalho árduo e é muito difícil acertar. Uma opção é usar uma estrutura construída para manter os clientes sincronizados com o banco de dados e entre si em tempo real.

Descobri que a estrutura do Meteor faz isso bem ( http://docs.meteor.com/#reactivity ).

"O Meteor adota o conceito de programação reativa. Isso significa que você pode escrever seu código em um estilo imperativo simples e o resultado será recalculado automaticamente sempre que houver alterações de dados das quais seu código depende."

"Este padrão simples (computação reativa + fonte de dados reativa) tem ampla aplicabilidade. O programador evita escrever chamadas de cancelar / resubscribe e garantir que sejam chamadas no momento certo, eliminando classes inteiras de código de propagação de dados que, de outra forma, obstruiriam seu aplicativo com lógica sujeita a erros. "


1

Não acredito que ninguém mencionou o Meteor . É uma estrutura nova e imatura com certeza (e oficialmente suporta apenas um banco de dados), mas leva todo o trabalho pesado e pensar em um aplicativo multiusuário como o pôster está descrevendo. Na verdade, você NÃO pode construir um aplicativo de atualização ao vivo com vários usuários. Aqui está um breve resumo:

  • Tudo está em node.js (JavaScript ou CoffeeScript), então você pode compartilhar coisas como validações entre o cliente e o servidor.
  • Ele usa websockets, mas pode recorrer a navegadores mais antigos
  • Ele se concentra em atualizações imediatas para o objeto local (ou seja, a interface do usuário parece ágil), com alterações enviadas ao servidor em segundo plano. Apenas atualizações atômicas são permitidas para tornar a combinação de atualizações mais simples. As atualizações rejeitadas no servidor são revertidas.
  • Como um bônus, ele lida com atualizações de código ao vivo para você e preserva o estado do usuário mesmo quando o aplicativo muda radicalmente.

O Meteor é tão simples que eu sugiro que você pelo menos dê uma olhada nele para ter ideias para roubar.


1
Eu realmente gosto da ideia do Derby e do Meteor para certos tipos de aplicativos .. propriedade e permissões de documentos / registros são apenas alguns problemas do mundo real que não são bem resolvidos na hora. Além disso, vindo do antigo mundo do MS que tornava 80% realmente fácil e gastava muito tempo com os outros 20%, hesito em usar essas soluções de PFM (pura magia f ** king).
Tracker1

1

Estas páginas da Wikipedia pode ajudar a perspectiva add para aprender sobre a simultaneidade e programação concorrente para a concepção de um ajax aplicação web que ou puxa ou é empurrado estado de eventos ( EDA ) mensagens em um padrão de mensagens . Basicamente, as mensagens são replicadas para assinantes de canal que respondem a eventos de mudança e solicitações de sincronização.

Existem muitas formas de software colaborativo simultâneo baseado na web .

Existem várias bibliotecas cliente de API HTTP para etherpad-lite , um editor colaborativo em tempo real .

django-realtime-playground implementa um aplicativo de bate-papo em tempo real no Django com várias tecnologias de tempo real como Socket.io .

Tanto o AppEngine quanto o AppScale implementam a API AppEngine Channel ; que é diferente da API do Google Realtime , que é demonstrada por googledrive / realtime-playground .


0

As técnicas de push do lado do servidor são o caminho a percorrer aqui. Cometa é (ou era?) Uma palavra da moda.

A direção específica que você toma depende muito da pilha do seu servidor e de quão flexível você é. Se você puder, eu daria uma olhada em socket.io , que fornece uma implementação cross-browser de websockets, que fornece uma maneira muito simplificada de comunicação bidirecional com o servidor, permitindo que o servidor envie atualizações para os clientes.

Em particular, veja esta demonstração do autor da biblioteca, que demonstra quase exatamente a situação que você descreve.


Essa é uma ótima biblioteca para reduzir os problemas de compactação, mas eu estava mais procurando por informações de alto nível sobre como projetar aplicativos
Raynos

1
Apenas para notar, que socket.io (e SignalR) são frameworks que usam websockets como a escolha de primeira classe, mas têm fallbacks compatíveis para usar outras técnicas como cometa, long polling, flash sockets e foreverframes.
Tracker1
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.