A dor e o sofrimento ao depurar microsserviços no desenvolvimento web

Em TI, você raramente vê uma pessoa que nunca ouviu falar de microsserviços. Existem muitos artigos na Internet e em sites especializados sobre esse assunto que geralmente explicam as diferenças entre o monólito e, de fato, os microsserviços. Um desenvolvedor Java inexperiente, depois de ler artigos da categoria "O que são microsserviços para aplicativos da Web e o que eles comem", está cheio de alegria e confiança de que agora tudo será maravilhoso. Afinal, o principal objetivo é "atravessar" o monólito monstruoso (o artefato final, que, em regra, é um arquivo de guerra / ouvido), que executa um monte de tudo, em vários serviços vivos separados, cada um deles executando uma função estritamente definida relacionada apenas a ele, e fará bem. Além disso, vem a escalabilidade horizontal - basta fazer o dimensionamentonós correspondentes, e tudo será ótimo. Mais usuários chegaram ou são necessárias mais capacidades - apenas adicionamos de 5 a 10 novas instâncias de serviço. Grosso modo, em geral, é assim que funciona, mas, como você sabe, o diabo está nos detalhes, e o que inicialmente parecia bastante simples, após uma análise mais aprofundada, pode se transformar em problemas que ninguém levou em consideração inicialmente.

Neste post, colegas da prática Java da Rexoft compartilham suas experiências sobre como depurar microsserviços para a web.



Como obter integridade de dados transacionais


Ao tentar transferir a arquitetura de um monólito para microsserviços, as equipes que não tinham essa experiência costumavam começar a dividir serviços em objetos de nível superior do modelo de domínio, por exemplo: Usuário / Cliente / Empregado etc. No futuro, com um estudo mais detalhado, o entendimento aparece, o que é mais conveniente para quebrar blocos maiores que agregam vários objetos do domínio do domínio dentro de si. Devido a isso, você pode evitar chamadas desnecessárias para serviços de terceiros.

O segundo ponto importante é o suporte à integridade de dados transacionais. No monólito, esse problema é resolvido através do Application Server, onde a guerra / ouvido está girando, dentro da qual o contêiner, de fato, descreve os limites das transações. No caso de microsserviços, os limites das transações são borrados e, além de escrever o código da lógica de negócios, é necessário ser capaz de gerenciar a integridade dos dados, manter a consistência entre diferentes partes do sistema. Esta é uma tarefa bastante não trivial. Recomendações para resolver esse tipo de problema de arquitetura podem ser encontradas na Internet e nas comunidades técnicas relevantes.

Neste artigo, tentaremos descrever dificuldades técnicas específicas que surgem quando as equipes tentam trabalhar com microsserviços e maneiras de solucioná-las. Observo imediatamente que as opções propostas não são as únicas verdadeiras. Talvez haja serviços mais elegantes, mas as recomendações que darei são testadas na prática e resolvem com precisão as dificuldades existentes, e a possibilidade de usá-las ou não é uma questão pessoal para todos.

O principal problema com microservices é que eles são extremamente fáceis de executar localmente (por exemplo, usando spring.io e IntelliJ IDEA , isso pode ser feito em apenas 5 minutos, ou até menos). No entanto, ao tentar fazer o mesmo no KubernetesNo cluster (se você tinha pouca experiência com ele antes), um simples lançamento do controlador que imprime “Hello World” ao acessar um endpoint específico pode levar meio dia. No caso de um monólito, a situação é mais simples. Cada desenvolvedor possui um servidor de aplicativos local. O processo de implantação também é bastante simples - você precisa copiar o artefato final war / ear para o lugar certo no Application Server manualmente ou usando o IDE . Geralmente isso não é um problema.

Depurando nuances


O segundo ponto importante é a depuração . Em situações com um monólito, presume-se que o desenvolvedor tenha um servidor de aplicativos em sua máquina, no qual seu war / ear esteja implantado. Você sempre pode depurar, porque tudo que você precisa está disponível. Com os microsserviços, tudo é um pouco mais complicado, um serviço geralmente é uma coisa em si. Como regra, ele tem seu próprio esquema de banco de dados, no qual seus dados estão localizados, executa funções específicas específicas a ele, toda a comunicação com outros serviços é organizada por meio de chamadas HTTP síncronas (por exemplo, via RestTemplate ou Feign), assíncronas (por exemplo, Kafka ou RabbitMQ). Portanto, a tarefa essencialmente simples de salvar ou validar um determinado objeto que foi implementado anteriormente em um só lugar, dentro de um único arquivo war / ear, no caso geral com uma abordagem de microsserviço, se torna representável na forma: vá para um ou N serviços adjacentes, sejam operações de aquisição de dados , por exemplo, alguns valores de referência ou a operação de salvar entidades adjacentes,cujos dados são necessários para executar a lógica de negócios em nosso serviço. Escrever lógica de negócios nesse caso se torna muito mais difícil.

Por conseguinte, as opções de solução são as seguintes :

  1. Escreva seu código lógico de negócios. Ao mesmo tempo, todas as chamadas externas são zombadas - contratos externos são emulados, testes são escritos sob a premissa de que os contratos externos são exatamente assim, depois que há uma implantação no circuito para verificação. Às vezes é uma sorte e a integração funciona imediatamente, às vezes é uma infelicidade - você precisa refazer o código da lógica de negócios uma enésima vez, porque durante o tempo em que implementamos a funcionalidade, o código no serviço adjacente foi atualizado, as assinaturas da API foram alteradas e precisamos refazê-lo parte da tarefa está do seu lado.
  2. . , , Kubernetes, . . , — , remote debug . , runtime , , . -, , 2–5 , . . , Kubernetes , . -, (Per thread), , .

Kubernetes


Uma solução para esse problema, de fato, é a telepresença . Provavelmente existem outros programas desse tipo, mas a experiência pessoal estava apenas com ele, e ele se estabeleceu positivamente. Em geral, o princípio de operação é o seguinte:

Na máquina local, o desenvolvedor instala a telepresença e configura o kubectl para acessar o cluster Kubernetes correspondente (adiciona a configuração do loop a ~ / .kube / config ). Depois disso, a telepresença é iniciada , que de fato atua como um proxy entre o computador desenvolvedor local e o Kubernetes. Existem diferentes opções de inicialização, é melhor consultar mais detalhadamente o guia oficial, mas, no caso mais básico, ele se resume a duas etapas:

  1. Sudo telepresence (, Linux- , sudo . , root/). Kubernetes deployment telepresence . deployment Kubernetes.
  2. Iniciar sua instância de serviço é como de costume no computador local do desenvolvedor. No entanto, nesse caso, ele terá acesso a toda a infraestrutura do cluster Kubernetes, seja Service Discovery (Eureka, Consul), Api Gateway (Zuul), Kafka e suas filas, se houver, e assim por diante. Na verdade, temos acesso a todo o ambiente de cluster de que precisamos, mas localmente. O bônus é a possibilidade de depuração local, mas em um ambiente de cluster, e já será muito mais rápido, porque, de fato, estamos dentro do Kubernetes (através do túnel) e não o acessamos de fora via porta para depuração remota.

Esta solução tem várias desvantagens:

  1. Telepresence Linux Mac, Windows VFS, , issue GitHub. . , - Linux/Mac, .
  2. , Service Discovery (Eureka, Consul)Round Robin , endpoint , , , :

  • kubernetes -> . telepresence deployment , «» Eureka ip-address:port/service-name dns-name:port/service-name , . . Kubernetes , timeout;
  • deployment - Kubernetes , ( ) (Round Robin), ;
  • endpoint, feature, HTTP 404 endpoint Gateway, Service Discovery , Round Robin . Service Discovery endpoint , HTTP 404.
  • , , .


Com o roteamento dinâmico de uma solicitação, queremos dizer que o API Gateway (Zuul) tem a capacidade de escolher entre várias instâncias do mesmo serviço que precisamos. No caso geral, esse problema pode ser resolvido adicionando um predicado que permite selecionar o serviço desejado no pool de serviços comum com o mesmo nome no estágio de processamento da solicitação. Naturalmente, cada serviço entre aqueles com os quais queremos rotear dinamicamente precisará ter algum tipo de meta-informação contendo dados que serão usados ​​para determinar se esse serviço é necessário ou não. O Spring Cloud (no caso do Eureka), por exemplo, permite fazer isso especificando em um bloco de metadados especial no application.yml :

eureka:
  instance:
    preferIpAddress: true
    metadata-map:
      service.label: develop

Após registrar esse serviço no Service Discovery em com.netflix.appinfo.InstanceInfo # getMetadata, haverá um rótulo com a chave service.label e o valor develop , que pode ser obtido em tempo de execução. Um ponto importante no início de um serviço é verificar se existe uma instância de serviço no Service Discovery com essas meta-informações ou não, a fim de evitar possíveis colisões.

Opções de roteamento


Depois disso, a solução do problema pode ser reduzida para duas opções:

  1. API Gateway . , , , , Headers: DestionationService: feature/PRJ-001. , , Header . , — - API Gateway.
  2. API Gateway, , . ., , , Zuul 1 endpoint- /api/users/… user, feature/PRJ-001, Zuul 2 endpoint- /api/users/… user, feature/PRJ-002. , N API Gateway N , . . , . . feature — , , , , , , . API Gateway, , . ., , , — , .






Como parte da API do Gateway, também vale a pena fornecer um mecanismo que permita alterar as regras de roteamento em tempo de execução. É melhor colocar essas configurações no mapa de configuração . Nesse caso, será suficiente reescrever as novas rotas e reiniciar a API do Gateway no Kubernetes para atualizar o roteamento ou usar o Spring Boot Actuator (desde que haja uma dependência correspondente na API do Gateway) - chame o endpoint / refresh, que basicamente relê dados do config-map e atualizará as rotas.

Um ponto importante é também que não deve haver, relativamente falando, uma instância de referência de serviço (por exemplo, identificado como desenvolver, que será coletado na ramificação principal do desenvolvimento do serviço) e em uma API de gateway principal separada, sempre especificada nas configurações em que ele acessará esse serviço. Em essência, estamos nos oferecendo um ambiente de preparação independente que sempre estará operacional no contexto do roteamento dinâmico.

Um exemplo de um bloco de mapa de configuração para a API do Gateway que contém configurações para roteamento (aqui é apenas um exemplo de como pode parecer, para uma operação adequada, é necessária uma ligação correspondente na forma de código no lado de back-end do serviço de Gateway da API) :

{
  "kind": "ConfigMap",
  "apiVersion": "v1",
  "metadata": {
    ...
  },  
"data": {
    ...        
    "rules.meta.user": "develop",
    "rules.meta.client": "develop",
    "rules.meta.notification": "feature/PRJ-010",
    ...    
  }
}

rules.meta é um mapa que contém regras de roteamento para serviços.
usuário / cliente / notificação - o nome do serviço em que está registrado no Eureka.

develop / feature / PRJ-010 - etiqueta de serviço de application.yml do serviço correspondente, com base no qual o serviço desejado será selecionado entre todos os serviços disponíveis com o mesmo nome no Service Discovery , se houver mais de uma instância desse serviço.

Conclusão


Como tudo neste mundo, as ferramentas e soluções em TI não são perfeitas. Não pense que, se você alterar a arquitetura, todos os problemas terminarão de uma só vez. Somente uma imersão detalhada nas tecnologias usadas e sua própria experiência fornecerão uma imagem real do que está acontecendo.

Espero que este material ajude você a resolver seu problema. Tarefas interessantes e venda sem bugs!

All Articles