Pontos Principais
- Entendimento sobre eventos de domínio
- Cuidado com a exposição de atributos internos de um dado domínio com relação ao mundo exterior
- Falta de tratamentos que aumentem a resiliência de consumo
- Utilização de cabeçalhos para manter atributos de controle dos eventos
- A importância de se conhecer e considerar idempotência
Nesse texto buscamos esclarecer aspectos subestimados em uma arquitetura orientada a eventos e que podem causar muitos problemas em um ambiente. Não abordaremos assuntos como transações distribuídas e missing events. Mas caso ainda não compreenda como isso pode afetar sua arquitetura orientada a eventos, aconselhamos um pouco mais de estudo antes de se aventurar nesse tipo de projeto.
Existem muitos motivos para se utilizar uma arquitetura orientada a eventos. O foco desse artigo é levantar erros que são cometidos quando se tenta alcançar desacoplamento entre produtores e consumidores e consequentemente independência entre times.
Tipos de Eventos (Domínio, State Transfer e Notification)
O termo "evento" é utilizado para muitas situações e às vezes causa confusão nas pessoas. Por isso antes de seguir com o texto, precisamos esclarecer que estamos conversando neste artigo sobre eventos como acontecimentos passados, que refletem alguma transição de estado em algum domínio de negócio.
Dentro desse contexto, temos basicamente 2 tipos de eventos:
- Eventos como notificação
- Eventos como propagação de transferência de mudança de estado
Evento como notificação
Quando desenhamos um evento como notificação, temos o mínimo possível de informação dele para identificar unicamente o acontecimento. É uma técnica que pode ser arriscada, pois com o passar do tempo talvez apenas a identificação do evento não seja suficiente e podem ocorrer efeitos colaterais não esperados.
Em um exemplo de domínio de aluguel de livros, poderíamos ter o seguinte evento:
Event Type: "Livro_Alugado_Com_Sucesso"
{
"livroId": 23487
"dataAluguel": <timestamp do momento do aluguel>
"links": [
"href": "http://endereco-servico/locação/livro/23487
]
}
Obviamente o formato do payload do evento é apenas uma exemplificação, mas particularmente para eventos de notificação consideramos uma boa prática apontar o endereço para o recurso onde é possível obter mais informações. Isso pode ser feito por meio do header do evento ou no corpo seguindo alguma padronização.
Nesse cenário, os atores que "ouvem" o evento de "Livro alugado com sucesso" vão saber do momento em que ele aconteceu, mas se quiserem mais informações, terão que fazer um request ao endpoint descrito para obter mais dados.
Se esse comportamento for repetido por diversos serviços podemos ter uma carga não desejada no serviço de aluguel de livros. O pior é que muitos desses acessos podem acontecer de forma concorrente, pois os serviços irão receber o evento ao mesmo tempo (teoricamente).
Além disso, nesse modelo de evento como notificação, talvez não sejam alcançados alguns objetivos de independência e disponibilidade dos serviços. Note que se necessitarmos fazer uma requisição ao serviço original para terminar o processamento, estamos atrelando a nossa possibilidade de concluir a operação a disponibilidade do serviço original.
Eventos como propagação de transferência de mudança de estado
Uma alternativa a utilização de evento como notificação, seria "exibir" a mudança de estado que ocorreu no domínio original. Seguindo nosso exemplo acima, isso evitaria requisições ao sistema de destino e promoveria uma independência real entre os sistemas para processamento.
Event Type: "Livro_Alugado_Com_Sucesso"
{
"livroId": 23487,
"titulo": Titulo do livro,
"clienteId": 755,
"condicao": "Estado de conservação excelente",
"dataPrevistaDevolucao": <data prevista para devolução>,
"dataAluguel": <timestamp do momento do aluguel>,
}
Esse seria um exemplo possível de evento. Note que não estamos apenas comunicando o que aconteceu (aluguel do livro).
Estamos expondo algumas informações sobre a "entidade" afetada pelo evento. Mais adiante vamos entrar na discussão sobre vazamento de domínio, mas se você está considerando seriamente utilizar uma abordagem orientada a eventos, espero que já tenha percebido que nenhum sistema que consome uma informação desse tipo, pode repassá-la a outro sistema como se fosse a fonte original.
Para transmissão de estado através de eventos temos 2 abordagens:
- Delta de mudança
- Último Estado
O Delta de mudança é uma abordagem onde o evento contém apenas informações que foram afetadas pela operação que originou o evento. Usando o exemplo acima, não teríamos por exemplo os campos de título e condição no payload, pois eles não foram alterados durante a operação de aluguel.
A outra abordagem para transferência de estado é a ideia de que o evento pode representar uma "foto" do último estado no momento do acontecimento. Essa é a abordagem mais comum, porém muita gente confunde a ideia de propagar o último estado com colocar todos os campos que você tem na base de dados no evento.
1 - O desafio de colocar apenas o suficiente no evento
Esse é um tema bem delicado, pois decidir o que é "razoável" para estar em um evento pode ser simples em alguns casos, mas é muito complexo em outros. A questão nesse ponto é entender que, se a decisão é por distribuir dezenas de campos estaremos fazendo todo o ecossistema pagar um preço alto para distribuir informações que possivelmente não serão utilizadas por ninguém. Então devemos sempre buscar fazer uma análise razoável do que faz sentido expor ao mundo, do ponto de vista de negócio.
Em um olhar para os dados, sempre haverá a tentação de expor tudo, mas em um olhar para o negócio, é possível descobrir um conjunto de informações que fazem sentido para comunicar ao mundo os seus acontecimentos.
Nesse ponto, ter microservices em granularidade adequada pode ajudar muito. Olhando apenas pelo ponto de vista de observação no dia a dia, percebemos que equipes que trabalham com grandes monolitos que propagam eventos tem a tendência de querer inflar seus eventos e esta é uma abordagem que sugerimos não adotar.
2 - Leaky Abstractions em eventos
Junto com o desafio de colocar apenas o suficiente no evento vem geralmente o erro comum de vazar dados que não importam para o mundo externo e só existem e têm significado pela maneira como o serviço foi implementado internamente.
Para tentar não cair nesse erro, se guiar pelo que é importante para o negócio é novamente o melhor caminho.
3 - Eventos baseados em mudanças de "tabelas", e não em acontecimentos do negócio
Talvez esse seja o erro mais comum de todos. É natural durante o desenvolvimento criarmos atalhos na nossa cabeça correlacionando algumas "tabelas" com uma operação de negócio, mas na hora de pensar sobre eventos temos que deixar um pouco as entidades de lado para não cair nessa armadilha.
Esse tipo de comportamento também é muito comum quando as pessoas começam a modelar operações CRUD como eventos. Ao fazer isso, normalmente você também está vazando a abstração de seu domínio.
Quando estivermos trabalhando com sistemas legados, sem possibilidade de alteração, utilizar alguma técnica de CDC que publique os eventos com base em tabelas já existentes pode ser uma solução. Mas se quiser construir eventos com significado para o negócio, evite essa abordagem.
4 - Eventos em excesso
Ser razoável pode parecer simples, mas não é. Frequentemente você irá perceber que o seu conceito de razoabilidade é diferente das outras pessoas.
É nesse momento que a discussão sobre granularidade de eventos pode ficar complicada. Uma operação que gera 3 eventos diferentes, durante seu ciclo de processamento, pode ser um exagero para você e perfeitamente normal para outras pessoas.
Você já se deparou com propostas do tipo "lançar um evento para cada atributo modificado"? É um pouco disso que estou falando.
Eventos em excesso podem dificultar a vida de todo seu ecossistema. Você pode se encontrar em uma situação onde qualquer novo serviço que precise mapear uma operação específica precise ouvir 5 ou 10 eventos para saber o que aconteceu naquela operação.
Isso é sinal de uma modelagem que pode estar errada, que normalmente não se atentou para a granularidade correta de acordo com o propósito de negócio. Isso é muito ruim pois você aumentará o lead time de muitas equipes.
É importante deixar claro que em algumas situações ouvir diversos eventos para entender uma situação será realmente necessário. Mas esse é um indício que você deveria olhar com calma a situação.
5 - Eventos Genéricos
Fazer eventos que carregam múltiplos significados podem parecer uma boa ideia em algum momento, mas a verdade é que geralmente não é.
Criar eventos genéricos que precisam ser interpretados pode trazer grandes dores de cabeça no futuro. Colocar "tipos" ou "status" no corpo de alguns eventos pode ser um sinal disso. Geralmente essas representações são feitas pelo próprio evento e não deveriam ficar a cargo de interpretação dos consumidores.
Lembre-se que um evento representa um acontecimento específico e fazer um evento genérico pode causar dor de cabeça desnecessária aos seus consumidores que precisam de apenas um tipo específico do evento. Casos práticos disso são:
- Overhead de cpu na desserialização de eventos para inferir tipos ou status desejados (em cenários de alta vazão isso pode ser um problema)
- Interpretação/mudança na inclusão de novos "status" ou campos que afetem o consumidor.
- Necessidade do consumidor de entender regras internas de outro domínio para saber como classificar uma operação(vazamento de domínio)
6 - Não inferir ordem dos eventos baseado nos dados
A dica para não cometer esse erro é nunca partir do pressuposto que os eventos irão chegar na ordem certa. Eventualmente os times que consomem eventos podem perceber que existe algum ID sequencial ou Timestamp no evento e podem ficar tentados a usá-los como garantidor de ordem.
Você consumidor não pode garantir o tratamento que o produtor está fazendo em casos de paralelismo de processamento, crash ou retentativa. Por isso, não tente inferir ordem pois isso pode causar grandes danos. Eventos podem chegar fora de ordem e você pode ter o evento de ID 2100 que foi gerado antes do evento de ID 1200, por estratégia de particionamento de chaves do produtor. Por isso, não se arrisque fazendo inferência em dados que você não tem o controle.
7 - Evite simulação de request/response
Às vezes uma interação entre dois serviços pode parecer que é um evento, quando na verdade só está fazendo um request/response assíncrono.
Quem comete esse erro geralmente cria eventos que só fazem sentido para um único consumidor e espera alguma resposta desse consumidor após o processamento do evento.
Se você está cometendo esse erro, talvez você esteja mascarando comandos como eventos.
Nesse momento eventualmente surgem discussões sobre gerar múltiplos eventos. Lembre-se que se você começar a lançar eventos específicos para satisfazer as necessidades de consumidores particulares, você terá problemas de evolução no futuro. Mudanças no sistema produtor levarão mais tempo para serem adequadas aos diversos eventos, além da possível explosão de mensagens customizadas no futuro.
Lembre-se que um dos objetivos desse estilo de arquitetura é promover desacoplamento de produtores e consumidores.
8 - Falta de tratamento/estratégias de Retry e Dead letters
Fazemos sistemas que consomem filas a muuuito tempo, no entanto parece que ainda não aprendemos que precisamos de estratégias de retry e dead letters. Na verdade esse problema não tem a ver só com o consumo de filas e tópicos, pois estratégias de retry devem ser abordadas para chamadas de APIs, bancos de dados e outros recursos de redes. O tema de processamento e reprocessamento com tópicos de maneira confiável tem muito material pela internet e se você está iniciando em arquiteturas orientadas a eventos e não entende muito bem esse ponto, aconselho que pare tudo e estude um pouco a respeito.
Deixar de pensar nesses aspectos vai prejudicar enormemente a resiliência da sua aplicação. Rotinas que "engasgam" quando o sistema não consegue tratar algum evento no tópico ainda são bastante comuns.
Outra situação comum diz respeito a estratégias de retentativas descuidadas que colocam a aplicação em loop infinito ou esgotam recursos da máquina.
Seus eventos são como água fluindo pelo encanamento. Não coloque uma rolha para entupir todo o cano.
9 - Falta de uso de cabeçalhos
A forma como você irá implementar cabeçalhos em eventos pode variar muito de acordo com a tecnologia que você está utilizando. Nem todas as ferramentas te darão todos os benefícios.
Por exemplo, se você não tem o suporte ideal do seu middleware de eventos, você pode criar um payload que tenha uma estrutura que suporte um cabeçalho, mas isso trará uma desvantagem, já que para tomar decisão baseado em algum campo do cabeçalho você precisará desserializar a mensagem.
Alguns campos que podem ajudar se estiverem no cabeçalho:
- Id de Correlação: Ajuda para tratamento de mensagens duplicadas nos consumidores, além de ajudar no tracing distribuído;
- Versão do evento: Versão do schema do evento. Caso a versão não seja conhecida pelo consumidor ele pode automaticamente colocar o evento em uma dead letter ou aplicar alguma lógica customizada para tradução;
- Tipo do Evento: O indicador de qual é o evento;
- Timestamp de quando o evento ocorreu;
- Marcação de depreciação, a depender da estratégia adotada de versionamento e descontinuidade.
Muitos outros campos podem fazer sentido para o seu cenário, como content-type por exemplo. Use o cabeçalho a seu favor para evitar "abrir" a mensagem sem necessidade.
Aconselho dar uma olhada na especificação do Cloud Events. Mesmo que você não implemente a especificação, é um ponto de partida para entender porque alguns campos podem ser relevantes.
10 - Não pensar em aspectos relacionados à idempotência
Idempotência em computação, é um característica que a operação necessita ter para que ela possa ser aplicada repetidas vezes sem causar nenhum efeito colateral.
Nesse ponto quando aplicamos o conceito a orientação a eventos estamos nos referindo a idempotência do ponto de vista de quem olha de fora de um sistema.
O que aconteceria com o sistema se o mesmo evento for duplicado, por uma falha no produtor?
Ao receber uma mensagem duplicada, o sistema precisa ter a capacidade de detectar a duplicidade e não deixar que essa mensagem deixe a base inconsistente. Em um cenário real podemos ilustrar com o exemplo de um pagamento utilizando o cartão de crédito em maquininhas dos estabelecimentos que frequentamos. Supondo um cenário onde fazemos uma transação com sucesso na API do banco, mas antes de receber a resposta a maquininha desliga por falta de energia.
Ao tentar fazer a mesma operação quando a maquininha for ligada novamente, o banco não vai processar 2 operações. Neste cenário, os sistemas do banco entendem que a operação está sendo repetida, e aceita apenas uma delas. Este é um exemplo do conceito de idempotência.
Ao se deparar com mensagens duplicadas, um sistema precisa estar preparado para reconhecer a situação e trabalhar de forma a não ficar inconsistente.
O erro mais comum nesse ponto é acreditar que os sistemas produtores de eventos nunca irão enviar mensagens duplicadas. Essa é uma premissa ingênua e que precisa ser mitigada em momento de planejamento da arquitetura.
11 - Falta de robustez na previsão de mudança de esquemas de eventos
Apesar de nos esforçarmos ao máximo para criar eventos significativos para o negócio e com dados que façam sentido, eventualmente iremos errar na construção dos eventos. E mesmo quando tudo der certo, mudanças vão surgir pelo caminho.
Por isso é importante pensar uma forma robusta de tratar os eventos antes mesmo de começar a desenvolver. Como boas práticas, a seguinte abordagem poderá ser utilizada:
- Você irá trabalhar com versionamento de schema, evolução de schema ou sem schema?
- Vai publicar múltiplas versões ou sempre a última?
- Terá "tradutores" de versão?
- Fará tópicos diferentes por versão de evento?
Essas são apenas as primeiras questões que necessitam ser respondidas com relação a mudança de versão de eventos.
Algumas ferramentas como o Schema Registry da Confluent podem ajudar nesse sentido. Mas para saber a ferramenta certa, é necessário primeiramente pensar na estratégia e objetivo que se deseja alcançar.
Conclusão
O objetivo deste texto foi mostrar um pouco dos problemas mais comuns no dia a dia das implementações de arquiteturas orientadas a eventos em empresas de diferentes tamanhos e com equipes de diferentes background.
Além de percepções, buscamos passar nossa experiência neste tema para estar atento durante uma implementação. Muitas técnicas e ferramentas novas que contribuem nesse espaço vem surgindo recentemente. É um campo de arquitetura que vem ganhando cada vez mais adeptos com o crescimento da adoção de microservices e sistemas reativos.
Se existe um desejo em se aventurar nesse tipo de arquitetura, é importante destacar que os pontos discutidos neste artigo são apenas o começo da jornada.
Sobre os Autores
|
João Bosco Seixas (LinkedIn, Twitter) MBA em gestão empresarial pela FGV e formado em computação. Atua promovendo transformação de equipes e empresas na área de tecnologia e desenvolvimento de software. Apaixonado por tecnologia, inovação e futuro. Tecnologista, arquiteto de soluções digitais, estrategista de software. Atualmente apoia times de arquitetura e engenharia na criação de soluções digitais no Banco Itaú. |
|
Marcelo Costa (LinkedIn) é pós-graduado em Engenharia de Software pela UNICAMP. Atua em sistemas de alta complexidade desde 2002, liderando equipes multidisciplinares no desenvolvimento de soluções de software nas áreas de varejo, aeroespacial, logística, educação, saúde e finanças. Especializa-se em liderança de equipes e arquiteturas de soluções, na coleta inteligente de informações na Internet e de conteúdo eletronicamente disponível; atualmente é consultor em Arquitetura de Soluções. |