Transição de monolito para microsserviços: história e prática

Neste artigo, falarei sobre como o projeto no qual estou trabalhando se transformou de um grande monólito em um conjunto de microsserviços.

O projeto começou sua história há bastante tempo, no início de 2000. As primeiras versões foram escritas em Visual Basic 6. Com o tempo, ficou claro que o desenvolvimento nesta linguagem seria difícil de suportar no futuro, já que o IDE e a própria linguagem são pouco desenvolvidas. No final dos anos 2000, decidiu-se mudar para o C# mais promissor. A nova versão foi escrita em paralelo com a revisão da antiga, gradualmente mais e mais código foi escrito em .NET. O backend em C# foi inicialmente focado em uma arquitetura de serviço, mas durante o desenvolvimento foram utilizadas bibliotecas comuns com lógica e os serviços foram lançados em um único processo. O resultado foi um aplicativo que chamamos de “monólito de serviço”.

Uma das poucas vantagens desta combinação foi a capacidade dos serviços se ligarem através de uma API externa. Havia pré-requisitos claros para a transição para um serviço mais correto e, no futuro, para uma arquitetura de microsserviços.

Começamos nosso trabalho em decomposição por volta de 2015. Ainda não atingimos um estado ideal - ainda existem partes de um grande projeto que dificilmente podem ser chamadas de monólitos, mas também não se parecem com microsserviços. No entanto, o progresso é significativo.
Vou falar sobre isso no artigo.

Transição de monolito para microsserviços: história e prática

Conteúdo

Arquitetura e problemas da solução existente


Inicialmente, a arquitetura era assim: a UI é um aplicativo separado, a parte monolítica é escrita em Visual Basic 6, o aplicativo .NET é um conjunto de serviços relacionados que trabalham com um banco de dados bastante grande.

Desvantagens da solução anterior

Ponto unico de falha
Tivemos um único ponto de falha: o aplicativo .NET foi executado em um único processo. Se algum módulo falhasse, todo o aplicativo falhava e precisava ser reiniciado. Como automatizamos um grande número de processos para diferentes usuários, devido a uma falha em um deles, todos puderam ficar algum tempo sem trabalhar. E em caso de erro de software, mesmo o backup não ajudou.

Fila de melhorias
Esta desvantagem é bastante organizacional. Nosso aplicativo tem muitos clientes e todos querem melhorá-lo o mais rápido possível. Anteriormente, era impossível fazer isso em paralelo e todos os clientes faziam fila. Este processo foi negativo para as empresas porque tiveram que provar que a sua tarefa era valiosa. E a equipe de desenvolvimento passou algum tempo organizando essa fila. Isso exigia muito tempo e esforço e, em última análise, o produto não poderia mudar tão rapidamente quanto eles gostariam.

Uso subótimo de recursos
Ao hospedar serviços em um único processo, sempre copiamos completamente a configuração de servidor para servidor. Queríamos colocar os serviços mais carregados separadamente para não desperdiçar recursos e obter um controle mais flexível sobre nosso esquema de implantação.

Difícil de implementar tecnologias modernas
Um problema familiar a todos os desenvolvedores: há desejo de introduzir tecnologias modernas no projeto, mas não há oportunidade. Com uma grande solução monolítica, qualquer atualização da biblioteca atual, sem falar na transição para uma nova, torna-se uma tarefa nada trivial. Leva muito tempo para provar ao líder da equipe que isso trará mais bônus do que nervosismo desperdiçado.

Dificuldade em emitir alterações
Esse era o problema mais sério: lançávamos lançamentos a cada dois meses.
Cada lançamento se transformou em um verdadeiro desastre para o banco, apesar dos testes e esforços dos desenvolvedores. O negócio entendeu que no início da semana algumas de suas funcionalidades não funcionariam. E os desenvolvedores entenderam que uma semana de incidentes graves os aguardava.
Todos tinham o desejo de mudar a situação.

Expectativas de microsserviços


Emissão de componentes quando estiver pronto. Entrega dos componentes prontos, decompondo a solução e separando os diferentes processos.

Pequenas equipes de produto. Isto é importante porque era difícil gerenciar uma grande equipe trabalhando no antigo monólito. Essa equipe foi forçada a trabalhar de acordo com um processo rígido, mas queria mais criatividade e independência. Apenas equipes pequenas poderiam pagar por isso.

Isolamento de serviços em processos separados. Idealmente, eu queria isolá-lo em contêineres, mas um grande número de serviços escritos no .NET Framework são executados apenas no Windows. Serviços baseados em .NET Core estão aparecendo agora, mas ainda são poucos.

Flexibilidade de implantação. Gostaríamos de combinar serviços da maneira que precisamos, e não da maneira que o código obriga.

Utilização de novas tecnologias. Isso é interessante para qualquer programador.

Problemas de transição


É claro que se fosse fácil dividir um monólito em microsserviços, não haveria necessidade de falar sobre isso em conferências e escrever artigos. Existem muitas armadilhas nesse processo, vou descrever as principais que nos atrapalharam.

Primeiro problema típico da maioria dos monólitos: coerência da lógica de negócios. Quando escrevemos um monólito, queremos reutilizar nossas classes para não escrever código desnecessário. E ao migrar para microsserviços, isso se torna um problema: todo o código está fortemente acoplado e é difícil separar os serviços.

No momento do início dos trabalhos, o repositório contava com mais de 500 projetos e mais de 700 mil linhas de código. Esta é uma decisão bastante importante e segundo problema. Não foi possível simplesmente pegá-lo e dividi-lo em microsserviços.

Terceiro problema — falta de infra-estruturas necessárias. Na verdade, estávamos copiando manualmente o código-fonte para os servidores.

Como migrar do monólito para microsserviços


Provisionando microsserviços

Em primeiro lugar, determinamos imediatamente por nós mesmos que a separação de microsserviços é um processo iterativo. Sempre fomos obrigados a desenvolver problemas de negócios em paralelo. Como implementaremos isso tecnicamente já é um problema nosso. Portanto, nos preparamos para um processo iterativo. Não funcionará de outra maneira se você tiver um aplicativo grande e ele não estiver inicialmente pronto para ser reescrito.

Que métodos usamos para isolar microsserviços?

O primeiro método — mover módulos existentes como serviços. Nesse quesito tivemos sorte: já existiam serviços cadastrados que funcionavam no protocolo WCF. Eles foram separados em conjuntos separados. Nós os portamos separadamente, adicionando um pequeno inicializador a cada compilação. Ele foi escrito usando a maravilhosa biblioteca Topshelf, que permite executar o aplicativo tanto como serviço quanto como console. Isso é conveniente para depuração, pois nenhum projeto adicional é necessário na solução.

Os serviços foram conectados de acordo com a lógica de negócio, pois utilizavam assemblies comuns e funcionavam com um banco de dados comum. Dificilmente poderiam ser chamados de microsserviços em sua forma pura. Porém, poderíamos prestar esses serviços separadamente, em processos diferentes. Só isso já permitiu reduzir a influência mútua, reduzindo o problema do desenvolvimento paralelo e de um único ponto de falha.

Assembly com o host é apenas uma linha de código na classe Program. Escondemos o trabalho com o Topshelf em uma classe auxiliar.

namespace RBA.Services.Accounts.Host
{
   internal class Program
   {
      private static void Main(string[] args)
      {
        HostRunner<Accounts>.Run("RBA.Services.Accounts.Host");

       }
    }
}

A segunda maneira de alocar microsserviços é: criá-los para resolver novos problemas. Se ao mesmo tempo o monólito não crescer, isso já é excelente, o que significa que estamos caminhando na direção certa. Para resolver novos problemas, tentamos criar serviços separados. Se houvesse essa oportunidade, criamos mais serviços “canônicos” que gerenciam completamente seu próprio modelo de dados, um banco de dados separado.

Nós, como muitos, começamos com serviços de autenticação e autorização. Eles são perfeitos para isso. Eles são independentes, via de regra, possuem um modelo de dados separado. Eles próprios não interagem com o monólito, apenas recorrem a eles para resolver alguns problemas. Usando esses serviços, você pode iniciar a transição para uma nova arquitetura, depurar a infraestrutura deles, tentar algumas abordagens relacionadas a bibliotecas de rede, etc. Não temos equipes em nossa organização que não consigam criar um serviço de autenticação.

A terceira maneira de alocar microsserviçosAquele que usamos é um pouco específico para nós. Esta é a remoção da lógica de negócios da camada de UI. Nosso principal aplicativo de UI é desktop; ele, assim como o backend, é escrito em C#. Os desenvolvedores cometiam erros periodicamente e transferiam partes da lógica para a IU que deveriam existir no backend e serem reutilizadas.

Se você observar um exemplo real do código da parte da UI, poderá ver que a maior parte desta solução contém lógica de negócios real que é útil em outros processos, não apenas para construir o formulário da UI.

Transição de monolito para microsserviços: história e prática

A lógica real da UI está presente apenas nas últimas linhas. Transferimos para o servidor para que pudesse ser reutilizado, reduzindo assim a UI e alcançando a arquitetura correta.

A quarta e mais importante maneira de isolar microsserviços, que permite reduzir o monólito, é a retirada dos serviços existentes com processamento. Quando retiramos os módulos existentes como estão, o resultado nem sempre é do agrado dos desenvolvedores e o processo de negócios pode ter ficado desatualizado desde que a funcionalidade foi criada. Com a refatoração, podemos dar suporte a um novo processo de negócios porque os requisitos de negócios estão em constante mudança. Podemos melhorar o código-fonte, remover defeitos conhecidos e criar um modelo de dados melhor. Há muitos benefícios acumulados.

Separar serviços de processamento está inextricavelmente ligado ao conceito de contexto limitado. Este é um conceito do Domain Driven Design. Significa uma seção do modelo de domínio na qual todos os termos de uma única linguagem são definidos de forma única. Vejamos o contexto de seguros e contas como exemplo. Temos um aplicativo monolítico e precisamos trabalhar com uma conta de seguro. Esperamos que o desenvolvedor encontre uma classe Account existente em outro assembly, faça referência a ela na classe Insurance e teremos um código funcional. O princípio DRY será respeitado, a tarefa será realizada mais rapidamente usando o código existente.

Como resultado, verifica-se que os contextos de contas e seguros estão conectados. À medida que surgem novos requisitos, este acoplamento irá interferir no desenvolvimento, aumentando a complexidade da já complexa lógica de negócios. Para resolver esse problema, você precisa encontrar os limites entre os contextos no código e remover suas violações. Por exemplo, no contexto dos seguros, é bem possível que um número de conta de 20 dígitos no Banco Central e a data de abertura da conta sejam suficientes.

Para separar esses contextos limitados uns dos outros e iniciar o processo de separação de microsserviços de uma solução monolítica, usamos uma abordagem como a criação de APIs externas dentro do aplicativo. Se soubéssemos que algum módulo deveria se tornar um microsserviço, de alguma forma modificado dentro do processo, imediatamente faríamos chamadas para a lógica que pertence a outro contexto limitado por meio de chamadas externas. Por exemplo, via REST ou WCF.

Decidimos firmemente que não evitaríamos códigos que exigiriam transações distribuídas. No nosso caso, foi bastante fácil seguir esta regra. Ainda não encontramos situações em que transações distribuídas estritas sejam realmente necessárias - a consistência final entre os módulos é suficiente.

Vejamos um exemplo específico. Temos o conceito de orquestrador – um pipeline que processa a entidade da “aplicação”. Ele cria um cliente, uma conta e um cartão bancário. Se o cliente e a conta forem criados com sucesso, mas a criação do cartão falhar, o aplicativo não passa para o status “com sucesso” e permanece no status “cartão não criado”. No futuro, a atividade em segundo plano irá buscá-lo e finalizá-lo. O sistema está num estado de inconsistência há algum tempo, mas em geral estamos satisfeitos com isso.

Se surgir uma situação em que seja necessário salvar parte dos dados de forma consistente, provavelmente iremos consolidar o serviço para processá-lo em um processo.

Vejamos um exemplo de alocação de um microsserviço. Como você pode colocá-lo em produção com relativa segurança? Neste exemplo, temos uma parte separada do sistema - um módulo de serviço de folha de pagamento, cuja seção de código gostaríamos de transformar em microsserviço.

Transição de monolito para microsserviços: história e prática

Em primeiro lugar, criamos um microsserviço reescrevendo o código. Estamos melhorando alguns aspectos com os quais não estávamos satisfeitos. Implementamos novos requisitos de negócio do cliente. Adicionamos um API Gateway à conexão entre a UI e o backend, que fornecerá encaminhamento de chamadas.

Transição de monolito para microsserviços: história e prática

A seguir, colocamos esta configuração em operação, mas em estado piloto. A maioria dos nossos usuários ainda trabalha com processos de negócios antigos. Para novos usuários, estamos desenvolvendo uma nova versão do aplicativo monolítico que não contém mais esse processo. Essencialmente, temos uma combinação de um monólito e um microsserviço funcionando como piloto.

Transição de monolito para microsserviços: história e prática

Com um piloto bem-sucedido, entendemos que a nova configuração é realmente viável, podemos remover o antigo monólito da equação e deixar a nova configuração no lugar da solução antiga.

Transição de monolito para microsserviços: história e prática

No total, usamos quase todos os métodos existentes para dividir o código-fonte de um monólito. Todos eles nos permitem reduzir o tamanho de partes da aplicação e traduzi-las para novas bibliotecas, criando um código-fonte melhor.

Trabalhando com o banco de dados


O banco de dados pode ser pior dividido que o código-fonte, pois contém não apenas o esquema atual, mas também dados históricos acumulados.

Nosso banco de dados, como muitos outros, tinha outra desvantagem importante: seu enorme tamanho. Este banco de dados foi projetado de acordo com a intrincada lógica de negócios de um monólito e relacionamentos acumulados entre as tabelas de vários contextos limitados.

No nosso caso, além de todos os problemas (banco de dados grande, muitas conexões, às vezes limites pouco claros entre tabelas), surgiu um problema que ocorre em muitos projetos grandes: o uso do modelo de banco de dados compartilhado. Os dados foram retirados das tabelas por meio de visualização, por meio de replicação e enviados para outros sistemas onde essa replicação era necessária. Como resultado, não foi possível mover as tabelas para um esquema separado porque elas foram usadas ativamente.

A mesma divisão em contextos limitados no código nos ajuda na separação. Geralmente nos dá uma boa ideia de como dividimos os dados no nível do banco de dados. Entendemos quais tabelas pertencem a um contexto limitado e quais pertencem a outro.

Usamos dois métodos globais de particionamento de banco de dados: particionamento de tabelas existentes e particionamento com processamento.

Dividir tabelas existentes é um bom método a ser usado se a estrutura de dados for boa, atender aos requisitos de negócios e todos estiverem satisfeitos com isso. Neste caso, podemos separar as tabelas existentes em um esquema separado.

Um departamento com processamento é necessário quando o modelo de negócios mudou muito e as tabelas não nos satisfazem mais.

Dividindo tabelas existentes. Precisamos determinar o que iremos separar. Sem esse conhecimento nada funcionará, e aqui a separação dos contextos limitados no código nos ajudará. Via de regra, se você conseguir entender os limites dos contextos no código-fonte, fica claro quais tabelas devem ser incluídas na lista do departamento.

Vamos imaginar que temos uma solução na qual dois módulos monolíticos interagem com um banco de dados. Precisamos ter certeza de que apenas um módulo interage com a seção de tabelas separadas, e o outro começa a interagir com ele através da API. Para começar, basta que apenas a gravação seja feita através da API. Essa é uma condição necessária para falarmos em independência dos microsserviços. As conexões de leitura podem permanecer enquanto não houver grandes problemas.

Transição de monolito para microsserviços: história e prática

O próximo passo é separar a seção de código que funciona com tabelas separadas, com ou sem processamento, em um microsserviço separado e executá-lo em um processo separado, um contêiner. Este será um serviço separado com uma conexão ao banco de dados monolítico e às tabelas que não estão diretamente relacionadas a ele. O monólito ainda interage para leitura com a parte destacável.

Transição de monolito para microsserviços: história e prática

Posteriormente iremos remover esta conexão, ou seja, a leitura dos dados de uma aplicação monolítica de tabelas separadas também será transferida para a API.

Transição de monolito para microsserviços: história e prática

A seguir, selecionaremos do banco de dados geral as tabelas com as quais apenas o novo microsserviço funciona. Podemos mover as tabelas para um esquema separado ou até mesmo para um banco de dados físico separado. Ainda existe uma conexão de leitura entre o microsserviço e o banco de dados monolítico, mas não há com o que se preocupar, nesta configuração ela pode durar bastante tempo.

Transição de monolito para microsserviços: história e prática

A última etapa é remover completamente todas as conexões. Neste caso, poderemos precisar migrar dados do banco de dados principal. Às vezes queremos reutilizar alguns dados ou diretórios replicados de sistemas externos em vários bancos de dados. Isso acontece conosco periodicamente.

Transição de monolito para microsserviços: história e prática

Departamento de processamento. Este método é muito semelhante ao primeiro, apenas na ordem inversa. Alocamos imediatamente um novo banco de dados e um novo microsserviço que interage com o monólito por meio de uma API. Mas, ao mesmo tempo, resta um conjunto de tabelas de banco de dados que queremos excluir no futuro. Não precisamos mais dele; nós o substituímos no novo modelo.

Transição de monolito para microsserviços: história e prática

Para que este esquema funcione, provavelmente precisaremos de um período de transição.

Existem então duas abordagens possíveis.

Primeiro: duplicamos todos os dados nos bancos de dados novos e antigos. Neste caso, temos redundância de dados e podem surgir problemas de sincronização. Mas podemos atender dois clientes diferentes. Um funcionará com a nova versão, o outro com a antiga.

Segundo: dividimos os dados de acordo com alguns critérios de negócio. Por exemplo, tínhamos 5 produtos no sistema que estavam armazenados no banco de dados antigo. Colocamos o sexto na nova tarefa de negócios em um novo banco de dados. Mas precisaremos de um API Gateway que sincronize esses dados e mostre ao cliente onde e o que obter.

Ambas as abordagens funcionam, escolha dependendo da situação.

Depois de termos certeza de que tudo funciona, a parte do monólito que funciona com estruturas de banco de dados antigas pode ser desabilitada.

Transição de monolito para microsserviços: história e prática

A última etapa é remover as estruturas de dados antigas.

Transição de monolito para microsserviços: história e prática

Resumindo, podemos dizer que temos problemas com o banco de dados: é difícil trabalhar com ele comparado ao código-fonte, é mais difícil compartilhar, mas pode e deve ser feito. Encontramos algumas maneiras que nos permitem fazer isso com bastante segurança, mas ainda é mais fácil cometer erros com os dados do que com o código-fonte.

Trabalhando com código-fonte


Esta era a aparência do diagrama do código-fonte quando começamos a analisar o projeto monolítico.

Transição de monolito para microsserviços: história e prática

Pode ser dividido aproximadamente em três camadas. Esta é uma camada de módulos lançados, plugins, serviços e atividades individuais. Na verdade, estes eram pontos de entrada dentro de uma solução monolítica. Todos eles foram hermeticamente fechados com uma camada comum. Tinha lógica de negócios compartilhada pelos serviços e muitas conexões. Cada serviço e plugin utilizava até 10 ou mais assemblies comuns, dependendo do tamanho e da consciência dos desenvolvedores.

Tivemos a sorte de ter bibliotecas de infraestrutura que poderiam ser usadas separadamente.

Às vezes, surgia uma situação em que alguns objetos comuns não pertenciam realmente a esta camada, mas eram bibliotecas de infraestrutura. Isso foi resolvido renomeando.

A maior preocupação eram os contextos limitados. Aconteceu que 3-4 contextos foram misturados em uma montagem comum e usados ​​entre si nas mesmas funções de negócios. Era necessário entender onde isso poderia ser dividido e em quais limites, e o que fazer a seguir com o mapeamento dessa divisão em montagens de código-fonte.

Formulamos várias regras para o processo de divisão de código.

O primeiro: não queríamos mais compartilhar a lógica de negócios entre serviços, atividades e plugins. Queríamos tornar a lógica de negócios independente dos microsserviços. Os microsserviços, por outro lado, são idealmente considerados como serviços que existem de forma totalmente independente. Acredito que esta abordagem é um tanto dispendiosa e difícil de alcançar, porque, por exemplo, os serviços em C# serão, de qualquer forma, conectados por uma biblioteca padrão. Nosso sistema é escrito em C#, ainda não utilizamos outras tecnologias. Portanto, decidimos que poderíamos nos dar ao luxo de usar montagens técnicas comuns. O principal é que eles não contêm fragmentos de lógica de negócios. Se você tiver um wrapper conveniente sobre o ORM que está usando, copiá-lo de um serviço para outro será muito caro.

Nossa equipe é fã de design orientado a domínio, então a arquitetura cebola foi uma ótima opção para nós. A base dos nossos serviços não é a camada de acesso a dados, mas sim uma montagem com lógica de domínio, que contém apenas lógica de negócio e não possui conexões com a infraestrutura. Ao mesmo tempo, podemos modificar independentemente a montagem do domínio para resolver problemas relacionados aos frameworks.

Nesta fase, encontramos nosso primeiro problema sério. O serviço tinha que se referir a um assembly de domínio, queríamos tornar a lógica independente e o princípio DRY nos atrapalhou muito aqui. Os desenvolvedores queriam reutilizar classes de assemblies vizinhos para evitar duplicação e, como resultado, os domínios começaram a ser vinculados novamente. Analisamos os resultados e decidimos que talvez o problema também esteja na área do dispositivo de armazenamento do código-fonte. Tínhamos um grande repositório contendo todo o código-fonte. A solução para todo o projeto foi muito difícil de montar em uma máquina local. Portanto, pequenas soluções separadas foram criadas para partes do projeto, e ninguém proibiu adicionar algum assembly comum ou de domínio a elas e reutilizá-las. A única ferramenta que não nos permitiu fazer isso foi a revisão de código. Mas às vezes também falhou.

Então começamos a migrar para um modelo com repositórios separados. A lógica de negócios não flui mais de serviço para serviço; os domínios tornaram-se verdadeiramente independentes. Contextos limitados são suportados de forma mais clara. Como reutilizamos bibliotecas de infraestrutura? Nós os separamos em um repositório separado e depois os colocamos em pacotes Nuget, que colocamos no Artifactory. Com qualquer alteração, a montagem e publicação ocorrem automaticamente.

Transição de monolito para microsserviços: história e prática

Nossos serviços passaram a referenciar pacotes de infraestrutura interna da mesma forma que os externos. Baixamos bibliotecas externas do Nuget. Para trabalhar com o Artifactory, onde colocamos esses pacotes, utilizamos dois gerenciadores de pacotes. Em repositórios pequenos também usamos Nuget. Em repositórios com múltiplos serviços, utilizamos o Paket, que fornece mais consistência de versão entre módulos.

Transição de monolito para microsserviços: história e prática

Assim, trabalhando no código-fonte, alterando um pouco a arquitetura e separando os repositórios, tornamos nossos serviços mais independentes.

Problemas de infraestrutura


A maioria das desvantagens de migrar para microsserviços está relacionada à infraestrutura. Você precisará de implantação automatizada e de novas bibliotecas para executar a infraestrutura.

Instalação manual em ambientes

Inicialmente instalamos a solução para ambientes manualmente. Para automatizar esse processo, criamos um pipeline de CI/CD. Escolhemos o processo de entrega contínua porque a implantação contínua ainda não é aceitável para nós do ponto de vista dos processos de negócios. Portanto, o envio para operação é feito por meio de um botão, e para teste - automaticamente.

Transição de monolito para microsserviços: história e prática

Usamos Atlassian, Bitbucket para armazenamento de código-fonte e Bamboo para construção. Gostamos de escrever scripts de construção no Cake porque é igual ao C#. Pacotes prontos chegam ao Artifactory e o Ansible chega automaticamente aos servidores de teste, após os quais podem ser testados imediatamente.

Transição de monolito para microsserviços: história e prática

Registro separado


Ao mesmo tempo, uma das ideias do monólito era fornecer registro compartilhado. Também precisávamos entender o que fazer com os logs individuais que estão nos discos. Nossos logs são gravados em arquivos de texto. Decidimos usar uma pilha ELK padrão. Não escrevemos para o ELK diretamente por meio dos provedores, mas decidimos finalizar os logs de texto e escrever o ID do rastreamento neles como um identificador, adicionando o nome do serviço, para que esses logs pudessem ser analisados ​​posteriormente.

Transição de monolito para microsserviços: história e prática

Usando o Filebeat, temos a oportunidade de coletar nossos logs dos servidores e, em seguida, transformá-los, usar o Kibana para criar consultas na UI e ver como foi a chamada entre os serviços. O Trace ID ajuda muito nisso.

Teste e depuração de serviços relacionados


Inicialmente, não entendemos totalmente como depurar os serviços que estão sendo desenvolvidos. Tudo era simples com o monólito; nós o rodamos em uma máquina local. No início, eles tentaram fazer o mesmo com os microsserviços, mas às vezes, para lançar totalmente um microsserviço, é necessário lançar vários outros, e isso é inconveniente. Percebemos que precisamos mudar para um modelo onde deixamos na máquina local apenas o serviço ou serviços que queremos depurar. Os serviços restantes são usados ​​em servidores que correspondem à configuração do prod. Após a depuração, durante o teste, para cada tarefa, apenas os serviços alterados são emitidos para o servidor de teste. Assim, a solução é testada na forma em que futuramente aparecerá em produção.

Existem servidores que executam apenas versões de produção de serviços. Esses servidores são necessários em caso de incidentes, para verificar a entrega antes da implantação e para treinamento interno.

Adicionamos um processo de teste automatizado usando a popular biblioteca Specflow. Os testes são executados automaticamente usando NUnit imediatamente após a implantação do Ansible. Se a cobertura de tarefas for totalmente automática, não haverá necessidade de testes manuais. Embora às vezes ainda sejam necessários testes manuais adicionais. Usamos tags no Jira para determinar quais testes executar para um problema específico.

Além disso, a necessidade de testes de carga aumentou; anteriormente eram realizados apenas em casos raros. Usamos JMeter para executar testes, InfluxDB para armazená-los e Grafana para construir gráficos de processos.

O que conseguimos?


Em primeiro lugar, nos livramos do conceito de “liberação”. Já se foram os monstruosos lançamentos de dois meses em que esse colosso era implantado em um ambiente de produção, interrompendo temporariamente os processos de negócios. Agora implantamos serviços em média a cada 1,5 dia, agrupando-os porque entram em operação após aprovação.

Não há falhas fatais em nosso sistema. Se lançarmos um microsserviço com um bug, a funcionalidade associada a ele será interrompida e todas as outras funcionalidades não serão afetadas. Isso melhora muito a experiência do usuário.

Podemos controlar o padrão de implantação. Você pode selecionar grupos de serviços separadamente do restante da solução, se necessário.

Além disso, reduzimos significativamente o problema com uma grande fila de melhorias. Agora temos equipes de produtos separadas que trabalham com alguns serviços de forma independente. O processo Scrum já se encaixa bem aqui. Uma equipe específica pode ter um Product Owner separado que atribui tarefas a ela.

Resumo

  • Os microsserviços são adequados para decompor sistemas complexos. No processo, começamos a compreender o que está no nosso sistema, que contextos limitados existem, onde estão os seus limites. Isso permite distribuir corretamente as melhorias entre os módulos e evitar confusão de código.
  • Microsserviços fornecem benefícios organizacionais. Freqüentemente, eles são mencionados apenas como arquitetura, mas qualquer arquitetura é necessária para resolver as necessidades de negócios, e não por si só. Portanto, podemos dizer que os microsserviços são adequados para resolver problemas em equipes pequenas, visto que o Scrum é muito popular atualmente.
  • A separação é um processo iterativo. Você não pode pegar um aplicativo e simplesmente dividi-lo em microsserviços. É improvável que o produto resultante seja funcional. Ao dedicar microsserviços, é benéfico reescrever o legado existente, ou seja, transformá-lo em um código que nos agrada e que atenda melhor às necessidades do negócio em termos de funcionalidade e velocidade.

    Uma pequena advertência: Os custos de mudança para microsserviços são bastante significativos. Demorou muito para resolver sozinho o problema de infraestrutura. Portanto, se você tem um aplicativo pequeno que não requer escalonamento específico, a menos que tenha um grande número de clientes competindo pela atenção e pelo tempo de sua equipe, então os microsserviços podem não ser o que você precisa hoje. É muito caro. Se você iniciar o processo com microsserviços, os custos serão inicialmente maiores do que se você iniciar o mesmo projeto com o desenvolvimento de um monólito.

    PS Uma história mais emocionante (e como se fosse para você pessoalmente) - de acordo com link.
    Aqui está a versão completa do relatório.

Fonte: habr.com

Adicionar um comentário