Como usar o GraphQL Federation para migrar incrementalmente do Monolith (Python) para os microsserviços (Go)  

Ou como mudar a fundação de uma casa velha para que ela não desmorone







Cerca de 10 anos atrás, escolhemos Python 2 para desenvolver nossa plataforma de aprendizagem monolítica. Mas a indústria mudou drasticamente desde então. Python 2 foi oficialmente enterrado em 1º de janeiro de 2020. No artigo anterior , explicamos por que decidimos não migrar para o Python 3. 



Milhões de pessoas usam nossa plataforma todos os meses. 



Corremos um certo risco quando decidimos reescrever nosso back-end em Go e mudar a arquitetura. 



Escolhemos Go por vários motivos:



  1.  Alta velocidade de compilação.
  2. Salvando RAM.
  3. Uma seleção bastante ampla de IDEs com suporte Go.


Mas adotamos uma abordagem que minimizou o risco.



GraphQL Federation



Decidimos construir nossa nova arquitetura em torno da GraphQL Apollo Federation . GraphQL foi criado pelos desenvolvedores do Facebook como uma alternativa à API REST. A federação trata da construção de um único gateway para vários serviços. Cada serviço pode ter seu próprio esquema GraphQL. Um gateway comum combina seus esquemas, gera uma única API e permite solicitações de vários serviços ao mesmo tempo. 



Antes de prosseguirmos, gostaria de destacar o seguinte:



  1. Ao contrário das APIs REST, cada servidor GraphQL tem seu próprio esquema de dados tipado. Ele permite que você obtenha qualquer combinação exata de dados com campos arbitrários de que você precisa.



  2. O gateway REST API permite que você envie uma solicitação para apenas um serviço de back-end; O gateway GraphQL gera um plano de consulta para um número arbitrário de serviços de back-end e permite que você retorne as seleções deles em uma única resposta genérica.


Assim, tendo incluído o gateway GraphQL em nosso sistema, obtemos algo assim:





URL Image:     https://lh6.googleusercontent.com/6GBj9z5WVnQnhqI19oNTRncw0LYDJM4U7FpWeGxVMaZlP46IAIcKfYZKTtHcl-bDFomedAoxSa9pFo6pdhL2daxyWNX2ZKVQIgqIIBWHxnXEouzcQhO9_mdf1tODwtti5OEOOFeb 



Gateway (aka serviço graphql-gateway) é responsável pela criação e envio de plano de consulta GraphQL-consultas aos nossos outros serviços - não só o monólito. Nossos serviços Go têm seus próprios esquemas GraphQL. Usamos gqlgen (esta é uma biblioteca GraphQL para Go) para gerar respostas às consultas



Como o GraphQL Federation fornece um esquema GraphQL comum e o gateway agrupa todos os esquemas de serviço individuais em um, nosso monolith irá interagir com ele como qualquer outro serviço. Este é um ponto fundamental.



A seguir, falaremos sobre como personalizamos o servidor Apollo GraphQL para escalar com segurança de nosso monólito (Python) para uma arquitetura de microsserviço (Go).



Teste lado a lado



GraphQL "pensa" com conjuntos de objetos e campos de certos tipos. O código que sabe o que fazer com a solicitação recebida, como e quais dados extrair dos campos é chamado de resolvedor. 



Vamos considerar o processo de migração usando um exemplo do tipo de dados para atribuições:



123 tipo Assignment {createdDate: Time ……….}


É claro que na realidade temos muito mais campos, mas para cada campo tudo parecerá igual.



Digamos que queremos que esse campo monolítico seja representado em nosso novo serviço escrito em Go. Como podemos ter certeza de que o novo serviço sob demanda retornará os mesmos dados do monólito? Para fazer isso, usamos uma abordagem semelhante à biblioteca Scientist : solicitamos dados do monólito e do novo serviço, mas depois comparamos os resultados e retornamos apenas um deles.



Etapa 1: modo manual



Quando o usuário pergunta pelo valor do campo createdDate, nosso gateway GraphQL primeiro acessa o monólito (que é escrito em Python, lembre-se). 





Na primeira etapa, precisamos garantir que o campo possa ser adicionado ao novo serviço de atribuições já escrito em Go. O arquivo com a extensão .graphql deve conter o seguinte código do resolvedor:



12345 estender tipo Atribuição chave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "manual")}


Aqui, estamos usando Federação para dizer que o serviço está adicionando um campo createdDate ao tipo de Atribuição. O campo é acessado por id. Também adicionamos um "ingrediente secreto" - a diretiva de migração. Escrevemos um código que entende essas diretivas e cria vários esquemas que o gateway GraphQL usará ao decidir se deve rotear uma solicitação.



No modo manual, a solicitação será endereçada apenas ao código monolith. Devemos considerar essa possibilidade ao desenvolver um novo serviço. Para obter o valor do campo createdDate, ainda podemos acessar o monólito diretamente (no modo primário) ou podemos consultar o gateway GraphQL para o esquema no modo manual. Ambas as opções devem funcionar.



Etapa 2: modo lado a lado



Depois de escrever o código do resolvedor para o campo createdDate, nós o alternamos para o modo lado a lado:



12345 estender tipo Atribuição chave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "side-by-side")}


E agora o gateway acessará o monolith (Python) e o novo serviço (Go). Ele irá comparar os resultados, registrar os casos em que houver diferenças e retornar o resultado do monólito para o usuário.



Este modo realmente inspira muita confiança de que nosso sistema não terá bugs durante o processo de migração. Ao longo dos anos, milhões de usuários e "quilotons" de dados passaram por nosso front-end e back-end. Observando como esse código funciona em condições reais, podemos garantir que até mesmo casos raros e outliers aleatórios sejam capturados e, em seguida, processados ​​de forma estável e correta.



Durante o teste, recebemos esses relatórios. 





Tente aumentar a imagem durante o layout de alguma forma sem uma grande perda de qualidade.



Eles se concentram nos casos em que são encontradas discrepâncias na operação do monólito e no novo serviço. 



No início, frequentemente encontramos esses casos. Com o tempo, aprendemos a identificar esses problemas, avaliá-los quanto à sua criticidade e, se necessário, eliminá-los.



Ao trabalhar com nossos servidores de desenvolvimento, usamos ferramentas que destacam as diferenças de cor. Isso torna mais fácil analisar problemas e testar soluções.



E quanto às mutações?



Você pode estar se perguntando se executarmos a mesma lógica em Python e Go, o que acontece com o código que modifica os dados, em vez de apenas consultá-los? Em termos de GraphQL, isso é chamado de mutação.



Nossos testes lado a lado não levam em consideração as mutações. Analisamos algumas das abordagens para fazer isso - elas acabaram sendo mais complexas do que pensávamos. Mas desenvolvemos uma abordagem que ajuda a resolver o próprio problema das mutações.



Etapa 2.5: modo canário



Se tivermos um campo ou mutação que sobreviveu com sucesso ao estágio de produção, habilitamos o modo canário.



12345 estender tipo Atribuição chave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "canary")}


Campos canários e mutações serão adicionados ao serviço Go para uma pequena porcentagem de nossos usuários. Além disso, os usuários internos da plataforma estão testando o esquema canário. Esta é uma maneira bastante segura de testar mudanças complexas. Podemos desabilitar rapidamente o circuito canário se algo não funcionar como esperado.



Usamos apenas um circuito de canário por vez. Na prática, poucos campos e mutações estão no modo canário ao mesmo tempo. Então, acho que não haverá problemas no futuro. Este é um bom compromisso porque o esquema é muito grande (mais de 5.000 campos) e as instâncias de gateway devem armazenar três esquemas na memória - primário, manual e canário.



Etapa 3: modo migrado



Nesta etapa, o campo createdDate deve estar no modo migrado:



12345 estender tipo Atribuição chave(campos: "id") {id: ID! externo     createdDate: Time @migrate (from: "python", state: "migrated")}


Nesse modo, o gateway GraphQL envia apenas solicitações para um novo serviço escrito em Go. Mas a qualquer momento podemos ver como o monólito processará o mesmo pedido. Isso torna muito mais fácil implantar e reverter as alterações se algo der errado.



Etapa 4: Concluindo a migração



Após uma implantação bem-sucedida, não precisamos mais do código monolith para este campo e removemos a diretiva @migrate do código do resolvedor:



12345 estender tipo Atribuição chave(campos: "id") {id: ID! externo     createdDate: Time}


A partir de agora, o gateway interpretará a expressão Assignment.createdDate como obtendo um valor de campo de um novo serviço escrito em Go.



É assim que a migração incremental é!



E quão longe nós fomos?



Concluímos nossa infraestrutura de testes lado a lado apenas este ano. Isso nos permitiu reescrever de forma segura, lenta mas segura um monte de código Go. Ao longo do ano, mantivemos a alta disponibilidade da plataforma em um cenário de crescente tráfego em nosso sistema. No momento em que este artigo foi escrito, ~ 40% de nossos campos GraphQL foram movidos para serviços Go. Portanto, a abordagem que descrevemos funcionou bem no processo de migração.



Mesmo depois que o projeto for concluído, podemos continuar a usar essa abordagem para outras tarefas relacionadas à mudança de nossa arquitetura.



PS Steve Coffman deu uma palestra sobre este assunto (no Google Open Source Live ). Você pode assistir a gravaçãoesta palestra no YouTube (ou apenas assista à apresentação ).






Os servidores em nuvem da Macleod são rápidos e seguros.



Cadastre-se pelo link acima ou clicando no banner e ganhe 10% de desconto no primeiro mês de aluguel de um servidor de qualquer configuração!






All Articles