API do contrato do GT: zoológico de serviços



Com o aumento do número de componentes em um sistema de software, o número de pessoas que participam do seu desenvolvimento geralmente também aumenta. Como resultado, para manter o ritmo de desenvolvimento e a facilidade de manutenção, as abordagens para a organização da API devem ser objeto de atenção especial.

Se você quiser ter uma visão mais detalhada de como a equipe da Wargaming Platform lida com a complexidade de um sistema de mais de cem serviços da web interagindo entre si, então seja bem-vindo ao gato.

Olá pessoal! Meu nome é Valentine e sou engenheiro na plataforma da Wargaming. Para quem não sabe o que é e o que é a plataforma, deixarei aqui um link para a recente publicação de um de meus colegas - max_posedon

No momento, trabalho na empresa há mais de cinco anos e encontrei parcialmente o período de crescimento ativo do World of Tanks. Para descobrir as questões levantadas neste artigo, preciso começar com uma breve digressão sobre a história da Wargaming Platform.

Um pouco de história


A crescente popularidade dos "tanques" acabou parecendo uma avalanche e, como é geralmente o caso nesses casos, a infraestrutura em torno do jogo começou a se desenvolver rapidamente. Como resultado, o jogo rapidamente superou vários serviços da Web e, no momento em que entrei para a equipe, a pontuação deles já estava em dezenas (agora, a propósito, mais de 100 componentes da plataforma funcionam e beneficiam a empresa).

Com o passar do tempo, novos jogos foram lançados e o entendimento dos meandros das integrações entre os serviços da web não era mais fácil. A situação só piorou quando equipes de outros escritórios da Wargaming ingressaram no desenvolvimento da plataforma. O desenvolvimento foi distribuído, com todas as consequências na forma de distância, fuso horário e barreira do idioma. E há mais serviços. Encontrar uma pessoa que entenda como a plataforma como um todo funciona não é tão fácil. As informações frequentemente precisavam ser coletadas em partes de diferentes fontes.

As interfaces de vários serviços da web podem diferir bastante no desempenho estilístico, o que dificulta o processo de integração com a plataforma. E as dependências diretas entre componentes reduziram a flexibilidade do desenvolvimento, complicando a decomposição da funcionalidade na plataforma. Para piorar a situação, os jogos - clientes da plataforma - conheciam bem nossa topologia, pois precisavam se integrar diretamente a cada serviço da plataforma. Isso lhes deu a oportunidade, usando conexões horizontais, de fazer lobby para a implementação de certas melhorias diretamente no componente ao qual estão integradas. Isso levou ao aparecimento de funcionalidades duplicadas em vários componentes da plataforma, bem como à incapacidade de estender a funcionalidade existente para outros jogos. Tornou-se óbvio que continuar construindo uma plataforma em torno de cada jogo específico é um ramo sem saída do desenvolvimento. Precisávamos de mudanças técnicas e organizacionais, como resultado das quais poderíamos controlar a complexidade crescente de um sistema em rápido crescimento e tornar toda a funcionalidade da plataforma adequada para uso em qualquer jogo.

Sobre isso, quero terminar a excursão histórica e, finalmente, falar sobre uma de nossas soluções técnicas, que ajuda a manter sob controle a complexidade causada pelo número cada vez maior de serviços. Além disso, reduz o custo de desenvolvimento de novas funcionalidades e simplifica bastante a integração com a plataforma.

Conheça a API do contrato


Dentro da plataforma, chamamos de API do contrato. Na sua essência, é uma estrutura de integração representada por um conjunto de bibliotecas de documentação e cliente para cada tecnologia de nossa pilha (Erlang / Elixir, Java / Scala, Python). Ele está sendo desenvolvido, em primeiro lugar, para simplificar a integração dos componentes da plataforma. Segundo, para nos ajudar a resolver vários dos seguintes problemas:

  • diferenças estilísticas das interfaces do programa
  • a presença de dependências diretas entre componentes
  • manter a documentação atualizada
  • funcionalidade de introspecção e depuração de ponta a ponta

Então, as primeiras coisas primeiro.

Diferenças estilísticas nas interfaces de software


Na minha opinião, esse problema surgiu como resultado de uma combinação de vários fatores:

  • Falta de um padrão estrito de como deve ser a API. O conjunto de recomendações geralmente não tem o efeito desejado, a API ainda é diferente. Especialmente se o desenvolvimento for realizado por equipes de diferentes escritórios da empresa. Cada equipe tem seus próprios hábitos e práticas. Coletivamente, essas APIs geralmente não se parecem com partes de um todo.
  • Falta de um único diretório com os nomes e formatos de entidades específicas da empresa. Como regra, você não pode obter uma entidade do resultado de uma API e passá-la para a API de outro serviço. Isso requer transformação.
  • Falta de um sistema de revisão centralizada obrigatório para a API. Sempre há prazos e não há tempo para coletar atualizações e, além disso, fazer alterações na API, que na verdade geralmente acabam sendo testadas pela metade.

A primeira coisa que fizemos ao projetar a API do contrato foi dizer que a partir de agora a API pertence à plataforma, e não a um único componente. Isso levou ao fato de que o desenvolvimento de novas funcionalidades começa com uma solicitação de recebimento para uma API de armazenamento centralizado. Atualmente, usamos o repositório GIT como armazenamento. Por conveniência, dividimos toda a API em funções comerciais separadas, formalizamos a estrutura dessa função e a denominamos Contrato.

Desde então, cada nova função comercial em nossa API de contrato deve ser descrita em um formato especial e passar pela solicitação de recebimento com uma revisão obrigatória. Não há outra maneira de publicar uma nova API na API do contrato. No mesmo repositório, definimos um diretório de entidades específicas do negócio e sugerimos que os desenvolvedores do contrato os reutilizassem em vez de descrever essas entidades.

Portanto, obtivemos uma API de plataforma conceitualmente integrada que parecia um único produto, apesar de ter sido realmente implementada em muitos componentes da plataforma usando várias pilhas tecnológicas.

A presença de dependências diretas entre componentes


Esse nosso problema se manifestou no fato de que cada componente da plataforma precisava saber quem atende especificamente a funcionalidade de que precisa.

E não foi nem a dificuldade de manter esse diretório atualizado, mas o fato de as dependências diretas complicarem significativamente a migração da funcionalidade de negócios de um componente da plataforma para outro. O problema foi especialmente grave quando começamos a decomposição de nossos monólitos em componentes menores. Verificou-se que convencer o cliente a substituir a integração de trabalho por qualquer funcionalidade com a mesma do ponto de vista comercial, mas outra do ponto de vista técnico, não é uma tarefa de gerenciamento trivial. O cliente simplesmente não entende o ponto, já que tudo funciona bem para ele. Como resultado, foram criadas camadas de mau cheiro de compatibilidade com versões anteriores que apenas complicaram o suporte da plataforma e tiveram um efeito ruim na qualidade do serviço. E como já estávamos padronizando a API da plataforma, foi necessário resolver simultaneamente esse problema.

Enfrentamos uma escolha de várias opções. Destes, consideramos especialmente com cuidado:

  • Implementação de protocolos de descoberta de serviço em cada um dos componentes.
  • Usando um mediador para redirecionar solicitações do cliente para o componente de plataforma correto.
  • Usando um intermediário de mensagens como um barramento de mensagens.

Como resultado de alguns pensamentos e experiências, a escolha recaiu sobre o intermediário de mensagens, apesar do fato de ele nos ver como um possível ponto único de falha e aumentar a sobrecarga de operação da plataforma. Um papel importante na seleção foi desempenhado pelo fato de a plataforma naquele momento já possuir experiência em trabalhar com o RabbitMQ. E o próprio corretor foi bem dimensionado e tinha mecanismos internos para garantir a tolerância a falhas. Como bônus, tivemos a oportunidade de implementar uma arquitetura orientada a eventos ( arquitetura orientada a eventos ou EDA ) "sob o capô". O que posteriormente abriu diante de nós possibilidades mais amplas de interação entre serviços, em comparação com a interação ponto a ponto.

Então, topologicamente, a plataforma começou a se transformar de um gráfico com conectividade aleatória em uma estrela. E os componentes da plataforma inverteram suas dependências e tiveram a oportunidade de interagir entre si exclusivamente por meio de contratos registrados em um repositório centralizado, sem a necessidade de saber quem implementa especificamente um contrato específico. Em outras palavras, todos os componentes da plataforma puderam interagir entre si usando um único ponto de integração, o que simplificou bastante a vida dos desenvolvedores.

Mantendo a documentação atualizada


Os problemas associados à falta de documentação ou à perda de sua relevância são quase sempre encontrados. E quanto maior o ritmo de desenvolvimento, mais frequentemente ele se manifesta. Depois disso, coletar todas as especificações da API em um único local e formato para mais de cem serviços em uma equipe distribuída e multinacional é uma tarefa difícil.

Ao desenvolver a API do contrato, estabelecemos o objetivo de resolver esse problema também. E nós fizemos isso. Um formato estritamente definido para a descrição do contrato nos permitiu construir um processo de acordo com o qual, imediatamente após o surgimento de um novo contrato, a montagem automática da documentação é iniciada. Isso nos dá confiança de que nossa documentação da API está sempre atualizada. Esse processo é totalmente automatizado e não requer esforço de desenvolvimento ou gerenciamento.

Funcionalidade de ponta a ponta para introspecção e depuração


À medida que dividimos nossos monólitos em componentes menores, naturalmente, começaram a surgir dificuldades na depuração da funcionalidade de ponta a ponta. Se o serviço de uma função de negócios foi distribuído por vários componentes da plataforma, muitas vezes para localizar e depurar o problema, era preciso procurar representantes de cada um dos componentes. O que às vezes era possível com dificuldade, dada a diferença de 11 horas com alguns de nossos colegas.

Com o advento da API Contract, e em particular graças ao intermediário de mensagens subjacente, conseguimos receber cópias das mensagens envolvidas na execução de uma função comercial, sem efeitos colaterais nos participantes da interação. Para isso, nem é necessário saber qual dos componentes da plataforma é responsável pelo processamento de um contrato específico. E após a localização do problema, podemos obter o identificador do componente quebrado a partir dos metadados da mensagem do problema.

O que mais desenvolvemos no topo da API do contrato


Além de seu principal objetivo e solucionar os problemas acima, a API do Contrato nos permitiu implementar uma série de serviços úteis.

Gateway para acessar a funcionalidade da plataforma


A padronização da API na forma de contratos nos permitiu desenvolver um único ponto de acesso à funcionalidade da plataforma via HTTP. Além disso, com o advento de novas funcionalidades (contratos), não precisamos modificar esse ponto de acesso de forma alguma. É compatível com todos os contratos futuros. Isso permite que você trabalhe com a plataforma como um único produto usando a interface HTTP usual.

Serviço de Operações em Massa


Qualquer contrato pode ser lançado como parte de uma operação em massa, com a capacidade de rastrear seu status e, em seguida, receber um relatório sobre os resultados dessa operação. Este serviço, assim como o anterior, é compatível com todos os contratos futuros com antecedência.

Manipulação de erro de plataforma unificada


O protocolo da API do contrato também padroniza os erros. Isso nos permitiu implementar um interceptador de erros, que analisa sua gravidade e notifica o sistema de monitoramento sobre possíveis problemas nos componentes da plataforma. E, no futuro, ele poderá decidir independentemente sobre a descoberta de um bug no componente da plataforma. O interceptador de erros os captura diretamente do intermediário de mensagens e não sabe nada sobre a finalidade de um contrato ou erro, agindo apenas com base em meta-informações. Isso permite que ele, assim como todos os serviços descritos nesta seção, seja compatível com todos os contratos futuros.

Gerar automaticamente interfaces de usuário


Contratos estritamente formalizados permitem a criação automática de componentes da interface do usuário. Desenvolvemos um serviço que permite gerar uma interface administrativa com base em uma coleção de contratos e incorporá-la a qualquer uma das ferramentas da plataforma. Assim, os administradores que escrevemos anteriormente com nossas mãos agora podem ser gerados (embora apenas parcialmente até agora) no modo automático.

Log de plataforma


Este componente ainda não foi implementado e está em desenvolvimento. Porém, no futuro, permitirá "on the fly" ativar e desativar o registro de qualquer função comercial na plataforma, extraindo essas informações diretamente do intermediário de mensagens, sem efeitos colaterais que afetem adversamente os componentes em interação.

O principal objetivo da API do contrato


Mas, ainda assim, o principal objetivo da API do contrato é reduzir o custo da integração de componentes da plataforma.

Os desenvolvedores são abstraídos do nível de transporte pelas bibliotecas que desenvolvemos para cada uma de nossas pilhas de tecnologia. Isso nos dá algum espaço de manobra, caso tenhamos que mudar o intermediário de mensagens ou até mudar para a interação ponto a ponto. A interface externa da biblioteca permanecerá inalterada.

A biblioteca sob o capô gera uma mensagem de acordo com certas regras e a envia ao broker, após o que, após aguardar uma mensagem de resposta, retorna o resultado ao desenvolvedor. Fora, parece uma solicitação síncrona regular (ou assíncrona, dependente da implementação). Como demonstração, darei alguns exemplos.

Exemplo de chamada de contrato em 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" } 

A mesma chamada de contrato, mas usando o 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" }} 

No lugar do contrato "ban-management.create-ban.v1", pode haver qualquer outra funcionalidade da plataforma, por exemplo: "account-management.rename-account.v1" ou "notification-center.create-sms-notification.v1". E tudo isso estará disponível através desse único ponto de integração com a plataforma.

A visão geral ficará incompleta se você não demonstrar a API do contrato do ponto de vista do desenvolvedor do servidor. Considere uma situação em que um desenvolvedor precisa implementar um manipulador para o mesmo 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() 

Esse código será suficiente para começar a atender a um determinado contrato. A biblioteca do servidor descompactará e verificará se os parâmetros de solicitação estão corretos e depois chamará o manipulador de contratos com os parâmetros de solicitação prontos para processamento. Portanto, o desenvolvedor do servidor é protegido por uma biblioteca que, no caso de receber parâmetros de solicitação incorretos, envia um erro de validação ao cliente e registra o fato de um problema.

Devido ao fato de que, sob o capô, a API do contrato é implementada com base em eventos, temos a oportunidade de ir além do escopo do script Solicitação / Resposta e implementar uma ampla variedade de interações entre serviços.

Por exemplo:

  • faça um pedido e esqueça (sem esperar por uma resposta)
  • faça solicitações para vários contratos simultaneamente (mesmo sem usar um loop de eventos)
  • faça uma solicitação e receba respostas de vários manipuladores de uma só vez (se fornecidos pelo script de integração)
  • registrar um manipulador de resposta (acionado se o manipulador de contrato relatou a conclusão, aceita o resultado do trabalho do manipulador de contrato, ou seja, sua resposta)

E essa não é uma lista completa de cenários que podem ser expressos por meio de um modelo de interação de eventos. Esta é uma lista daqueles que estamos usando atualmente.

Em vez de uma conclusão


Estamos usando a API do contrato há vários anos. Portanto, não é possível falar sobre todos os cenários de seu uso na estrutura de um artigo de revisão. Pelo mesmo motivo, não sobrecarreguei o artigo com detalhes técnicos. Ela já ficou bastante volumosa. Faça perguntas e tentarei respondê-las diretamente nos comentários. Se um tópico for particularmente interessante, será possível divulgá-lo com mais detalhes em um artigo separado.

Source: https://habr.com/ru/post/pt441708/


All Articles