Escalamiento de WebSockets
Al hablar con los desarrolladores que aún no han usado WebSockets , por lo general tienen la misma preocupación: ¿cómo escalarlo en varios servidores?
Publicar en un canal en un servidor está bien, siempre que todos los suscriptores estén conectados a ese servidor. Tan pronto como tenga varios servidores, debe agregar algo más a la mezcla. Eso es lo que esta publicación intenta abordar.
Para ver por qué escalar WebSockets puede parecer desalentador, comparémoslo con HTTP, porque la mayoría de la gente lo entiende bien.
Con HTTP, tiene un patrón de solicitud/respuesta único, no espera que la próxima solicitud del cliente regrese al mismo servidor. Al menos no debería, porque eso significa que tiene un problema de sesión persistente y que no puede escalar fácilmente para obtener rendimiento o redundancia.
Con HTTP, puede ejecutar una cantidad prácticamente ilimitada de instancias de servidor web detrás de un balanceador de carga. El equilibrador de carga pasa la solicitud a una instancia de servidor web en buen estado cuando ingresa y la respuesta se devuelve al cliente una vez que el servidor web la ha calculado. Las conexiones HTTP suelen ser de muy corta duración, solo existen hasta que se da la respuesta. Este es un enfoque bien entendido y ubicuo y escala muy bien. Hay una excepción a esto en forma de encuestas largas, pero no es tan común y no es tan importante para esta publicación.
Por otro lado, los WebSockets se diferencian de las solicitudes HTTP en el sentido de que son persistentes. El cliente WebSocket abre una conexión con el servidor y la reutiliza. En esta conexión de larga duración, tanto el servidor como el cliente pueden publicar y responder a los eventos. Este concepto se denomina conexión dúplex . Se puede abrir una conexión a través de un balanceador de carga, pero una vez que se abre la conexión, permanece con el mismo servidor hasta que se cierra o se interrumpe.
Esto, a su vez, significa que la interacción tiene estado; que terminará almacenando al menos algunos datos en la memoria del servidor WebSocket para cada conexión de cliente abierta. Por ejemplo, probablemente sabrá qué usuario está en el lado del cliente del socket y qué tipo de datos le interesan.
El hecho de que las conexiones de WebSocket sean persistentes es lo que lo hace tan poderoso para las aplicaciones en tiempo real , pero también es lo que lo hace más difícil de escalar .
Hablemos de una aplicación de ejemplo, de esa manera podemos discutir problemas y enfoques en el contexto de algo un poco más concreto.
Para nuestro ejemplo, optemos por una aplicación de pizarra colaborativa. En esta aplicación, hay múltiples pizarras, lo que significa múltiples dibujos en los que las personas pueden colaborar. Cuando un usuario dibuja en una pizarra en particular, publica las coordenadas a través de una conexión WebSocket y a través de todos los demás usuarios que tienen la misma pizarra abierta. En otras palabras, tenemos un patrón pub/sub expuesto sobre WebSockets.
En este ejemplo, significa que el lado del servidor de la conexión de socket para cada usuario de la aplicación necesitará saber, al menos, qué pizarra tiene abierta el usuario.
Las implementaciones de socket web como socket.io tienen el concepto de canales. Piense en ello como una dirección a la que los clientes se suscriben y en la que publican un servicio u otros clientes.
Puede ser tentador pensar que todo lo que tenemos que hacer para construir nuestra aplicación de pizarra colaborativa es emplear canales (cada pizarra tiene su propio canal) y luego sentarse y relajarse, pero como verá en el resto de esta publicación , seguirá teniendo problemas con el escalado horizontal y la tolerancia a errores.
En primer lugar, ¿a qué me refiero cuando digo " agente de publicación/suscripción" ? Existe una variedad de tecnologías que respaldan el patrón pub/sub a una escala considerable.
Cuando necesite escalar horizontalmente una arquitectura de publicación/suscripción a través de sockets, deberá encontrar una buena tecnología de publicación/suscripción para tener en el núcleo de su solución.
No necesitamos atar una opción específica para que esta publicación tenga sentido, pero aquí hay algunas buenas opciones: Redis , RabbitMQ , Kafka y RethinkDB .
Para ver por qué necesitamos agregar un agente de publicación/suscripción a la combinación para ayudarlo a escalar sus WebSockets, pensemos primero en nuestro ejemplo en el contexto de un servidor.
Con un servidor, en realidad es bastante fácil crear un servicio pub/sub con solo WebSockets. Esto se debe a que en un servidor el servidor conocerá a todos los clientes y qué datos les interesan.
Piense en nuestra aplicación de ejemplo. Cuando un cliente envía coordenadas para un dibujo, solo encontramos el canal correcto para el dibujo y publicamos las actualizaciones realizadas en el dibujo en ese canal. Todos los clientes están conectados a un servidor, por lo que todos reciben una notificación del cambio. Es una especie de pub/sub en memoria .
Pero en realidad, nos gustaría escalar a través de varios servidores y queremos hacerlo por 2 razones: 1) compartir la potencia de procesamiento y 2) redundancia.
Entonces, ¿qué podemos hacer para garantizar que nuestra aplicación se escale horizontalmente? Bueno, necesitamos alguna forma de que otros servicios con clientes conectados sepan que los datos han cambiado.
Al crear una aplicación como esta, probablemente ya tenga una base de datos en la combinación, incluso antes de comenzar a pensar en escalar. No solo confiaría en los clientes conectados para almacenar todos los datos de todos los dibujos, ¿verdad? No, deseará conservar los datos del dibujo tal como llegan de los clientes, de modo que pueda mostrar los datos del dibujo cada vez que un usuario abra un dibujo.
Pero aquí está el problema. Si un WebSocket en el Servidor A escribe algunos datos en la base de datos, ¿cómo sabría el WebSocket en el Servidor B para ir y obtener los datos más recientes de la base de datos para que pueda notificar a sus clientes sobre los nuevos datos?
Hablemos del proceso de usar Redis en el centro de su solución. Aunque es posible que tenga cientos de servidores WebSocket en su clúster, imaginemos que solo tiene 3 para simplificar un poco las cosas. Llamaremos a estos servidores WS1 , WS2 y WS3 . ¡A veces me sorprendo con mis nombres increíblemente creativos para cosas!
Ok, supongamos que tiene 9 personas que abrieron un dibujo específico de un perro montando un pony montando un dinosaurio, guardado en su base de datos con una identificación de abc123 . Y digamos que tiene 3 personas conectadas a cada servidor en el clúster (WS1, WS2, WS3).
Uno de los usuarios conectados a WS1 dibuja algo en la pizarra. En la lógica de su servidor WebSocket, escribe en la base de datos para asegurarse de que los cambios se han mantenido y luego publica en un canal basado en un identificador único asociado al dibujo, muy probablemente basado en la identificación de la base de datos para el dibujo. Digamos que el nombre del canal en este caso es drawing_abc123 .
En este punto, tiene los datos escritos de forma segura en la base de datos y ha publicado un evento en su agente de publicación/suscripción (canal de Redis) notificando a otras partes interesadas que hay nuevos datos.
Debido a que tiene usuarios conectados a los otros servidores WebSocket (WS2, WS3), interesados en el mismo dibujo, tendrán suscripciones abiertas a Redis en el canal drawing_abc123 . Reciben una notificación del evento y cada uno de los servidores consulta la base de datos para obtener actualizaciones y las emite en el canal WebSocket utilizado en su nivel de WebSocket.
Como puede ver, el intermediario de publicación/suscripción se utiliza para permitirle exponer un modelo de publicación/suscripción con un clúster de WebSocket escalado.
Otro beneficio de usar un agente de publicación/suscripción para coordinar sus WebSockets es que ahora es posible manejar fácilmente la conmutación por error.
Cuando un cliente está conectado a un servidor WebSocket y ese servidor falla, el cliente puede abrir una conexión a través del equilibrador de carga a otro servidor WebSocket. El nuevo servidor WebSocket solo se asegurará de que haya una suscripción al intermediario de publicación/suscripción para los datos que le interesan al cliente WebSocket y comenzará a canalizar los cambios en el WebSocket cuando ocurran.
Una cosa a tener en cuenta cuando un cliente se vuelve a conectar es hacer que el cliente sea lo suficientemente inteligente como para enviar a través de algún tipo de compensación de sincronización de datos, probablemente en forma de marca de tiempo, para que el servidor no envíe todos los datos nuevamente.
Si cada actualización del dibujo en cuestión tiene una marca de tiempo, los clientes pueden almacenar fácilmente la última marca de tiempo que recibieron. Cuando el cliente pierde la conexión a un servidor en particular, simplemente puede volver a conectarse a su clúster websocket (a través de su balanceador de carga) al pasar la última marca de tiempo que recibió y de esa manera la consulta a la base de datos se puede construir para que ' Solo devolverá las actualizaciones que se produzcan después de que el cliente haya recibido actualizaciones correctamente por última vez.
En muchas aplicaciones, puede que no sea tan importante preocuparse de que los duplicados lleguen al cliente. Pero incluso entonces, podría ser una buena idea utilizar un enfoque de marca de tiempo para ahorrar tanto sus recursos como el ancho de banda de sus usuarios.
Crear un servicio pub/sub que se ejecute en un servidor es relativamente fácil. El desafío es crear un servicio que se pueda escalar horizontalmente para compartir la carga y la tolerancia a fallas.
Cuando escala horizontalmente, necesita una forma para que los servicios de socket web se suscriban a los datos modificados, porque los cambios en dichos datos también se originarán en otros servidores además de ellos. Una base de datos que admita consultas en vivo es perfecta para este propósito, por ejemplo, RethinkDB. De esa manera, solo tiene WebSockets y su base de datos. Dicho esto, es posible que ya esté utilizando una tecnología compatible con pub/sub (Redis, RabbitMQ, Kafka) en su entorno, y será mucho más fácil de vender que introducir una nueva tecnología DB en la mezcla.