Mikhail Salosin (en adelante – MS): - ¡Hola a todos! Mi nombre es Michael. Trabajo como desarrollador backend en MC2 Software y hablaré sobre el uso de Go en el backend de la aplicación móvil Look+.
¿A alguien aquí le gusta el hockey?
Entonces esta aplicación es para ti. Es para Android e iOS y sirve para ver retransmisiones de diversos eventos deportivos online y grabados. La aplicación también contiene diversas estadísticas, retransmisiones de texto, tablas de conferencias, torneos y otra información útil para los aficionados.
También en la aplicación existe el concepto de momentos en vídeo, es decir, puedes ver los momentos más importantes de los partidos (goles, peleas, penales, etc.). Si no quieres ver la transmisión completa, puedes ver solo las más interesantes.
¿Qué usaste en el desarrollo?
La parte principal fue escrita en Go. La API con la que se comunicaban los clientes móviles se escribió en Go. También se escribió en Go un servicio para enviar notificaciones automáticas a teléfonos móviles. También tuvimos que escribir nuestro propio ORM, del que podríamos hablar algún día. Bueno, algunos pequeños servicios fueron escritos en Go: cambiar el tamaño y cargar imágenes para los editores...
Utilizamos PostgreSQL como base de datos. La interfaz del editor fue escrita en Ruby on Rails usando la gema ActiveAdmin. La importación de estadísticas desde un proveedor de estadísticas también está escrita en Ruby.
Para las pruebas de API del sistema, utilizamos Python unittest. Memcached se utiliza para acelerar las llamadas de pago API, "Chef" se utiliza para controlar la configuración, Zabbix se utiliza para recopilar y monitorear estadísticas internas del sistema. Graylog2 es para recopilar registros, Slate es documentación API para clientes.
Selección de protocolo
El primer problema que encontramos: necesitábamos elegir un protocolo para la interacción entre el backend y los clientes móviles, basándonos en los siguientes puntos...
- El requisito más importante: los datos de los clientes deben actualizarse en tiempo real. Es decir, todos los que estén viendo la transmisión actualmente deberían recibir actualizaciones casi al instante.
- Para simplificar las cosas, asumimos que los datos que se sincronizan con los clientes no se eliminan, sino que se ocultan mediante indicadores especiales.
- Todo tipo de solicitudes raras (como estadísticas, composiciones de equipos, estadísticas de equipos) se obtienen mediante solicitudes GET ordinarias.
- Además, el sistema tenía que admitir fácilmente 100 usuarios al mismo tiempo.
En base a esto, teníamos dos opciones de protocolo:
- Enchufes web. Pero no necesitábamos canales del cliente al servidor. Solo necesitábamos enviar actualizaciones desde el servidor al cliente, por lo que un websocket es una opción redundante.
- ¡Los eventos enviados por el servidor (SSE) surgieron perfectamente! Es bastante sencillo y básicamente satisface todo lo que necesitamos.
Eventos enviados por el servidor
Unas pocas palabras sobre cómo funciona esto...
Se ejecuta sobre una conexión http. El cliente envía una solicitud, el servidor responde con Content-Type: text/event-stream y no cierra la conexión con el cliente, pero continúa escribiendo datos en la conexión:
Los datos se pueden enviar en un formato acordado con los clientes. En nuestro caso, lo enviamos de esta forma: el nombre de la estructura modificada (persona, jugador) se envió al campo del evento y JSON con campos nuevos y modificados para el jugador se envió al campo de datos.
Ahora hablemos de cómo funciona la interacción en sí.
- Lo primero que hace el cliente es determinar la última vez que se realizó la sincronización con el servicio: mira su base de datos local y determina la fecha del último cambio registrado por ella.
- Envía una solicitud con esta fecha.
- En respuesta, le enviamos todas las actualizaciones que han ocurrido desde esa fecha.
- Después de eso, se conecta al canal en vivo y no se cierra hasta que necesita estas actualizaciones:
Le enviamos una lista de cambios: si alguien marca un gol, cambiamos el resultado del partido, si se lesiona, esto también se envía en tiempo real. Por lo tanto, los clientes reciben instantáneamente datos actualizados en el feed del evento del partido. Periódicamente, para que el cliente comprenda que el servidor no ha muerto, que no le pasó nada, enviamos una marca de tiempo cada 15 segundos, para que sepa que todo está en orden y que no es necesario volver a conectarse.
¿Cómo se mantiene la conexión en vivo?
- En primer lugar, creamos un canal en el que se recibirán las actualizaciones almacenadas.
- Después de eso, nos suscribimos a este canal para recibir actualizaciones.
- Configuramos el encabezado correcto para que el cliente sepa que todo está bien.
- Envía el primer ping. Simplemente registramos la marca de tiempo de la conexión actual.
- Después de eso, leemos del canal en un bucle hasta que se cierra el canal de actualización. El canal recibe periódicamente la marca de tiempo actual o los cambios que ya estamos escribiendo para abrir conexiones.
El primer problema que encontramos fue el siguiente: para cada conexión abierta con el cliente, creamos un temporizador que marcaba una vez cada 15 segundos; resulta que si tuviéramos 6 mil conexiones abiertas con una máquina (con un servidor API), 6 Se crearon mil temporizadores. Esto provocó que la máquina no aguantara la carga requerida. El problema no era tan obvio para nosotros, pero conseguimos un poco de ayuda y lo solucionamos.
Como resultado, ahora nuestro ping proviene del mismo canal del que proviene la actualización.
En consecuencia, solo hay un temporizador que funciona una vez cada 15 segundos.
Aquí hay varias funciones auxiliares: enviar el encabezado, ping y la estructura misma. Es decir, aquí se transmite el nombre de la tabla (persona, partido, temporada) y la información sobre esta entrada:
Mecanismo de envío de actualizaciones
Ahora un poco sobre de dónde vienen los cambios. Contamos con varias personas, redactores, que ven la retransmisión en tiempo real. Crean todos los eventos: alguien fue expulsado, alguien resultó herido, algún tipo de reemplazo...
Usando un CMS, los datos ingresan a la base de datos. Después de esto, la base de datos notifica a los servidores API sobre esto utilizando el mecanismo Escuchar/Notificar. Los servidores API ya envían esta información a los clientes. Por lo tanto, esencialmente solo tenemos unos pocos servidores conectados a la base de datos y no hay una carga especial en la base de datos, porque el cliente no interactúa directamente con la base de datos de ninguna manera:
PostgreSQL: escuchar/notificar
El mecanismo Escuchar/Notificar en Postgres le permite notificar a los suscriptores de eventos que algún evento ha cambiado: se ha creado algún registro en la base de datos. Para hacer esto, escribimos un disparador y una función simples:
Al insertar o cambiar un registro, llamamos a la función notify en el canal data_updates, pasando allí el nombre de la tabla y el identificador del registro que fue cambiado o insertado.
Para todas las tablas que deben sincronizarse con el cliente, definimos un disparador que, después de cambiar/actualizar un registro, llama a la función indicada en la diapositiva a continuación.
¿Cómo se suscribe la API a estos cambios?
Se crea un mecanismo Fanout: envía mensajes al cliente. Recopila todos los canales de clientes y envía las actualizaciones que recibió a través de estos canales:
Aquí la biblioteca pq estándar, que se conecta a la base de datos y dice que quiere escuchar el canal (data_updates), verifica que la conexión esté abierta y que todo esté bien. Estoy omitiendo la verificación de errores para ahorrar espacio (no verificar es peligroso).
A continuación, configuramos Ticker de forma asincrónica, que enviará un ping cada 15 segundos y comenzaremos a escuchar el canal al que nos suscribimos. Si recibimos un ping, lo publicamos. Si recibimos algún tipo de entrada, la publicamos para todos los suscriptores de este Fanout.
¿Cómo funciona el Fan-out?
En ruso esto se traduce como "divisor". Tenemos un objeto que registra suscriptores que desean recibir algunas actualizaciones. Y tan pronto como llega una actualización a este objeto, la distribuye a todos sus suscriptores. Suficientemente simple:
Cómo se implementa en Go:
Hay una estructura, se sincroniza mediante Mutexes. Tiene un campo que guarda el estado de la conexión de Fanout a la base de datos, es decir, actualmente está escuchando y recibirá actualizaciones, así como una lista de todos los canales disponibles: un mapa, cuya clave es el canal y la estructura en forma de valores (esencialmente no se utiliza de ninguna manera).
Dos métodos, Conectado y Desconectado, nos permiten decirle a Fanout que tenemos una conexión con la base, ha aparecido y que la conexión con la base se ha roto. En el segundo caso, es necesario desconectar a todos los clientes y decirles que ya no pueden escuchar nada y que se vuelven a conectar porque la conexión con ellos se ha cerrado.
También existe un método de suscripción que agrega el canal a los “oyentes”:
Existe un método para cancelar la suscripción, que elimina el canal de los oyentes si el cliente se desconecta, así como un método para publicar, que le permite enviar un mensaje a todos los suscriptores.
Pregunta: – ¿Qué se transmite por este canal?
EM: – Se transmite el modelo que ha cambiado o se transmite el ping (esencialmente solo un número, un número entero).
EM: – Puedes enviar cualquier cosa, enviar cualquier estructura, publicarla – simplemente se convierte en JSON y listo.
EM: – Recibimos una notificación de Postgres que contiene el nombre de la tabla y el identificador. Según el nombre de la tabla y el identificador, obtenemos el registro que necesitamos y luego enviamos esta estructura para su publicación.
Infraestructura
¿Cómo se ve esto desde una perspectiva de infraestructura? Contamos con 7 servidores de hardware: uno de ellos está completamente dedicado a la base de datos, los otros seis ejecutan máquinas virtuales. Hay 6 copias de la API: cada máquina virtual con la API se ejecuta en un servidor de hardware independiente; esto es por motivos de confiabilidad.
Tenemos dos frontends con Keepalived instalado para mejorar la accesibilidad, de modo que si pasa algo, un frontend pueda reemplazar al otro. Además, dos copias del CMS.
También hay un importador de estadísticas. Se dispone de una DB Slave de la que se realizan copias de seguridad periódicamente. Existe Pigeon Pusher, una aplicación que envía notificaciones push a los clientes, así como elementos de infraestructura: Zabbix, Graylog2 y Chef.
De hecho, esta infraestructura es redundante, porque se pueden atender 100 mil con menos servidores. Pero había hierro, lo usamos (nos dijeron que era posible, por qué no).
Ventajas de ir
Después de trabajar en esta aplicación, surgieron ventajas tan obvias de Go.
- Genial biblioteca http. Con él puedes crear muchas cosas desde el primer momento.
- Además, canales que nos permitieron implementar muy fácilmente un mecanismo para enviar notificaciones a los clientes.
- Lo maravilloso del detector de carreras nos permitió eliminar varios errores críticos (infraestructura de preparación). Se lanza todo lo que funciona en la puesta en escena, compilado con la clave Race; y, en consecuencia, podemos observar la infraestructura de preparación para ver qué problemas potenciales tenemos.
- Minimalismo y sencillez del lenguaje.
¡Buscamos desarrolladores! Si alguien quiere, por favor.
preguntas
Pregunta del público (en adelante – B): – Me parece que omitiste un punto importante respecto al Fan-out. ¿Estoy en lo cierto al entender que cuando envías una respuesta a un cliente, la bloqueas si el cliente no quiere leer?
EM: - No, no estamos bloqueando. En primer lugar, tenemos todo esto detrás de nginx, es decir, no hay problemas con clientes lentos. En segundo lugar, el cliente tiene un canal con un búfer; de hecho, podemos colocar allí hasta cien actualizaciones... Si no podemos escribir en el canal, lo elimina. Si vemos que el canal está bloqueado, simplemente cerraremos el canal y listo, el cliente se volverá a conectar si surge algún problema. Por lo tanto, en principio, aquí no hay ningún bloqueo.
A: – ¿No sería posible enviar inmediatamente un registro a Escuchar/Notificar y no una tabla de identificadores?
EM: – Escuchar/Notificar tiene un límite de 8 mil bytes en la precarga que envía. En principio, sería posible enviar si tratáramos con una pequeña cantidad de datos, pero me parece que así [la forma en que lo hacemos] es simplemente más confiable. Las limitaciones están en el propio Postgres.
A: – ¿Los clientes reciben actualizaciones sobre partidos que no les interesan?
EM: - En general, sí. Como regla general, se juegan 2 o 3 partidos en paralelo, y aun así, muy raramente. Si un cliente está viendo algo, normalmente está viendo el partido que se está desarrollando. Luego, el cliente tiene una base de datos local en la que se suman todas estas actualizaciones, e incluso sin una conexión a Internet, el cliente puede ver todas las coincidencias anteriores para las que tiene actualizaciones. Básicamente, sincronizamos nuestra base de datos en el servidor con la base de datos local del cliente para que pueda trabajar sin conexión.
A: – ¿Por qué hiciste tu propio ORM?
Alexey (uno de los desarrolladores de Look+): – En ese momento (fue hace un año) había menos ORM que ahora, cuando hay bastantes. Lo que más me gusta de la mayoría de los ORM que existen es que la mayoría se ejecutan en interfaces vacías. Es decir, los métodos en estos ORM están listos para asumir cualquier cosa: una estructura, un puntero de estructura, un número, algo completamente irrelevante...
Nuestro ORM genera estructuras basadas en el modelo de datos. Mí mismo. Y por lo tanto todos los métodos son concretos, no usan reflexión, etc. Aceptan estructuras y esperan usar aquellas estructuras que vienen.
A: – ¿Cuántas personas participaron?
EM: – En la etapa inicial participaron dos personas. Comenzamos en junio y en agosto la parte principal estaba lista (la primera versión). Hubo un lanzamiento en septiembre.
A: – Cuando describe SSE, no utiliza el tiempo de espera. ¿Porqué es eso?
EM: – Para ser honesto, SSE sigue siendo un protocolo html5: el estándar SSE está diseñado para comunicarse con los navegadores, hasta donde tengo entendido. Tiene funciones adicionales para que los navegadores puedan volver a conectarse (y así sucesivamente), pero no las necesitamos porque teníamos clientes que podían implementar cualquier lógica para conectarse y recibir información. No hicimos la ESS, sino algo similar a la ESS. Este no es el protocolo en sí.
No había necesidad. Según tengo entendido, los clientes implementaron el mecanismo de conexión casi desde cero. Realmente no les importaba.
A: – ¿Qué utilidades adicionales usaste?
EM: – Utilizamos más activamente govet y golint para unificar el estilo, así como gofmt. No se utilizó nada más.
A: – ¿Qué usaste para depurar?
EM: – La depuración se llevó a cabo en gran medida mediante pruebas. No utilizamos ningún depurador ni GOP.
A: – ¿Puedes devolver la diapositiva donde se implementa la función Publicar? ¿Te confunden los nombres de variables de una sola letra?
EM: - No. Tienen un alcance de visibilidad bastante “estrecho”. No se usan en ningún otro lugar excepto aquí (a excepción de los componentes internos de esta clase) y es muy compacto: solo ocupa 7 líneas.
A: – De alguna manera todavía no es intuitivo…
EM: - ¡No, no, este es un código real! No se trata de estilo. Es una clase tan utilitaria y muy pequeña: sólo 3 campos dentro de la clase...
EM: – En general, todos los datos que se sincronizan con los clientes (partidos de temporada, jugadores) no cambian. En términos generales, si creamos otro deporte en el que necesitamos cambiar el partido, simplemente tendremos todo en cuenta en la nueva versión del cliente y las versiones antiguas del cliente serán prohibidas.
A: – ¿Existen paquetes de gestión de dependencias de terceros?
EM: – Solíamos ir dep.
A: – Había algo sobre vídeo en el tema del informe, pero no había nada sobre vídeo en el informe.
EM: – No, no tengo nada en el tema sobre el video. Se llama "Look+", ese es el nombre de la aplicación.
A: – ¿Dijiste que se transmite a los clientes?
EM: – No estábamos involucrados en la transmisión de video. Esto fue hecho íntegramente por Megafon. Sí, no dije que la aplicación fuera MegaFon.
EM: – Go – para enviar todos los datos – sobre el marcador, los eventos del partido, estadísticas... Go es el backend completo de la aplicación. El cliente debe saber de algún lugar qué enlace utilizar para el jugador para que el usuario pueda ver el partido. Tenemos enlaces a videos y transmisiones que se han preparado.
Algunos anuncios 🙂
Gracias por estar con nosotros. ¿Te gustan nuestros artículos? ¿Quieres ver más contenido interesante? Apóyanos haciendo un pedido o recomendándonos a amigos,
Dell R730xd 2 veces más barato en el centro de datos Equinix Tier IV en Amsterdam? Solo aqui
Fuente: habr.com