Cómo diseñar una aplicación web ajax multiusuario para que sea simultáneamente segura

95

Tengo una página web que muestra una gran cantidad de datos del servidor. La comunicación se realiza a través de ajax.

Cada vez que el usuario interactúa y cambia estos datos (digamos que el usuario A cambia el nombre de algo), le dice al servidor que realice la acción y el servidor devuelve los nuevos datos modificados.

Si el usuario B accede a la página al mismo tiempo y crea un nuevo objeto de datos, se lo dirá nuevamente al servidor a través de ajax y el servidor regresará con el nuevo objeto para el usuario.

En la página de A tenemos los datos con un objeto renombrado. Y en la página de B tenemos los datos con un nuevo objeto. En el servidor, los datos tienen un objeto renombrado y un objeto nuevo.

¿Cuáles son mis opciones para mantener la página sincronizada con el servidor cuando varios usuarios la usan al mismo tiempo?

Más bien se evitan opciones como bloquear toda la página o volcar el estado completo al usuario en cada cambio.

Si ayuda, en este ejemplo específico la página web llama a un método web estático que ejecuta un procedimiento almacenado en la base de datos. El procedimiento almacenado devolverá cualquier dato que haya cambiado y no más. Luego, el método web estático reenvía la devolución del procedimiento almacenado al cliente.

Edición de recompensa:

¿Cómo se diseña una aplicación web multiusuario que usa Ajax para comunicarse con el servidor pero evita problemas de concurrencia?

Es decir, acceso simultáneo a la funcionalidad y a los datos de una base de datos sin riesgo de corrupción de datos o estado

Raynos
fuente
no estoy tan seguro, pero puede tener una página como Facebook donde el navegador envía una solicitud ajax buscando constantemente cambios en la base de datos del servidor y actualizándolos en el navegador
Santosh Linkha
Serializar el estado del cliente y luego decirle al servidor a través de ajax aquí es mi estado, ¿qué necesito actualizar? Es una opción. Pero requiere que el cliente sepa cómo actualizar toda la información en un solo lugar.
Raynos
1
¿La mejor solución para la concurrencia del usuario final no es simplemente una de las variantes de inserción? Websockets, cometas, etc.
davin
@davin bien podría ser. Pero no estoy familiarizado con cometa y los websockets no están disponibles para la compatibilidad con varios navegadores.
Raynos
2
hay buenos paquetes para el soporte de varios navegadores, específicamente recomiendo socket.io, aunque también hay jWebSocket y muchos otros. Si usted va la manera socket.io, puede incorporar todo tipo de golosinas, como Node.js y marcos (del lado del cliente) motores de plantillas, etc
davin

Respuestas:

157

Visión general:

  • Intro
  • Arquitectura del servidor
  • Arquitectura del cliente
  • Actualizar caso
  • Confirmar caso
  • Caso de conflicto
  • Rendimiento y escalabilidad

Hola Raynos,

No discutiré ningún producto en particular aquí. Lo que otros mencionaron es un buen conjunto de herramientas para echar un vistazo (tal vez agregue node.js a esa lista).

Desde un punto de vista arquitectónico, parece que tiene el mismo problema que se puede ver en el software de control de versiones. Un usuario registra un cambio en un objeto, otro usuario quiere alterar el mismo objeto de otra manera => conflicto. Debe integrar los cambios de los usuarios en los objetos y, al mismo tiempo, poder entregar actualizaciones de manera oportuna y eficiente, detectando y resolviendo conflictos como el anterior.

Si estuviera en tu lugar, desarrollaría algo como esto:

1. Lado del servidor:

  • Determine un nivel razonable en el que definiría lo que yo llamaría "artefactos atómicos" (¿la página? ¿Objetos en la página? ¿Valores dentro de los objetos?). Esto dependerá de sus servidores web, base de datos y hardware de almacenamiento en caché, número de usuarios, número de objetos, etc. No es una decisión fácil de tomar.

  • Para cada artefacto atómico tiene:

    • un ID único para toda la aplicación
    • un id. de versión creciente
    • un mecanismo de bloqueo para el acceso de escritura (tal vez mutex)
    • un pequeño historial o "registro de cambios" dentro de un búfer de anillo (la memoria compartida funciona bien para ellos). Un solo par clave-valor también podría estar bien, aunque es menos ampliable. ver http://en.wikipedia.org/wiki/Circular_buffer
  • Un servidor o componente pseudo-servidor que puede entregar registros de cambios relevantes a un usuario conectado de manera eficiente. Observer-Pattern es tu amigo para esto.

2. Lado del cliente:

  • Un cliente javascript que puede tener una conexión HTTP de larga duración a dicho servidor anterior, o utiliza un sondeo ligero.

  • Un componente de actualización de artefactos de JavaScript que actualiza el contenido de los sitios cuando el cliente de JavaScript conectado notifica cambios en el historial de artefactos observados. (de nuevo, un patrón de observador podría ser una buena opción)

  • Un componente de confirmación de artefactos de JavaScript que puede solicitar cambiar un artefacto atómico, tratando de adquirir un bloqueo mutex. Detectará si el estado del artefacto ha sido cambiado por otro usuario unos segundos antes (latencia del cliente javascript y factores de proceso de confirmación) comparando el identificador de versión de artefacto conocido del lado del cliente y el identificador de versión de artefacto actual del lado del servidor.

  • Un solucionador de conflictos de JavaScript que permite una decisión humana sobre cuál es el cambio correcto. Es posible que no desee simplemente decirle al usuario "Alguien fue más rápido que usted. Eliminé su cambio. Ve a llorar". Parecen posibles muchas opciones de diferencias bastante técnicas o soluciones más fáciles de usar.

Entonces, ¿cómo iría?

Caso 1: tipo de diagrama de secuencia para actualización:

  • El navegador representa la página
  • javascript "ve" artefactos que cada uno tiene al menos un campo de valor, único y un ID de versión
  • El cliente javascript se inicia, solicitando "ver" el historial de artefactos encontrados a partir de sus versiones encontradas (los cambios anteriores no son interesantes)
  • El proceso del servidor toma nota de la solicitud y comprueba y / o envía continuamente el historial
  • Las entradas del historial pueden contener notificaciones simples "el artefacto x ha cambiado, el cliente solicita datos" lo que permite al cliente sondear de forma independiente o conjuntos de datos completos "el artefacto x ha cambiado a valor foo"
  • El actualizador de artefactos javascript hace lo que puede para buscar nuevos valores tan pronto como se sabe que se han actualizado. Ejecuta nuevas solicitudes ajax o es alimentado por el cliente javascript.
  • El contenido DOM de las páginas se actualiza, opcionalmente se notifica al usuario. Continúa la observación de la historia.

Caso 2: Ahora para comprometerse:

  • artifact-committer conoce el nuevo valor deseado de la entrada del usuario y envía una solicitud de cambio al servidor
  • se adquiere el mutex del lado del servidor
  • El servidor recibe "Oye, conozco el estado del artefacto x de la versión 123, déjame configurarlo en valor foo pls".
  • Si la versión del lado del servidor del artefacto x es igual (no puede ser menor) que 123, se acepta el nuevo valor y se genera una nueva identificación de la versión 124.
  • La nueva información de estado "actualizada a la versión 124" y, opcionalmente, el nuevo valor foo se colocan al comienzo del búfer de anillo del artefacto x (registro de cambios / historial)
  • se libera el mutex del lado del servidor
  • El solicitante de confirmación de artefactos se complace en recibir una confirmación de confirmación junto con la nueva identificación.
  • mientras tanto, el componente del servidor del lado del servidor sigue sondeando / empujando los ringbuffers a los clientes conectados. Todos los clientes que observen el búfer del artefacto x obtendrán la información y el valor del nuevo estado dentro de su latencia habitual (ver caso 1).

Caso 3: para conflictos:

  • El confirmador de artefactos conoce el nuevo valor deseado de la entrada del usuario y envía una solicitud de cambio al servidor
  • Mientras tanto, otro usuario actualizó el mismo artefacto con éxito (ver el caso 2), pero debido a varias latencias, nuestro otro usuario aún lo desconoce.
  • Entonces, se adquiere un mutex del lado del servidor (o se espera hasta que el usuario "más rápido" haya confirmado su cambio)
  • El servidor recibe "Oye, conozco el estado del artefacto x de la versión 123, déjame configurarlo en valor foo".
  • En el lado del servidor, la versión de artifact x ahora es 124. El cliente solicitante no puede saber el valor que estaría sobrescribiendo.
  • Obviamente, el servidor tiene que rechazar la solicitud de cambio (sin contar las prioridades de sobrescritura que intervienen en Dios), libera el mutex y tiene la amabilidad de devolver la nueva identificación de la versión y el nuevo valor directamente al cliente.
  • Al enfrentarse a una solicitud de confirmación rechazada y un valor que el usuario que solicita el cambio aún no conocía, el confirmador de artefactos de JavaScript se refiere al solucionador de conflictos que muestra y explica el problema al usuario.
  • El usuario, al ser presentado con algunas opciones por el JS de resolución de conflictos inteligente, se le permite otro intento de cambiar el valor.
  • Una vez que el usuario seleccionó un valor que considera correcto, el proceso comienza desde el caso 2 (o el caso 3 si alguien más fue más rápido, nuevamente)

Algunas palabras sobre rendimiento y escalabilidad

Sondeo HTTP frente a "empuje" HTTP

  • El sondeo crea solicitudes, una por segundo, 5 por segundo, lo que considere una latencia aceptable. Esto puede ser bastante cruel para su infraestructura si no configura su (¿Apache?) Y (¿php?) Lo suficientemente bien como para ser principiantes "ligeros". Es deseable optimizar la solicitud de sondeo en el lado del servidor para que se ejecute durante mucho menos tiempo que la duración del intervalo de sondeo. Dividir ese tiempo de ejecución a la mitad podría significar reducir la carga de todo el sistema hasta en un 50%,
  • Empujar a través de HTTP (asumiendo que los webworkers están demasiado lejos para admitirlos) requerirá que tenga un proceso apache / lighthttpd disponible para cada usuario todo el tiempo . La memoria residente reservada para cada uno de estos procesos y la memoria total de su sistema será un límite de escala muy seguro que encontrará. Será necesario reducir la huella de memoria de la conexión, así como también limitar la cantidad de trabajo continuo de CPU y E / S realizado en cada uno de estos (desea mucho tiempo de inactividad / suspensión)

escalado de backend

  • Olvídese de la base de datos y el sistema de archivos, necesitará algún tipo de backend basado en memoria compartida para el sondeo frecuente (si el cliente no realiza el sondeo directamente, cada proceso del servidor en ejecución lo hará)
  • si opta por Memcache, puede escalar mejor, pero sigue siendo caro
  • El mutex para confirmaciones tiene que funcionar globalmente incluso si desea tener varios servidores frontend para equilibrar la carga.

escala de frontend

  • Independientemente de si está votando o recibiendo "mensajes", intente obtener información de todos los artefactos observados en un solo paso.

ajustes "creativos"

  • Si los clientes están sondeando y muchos usuarios tienden a ver los mismos artefactos, podría intentar publicar el historial de esos artefactos como un archivo estático, permitiendo que apache lo almacene en caché, sin embargo, actualizándolo en el lado del servidor cuando cambian los artefactos. Esto saca PHP / Memcache del juego para algunas solicitudes. Lighthttpd es muy eficaz en el servicio de archivos estáticos.
  • utilice una red de distribución de contenido como cotendo.com para impulsar el historial de artefactos allí. La latencia de inserción será mayor, pero la escalabilidad es un sueño
  • escriba un servidor real (sin usar HTTP) al que los usuarios se conecten usando java o flash (?). Tienes que lidiar con atender a muchos usuarios en un hilo de servidor. Ciclismo a través de enchufes abiertos, haciendo (o delegando) el trabajo requerido. Puede escalar mediante procesos de bifurcación o iniciando más servidores. Sin embargo, los muttex deben seguir siendo únicos a nivel mundial.
  • Dependiendo de los escenarios de carga, agrupe sus servidores frontend y backend por rangos de ID de artefacto. Esto permitirá un mejor uso de la memoria persistente (ninguna base de datos tiene todos los datos) y hace posible escalar el mutexing. Sin embargo, su javascript tiene que mantener conexiones a varios servidores al mismo tiempo.

Bueno, espero que esto pueda ser un comienzo para sus propias ideas. Estoy seguro de que hay muchas más posibilidades. Estoy más que agradecido por cualquier crítica o mejora a esta publicación, wiki está habilitado.

Christoph Strasen

Christoph Strasen
fuente
1
@ChristophStrasen Observe servidores con eventos como node.js que no dependen de un hilo por usuario. Esto permite manejar la técnica de empuje con un menor consumo de memoria. Creo que un servidor node.js y confiar en TCP WebSockets realmente ayuda con el escalado. Sin embargo, arruina por completo el cumplimiento de todos los navegadores.
Raynos
¡Estoy totalmente de acuerdo y espero que mi artículo no anime a reinventar la rueda! Aunque algunas ruedas son algo nuevas, apenas comienzan a ser conocidas y no se explican lo suficientemente bien como para que los arquitectos de software de nivel intermedio puedan juzgar su aplicación para una idea específica. EN MI HUMILDE OPINIÓN. Node.js merece un libro "para tontos";). Ciertamente compraría.
Christoph Strasen
2
+500 Definitivamente has ganado uno. Es una gran respuesta.
Raynos
1
@luqmaan esta respuesta es de febrero de 2011. Los Websockets seguían siendo una novedad y solo se lanzaron sin prefijo en Chrome alrededor de agosto. Sin embargo, Comet y socket.io estaban bien, creo que fue simplemente una sugerencia para un enfoque más eficaz.
Ricardo Tomasi
1
Y si Node.js está un poco fuera de su zona de confort (o de la zona de confort del equipo de Operaciones, pero seguro del contexto empresarial de la pregunta), también puede usar Socket.io con un servidor basado en Java. Tanto Tomcat como Jetty admiten conexiones sin subprocesos para configuraciones de tipo push de servidor (consulte, por ejemplo: wiki.eclipse.org/Jetty/Feature/Continuations )
Tomas
13

Sé que esta es una vieja pregunta, pero pensé en intervenir.

OT (transformaciones operativas) parece una buena opción para su requisito de edición multiusuario simultánea y consistente. Es una técnica utilizada en Google Docs (y también se utilizó en Google Wave):

Hay una biblioteca basada en JS para usar Operational Transforms: ShareJS ( http://sharejs.org/ ), escrita por un miembro del equipo de Google Wave.

Y si lo desea, hay un marco web MVC completo: DerbyJS ( http://derbyjs.com/ ) construido en ShareJS que lo hace todo por usted.

Utiliza BrowserChannel para la comunicación entre el servidor y los clientes (y creo que la compatibilidad con WebSockets debería estar en proceso; estaba allí anteriormente a través de Socket.IO, pero se eliminó debido a problemas del desarrollador con Socket.io). un poco escaso en este momento, sin embargo.

victorhooi
fuente
5

Consideraría agregar un sello modificado basado en el tiempo para cada conjunto de datos. Por lo tanto, si está actualizando tablas de base de datos, debería cambiar la marca de tiempo modificada en consecuencia. Con AJAX, puede comparar la marca de tiempo modificada del cliente con la marca de tiempo de la fuente de datos; si el usuario se retrasa, actualice la pantalla. Similar a cómo este sitio revisa una pregunta periódicamente para ver si alguien más ha respondido mientras usted escribe una respuesta.

Chris Baker
fuente
Ese es un punto útil. También me ayuda a comprender los campos "Última edición" en nuestra base de datos más desde un punto de vista de diseño.
Raynos
Exactamente. Este sitio utiliza un "latido", es decir, cada x cantidad de tiempo que envía una solicitud AJAX al servidor, y transmite el ID de los datos para verificar, así como la marca de tiempo modificada que tiene para esos datos. Digamos que estamos en la Pregunta # 1029. Cada solicitud de AJAX, el servidor solo mira la marca de tiempo modificada para la pregunta # 1029. Si alguna vez descubre que el cliente tiene una versión anterior de los datos, responde al latido con una nueva copia. Luego, el cliente puede volver a cargar la página (actualizar) o mostrar algún tipo de mensaje al usuario advirtiéndole sobre nuevos datos.
Chris Baker
los sellos modificados son mucho más agradables que hacer hash de nuestros "datos" actuales y compararlos con un hash en el otro lado.
Raynos
1
Tenga en cuenta que el cliente y el servidor deben tener acceso a la misma hora exacta para evitar inconsistencias.
prayerslayer
3

Debe utilizar técnicas de empuje (también conocidas como Comet o Ajax inverso) para propagar los cambios al usuario tan pronto como se realicen en la base de datos. La mejor técnica disponible actualmente para esto parece ser el sondeo largo Ajax, pero no es compatible con todos los navegadores, por lo que necesita alternativas. Afortunadamente, ya existen soluciones que se encargan de esto por usted. Entre ellos se encuentran: orbited.org y el ya mencionado socket.io.

En el futuro, habrá una forma más fácil de hacer esto que se llama WebSockets, pero aún no se sabe cuándo estará listo ese estándar para el horario de máxima audiencia, ya que existen preocupaciones de seguridad sobre el estado actual del estándar.

No debería haber problemas de concurrencia en la base de datos con nuevos objetos. Pero cuando un usuario edita un objeto, el servidor necesita tener alguna lógica que verifique si el objeto ha sido editado o eliminado mientras tanto. Si el objeto ha sido eliminado, la solución es, nuevamente, simple: simplemente descarte la edición.

Pero el problema más difícil aparece cuando varios usuarios están editando el mismo objeto al mismo tiempo. Si el usuario 1 y 2 comienzan a editar un objeto al mismo tiempo, ambos realizarán sus ediciones en los mismos datos. Digamos que los cambios realizados por el Usuario 1 se envían primero al servidor mientras el Usuario 2 todavía está editando los datos. Entonces tiene dos opciones: puede intentar fusionar los cambios del Usuario 1 con los datos del Usuario 2 o puede decirle al Usuario 2 que sus datos están desactualizados y mostrarle un mensaje de error tan pronto como sus datos sean enviados al servidor. La última opción no es muy fácil de usar aquí, pero la primera es muy difícil de implementar.

Una de las pocas implementaciones que realmente hizo esto bien por primera vez fue EtherPad , que fue adquirida por Google. Creo que luego usaron algunas de las tecnologías de EtherPad en Google Docs y Google Wave, pero no puedo asegurarlo. Google también abrió EtherPad, por lo que tal vez valga la pena echarle un vistazo, dependiendo de lo que intente hacer.

Realmente no es fácil hacer estas cosas de edición simultáneamente, porque no es posible realizar operaciones atómicas en la web debido a la latencia. Quizás este artículo te ayude a aprender más sobre el tema.

Jannes
fuente
2

Tratar de escribir todo esto usted mismo es un gran trabajo y es muy difícil hacerlo bien. Una opción es utilizar un marco creado para mantener a los clientes sincronizados con la base de datos y entre sí, en tiempo real.

Descubrí que el marco Meteor lo hace bien ( http://docs.meteor.com/#reactivity ).

"Meteor adopta el concepto de programación reactiva. Esto significa que puede escribir su código en un estilo imperativo simple, y el resultado se recalculará automáticamente siempre que los datos cambien de los que depende su código".

"Este patrón simple (cálculo reactivo + fuente de datos reactiva) tiene una amplia aplicabilidad. El programador no tiene que escribir llamadas de cancelación de suscripción / resuscripción y asegurarse de que se llaman en el momento correcto, eliminando clases enteras de código de propagación de datos que, de otro modo, obstruirían su aplicación con lógica propensa a errores ".

megabyte.
fuente
1

No puedo creer que nadie haya mencionado a Meteor . Es un marco nuevo e inmaduro con seguridad (y solo admite oficialmente una base de datos), pero requiere todo el trabajo pesado y el pensamiento de una aplicación multiusuario como la que describe el póster. De hecho, NO puede crear una aplicación de actualización en vivo para múltiples usuarios. Aquí hay un resumen rápido:

  • Todo está en node.js (JavaScript o CoffeeScript), por lo que puede compartir cosas como validaciones entre el cliente y el servidor.
  • Utiliza websockets, pero puede recurrir a navegadores más antiguos.
  • Se centra en las actualizaciones inmediatas del objeto local (es decir, la interfaz de usuario se siente rápida), con los cambios enviados al servidor en segundo plano. Solo se permiten las actualizaciones atómicas para simplificar la mezcla de actualizaciones. Las actualizaciones rechazadas en el servidor se revierten.
  • Como beneficio adicional, maneja las recargas de código en vivo por usted y preserva el estado del usuario incluso cuando la aplicación cambia radicalmente.

Meteor es lo suficientemente simple como para sugerirle que al menos le eche un vistazo en busca de ideas para robar.

ValienteNuevoMoneda
fuente
1
Realmente me gusta la idea de Derby y Meteor para ciertos tipos de aplicaciones ... la propiedad y los permisos de documentos / registros son solo un par de problemas del mundo real que no están bien resueltos en mi humilde opinión. Además, viniendo del mundo de MS desde hace mucho tiempo de hacer que ese 80% sea realmente fácil y de dedicar demasiado tiempo al otro 20%, dudo en usar tales soluciones PFM (pura magia).
Tracker1
1

Estas páginas de Wikipedia pueden ayudar perspectiva añadir a aprender acerca de la concurrencia y computación concurrente para el diseño de un ajax aplicación web que, o bien tirones o se empujaron Estado de eventos ( EDA ) mensajes en un patrón de mensajería . Básicamente, los mensajes se replican en los suscriptores del canal que responden a eventos de cambio y solicitudes de sincronización.

Hay muchas formas de software colaborativo simultáneo basado en la web .

Hay una serie de bibliotecas cliente HTTP API para etherpad-lite , un editor colaborativo en tiempo real .

django-realtime-playground implementa una aplicación de chat en tiempo real en Django con varias tecnologías en tiempo real como Socket.io .

Tanto AppEngine como AppScale implementan la API del canal AppEngine ; que es diferente de la API de Google Realtime , que se demuestra mediante googledrive / realtime-playground .

Wes Turner
fuente
0

Las técnicas de inserción del lado del servidor son el camino a seguir aquí. Cometa es (¿o era?) Una palabra de moda.

La dirección particular que tome depende en gran medida de su pila de servidores y de su flexibilidad. Si puede, echaría un vistazo a socket.io , que proporciona una implementación de websockets entre navegadores, que proporciona una forma muy ágil de tener comunicación bidireccional con el servidor, lo que permite que el servidor envíe actualizaciones a los clientes.

En particular, vea esta demostración del autor de la biblioteca, que demuestra casi exactamente la situación que describe.

davin
fuente
Esa es una gran biblioteca para reducir los problemas con la trituración, pero buscaba más información de alto nivel sobre cómo diseñar una aplicación
Raynos
1
Solo para tener en cuenta, que socket.io (y SignalR) son marcos que usan websockets como la opción de primera clase, pero tienen alternativas compatibles para usar otras técnicas como cometa, sondeo largo, sockets flash y marcos para siempre.
Tracker1