O que era? Eram microservices
Buzzwords frequentemente são criados para conceitos que evoluíram, e que precisam de uma palavra para identificá-los e facilitar o diálogo. Os microservices são um dos termos da vez. Definem um conjunto de técnicas que já é usado no desenvolvimento de software há algum tempo. A descrição de microservices encontrada em artigos e conferências está associada a técnicas e práticas que descobri e evoluí lentamente através da minha própria experiência nos últimos anos. Enquanto as discussões sobre microservices, tanto pessoais como no âmbito da indústria de software, reconhecem o trabalho de empresas como Netflix, Google, Amazon e de profissionais que conseguiram aplicar essa tecnologia com sucesso, minha experiência pode oferecer algumas outras dicas que visam à implementação de sucesso utilizando microservices.
Os três padrões e principais motivadores para o desenvolvimento e uso de qualquer arquitetura são:
- Aumento em agilidade: maior habilidade de responder as necessidades do negócio rapidamente para permitir o crescimento do negócio;
- Melhoria da experiência dos clientes: aperfeiçoar a experiência de forma a diminuir a perda de clientes;
- Redução de custos: reduzir custos ao adicionar mais produtos, clientes ou soluções de negócio.
Na verdade, todos tentamos alcançar essas metas no nosso dia-a-dia profissional. O SOA (Service Oriented Architecture), por exemplo, define um framework de software alinhado aos negócios que permite às organizações alcançarem essas metas. Muitas das grandes empresas que comercializam software afirmam que seus produtos permitem que as organizações tenham uma arquitetura orientada a serviços.
Mas quando não se tem as pessoas certas nem a cultura adequada, e não são feitos os investimentos indicados, usar SOA não vai resultar em valor agregado para o negócio. A arquitetura de microservices, fundamentalmente, não é diferente do SOA. As metas e objetivos são os mesmos, mas a abordagem é refinada. Pode-se, até mesmo, simplificar dizendo que Microservices são uma espécie de SOA escalável. Os microservices permitem que sistemas migrem de uma implementação monolítica para outra que seja distribuída e escalável e atenda a diversas aplicações. Microservices são independentes e adotam a agilidade e melhoria contínua como meios para provocar a transformação digital de uma organização. O sucesso dos microservices depende da independência e flexibilidade dos serviços que disponibilizam.
É possível definir microservices como uma abordagem para entregar SOA através da construção de serviços com alta granularidade que dão suporte às capacidades necessárias ao funcionamento do negócio, distribuídas e organizadas em domínios funcionais. Não há padrões mágicos ou que sirvam como balas de prata. Padrões devem ser ajustados e adequados quando aplicados em uma organização. Por isso, as organizações devem manter o foco na solução dos problemas envolvidos na implantação de uma arquitetura que resulte em uma plataforma adaptativa.
Algumas organizações falharam na implementação da SOA por que não analisaram o Modelo de Capacidades do Negócio (Business Capability Model) e acreditaram que desenvolver webservices ou simplesmente comprar uma plataforma de SOA seria suficiente para que tivessem uma arquitetura orientada a serviços e que permitisse mostrar o alinhamento entre a arquitetura e as metas/objetivos do negócio.
Exemplo
Um exemplo disso, é uma empresa que desejava melhorar sua agilidade e a experiência dos clientes ao mesmo tempo em que cortava custos. Optou-se pelo desenvolvimento de uma plataforma SOA padrão capaz de atender múltiplos clientes. A ideia era desenvolver um conjunto de serviços com alta granularidade de forma que fosse possível fazer alterações para conseguir implantar mudanças pequenas e gerenciáveis no sistema com frequência. Hoje, essa abordagem é equivalente à uma arquitetura baseada em microservices.
Os serviços foram modelados com base no Modelo de Capacidades do Negócio e a primeira versão funcionou bem. Baseados em XML com sincronização via JMS, o foco primário dos serviços foi a disponibilização das capacidades necessárias para uma plataforma de requisições. Foram desenvolvidos agentes acessíveis pela web e através de uma aplicação em um canal de voz. Isso permitiu entregar com frequência pequenas mudanças. Além disso o suporte para funcionalidades do tipo A/B era transparente.
A medida que requisitos foram adicionados de forma incremental, tornou-se muito difícil publicar a solução rapidamente, devido a complexidade da integração entre as aplicações e consumidores dos serviços. Os testes funcionais, a integração e a publicação de uma nova versão tinham que ser cuidadosamente coordenados. Com a expansão do negócio, as mudanças se tornaram 10 vezes mais frequentes em relação à frequência de mudanças inicial. Uma vez que a maioria das tarefas relacionadas à entrega de uma nova versão eram manuais, o tempo para publicação de uma versão deixou de atender as expectativas do negócio. Em pouco tempo, metas deixaram de ser atingidas, uma vez que problemas na criação dos microservices e no gerenciamento do ciclo de vida da solução causaram o caos no processo de entregas.
Lições aprendidas - Não faça isso, faça aquelas outras coisas...
A experiência relatada anteriormente serviu como motivação para compartilhar o que aprendi de forma que outros possam manter os olhos abertos em relação à boas práticas quando se trabalhar com microservices.
1) Caos na coesão
Foi desenvolvido um serviço para obter informações do cliente, com objetivo de buscar as políticas adotadas para o usuário, informações pessoais e o plano contratado. Após um tempo, esse serviço passou a ter mais atribuições do que apenas obter as informações do usuário. Surgiram novos requisitos, foram necessárias várias mudanças e feitas muitas entregas. O serviço deixou de ser escalável, passou a não atender os critérios de disponibilidade especificados e, assim, se tornou uma tradicional "Big ball of mud". Como isso aconteceu? Para começar, não havia governança que reforçasse a separação de atribuições funcionais. Se um cliente influente solicitasse que fosse adicionada uma lógica não associada ao serviço, afim de reduzir o número de requisições, a nova função era adicionada sem questionamentos. Talvez o uso do Padrão de Projeto Gateway ou de uma camada BPM pudesse ter evitado esse cenário, mas… não havia tempo para isso. O importante era implementar novas funcionalidades.
Para evitar esse tipo de cenário é necessário monitorar as funcionalidades que não são relevantes para o serviço. Os serviços devem estar claramente alinhados com a capacidade do negócio e não devem executar ações fora dos limites desta capacidade. O controle da separação de domínios funcionais é vital para arquitetura e, caso não seja feito adequadamente, vai destruir a agilidade, desempenho e escalabilidade, resultando em uma arquitetura altamente acoplada que tem como consequência o caos que mina a coesão e a desorganização do processo de entregas.
2) Não levar automação a sério
Não havia uma estratégia para automação da publicação de novas versões ou para o monitoramento dos serviços em operação (métricas de QoS do ambiente de execução). Isso resultou no aumento de custos operacionais e em erros devidos a execução de publicações manuais. Muitas vezes o ambiente de produção parou devido a erros de configuração. Os serviços, com frequência, eram publicados em modo HA (alta disponibilidade) de forma que o número de containers era três vezes maior que o número total de serviços. A equipe de operações não conseguia tratar a configuração de cada serviço individualmente. Algum tempo depois, a equipe de operações passou a reclamar da ineficiência da arquitetura, pois era impossível lidar com o número crescente de containers.
Qual a solução para esse problema? A solução é composta de algumas ações. Em primeiro lugar, a entrega contínua, caso ainda não exista, é um investimento obrigatório e causa uma mudança cultural que deveria ser um objetivo de toda empresa que desenvolve software. Caso não seja possível testar e entregar automaticamente, não use microservices. O uso destes visa incentivar a agilidade e aumentar a velocidade de resposta à mudanças. Em segundo lugar, é importante lembrar que o controle de qualidade exige que cada serviço tenha testes unitários, testes funcionais, testes de segurança e testes de desempenho. Estes testes devem ser automatizados. Por fim, a virtualização de serviços é outro conceito poderoso quando se desenvolve serviços integrados à serviços de terceiros sobre os quais não se tem controle.
3) Arquitetura de serviços em camadas
Um erro comum ao utilizar SOA é a não compreensão de como alcançar a reusabilidade de serviços. No que diz respeito a reusabilidade, as equipes normalmente focam na coesão técnica ao invés da coesão funcional. Por exemplo, muitos serviços funcionam como uma camada de acesso de dados (ORM), pois expõem tabelas através serviços que se acreditam ser altamente reusáveis. Isso cria uma camada física artificial, gerenciada por uma equipe horizontal, o que causa dependências entre entregas de serviços diferentes. Qualquer serviço criado deve ser altamente autônomo - isso quer dizer independente de todos os outros.
Criar múltiplas camadas físicas de serviços resulta em complexidade na entrega e ineficiência em tempo de execução. No fim das contas, são necessários serviços de encapsulamento, serviços de orquestração, serviços de negócios e serviços de dados modelados de forma a atender demandas técnicas. Equipes foram formadas para gerenciar as diferentes camadas e, uma vez que não havia responsáveis específicos para cada funcionalidades, a lógica de negócios acabou espalhada nas camadas. Com isso, houve perda de eficiência e começou a busca constante por "culpados" pelos problemas eventualmente encontrados.
A separação lógica em camadas dentro de um serviço é algo bom, mas não deveriam existir chamadas que esteja fora do âmbito do processo associado ao serviço. É necessário encarar um serviço como uma entidade de negócio atômica, que deve implementar o necessário para alcançar a funcionalidade desejada para o negócio. Serviços "autocontidos" são mais autônomos e escaláveis do que serviços em camadas. É normal reescrever código que é comum a múltiplos serviços e esse é um bom trade-off quando, além de permitir o reuso, mantém o nível de autonomia entre os serviços. A regra é: serviços não devem ser separados em função de preocupações técnicas. Serviços devem ser separados com base em necessidades do negócio. O conceito de "uso de containers" está se difundindo amplamente por permitir esse tipo de separação.
4) Confiando no Sign-off dos consumidores
Em outro caso, um serviço era consumido por múltiplas aplicações em diferentes canais incluindo um agente, a internet e voz. O agente correspondia ao canal principal e os serviços precisavam esperar o sign-off de todos os agentes consumidores antes de entrar em produção. Isso atrasava a entrega das aplicações de produção referentes aos canais de internet e voz. O que causou este acoplamento forte entre estes três canais?
O acoplamento forte era consequência das funcionalidades específicas dos canais. Serviços devem ser independentes. Cada serviço criado deve possuir seu próprio conjunto de testes que, por sua vez, devem cobrir funcionalidades, segurança e tratamento de erros e viabilizar testes de aceitação para consumidores, presentes e futuros, do serviço. Estes testes devem ser incluídos como parte da pipeline de construção junto com outros testes automatizados de regressão.
5) Gerenciamento manual de configurações:
A medida que o número de serviços aumentou (juntamente com o a dispersão das regras de negócio nas várias camadas devido a falta de governança no ciclo de vida dos serviços), perdeu-se o controle do gerenciamento de configurações de cada serviço. As entregas e implantações em produção deixaram de ser suaves devido a falhas de configuração como senhas ruins, URLs erradas e valores incorretos. Foi ficando cada vez mais difícil gerenciar esses itens manualmente. Teria sido mais fácil se houvesse algum tipo de ferramenta de gerenciamento de configuração como parte do nosso PaaS ou do processo de Entrega Contínua.
(Clique na imagem para ampliá-la)
6) Evitar o versionamento
A princípio, pensamos que precisaríamos de apenas uma versão dos serviços. Mas logo começamos a adicionar versões visando acomodar diferentes consumidores e mudanças frequentes. Eventualmente, cada entrega tinha de ser uma versão principal, pois os serviços dependiam do cancelamento de assinatura por parte dos consumidores. Assim, o número de containers cresceu rapidamente e, com isso, tornou-se complicado realizar seu gerenciamento. A falta de governança do ambiente de execução foi outro aspecto que contribuiu para esse problema. Serviços devem ser arquitetados assumindo que a mudança é inevitável. É necessária uma estratégia para gerenciar mudanças que permita retrocompatibilidade a medida que o serviço evoluí, facilitando o processo de atualização do serviço pelos consumidores. Sem esta estratégia, o alto acoplamento entre os serviços e seus consumidores vão resultar em falhas sempre que houver necessidade de mudanças.
A complexidade cresce a medida que o número de serviços cresce, o que é esperado no universo dos microservices. Por isso, tenha sempre uma estratégia de versionamento que permita aos consumidores dos serviços um processo de migração tranquilo e garanta que seja possível entregar alterações de forma transparente sem afetar ninguém. Também é importante limitar o número de versões principais em paralelo no ambiente de produção e estabelecer uma governança para o processo de versionamento.
(Clique na imagem para ampliá-la)
7) Construindo um gateway em cada serviço
Não tínhamos uma API seguindo o padrão de gateway e não tínhamos governança do ambiente de execução (não sabíamos quem estava consumindo o que, quando e com que velocidade). Assim, implementamos autenticação de usuários finais, controle de fluxo, orquestração, roteamento, etc, em cada serviço. Isso aumentou a complexidade, gerou inconsistências na implementação e, por fim, não sabíamos quem implementava o quê em que pontos do código. Para completar, os serviços foram construídos para satisfazer requisitos não funcionais de consumidores específicos, tornando difícil atender os requisitos de todos. Se tivéssemos um gateway poderíamos filtrar dados e usar padrões de enriquecimento que permitiram atender a todas as demandas.
Invista em soluções de gerenciamento de APIs de forma a centralizar, gerenciar e monitorar as necessidades não funcionais e eliminar a necessidade de que consumidores gerenciem a configuração de vários serviços ao mesmo tempo. Uma API de Gateway pode ser usada para orquestrar microservices multifuncionais que podem reduzir o número de requisições para aplicações web.
Conclusões
O objetivo dos microservices é solucionar pelo menos três problemas comuns: melhorar a experiência dos usuários, alcançar agilidade na implementação de novos requisitos e diminuir os custos ao entregar regras de negócio por meio de serviços de baixa granularidade. Essa abordagem não é uma bala de prata; exige uma disciplina na construção de uma plataforma que permita a entrega de serviços com qualidade e agilidade. É necessário aprender com os erros dos outros e evitar antipadrões na arquitetura e no processo de entrega. Esse é o primeiro pequeno passo antes que se possa discutir o "uso de containers", adoção da nuvem etc. Espero que este artigo tenha levado os leitores a uma reflexão e resulte na busca por eliminar tais antipadrões de suas arquiteturas. A maioria dos itens mencionados irá resultar em mudanças culturais dentro da organização, que não podem ser realizadas por apenas uma pessoa. Será necessária a parceria com executivos e líderes.
Sobre o autor
Vijay Alagarasan trabalha como Arquiteto Chefe no departamento de Arquitetura e Estratégia Empresarial da Asurion, uma empresa líder global em suporte e proteção tecnológica. Seu foco está em Serviços, APIs e Integrações (A2A, B2B e B2C) Corporativas. É responsável por definir e gerencia princípios, padrões, escolhas tecnológicas e padrões transversais a todos esses domínios. Também disponibiliza implementações de referência através do desenvolvimento de soluções completas ou de protótipos, afim de permitir que as equipes absorvam novos conceitos e tecnologias mais rapidamente. Também avalia e seleciona novos produtos tecnológicos para atingir metas estratégicas organizacionais de forma mais rápida o que proporciona diferencial competitivo. Começou como desenvolvedor de aplicações java/j2ee, posteriormente trabalhou como desenvolvedor EAI e logo se tornou um desenvolvedor/arquiteto SOA. Possui experiência com produtos TIBCO, Java, Web Logic e JMS, dentre outras tecnologias conforme pode ser visto em seu perfil no LinkedIn. Após atuar em diversas áreas como desenvolvedor, líder de equipes, gerente de entregas, arquiteto de integração, arquiteto de soluções e arquiteto de domínio, recentemente atua com Web APIs (ferramentas de gerenciamento de APIs com o Layer 7, Apigee, AWS API Gateway, Azure API Management), tecnologias de nuvem (AWS, Azure), Containers, Virtualização de Serviços e Bancos de Dados baseados em grafos.