Utilizando CDC e consistência eventual para criar Views Materializadas e dar mais independência a seus microserviços

Christian Dorner
6 min readMar 15, 2021

--

Utilizando Change Data Capture (CDC) para desacoplar serviços altamente requisitados.

Para leitura desde artigo estou assumindo que você já tenho conhecimento básico sobre Kafka, incluindo ordenação entre partições.

Por que?

Com o crescimento rápido da Intelipost em 2020 começamos a sentir a necessidade de migrar nosso sistema até então monolítico para o padrão de microserviços, nosso desafio então era separar os módulos mas descobrimos que todos eles estão fortemente amarrados a esse conceito/entidade "pedido", é nossa entidade de negócio principal, aquela na qual todos os módulos rodeiam sempre buscando informações e realizando atualizações. Essa informação está armazenada em um banco de dados relacional então começamos a vê-lo crescer tanto em tamanho (storage e usage) quando em complexidade, em termos de infraestrutura já estávamos com uma instância grande de escrita e “duas” de leitura do mesmo tamanho, tabelas com dezenas de campos e índices pois as mesmas tabelas são utilizadas em diversos contextos diferentes com necessidades diferentes, estava claro que estava na hora de mudar.

Acima esta uma breve demonstração, temos nosso monolito conectado a instancias de escrita e leitura, microserviços conectados somente na leitura e também microserviços conectados em ambos, isso quer dizer que todos fazem alteração no mesmo banco de dados e alguma indisponibilidade em nosso banco principal ocasionava um problema em cascata em diversos serviços.

Como?

Nosso desafio era conseguir prover a informação para todos esses módulos para que pudessem evoluir independentemente, não era possível simplesmente incluir mensageria dentro do monolito pois diversas informações eram alteradas via procedure complexas dentro do banco de dados, e mudanças nas tabelas também poderiam surgiam de outras formas, seja por algum outro sistema conectado ou até mesmo algum ajuste manual direto no banco de dados.

Cheguei a conclusão de que utilizar “Change Data Capture” ou CDC seria a melhor opção considerando todo nosso cenário.

CDC vai dar a possibilidade de se conectar diretamente no banco de dados seja ele relacional ou até mesmo NoSQL como MongoDB ou Cassandra e consumir diretamente do banco todas as modificações realizadas nas tabelas, por exemplo, todas as vezes que uma tupla é inserida, alterada ou deletada você irá poder consumir um evento com as essas informações.

Utilizamos o Debezium que é um Kafka Connector para tal façanha, após configurado ele começou a enviar todas as mudanças para tópicos do kafka, é enviado um payload (json ou avro) contendo informações do antes e depois do commit, algo como o exemplo abaixo.
{
"before" : {"name": "John"},
"after" : {"name" : "John Doe"}
}

Em sua configuração default o debezium vai ler ordenadamente todos os DMLs realizados no banco de dados e enviar cada tabela para um tópico distinto e a informação vai estar disponível para todos os consumers desse tópico.

O problema (ou pelo menos o mais complexo deles)

Nosso principal problema foi com a ordenação dos eventos nos tópicos, sabemos que não existe ordenação entre os tópicos (somente dentro do mesmo tópico/partição utilizando a mesma chave) conversando com a equipe vi que trabalhar com consistência eventual desde o Kafka seria um pouco dolorido, isso porquê não tínhamos somente uma tabela pedidos, eram no mínimo quatro tabelas que juntas continham informações importantes para o contexto total do pedido, tínhamos além dele também volumes, faturas e o histórico de status dos volumes.
Se deixássemos a replicação padrão tomar seu curso no momento do consumo era provável que iriamos consumir eventos de insert de volumes ou faturas antes dos eventos de insert de pedidos. Apesar de ser possível criar mecanismos para trabalhar dessa forma precisaríamos de tratamentos de retry ou cache em diversos locais e uma complexidade alta para a maioria dos módulos, eu não queria que todos tivessem que pagar esse preço.

Ficou claro para mim, precisávamos que os eventos fossem ordenados e garantir a consistência da "timeline" do pedido para quem fosse consumi-los, i.e recebe-se primeiro sua criação, depois dos volumes, faturas e históricos e assim por diante.

A solução

Para garantir a ordenação primeiro precisávamos enviar todos os eventos para o mesmo tópico, o debezium possui bons mecanismos de roteamento para isso, configuramos para que todas as informações das tabelas fossem para o mesmo tópico.

Agora precisávamos garantir que todos os eventos do mesmo pedido independentemente da tabela fossem enviados para a mesma partição. A mensagem para o Kafka consiste na chave/valor, sendo a chave normalmente composta pelas chaves primarias das tabelas, para nós a chave enviada para o kafka precisava ser o id do pedido o que era um problema pois não tínhamos acesso a essa informação nas tabelas de fatura ou histórico.

- Primeira tentativa

Nossa primeira tentativa foi criar dois tipos de tópicos no primeiro iriam informações de pedidos e volumes (topic.facts.pedidos) pois elas tem o id do pedido e conseguimos criar a chave do kafka direto no debezium, e em um segundo tópico (topic.historico, topic.fatura) iriamos enviar as outras informações de histórico e fatura, nesse segundo tipo iriamos ter um consumidor que faria uma consulta ao banco de dados para obter o id do pedido de acordo com o id da fatura ou histórico, a solução até funciona porém não tivemos o throughput necessário considerando todos nosso volume de operações de escrita, teríamos que ter uma quantidade muito grande de partições e processamento para conseguir o numero necessário, simplesmente não era viável.

- A segunda tentativa

Tivemos então a seguinte idéia, não depender mais das tabelas originais de histórico e fatura mas criar tabelas que seriam utilizadas somente para CDC e iriamos colocar nelas o id do pedido, criamos triggers que a cada inserção de histórico ou fatura busca o id do pedido e insere nessa tabela para CDC como todo o processo ocorre dentro do banco de dados a operação de busca do id do pedido é extremamente rápida, funcionou perfeitamente, agora podemos a partir do debezium redirecionar todos os eventos para o mesmo tópico e com a mesma chave do id do pedido deixando os eventos ordenados dentro da partição.

Views Materializadas

Agora com todas as informações dentro do Kafka e ordenada, nossos times estão criando consumidores que obtém esses eventos e inserem essas informações em banco de dados específicos criando "views" das informações de acordo com a necessidade de cada um, alguns estão utilizando NoSQL, outros banco de dados relacionais porém mantém as informações por um curto período de tempo e somente algumas colunas que realmente importam para eles. Normalmente as alterações de nesses bancos tem como origem somente da alteração vinda do CDC que eventualmente é aplicada nessas tabelas locais que cada microserviço utiliza.

Como puderam ver não estamos utilizando views materializadas no conceito literal (e.g com o comando create materialized view … ) mas sim simbólico considerando que as informações vão sendo atualizadas por outro caminho.

Vantagens

  1. Utilizando CDC conseguimos capturar novos registros e também atualizações independentemente de onde estão sendo realizadas (via serviço, procedure, ou até um DML forçado no banco de dados)
  2. Tendo toda informação no kafka e utilizando esse padrão de "views" agora temos mais independência para os microserviços, não iremos mais precisar de 3 instâncias grandes de banco de dados relacional pois cada serviço terá um pequeno de acordo com sua necessidade.
  3. Não existe mais um "single point of failure", sem a dependencia a um unico banco de dados qualquer interrupção nesse serviço não mais indisponibiliza todos os outros, eles somente irão trabalhar com dados mais desatualizados até que o serviço seja reestabelecido e as informações voltem a ser replicadas.
  4. Podemos plugar processos de BI que faziam buscam grandes nas tabelas, com a informação no kafka podemos fazer streaming da informação em realtime para BI.
  5. Podemos escalar nossas aplicações e serviços de banco de dados e consumidores de acordo com as necessidades individuais dos módulos, se um serviço precisar de mais processamento por parte do banco de dados não precisamos mais escalar um banco inteiro só por causa dele.
  6. A interdependência dos times diminui pois cada um está criando seus serviços de forma isolada.

--

--