
Con el aumento en el número de componentes en un sistema de software, el número de personas que participan en su desarrollo generalmente también crece. Como resultado, para mantener el ritmo de desarrollo y la facilidad de mantenimiento, los enfoques para la organización de la API deben ser objeto de especial atención.
Si desea ver más de cerca cómo el equipo de Wargaming Platform hace frente a la complejidad de un sistema de más de cien servicios web que interactúan entre sí, entonces bienvenido a cat.
Hola a todos! Mi nombre es Valentine y soy ingeniero en la Plataforma de Wargaming. Para aquellos que no saben qué es la plataforma y qué hace, les dejo aquí un
enlace a la publicación reciente de uno de mis colegas:
max_posedonEn este momento, llevo más de cinco años trabajando en la empresa y descubrí parcialmente el período de crecimiento activo de World of Tanks. Para descubrir los problemas planteados en este artículo, necesito comenzar con una breve digresión sobre la historia de la Plataforma Wargaming.
Un poco de historia
La creciente popularidad de los "tanques" resultó ser una avalancha, y como suele ser el caso en tales casos, la infraestructura alrededor del juego comenzó a desarrollarse rápidamente. Como resultado, el juego se superpuso rápidamente con varios servicios web, y cuando me uní al equipo, su puntaje ya estaba llegando a decenas (ahora, por cierto, más de 100 componentes de la plataforma funcionan y benefician a la empresa).
A medida que pasaba el tiempo, aparecieron nuevos juegos, y entender las complejidades de las integraciones entre los servicios web ya no era fácil. La situación solo empeoró cuando los equipos de otras oficinas de Wargaming se unieron al desarrollo de la plataforma. El desarrollo se ha distribuido, con todas las consecuencias en forma de distancia, zonas horarias y barrera del idioma. Y hay más servicios. Encontrar una persona que entienda cómo funciona la plataforma en su conjunto no es tan fácil. La información a menudo tuvo que ser recopilada en partes de diferentes fuentes.
Las interfaces de varios servicios web pueden diferir enormemente en el rendimiento estilístico, lo que dificulta aún más el proceso de integración con la plataforma. Y las dependencias directas entre componentes redujeron la flexibilidad de desarrollo al complicar la descomposición de la funcionalidad dentro de la plataforma. Para empeorar las cosas, los juegos, clientes de la plataforma, conocían bien nuestra topología, ya que tenían que integrarse directamente con cada servicio de la plataforma. Esto les dio la oportunidad, mediante conexiones horizontales, de presionar para la implementación de ciertas mejoras directamente en el componente con el que se integran. Esto condujo a la aparición de funciones duplicadas en varios componentes de la plataforma, así como a la incapacidad de extender la funcionalidad existente a otros juegos. Se hizo obvio que continuar construyendo una plataforma alrededor de cada juego específico es una rama de desarrollo sin salida. Necesitábamos cambios técnicos y organizativos, como resultado de lo cual pudimos tomar el control de la creciente complejidad de un sistema en rápido crecimiento y hacer que toda la funcionalidad de la plataforma sea adecuada para su uso en cualquier juego.
Con esto quiero terminar la excursión histórica y, finalmente, hablar sobre una de nuestras soluciones técnicas, que ayuda a mantener bajo control la complejidad causada por el número cada vez mayor de servicios. Además, reduce el costo de desarrollar nuevas funciones y simplifica enormemente la integración con la plataforma.
Conoce la API del contrato
Dentro de la plataforma, la llamamos API de contrato. En esencia, es un marco de integración representado por un conjunto de documentación y bibliotecas de clientes para cada tecnología de nuestra pila (Erlang / Elixir, Java / Scala, Python). Se está desarrollando, en primer lugar, para simplificar la integración de los componentes de la plataforma entre sí. Segundo, para ayudarnos a resolver varios de los siguientes problemas:
- diferencias estilísticas de las interfaces del programa
- la presencia de dependencias directas entre componentes
- mantener la documentación actualizada
- introspección y depuración de funcionalidad de extremo a extremo
Entonces, lo primero es lo primero.
Diferencias estilísticas en las interfaces de software.
En mi opinión, este problema surgió como resultado de una combinación de varios factores:
- Falta de un estándar estricto de cómo debería ser la API. El conjunto de recomendaciones a menudo no tiene el efecto deseado, la API sigue siendo diferente. Especialmente si el desarrollo se lleva a cabo por equipos de diferentes oficinas de la empresa. Cada equipo tiene sus propios hábitos y prácticas. Colectivamente, tales API a menudo no parecen partes de un todo.
- Falta de un solo directorio con los nombres y formatos de entidades específicas del negocio. Como regla general, no puede tomar una entidad del resultado de una API y pasarla a la API de otro servicio. Esto requiere transformación.
- Falta de un sistema de revisión centralizado obligatorio para la API. Siempre hay fechas límite y no hay tiempo para recopilar actualizaciones y, además, hacer cambios en la API, que de hecho a menudo resulta estar ya a medias.
Lo primero que hicimos al diseñar la API de contrato fue decir que a partir de ahora la API pertenece a la plataforma, y no a un solo componente. Esto llevó al hecho de que el desarrollo de una nueva funcionalidad comienza con una solicitud de extracción a una API de almacenamiento centralizado. Actualmente, utilizamos el repositorio GIT como almacenamiento. Por conveniencia, dividimos la API completa en funciones comerciales separadas, formalizamos la estructura de esta función y la llamamos Contrato.
Desde entonces, cada nueva función comercial en nuestra API de contrato debe describirse en un formato especial y pasar por la solicitud de extracción con una revisión obligatoria. No hay otra forma de publicar una nueva API en la API del contrato. En el mismo repositorio, definimos un directorio de entidades específicas del negocio y sugerimos que los desarrolladores de contratos los reutilicen en lugar de describir estas entidades por sí mismos.
Por lo tanto, obtuvimos una API de plataforma conceptualmente integrada que parecía un solo producto, a pesar del hecho de que en realidad se implementó en muchos componentes de la plataforma utilizando varias pilas tecnológicas.
La presencia de dependencias directas entre componentes.
Este problema nuestro se manifestó en el hecho de que se requería que cada componente de la plataforma supiera quién atiende específicamente la funcionalidad que necesita.
Y ni siquiera fue la dificultad de mantener este directorio actualizado, sino el hecho de que las dependencias directas complicaron significativamente la migración de la funcionalidad empresarial de un componente de la plataforma a otro. El problema fue especialmente agudo cuando comenzamos la descomposición de nuestros monolitos en componentes más pequeños. Resultó que convencer al cliente para que reemplace la integración de trabajo con cualquier funcionalidad con la misma desde el punto de vista del negocio, pero otra desde el punto de vista técnico, no es una tarea de administración trivial. El cliente simplemente no ve el punto en esto, ya que todo funciona bien para él. Como resultado, se escribieron capas malolientes de compatibilidad con versiones anteriores que solo complicaron el soporte de la plataforma y tuvieron un efecto negativo en la calidad del servicio. Y como ya íbamos a estandarizar la API de la plataforma, era necesario resolver este problema simultáneamente.
Nos enfrentamos a una elección de varias opciones. De estos, consideramos especialmente cuidadosamente:
- Implementación de protocolos de descubrimiento de servicios en cada uno de los componentes.
- Usar un mediador para redirigir las solicitudes de los clientes al componente de plataforma correcto.
- Usar un agente de mensajes como un bus de mensajería.
Como resultado de algunas reflexiones y experimentos, la elección recayó en el intermediario de mensajes, a pesar del hecho de que nos veía como un posible punto único de falla y aumentó la sobrecarga de operar la plataforma. El hecho de que la plataforma en ese momento ya tenía experiencia en trabajar con RabbitMQ desempeñó un papel importante en la selección. Y el corredor en sí mismo escalaba bien y tenía mecanismos incorporados para garantizar la tolerancia a fallas. Como beneficio adicional, tuvimos la oportunidad de implementar una
arquitectura basada en eventos (
arquitectura dirigida por eventos o
EDA ) "bajo el capó". Lo que posteriormente abrió ante nosotros posibilidades más amplias de interacción entre servicios, en comparación con la interacción punto a punto.
Entonces, topológicamente, la plataforma comenzó a pasar de un gráfico con conectividad aleatoria a una estrella. Y los componentes de la plataforma invirtieron sus dependencias y tuvieron la oportunidad de interactuar entre ellos exclusivamente a través de contratos registrados en un repositorio centralizado, sin la necesidad de saber quién implementa específicamente un contrato en particular. En otras palabras, todos los componentes dentro de la plataforma pudieron interactuar entre sí utilizando un único punto de integración, lo que simplificó enormemente la vida de los desarrolladores.
Mantener la documentación actualizada
Los problemas asociados con la falta de documentación o la pérdida de su relevancia casi siempre se encuentran. Y cuanto mayor es el ritmo de desarrollo, más a menudo se manifiesta. Y después del hecho, reunir todas las especificaciones API en un solo lugar y formato para más de cien servicios en un equipo distribuido y multinacional es una tarea difícil.
Al desarrollar la API de contrato, también nos fijamos el objetivo de resolver este problema. Y lo hicimos Un formato estrictamente definido para la descripción del contrato nos permitió construir un proceso de acuerdo con el cual, inmediatamente después de la aparición de un nuevo contrato, se inicia el montaje automático de la documentación. Esto nos da la confianza de que nuestra documentación de la API siempre está actualizada. Este proceso está totalmente automatizado y no requiere ningún esfuerzo de desarrollo o gestión.
Introspección y depuración de funcionalidad de extremo a extremo
A medida que dividimos nuestros monolitos en componentes más pequeños, naturalmente, comenzaron a surgir dificultades para depurar la funcionalidad de extremo a extremo. Si el servicio de una función comercial se distribuía entre varios componentes de la plataforma, a menudo para localizar y depurar el problema, uno tenía que buscar representantes de cada uno de los componentes. Lo que a veces se podía lograr con dificultad, dada la diferencia horaria de 11 horas con algunos de nuestros colegas.
Con el advenimiento de la API de contrato, y en particular gracias al intermediario de mensajes subyacente, tuvimos la oportunidad de recibir copias de mensajes involucrados en la ejecución de una función comercial, sin efectos secundarios en la interacción de los participantes. Para hacer esto, ni siquiera es necesario saber cuál de los componentes de la plataforma es responsable de procesar un contrato en particular. Y después de la localización del problema, podemos obtener el identificador del componente roto a partir de los metadatos del mensaje del problema.
¿Qué más desarrollamos sobre la API del contrato?
Además de su propósito principal y de resolver los problemas anteriores, la API de Contrato nos permitió implementar una serie de servicios útiles.
Pasarela para acceder a la funcionalidad de la plataforma
La estandarización de la API en forma de contratos nos permitió desarrollar un único punto de acceso a la funcionalidad de la plataforma a través de HTTP. Además, con la llegada de nuevas funcionalidades (contratos), no necesitamos modificar este punto de acceso de ninguna manera. Es compatible con todos los contratos futuros. Esto le permite trabajar con la plataforma como un solo producto utilizando la interfaz HTTP habitual.
Servicio de operaciones masivas
Cualquier contrato puede iniciarse como parte de una operación masiva, con la capacidad de rastrear su estado y luego recibir un informe sobre los resultados de esta operación. Este servicio, al igual que el anterior, es compatible con todos los contratos futuros por adelantado.
Manejo unificado de errores de plataforma
El protocolo Contract API también estandariza los errores. Esto nos permitió implementar un interceptor de errores, que analiza su gravedad y notifica al sistema de monitoreo de posibles problemas en los componentes de la plataforma. Y en el futuro, podrá decidir independientemente el descubrimiento de un error en el componente de la plataforma. El interceptor de errores los atrapa directamente del agente de mensajes y no sabe nada sobre el propósito de un contrato o error, actuando solo sobre la base de metainformación. Esto le permite, al igual que todos los servicios descritos en esta sección, ser compatible con todos los contratos futuros.
Generar automáticamente interfaces de usuario
Los contratos estrictamente formalizados le permiten crear automáticamente componentes de interfaz de usuario. Hemos desarrollado un servicio que le permite generar una interfaz administrativa basada en una colección de contratos y luego integrar esta interfaz en cualquiera de nuestras herramientas de plataforma. Por lo tanto, los administradores que anteriormente escribimos con nuestras manos ahora se pueden generar (aunque solo parcialmente hasta ahora) en modo automático.
Registro de plataforma
Este componente aún no se ha implementado y está en desarrollo. Pero en el futuro, permitirá "sobre la marcha" activar y desactivar el registro de cualquier función comercial en la plataforma, extrayendo esta información directamente del agente de mensajes, sin ningún efecto secundario que afecte negativamente a los componentes que interactúan.
El objetivo principal de la API de contrato
Pero aún así, el objetivo principal de la API de contrato es reducir el costo de integrar componentes de la plataforma.
Los desarrolladores se abstraen del nivel de transporte por las bibliotecas que desarrollamos para cada una de nuestras pilas de tecnología. Esto nos da margen de maniobra en caso de que tengamos que cambiar el intermediario de mensajes o incluso cambiar a la interacción punto a punto. La interfaz externa de la biblioteca permanecerá sin cambios.
La biblioteca debajo del capó genera un mensaje de acuerdo con ciertas reglas y lo envía al agente, después de lo cual, después de esperar un mensaje de respuesta, devuelve el resultado al desarrollador. En el exterior, parece una solicitud síncrona regular (o asíncrona, dependiente de la implementación). Como demostración, daré algunos ejemplos.
Ejemplo de llamada de contrato de Python
from platform_client import Client client = Client(contracts_path=CONTRACTS_PATH, url=AMQP_URL, app_id='client') client.call("ban-management.create-ban.v1", { "wgid": 1234567890, "reason": "Fraudulent activity", "title": "ru.wot", "component": "game", "bantype": "access_denied", "author_id": "v_nikonovich", "expires_at": "2038-01-19 03:14:07Z" }) { u'ban_id': 31415926, u'wgid': 1234567890, u'title': u'ru.wot', u'component': u'game', u'reason': u'Fraudulent activity', u'bantype': u'access_denied', u'status': u"active", u'started_at': u"2019-02-15T15:15:15Z", u'expires_at': u"2038-01-19 03:14:07Z" }
La misma llamada de contrato, pero usando Elixir
:platform_client.call("ban-management.create-ban.v1", %{ "wgid" => 1234567890, "reason" => "Fraudulent activity", "title" => "ru.wot", "component" => "game", "bantype" => "access_denied", "author_id" => "v_nikonovich", "expires_at" => "2038-01-19 03:14:07Z" }) {:ok, %{ "ban_id" => 31415926, "wgid" => 1234567890, "title" => "ru.wot", "conponent" => "game", "reason" => "Fraudulent activity", "bantype" => "access_denied", "status" => "active", "started_at" => "2019-02-15T15:15:15Z", "expires_at" => "2038-01-19 03:14:07Z" }}
En lugar del contrato "ban-management.create-ban.v1" puede haber cualquier otra funcionalidad de plataforma, por ejemplo: "account-management.rename-account.v1" o "notify-center.create-sms-notify.v1". Y todo estará disponible a través de este único punto de integración con la plataforma.
La descripción general estará incompleta si no demuestra la API del contrato desde el punto de vista del desarrollador del servidor. Considere una situación en la que un desarrollador necesita implementar un controlador para el mismo contrato ban-management.create-ban.v1.
from platform_server import BlockingServer, handler class CustomServer(BlockingServer): @handler('ban-management.create-ban.v1') def handle_create_ban(self, params, context): response = do_some_usefull_job(params) return response d = CustomServer(app_id="server", amqp_url=AMQP_URL, contracts_path=CONTRACTS_PATH) d.serve()
Este código será suficiente para comenzar a cumplir un contrato determinado. La biblioteca del servidor desempaquetará y verificará que los parámetros de la solicitud sean correctos, y luego llamará al manejador del contrato con los parámetros de la solicitud listos para su procesamiento. Por lo tanto, el desarrollador del servidor está protegido por una biblioteca que, en caso de recibir parámetros de solicitud incorrectos, enviará un error de validación al cliente y registrará el hecho de un problema.
Debido al hecho de que, bajo el capó, la API de contrato se implementa en función de los eventos, tenemos la oportunidad de ir más allá del alcance del script de Solicitud / Respuesta e implementar una gama más amplia de interacciones entre servicios.
Por ejemplo:
- haga una solicitud y olvide (sin esperar una respuesta)
- realizar solicitudes a varios contratos simultáneamente (incluso sin usar un bucle de eventos)
- haga una solicitud y reciba respuestas de varios manejadores a la vez (si lo proporciona el script de integración)
- registrar un manejador de respuestas (se activa si el manejador de contratos informó la finalización, acepta el resultado del trabajo del manejador de contratos, es decir, su respuesta)
Y esta no es una lista completa de escenarios que se pueden expresar a través de un modelo de interacción de eventos. Esta es una lista de los que estamos usando actualmente.
En lugar de una conclusión
Hemos estado utilizando la API de contrato durante varios años. Por lo tanto, no es posible hablar sobre todos los escenarios de su uso en el marco de un artículo de revisión. Por la misma razón, no sobrecargué el artículo con detalles técnicos. Ella ya resultó bastante voluminosa. Haga preguntas y trataré de responderlas directamente en los comentarios. Si un tema es particularmente interesante, será posible revelarlo con más detalle en un artículo separado.