Algunos lectores pueden haber oído hablar de Centrifugo antes. Este artículo se centrará en el desarrollo de la segunda versión del servidor y la nueva biblioteca en tiempo real para el lenguaje Go que subyace.
Me llamo Alexander Emelin. El verano pasado, me uní al equipo de Avito, donde ahora estoy ayudando a desarrollar el backend de mensajería Avito. El nuevo trabajo, directamente relacionado con la entrega rápida de mensajes a los usuarios, y los nuevos colegas me inspiraron a seguir trabajando en el proyecto de código abierto Centrifugo.

En pocas palabras: este es un servidor que asume la tarea de mantener conexiones constantes de los usuarios de su aplicación. El polyfill de Websocket o SockJS se usa como transporte; puede, si no es posible establecer una conexión Websocket, trabajar a través de Eventsource, XHR-streaming, sondeo largo y otros transportes basados en HTTP. Los clientes se suscriben a canales en los que el backend a través de la API Centrifuge publica nuevos mensajes a medida que surgen, después de lo cual los mensajes se entregan a los usuarios suscritos al canal. En otras palabras, es un servidor PUB / SUB.

Actualmente, el servidor se utiliza en una cantidad bastante grande de proyectos. Entre ellos, por ejemplo, se encuentran algunos proyectos de Mail.Ru (intranet, plataformas de capacitación Technopark / Technosphere, Centro de Certificación, etc.), con Centrifugo, un hermoso tablero de instrumentos funciona en la recepción en la oficina de Badoo Moscú, y 350 mil usuarios están conectados simultáneamente al servicio spot.im a la centrífuga
Algunos enlaces a artículos anteriores sobre el servidor y su aplicación para quienes escuchan por primera vez sobre el proyecto:
Comencé a trabajar en la segunda versión en diciembre del año pasado y continúo hasta el día de hoy. Veamos que pasa. Estoy escribiendo este artículo no solo para popularizar de alguna manera el proyecto, sino también para obtener un poco más de información constructiva antes del lanzamiento de Centrifugo v2: ahora hay espacio para maniobras y cambios incompatibles con versiones anteriores.
Biblioteca en tiempo real para Go
En la comunidad Go, la pregunta surge de vez en cuando: ¿hay alguna alternativa a socket.io en Go? A veces noté cómo se aconseja a los desarrolladores en respuesta a esto que miren hacia Centrifugo. Sin embargo, Centrifugo es un servidor autohospedado, no una biblioteca; la comparación no es justa. También me han preguntado varias veces si el código Centrifugo se puede reutilizar para escribir aplicaciones en tiempo real en Go. Y la respuesta fue: teóricamente posible, pero no podía garantizar la compatibilidad con versiones anteriores de la API de los paquetes internos bajo mi propio riesgo. Está claro que no hay razón para que nadie se arriesgue, y bifurcar también es una opción regular. Además, no diría que la API para paquetes internos generalmente se preparó para tal uso.
Por lo tanto, una de las tareas ambiciosas que quería resolver en el proceso de trabajar en la segunda versión del servidor era tratar de separar el núcleo del servidor en una biblioteca separada en Go. Creo que esto tiene sentido, considerando cuántas características tiene la Centrifuge para adaptarse a la producción. Hay muchas funciones disponibles de fábrica para ayudar a crear aplicaciones escalables en tiempo real, eliminando la necesidad de que los desarrolladores escriban sus propias soluciones. Escribí sobre estas características anteriormente y también describiré algunas de ellas a continuación.
Trataré de justificar una ventaja más de la existencia de dicha biblioteca. La mayoría de los usuarios de Centrifugo son desarrolladores que escriben backends en idiomas / frameworks con escaso soporte de concurrencia (por ejemplo, Django / Flask / Laravel / ...): trabajen con muchas conexiones persistentes si es posible, de una manera no evidente o ineficiente. En consecuencia, no todos los usuarios pueden ayudar con el desarrollo de un servidor escrito en Go (cursi debido a la falta de conocimiento del idioma). Por lo tanto, incluso una comunidad muy pequeña de desarrolladores de Go en la biblioteca podrá ayudar a desarrollar el servidor Centrifugo que lo utiliza.
El resultado es una biblioteca Centrifuge . Esto sigue siendo WIP, pero absolutamente todas las características indicadas en la descripción de Github están implementadas y funcionan. Dado que la biblioteca proporciona una API bastante rica, antes de garantizar la compatibilidad con versiones anteriores, me gustaría escuchar sobre varios ejemplos exitosos de uso en proyectos reales en Go. No hay ninguno todavía. Además de fracasado :). No hay ninguno
Entiendo que al nombrar la biblioteca de la misma manera que el servidor, siempre enfrentaré la confusión. Pero creo que esta es la opción correcta, ya que los clientes (como centrifuge-js, centrifuge-go) trabajan tanto con la biblioteca Centrifuge como con el servidor Centrifugo. Además, el nombre ya está firmemente arraigado en la mente de los usuarios, y no quiero perder estas asociaciones. Y sin embargo, para un poco más de claridad, aclararé nuevamente:
- Centrifuge - una biblioteca para el idioma Go,
- Centrifugo es una solución llave en mano, un servicio separado, que en la versión 2 se construirá en la biblioteca Centrifuge.
Debido a su diseño, Centrifugo (un servicio independiente que no sabe nada sobre su backend) asume que el flujo de mensajes a través del transporte en tiempo real irá del servidor al cliente. A que te refieres Si, por ejemplo, el usuario escribe un mensaje en el chat, este mensaje primero debe enviarse al backend de la aplicación (por ejemplo, AJAX en el navegador), validado en el lado del backend, guardado en la base de datos si es necesario y luego enviado a la API Centrifuge. La biblioteca elimina esta restricción, lo que le permite organizar el intercambio bidireccional de mensajes asincrónicos entre el servidor y el cliente, así como las llamadas RPC.

Veamos un ejemplo simple: implementamos un pequeño servidor en Go usando la biblioteca Centrifuge. El servidor recibirá mensajes de los clientes del navegador a través de Websocket, el cliente tendrá un campo de texto en el que puede enviar un mensaje, presione Entrar y el mensaje se enviará a todos los usuarios suscritos al canal. Es decir, la versión más simplificada del chat. Me pareció que sería más conveniente colocar esto en forma de esencia .
Puedes correr como siempre:
git clone https:
Y luego vaya a http: // localhost: 8000 , abra varias pestañas del navegador.
Como puede ver, el punto de entrada a la lógica de negocios de la aplicación ocurre cuando se cuelga On().Connect()
Funciones de devolución de llamada On().Connect()
:
node.On().Connect(func(ctx context.Context, client *centrifuge.Client, e centrifuge.ConnectEvent) centrifuge.ConnectReply { client.On().Disconnect(func(e centrifuge.DisconnectEvent) centrifuge.DisconnectReply { log.Printf("client disconnected") return centrifuge.DisconnectReply{} }) log.Printf("client connected via %s", client.Transport().Name()) return centrifuge.ConnectReply{} })
El enfoque basado en la devolución de llamada me pareció el más conveniente para interactuar con la biblioteca. Además, se utiliza un enfoque similar, de tipo débil, en la implementación del servidor socket-io en Go . Si de repente tiene pensamientos sobre cómo se podría hacer la API de manera más idiomática, me alegrará saberlo.
Este es un ejemplo muy simple que no muestra todas las características de la biblioteca. Alguien puede notar que para tales propósitos es más fácil tomar una biblioteca para trabajar con Websocket. Por ejemplo, Gorilla Websocket. Esto es realmente así. Sin embargo, incluso en este caso, tendrá que copiar una pieza decente de código de servidor del ejemplo en el repositorio Gorilla Websocket. ¿Qué pasa si:
- necesita escalar la aplicación a varias máquinas,
- o no necesita un canal común, sino varios, y los usuarios pueden suscribirse y darse de baja dinámicamente a medida que navega por su aplicación,
- o necesita trabajar cuando no se pudo establecer la conexión Websocket (no hay soporte en el navegador del cliente, hay una extensión del navegador, algún proxy en el camino entre el cliente y el servidor corta la conexión),
- o necesita restaurar mensajes perdidos por el cliente durante breves interrupciones en la conexión a Internet sin cargar la base de datos principal,
- o necesita control de la autorización del usuario en el canal,
- o necesita desconectar la conexión permanente de los usuarios que están desactivados en la aplicación,
- o necesita información sobre quién está actualmente en el canal o los eventos que alguien ha suscrito / anulado del canal,
- ¿o necesita métricas y monitoreo?
La biblioteca Centrifuge puede ayudarlo con esto; de hecho, heredó todas las características básicas que anteriormente estaban disponibles en Centrifugo. Se pueden encontrar más ejemplos que muestran los puntos indicados anteriormente en Github .
El fuerte legado de Centrifugo puede ser un inconveniente, ya que la biblioteca ha adoptado todas las mecánicas del servidor, que son bastante originales y, tal vez, pueden parecer obvias o sobrecargadas con características innecesarias para alguien. Traté de organizar el código de tal manera que las características no utilizadas no afectaran el rendimiento general.
Hay algunas optimizaciones en la biblioteca que permiten un uso más eficiente de los recursos. Esto está combinando varios mensajes en un marco Websocket para guardar en las llamadas del sistema Write o, por ejemplo, usando Gogoprotobuf para serializar mensajes Protobuf y otros. Hablando de Protobuf.
Protocolo binario Protobuf
Realmente quería que Centrifugo trabajara con datos binarios ( y no solo yo ), por lo que en la nueva versión quería agregar un protocolo binario además del existente basado en JSON. Ahora todo el protocolo se describe como un esquema Protobuf . Esto nos permitió hacerlo más estructurado, repensar algunas decisiones no obvias en el protocolo de la primera versión.
Creo que no necesita decir durante mucho tiempo cuáles son las ventajas de Protobuf sobre JSON: compacidad, velocidad de serialización, esquema estricto. Existe un inconveniente en forma de ilegibilidad, pero ahora los usuarios tienen la oportunidad de decidir qué es más importante para ellos en una situación particular.
En general, el tráfico generado por el protocolo Centrifugo cuando se usa Protobuf en lugar de JSON debería disminuir en ~ 2 veces (excluyendo los datos de la aplicación). El consumo de CPU en mis pruebas de carga sintética disminuyó por lo mismo ~ 2 veces en comparación con JSON. Estos números en realidad hablan poco de lo que, en la práctica, todo dependerá del perfil de carga de una aplicación en particular.
En aras del interés, lancé en una máquina con Debian 9.4 y 32 CPU Intel® Xeon® Platinum 8168 @ 2.70GHz vCPU benchmark, lo que nos permitió comparar el ancho de banda de la interacción cliente-servidor en caso de usar el protocolo JSON y el protocolo Protobuf. Había 1000 suscriptores a 1 canal. En este canal, los mensajes se publicaron en 4 transmisiones y se entregaron a todos los suscriptores. El tamaño de cada mensaje era de 128 bytes.
Resultados para JSON:
$ go run main.go -s ws:
Resultados para el caso Protobuf:
$ go run main.go -s ws:
Puede notar que el rendimiento de dicha instalación es más de 2 veces mayor en el caso de Protobuf. El script del cliente se puede encontrar aquí : este es el script de referencia de Nats adaptado a las realidades de Centrifuge .
También vale la pena señalar que el rendimiento de la serialización JSON en el servidor se puede "aumentar" usando el mismo enfoque que en gogoprotobuf - agrupación de almacenamiento intermedio y generación de código - actualmente JSON es serializado por un paquete de la biblioteca estándar Go incorporada en reflect. Por ejemplo, en Centrifugo, la primera versión de JSON se serializa manualmente utilizando una biblioteca que proporciona un grupo de búferes . Algo similar se puede hacer en el futuro como parte de la segunda versión.
Vale la pena enfatizar que protobuf también se puede usar cuando se comunica con el servidor desde un navegador. El cliente javascript utiliza la biblioteca protobuf.js para esto. Dado que la biblioteca protobufjs es bastante grande y la cantidad de usuarios en formato binario será pequeña, usando webpack y su algoritmo de sacudida de árbol, generamos dos versiones del cliente, una con solo soporte de protocolo JSON y la otra con soporte de JSON y protobuf. Para otros entornos donde el tamaño de los recursos no juega un papel tan crítico, los clientes no pueden preocuparse por esta separación.
Token web JSON (JWT)
Uno de los problemas con el uso de un servidor independiente como Centrifugo es que no sabe nada acerca de sus usuarios y su método de autenticación, y qué tipo de mecanismo de sesión utiliza su back-end. Y necesita autenticar la conexión de alguna manera.
Para hacer esto, en la primera versión de Centrifuge, al conectarse, se utilizó la firma SHA-256 HMAC, basada en una clave secreta conocida solo por el backend y la Centrifuge. Esto aseguró que la ID de usuario transmitida por el cliente realmente le pertenece.
Quizás la transferencia correcta de los parámetros de conexión y la generación de un token fueron una de las principales dificultades para integrar Centrifugo en el proyecto.
Cuando apareció la Centrífuga, el estándar JWT aún no era tan popular. Ahora, unos años más tarde, las bibliotecas para la generación JWT están disponibles para los idiomas más populares . La idea principal de JWT es exactamente lo que necesita la Centrífuga: confirmación de la autenticidad de los datos transmitidos. En la segunda versión de HMAC, una firma generada manualmente dio paso al uso de JWT. Esto permitió eliminar la necesidad de soporte para funciones auxiliares para la generación correcta de tokens en bibliotecas para diferentes idiomas.
Por ejemplo, en Python, un token para conectarse a Centrifugo se puede generar de la siguiente manera:
import jwt import time token = jwt.encode({"user": "42", "exp": int(time.time()) + 10*60}, "secret").decode() print(token)
Es importante tener en cuenta que si usa la biblioteca Centrifuge, puede autenticar al usuario usando el método nativo Go, dentro del middleware. Los ejemplos están en el repositorio.
GRPC
Durante el desarrollo, probé la transmisión bidireccional GRPC como transporte para la comunicación entre el cliente y el servidor (además de los fallos de SockJS basados en Websocket y HTTP). Que puedo decir El trabajo. Sin embargo, no encontré un solo escenario en el que la transmisión bidireccional de GRPC sería mejor que Websocket. Observé principalmente las métricas del servidor: tráfico generado a través de la interfaz de red, consumo de CPU por parte del servidor con una gran cantidad de conexiones entrantes, consumo de memoria por conexión.
GRPC perdió ante Websocket en todos los aspectos:
- GRPC genera un 20% más de tráfico en escenarios similares,
- GRPC consume 2-3 veces más CPU (dependiendo de la configuración de las conexiones; todas están suscritas a diferentes canales o todas están suscritas a un canal),
- GRPC consume 4 veces más RAM por conexión. Por ejemplo, en conexiones de 10k, el servidor Websocket consumió 500Mb de memoria y GRPC - 2Gb.
Los resultados fueron bastante ... esperados. En general, en GRPC, como cliente de transporte, no tenía mucho sentido, y borré el código con la conciencia tranquila hasta, quizás, mejores momentos.
Sin embargo, GRPC es bueno para lo que fue creado principalmente: para generar código que le permite realizar llamadas RPC entre servicios utilizando un esquema predeterminado. Por lo tanto, además de la API HTTP, Centrifuge ahora también tendrá soporte API basado en GRPC, por ejemplo, para publicar nuevos mensajes en el canal y otros métodos API de servidor disponibles.
Dificultades con los clientes.
Con los cambios realizados en la segunda versión, eliminé el soporte obligatorio de las bibliotecas para la API del servidor; se hizo más fácil la integración en el lado del servidor, sin embargo, el protocolo del cliente en el proyecto se modificó y tiene una cantidad suficiente de características. Esto hace que la implementación de los clientes sea bastante difícil. Para la segunda versión, ahora tenemos un cliente para Javascript que funciona en los navegadores, debería funcionar con NodeJS y React-Native. Hay un cliente en Go y se construyó sobre la base de las carpetas de proyectos de Gomobile para iOS y Android .
Para una felicidad completa, no hay suficientes bibliotecas nativas para iOS y Android. Para la primera versión de Centrifugo, fueron comprados por chicos de la comunidad de código abierto. Quiero creer que algo así sucederá ahora.
Recientemente probé suerte enviando una solicitud para una subvención MOSS de Mozilla , con la intención de invertir en el desarrollo del cliente, pero me rechazaron. La razón es la comunidad insuficientemente activa en Github. Desafortunadamente, esto es cierto, pero como puede ver, estoy tomando algunas medidas para mejorar la situación.

Conclusión
No anuncié todas las características que aparecerán en Centrifugo v2; hay un poco más de información en Github . La versión del servidor aún no se ha llevado a cabo, pero sucederá pronto. Todavía hay momentos sin terminar, incluida la necesidad de completar la documentación. El prototipo de la documentación se puede ver aquí . Si es usuario de Centrifugo, ahora es el momento adecuado para influir en la segunda versión del servidor. Un momento en que no es tan aterrador romper algo, para luego hacerlo mejor. Para aquellos interesados: el desarrollo se concentra en la rama c2 .
Es difícil para mí juzgar cuánta demanda tendrá la biblioteca Centrifuge que subyace a Centrifugo v2. Por el momento, me complace haber podido llevarlo a su estado actual. El indicador más importante para mí ahora es la respuesta a la pregunta "¿Yo mismo usaría esta biblioteca en mi proyecto personal?" Mi respuesta es si. En el trabajo Si Por lo tanto, creo que otros desarrolladores lo apreciarán.
PD: Me gustaría agradecer a los chicos que ayudaron con el trabajo y los consejos: Dmitry Korolkov, Artemy Ryabinkov, Oleg Kuzmin. Sería difícil sin ti.