Cómo utilizar GraphQL Federation para migrar de forma incremental de Monolith (Python) a Microservices (Go)  

O cómo cambiar los cimientos de una casa antigua para que no se derrumbe.







Hace unos 10 años, elegimos Python 2 para desarrollar nuestra plataforma de aprendizaje monolítica. Pero la industria ha cambiado drásticamente desde entonces. Python 2 fue enterrado oficialmente el 1 de enero de 2020. En el artículo anterior , explicamos por qué decidimos no migrar a Python 3. 



Millones de personas usan nuestra plataforma cada mes. 



Asumimos cierto riesgo cuando decidimos reescribir nuestro backend en Go y cambiar la arquitectura. 



Elegimos Go por varias razones:



  1.  Alta velocidad de compilación.
  2. Ahorro de RAM.
  3. Una amplia selección de IDE compatibles con Go.


Pero adoptamos un enfoque que minimizó el riesgo.



Federación GraphQL



Decidimos construir nuestra nueva arquitectura alrededor de la Federación GraphQL Apollo . GraphQL fue creado por los desarrolladores de Facebook como una alternativa a la API REST. La federación consiste en construir una única puerta de enlace para múltiples servicios. Cada servicio puede tener su propio esquema GraphQL. Una puerta de enlace común combina sus esquemas, genera una única API y permite solicitudes de múltiples servicios al mismo tiempo. 



Antes de continuar, me gustaría destacar lo siguiente:



  1. A diferencia de las API REST, cada servidor GraphQL tiene su propio esquema de datos escrito. Le permite obtener cualquier combinación de exactamente los datos con campos arbitrarios que necesite.



  2. La puerta de enlace de la API REST le permite enviar una solicitud a un solo servicio de backend; La puerta de enlace GraphQL genera un plan de consulta para un número arbitrario de servicios de backend y le permite devolver selecciones de ellos en una única respuesta genérica.


Entonces, habiendo incluido la puerta de enlace GraphQL en nuestro sistema, obtenemos algo como esto:





URL de la imagen:     https://lh6.googleusercontent.com/6GBj9z5WVnQnhqI19oNTRncw0LYDJM4U7FpWeGxVMaZlP46IAIcKfYZKTtHcl-bDFomedAoxSa9pFo6pdhL2daxyWNX2ZKVQIgqIIBWHxnXEouzcQhO9_mdf1tODwtti5OEOOFeb 



Gateway (también conocido como servicio de graphql-gateway) es responsable de la creación y el envío del plan de consulta GraphQL-consultas a nuestros otros servicios - no sólo el monolito. Nuestros servicios Go tienen sus propios esquemas GraphQL. Usamos gqlgen (esta es una biblioteca GraphQL para Go) para generar respuestas a consultas



Dado que GraphQL Federation proporciona un esquema GraphQL común y la puerta de enlace agrupa todos los esquemas de servicios individuales en uno, nuestro monolito interactuará con él como con cualquier otro servicio. Este es un punto fundamental.



A continuación, hablaremos sobre cómo personalizamos el servidor Apollo GraphQL para escalar de forma segura desde nuestro monolito (Python) a una arquitectura de microservicio (Go).



Pruebas en paralelo



GraphQL "piensa" con conjuntos de objetos y campos de ciertos tipos. El código que sabe qué hacer con la solicitud entrante, cómo y qué datos extraer de los campos se llama resolutor. 



Consideremos el proceso de migración usando un ejemplo del tipo de datos para asignaciones:



123 escriba Asignación {createdDate: Time ……….}


Está claro que en realidad tenemos muchos más campos, pero para cada campo todo lucirá igual.



Digamos que queremos que este campo monolito esté representado en nuestro nuevo servicio escrito en Go. ¿Cómo podemos estar seguros de que el nuevo servicio a pedido devolverá los mismos datos que el monolito? Para hacer esto, usamos un enfoque similar a la biblioteca de Scientist : solicitamos datos tanto del monolito como del nuevo servicio, pero luego comparamos los resultados y devolvemos solo uno de ellos.



Paso 1: modo manual



Cuando el usuario pregunta por el valor del campo createdDate, nuestra puerta de enlace GraphQL accede primero al monolito (que está escrito en Python, recuerde). 





En el primer paso, debemos asegurarnos de que el campo se pueda agregar al nuevo servicio de asignaciones ya escrito en Go. El archivo con la extensión .graphql debe contener el siguiente código de resolución:



12345 extender asignación de tipo clave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "manual")}


Aquí estamos usando Federación para decir que el servicio está agregando un campo createdDate al tipo de Asignación. Se accede al campo por id. También agregamos un "ingrediente secreto": la directiva migrate. Escribimos código que comprende estas directivas y crea varios esquemas que la puerta de enlace GraphQL usará al decidir si enrutar una solicitud.



En modo manual, la solicitud solo se dirigirá al código monolito. Debemos considerar esta posibilidad al desarrollar un nuevo servicio. Para obtener el valor del campo createdDate, aún podemos acceder al monolito directamente (en modo primario), o podemos consultar la puerta de enlace GraphQL para el esquema en modo manual. Ambas opciones deberían funcionar.



Paso 2: modo de lado a lado



Después de haber escrito el código de resolución para el campo createdDate, lo cambiamos al modo en paralelo:



12345 extender asignación de tipo clave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "side-by-side")}


Y ahora la puerta de enlace accederá tanto al monolito (Python) como al nuevo servicio (Ir). Compara los resultados, registra los casos en los que hay diferencias y devuelve el resultado del monolito al usuario.



Este modo realmente infunde mucha confianza en que nuestro sistema no tendrá errores durante el proceso de migración. A lo largo de los años, millones de usuarios y "kilotones" de datos han pasado por nuestro frontend y backend. Al observar cómo funciona este código en condiciones reales, podemos asegurarnos de que incluso los casos raros y los valores atípicos aleatorios se capturen y luego se procesen de manera estable y correcta.



Durante las pruebas, recibimos dichos informes. 





Intente ampliar esta imagen durante el diseño de alguna manera sin una gran pérdida de calidad.



Se centran en casos en los que se encuentran discrepancias en el funcionamiento del monolito y el nuevo servicio. 



Al principio, a menudo nos encontramos con tales casos. Con el tiempo, hemos aprendido a identificar estos problemas, evaluar su criticidad y, si es necesario, eliminarlos.



Cuando trabajamos con nuestros servidores de desarrollo, utilizamos herramientas que resaltan las diferencias de color. Esto facilita el análisis de problemas y la prueba de soluciones.



¿Qué pasa con las mutaciones?



Quizás se pregunte si ejecutamos la misma lógica tanto en Python como en Go, ¿qué sucede con el código que modifica los datos, en lugar de simplemente consultarlos? En términos de GraphQL, esto se llama mutación.



Nuestras pruebas paralelas no tienen en cuenta las mutaciones. Analizamos algunos de los enfoques para hacer esto; resultaron ser más complejos de lo que pensábamos. Pero hemos desarrollado un enfoque que ayuda a resolver el problema mismo de las mutaciones.



Paso 2.5: modo canario



Si tenemos un campo o mutación que ha sobrevivido con éxito a la etapa de producción, habilitamos el modo canario.



12345 extender asignación de tipo clave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "canary")}


Los campos canarios y las mutaciones se agregarán al servicio Go para un pequeño porcentaje de nuestros usuarios. Además, los usuarios internos de la plataforma están probando el esquema canario. Esta es una forma bastante segura de probar cambios complejos. Podemos desactivar rápidamente el circuito canario si algo no funciona como se esperaba.



Solo usamos un circuito canario a la vez. En la práctica, no muchos campos y mutaciones están en modo canario al mismo tiempo. Entonces, creo que no habrá problemas en el futuro. Este es un buen compromiso porque el esquema es bastante grande (más de 5000 campos) y las instancias de puerta de enlace deben almacenar tres esquemas en la memoria: primario, manual y canario.



Paso 3: modo migrado



En este paso, el campo createdDate debe estar en modo migrado:



12345 extender asignación de tipo clave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "migrated")}


En este modo, la puerta de enlace GraphQL solo envía solicitudes a un nuevo servicio escrito en Go. Pero en cualquier momento podemos ver cómo el monolito procesará la misma solicitud. Esto hace que sea mucho más fácil implementar y revertir los cambios si algo sale mal.



Paso 4: completar la migración



Después de una implementación exitosa, ya no necesitamos el código monolito para este campo, y eliminamos la directiva @migrate del código de resolución:



12345 extender asignación de tipo clave(campos: "id") {id: ID! externo     createdDate: Time}


A partir de ahora, la puerta de enlace interpretará la expresión Assignment.createdDate como la obtención de un valor de campo de un nuevo servicio escrito en Go.



¡Así es la migración incremental!



¿Y que tan lejos hemos llegado?



Completamos nuestra infraestructura de pruebas en paralelo solo este año. Esto nos permitió reescribir de forma segura, lenta pero segura, un montón de código Go. Durante todo el año, hemos mantenido la alta disponibilidad de la plataforma en un contexto de tráfico creciente en nuestro sistema. En el momento de escribir este artículo, ~ 40% de nuestros campos GraphQL se mueven a los servicios Go. Entonces, el enfoque que describimos funcionó bien en el proceso de migración.



Incluso después de que se complete el proyecto, podemos seguir utilizando este enfoque para otras tareas relacionadas con el cambio de nuestra arquitectura.



PD: Steve Coffman dio una charla sobre este tema (en Google Open Source Live ). Puedes ver la grabaciónesta charla de YouTube (o simplemente mira la presentación ).






Los servidores en la nube de Macleod son rápidos y seguros.



Regístrese usando el enlace de arriba o haciendo clic en el banner y obtenga un 10% de descuento durante el primer mes de alquiler de un servidor de cualquier configuración.






All Articles