Event-streaming (Apache Kafka + Kafka Streams) em uma Arquitetura de Microsserviços

Event-streaming (Apache Kafka + Kafka Streams) em uma Arquitetura de Microsserviços

Ao adotarmos event-driven architectures (arquiteturas orientadas a eventos), adquirimos vantagens como o desacoplamento dos componentes e centralização do event-flow (fluxo de eventos) em um event-store (persistência de eventos). Determinado fluxo pode ser gerenciado pelo message broker, executando através de um backbone (infraestrutura principal responsável pela execução do processo). Desta forma, é possível separar eventos, juntamente com suas consequências, de sua fonte de origem (source). Aplicando EDA (event-driven architecture), enfrentamos naturalmente uma mudança de paradigma, tornando-se necessário repensarmos a maneira como construímos nossos serviços. Obrigatoriamente, em um contexto EDA, a comunicação deve ocorrer de forma assíncrona, devendo reagir a eventos com fontes heterogêneas compondo um pipeline de execução, normalmente conhecido como event-stream. A composição de um fluxo event-stream pode alterar o estado de nossa aplicação, sofrendo transformações a cada stage (etapa) do pipeline conforme o avanço de sua execução. Streaming de dados representam um fluxo constante de eventos na linha do tempo, sem início nem fim definidos (unbounded), podendo ser processados em real-time (tempo real). Cada evento é processado nas circunstâncias do cenário o qual ele ocorre, contendo informações relevantes de execução (payload do evento), sendo gerenciado de acordo com os limites do contexto onde está inserido. Eventos podem desencadear comportamentos específicos de uma aplicação, refletindo a maneira de como os processos de negócio (business process) funciona. Quando combinados com microsserviços, o padrão event-streaming abre uma série de possibilidades, apresentando benefícios relacionados ao estilo arquitetural ("Tell, don’t ask"), implementado pelo EDA (Event-driven Architecture). Contrapondo a percepção sobre paradigmas presentes em estilos de arquitetura tradicionais ("Ask, don’t tell") mais evidentes em serviços que utilizam protocolos como SOAP/REST/HTTP, possuindo sua comunicação síncrona sob demanda.

Comparando EDA com SOA/ESB

Arquiteturas clássicas utilizando service-oriented architecture (SOA) e enterprise service bus (ESB), ancestrais dos microsserviços, funcionam bem em cenários de execução distribuídos entre múltiplos servidores, ao mesmo tempo em que arquiteturas inter-process communications (IPC) e shared-memory style (memória compartilhada) continuam encaixando-se perfeitamente em contextos onde necessitamos apenas de um único servidor. Em contrapartida, podendo apresentar performance pobre além de causar forte acoplamento entre os serviços. Serviços entregues sob esta natureza de deploy, atrelados a um backbone central e rígido, impossibilitam os deployments (implantações) mais granulares e flexíveis. SOA (service-oriented architecture) mostrou-se na prática pouco efetivo apresentando perda de agilidade na manutenção do software. Desta forma, ao evoluirmos para uma arquitetura de microsserviços obtemos diversos benefícios, principalmente quando nossos componentes são implementados e orquestrados dentro de containers (containerized applications), podendo na prática utilizarmos docker. Adotando uma arquitetura e processo de deploy mais flexível, conseguimos entregar pipelines mais automatizados, escaláveis e mais inteligentes. A partir deste estilo de arquitetura, a comunicação inter serviços se torna mais versátil e isolamentos com maior resiliência são alcançados com facilidade. Ao atingirmos na prática determinado patamar arquitetural de maturidade no desenvolvimento de microsserviços assíncronos, temos a opção de aplicar conceitos EDA (event-driven architecture) a estes componentes, promovendo uma comunicação com baixo acoplamento, isolando-os em cadeia autônoma. EDA (Event-driven Architecture) descreve conceitos teóricos em direção a idealização de um ecossistema onde a comunicação seria guiada por eventos sem abordar a parte prática de funcionamento ou detalhes de implemementação. Event-driven architecture beneficia-se dos princípios encontrados em arquiteturas encontrados em sistemas distribuídos, como a de microsserviços. Componentes auto-contidos, sem dependências externas (upstream services), com maior agilidade. Na prática, arquiteturas orientadas a eventos visam explorar a aplicabilidade de padrões como publish-subscribe pattern na composição dos fluxos de streams, funcionando de forma semelhante se comparado a plataformas de troca de mensagens.

EDA e o Apache Kafka como Message Broker

Conforme avançamos no desenvolvimento de soluções envolvendo conceitos como EDA (event-driven architecture) e arquitetura de microsserviços, percebemos a necessidade de padrões como event-streaming, dado sua natureza responsiva relacionada à infraestrutura e arquitetura. Apache Kafka se propõe a atuar na camada de implementação dos conceitos EDA (event-driven architecture), posicionando-se como principal orquestrador na garantia de uma execução apropriada envolvendo conceitos arquiteturais de natureza desacoplada. A maior vantagem apresentada em plataformas de stream publish-subscribe, trata-se da natureza desacoplada de comunicação, a qual elimina a necessidade de rastreamento de mensagens por parte do publisher ("Fire-and-forget"). Além disso, possibilita acrescentarmos novos subscribers ao fluxo sem risco de comprometer componentes existentes. Estratégias de messaging (mensageria), storage (armazenamento de estado para processos stateful) e stream processing (processamento dos fluxos de eventos) podem ser combinadas com microsserviços rodando em containers, sustentados pelo EDA (event-driven architecture) em conjunto com Apache Kafka, ou Kafka Streams API, proporcionando uma base sólida para que uma aplicação event-streaming (orientada pelos fluxos de eventos) seja entregue em um ambiente distribuído de forma consistente e segura. Arquiteturas event-streaming promovem o poder de notificações através de eventos, sendo utilizadas em diversas áreas como machine learning com análise de riscos, modelos de detecção de atividades suspeitas, processamentos em batch (lote), etc. Streaming proporcianam análises mais robustas, beneficiando áreas de inteligência de negócio com processos stateful (mantendo estado). Processos stateful proporcionam possibilidade de armazenamento e agregação de dados de diversas fontes em uma determinada janela de execução, podendo ser utilizados em processos unbounded (sem início nem fim definidos). Sejam implementaçãoes simples ou complexas, envolvendo estratégias event-streaming, plugar novas features (funcionlidades) ou desplugar componentes problemáticos, evidentemente são efetuados com maior facilidade se comparados a reconfiguração de grandes monólitos.

Componentes Internos do Kafka

Visando suportar determinados requisitos precisamos de uma tecnologia de mensagens durável, escalável, tolerante a falhas, confiável e altamente performática como Apache Kafka. Kafka poderia ser definido como uma plataforma para processamento de streams de eventos em tempo real. Diferente de Message Brokers tradicionais (RabbitMQ, ActiveMQ, etc), Kafka não foi desenhado para implementar JMS, AMQP, etc. Ele foi desenhado para escalar através de um protocolo de mensagem binário sob TCP, podendo controlar centenas de operações de leitura e escrita por segundo de muitos producers e consumers em paralelo. Kafka utiliza um protocolo binário próprio, não implementando ou utilizando protocolos como AMQP (advanced message queuing protocol), STOMP (streaming text oriented messaging protocol), MQTT (message queuing telemetry transport), implementados pelo RabbitMQ. AMQP trata-se de um poderoso protocolo de comunicação com possibilidade de roteamentos avançados, mostrando como a mensagem deve ser transmitida, processada e consumida. Kafka armazena streams de records em disco (file system), replicando para todos os brokers, promovendo fault-tolerance. Os dados são persistidos (duráveis) por um determinado período de tempo (de acordo com seu tamanho em bytes) e após isso descartados por um processo de higienização do Kafka (destinado a este tipo de tarefa). Kafka essencialmente provê um durable message store (armazenamento durável), similar ao um log, o qual armazena streams de records em categorias chamadas topics (tópicos), rodando em formato de cluster em um ou mais broker servers (node), em teoria podendo ser distribuídos em múltiplos data centers. Kafka topics (tópicos Kakfa) possuem partições as quais são endpoints para obtenção e armazenagem de records. Partições possuem segmentos onde cada record consiste de uma key (chave), value (payload persistido como byte array) e timestamp (data e hora). Kafka utiliza offsets nas partições para demarcar o índice de consumo de dados de um tópico, controlado pelo consumer. Cada consumer deve pertencer a um consumer group (grupo de consumo), conceito utilizado pelo Kafka visando paralelizar o processamento entre uma coleção de consumers dentro de uma mesma partição.

Partições no Kafka são utilizadas para paralelizar o processamento de eventos balanceando a carga entre os brokers do cluster (escalabilidade horizontal). Cada broker pode ter 0 ou N partições por topic (tópico). Quando criamos um topic, informamos o número de partições que gostaríamos de usar, podendo ser somente acrescido posteriormente sendo o oposto não permitido. Estruturalmente, cada partição apresenta uma sequência ordenada, segmentada e imutável de records (Kafka change log), persistidos por um período de tempo relacionado a política de retenção. Nesse caso, sendo cada partição replicada entre os brokers, promovendo a tolerância a falhas, podendo se ter 2000 partições por broker, chegando a 200.000 por cluster. Cada record armazenado em uma partição possui seu offset definido quando o evento é persistido no broker. Desta forma, os consumers apontam para a última mensagem lida, escaneiam sequencialmente, lendo as mensagens em ordem enquanto salvam as últimas posições lidas no log com o auxílio do Zookeeper (armazena os metadados do cluster). Ocorre então a persistência dos metadados com informações sobre o processo e o estado atual do cluster. Em versões novas do Kafka, brokers mantém informações sobre offset em um hidden topic (__consumer_offsets). No Kafka, brokers são nodes (nós) de um cluster, responsáveis por permitir ao Kafka receber mensagens de um producer, entregando-as ao consumer. Eventos são armazenados nas partições de um topic (fisicamente no disco), compondo o estado do cluster gerenciado pelo Zookeeper. Conceitualmente, dentro do contexto do Kafka, termos como Kafka server, Kafka broker e Kafka node todos se referem ao mesmo significado, sendo sinônimos. O papel principal de um broker resume-se a traduzir mensagens formais, de um remetente para um recipiente, assegurando a transmissão do ponto A para o ponto B por meio de roteamento.

Partições nos tópicos são divididas em segmentos os quais são nomeados pela posição do último offset, possuindo um tamanho padrão de 1GB. Cada segmento é composto de um arquivo de log para armazenar a mensagem atual além de um arquivo de índice o qual armazena a posição da uma mensagem no log. Segmentos são configurados pelo tamanho em bytes e pelo tempo em que o Kafka aguardará até criar um novo log segment file, podendo variar com base em configuração do cluster. Partições são diretórios onde cada segmento consiste de um index file, time index file e log file, de forma que somente um segmento permanece ativo durante o ciclo de vida do processo. O número de partições nos mostra quantas requisições podem ser atendidas em paralelo pelo microsserviço (consumer). Consumers são aplicações que se inscrevem em tópicos de forma a consumir seus records. Após a publicação da mensagem, pelo producer, todos os inscritos recebem o evento ocorrido em formato broadcast, bastando que ao inscrever-se, os consumidores informem qual a estratégia de commit do offset de mensagens (lidas no mínimo uma vez, lidas mais de uma vez, lidas somente uma vez) para que desta forma possam ser entregues pelo Kafka da maneira mais consistente e segura possível. No contexto de processamento de eventos, Kafka apresenta duas estratégias de entrega de mensagens, synchronous replication mode (modo síncrono de replicação) e assynchronous replication mode (modo assíncrono de replicação). Desta forma, ao utilizarmos a primeira estratégia, as mensagens somente estarão disponíveis para consumo quando todas as réplicas da partição confirmarem o recebimento dos dados, comitados no log principal. Por outro lado, ao utilizarmos a segunda estratégia, o Kafka torna o record visível tão logo a mensagem é persistida na partição líder.

Kafka e o Zookeeper

Zookeeper trata-se de um serviço de key/value (chave e valor) hierárquico para centralização de informações e sincronização de aplicações distribuídas. Provendo principalmente serviços de configuração, onde o Kafka utiliza o serviço oferecido pelo Zookeeper para armazenar informações de metadados do cluster, mantendo estes dados atualizados para brokers, tópicos, partições e réplicas. Kafka confia no Zookeeper para manter a coordenação e comunicação sobre a execução nos brokers (brokers são stateless) e também para notificar producers e consumers da existência de novos componentes adicionados ao Cluster, pois componentes podem ser adicionados em tempo real, como brokers, tópicos, partições, etc. Zookeeper ainda mantém informações sobre as partições líder presentes no cluster, armazenando o último offset de cada consumidor, possibilitando a recuperação de sua posição em caso de falhas. Além disso, Zookeeper mantém o controle de acesso aos topics e partitions utilizados pelo Kafka. Zookeeper também é responsável por determinar partições líder (leader partition) em tempo real, onde neste caso líderes são encarregadas de controlar todas as leituras e escritas nas partições. Enquanto partições réplicas (follower replicas) somente replicam o estado da partição líder distribuídas ao longo do cluster, réplicas seguidoras são elegíveis a se tornarem líderes quando encontram-se sincronizadas com o líder (in-sync), além de apresentarem bom histórico sem ocorrência de problemas em seu ciclo de vida (life cycle). Réplicas de partições são espalhadas por diversos brokers no cluster proporcionando alta disponibilidade de acesso. No Kafka, partições líder gerenciam todas as leituras e escritas das partições réplica, realizando o tracking dos dados entre as operações de escrita. A principal vantagem do uso de partições no Kafka são escalabilidade horizontal, aumento de performance, tolerância a falhas (replicação), e possibilidade de priorização no consumo dos eventos, podendo determinadas partições serem de consumo prioritário em relação as demais.

Vantagens do Kafka + Kafka Streams API

Um dos motivos que levam o Kafka a ser escolhido para implementar event-driven architecture, são que suas partições permitem escalabilidade horizontal, sendo as partições referenciadas como a grande mágica por trás da arquitetura do Kafka. Em termos de funcionamento, partições com a mesma key (header partition key) serão encaminhadas para o mesmo consumer no mesmo tópico, exatamente na mesma ordem em que foram enviadas para o tópico, possibilitando que eventos sejam processados na ordem exata em que foram emitidos, garantindo com que a ordenação de ocorrência dos eventos na linha do tempo seja alcançada em sua totalidade. Para efeitos de comparação, Kafka funciona de forma oposta ao RabbitMQ, onde a inteligência de rotas encontra-se nos exchanges, entre o produtor e as queues. No Kafka a inteligência de consumo encontra-se localizada no consumer e não no producer. Dentre os casos de uso para o Kafka, pode ser citado o processamento de streams em arquiteturas orientadas a evento (event-driven architecture), event-sourcing (persistência com base em event-store incremental), etc. Diferente da natureza de comunicação síncrona (synchronous mode), implementada através de interações utilizando HTTP/REST/SOAP, quando abordamos uma natureza assíncrona (asynchronous) temos o Apache Kafka como opção para atuar como Message Broker (roteamento de mensagens). Adicionalmente, sustentando em camadas inferiores de implementação a base para o sucesso em direção a adoção do EDA (event-driven architecture) como design pattern arquitetural. Ao adotarmos Apache Kafka, entregaremos pipelines de streams de dados mais elegantes e inteligentes, comunicando-se de maneira assíncrona em real-time (tempo real), conectando producers (produtores) a consumers (consumidores) de forma desacoplada, horizontalmente escaláveis, resilientes (tolerante a falhas) e consequentemente mais responsivos.

Kafka Streams API trata-se de uma library (Java, Scala) para construção de stream aplications em cima do Kafka, adicionada a partir da versão 0.10 e tornando-se madura a partir da versão 0.11. A library é completamente integrada com Kafka e atua em cima das suas APIs consumer e producer, sendo mais poderosa e contendo maior riqueza de detalhes em termos de transformação do processo envolvendo streams de eventos. A única dependência para rodarmos Kafka streams seria um Kafka cluster em running mode. Kafka streams foi construído para ler dados de tópicos Kafka, processá-los, e persistir os resultados em novos tópicos. Apresentado-se como uma poderosa ferramenta para processamento de streams de eventos em tempo real, abstraindo os dados de execução em streams e tables, suportando uma diversificada quantidade de cenários onde podem ser aplicadas transformações e filtros, agregando mais valor ao fluxo de dados. Kafka streams utiliza por padrão instâncias RocksDB como default state store. Desta forma, RocksDB é utilizado no contexto do Kafka streams API como cached key-value lookup além de persistent key-value store (persitindo o estado da instância em execução) como solução de armazenamento. RocksDB trata-se de uma library de acesso rápido, permitindo ser embarcável na aplicação. No contexto de processamento de transformações, Kafka streams possui o DSL (domain specific language) para implementação dos fluxos de Stream. DSL (domain specific language) é construído em cima do stream processor API, e recomendado para a maior parte dos casos de uso. Fluxos DSL (domain specific language) utilizam instâncias RocksDB para armazenar o estado da aplicação. DSL proporciona abstrações para streams e tables em forma de KStream e KTable. Além disso, Kafka streams possui a processor API para operações de baixo nível. Em aplicações Kafka stream, streams são a mais importante abstração provida, representando um data set (fonte de dados) continuamente atualizado, com tamanho indeterminado, sem um início bem definido na linha do tempo.

Kafka Streams e API's Baixo Nível

Streams DSL trata-se de uma API de alto nível, oferecida pelo Kafka streams, com estilo declarativo permitindo que seu processo siga um modelo de programação funcional. Streams DSL é construído em cima da streams processor API (API baixo nível imperativa pertencente ao Kafka streams), possibilitando a criação de stateless transformations, sem armazenamento de estado entre os steps de execução, como .map() e .filter(). Da mesma forma, podemos criar stateful transformations, contendo persistência de estado do fluxo do processo em cima de operações como .count(), .reduce(), .leftJoin(). Recomendado para a maior parte dos casos, sendo a maior parte das operações em records envolvendo transformação de dados relacionadas aos requisitos de negócio e não técnicos, as transformações podem ser realizadas com apenas algumas linhas de código DSL. As abstrações oferecidas pelo Streams DSL, como os objetos KStream, KTable, and GlobalKTable possuem suporte de primeiro nível dentro do contexto de processamento dos fluxos envolvendo Kafka streams. Utilizando DSL, podemos definir topologias (através do streams builder) de processamento em nossa aplicação, descrevendo uma ordem lógica de execução envolvendo os steps (passos) do processo, permitindo ao Kafka Streams paralelizar o fluxo. Topologias oferecem informações sobre os detalhes dos objetos presentes na estratégia de execução do fluxo. DSL encapsula a maior parte das complexidades de processamento dos fluxos ao mesmo tempo em que esconde funcionalidades baixo nível as quais são úteis em determinados casos de uso mais avançados. Utilizando Streams DSL API, descrevemos nossos fluxos de forma similar ao Java stream API, especificando os mappings e joins além de não precisarmos nos preocupar com error handling (controle de erros) como ocorre ao usarmos as client APIs (consumer e producer) pertencentes ao Apache Kafka. No caso do Kafka streams, todo o controle de exceções e erros já encontra-se embutido dentro de sua implementação.

Assim como um tópico no Kafka, um stream pode ter N partições (stream partitions), tratando-se de algo significativamente mais poderoso e expressivo em relação ao Kafka client APIs (consumer e producer). No Kafka streams, topologias definem a lógica para o processamento dos dados no fluxo de streams, sendo estes fluxos compostos de stream processors (.map(), .filter(), .join() e .aggreates()) e Streams (fluxo). Basicamente, no Kafka streams existem três tipos de nodes (nós) disponíveis para a composição das transformações nos fluxos, sendo source processor (não possui Upstream node), sink processor (não possui downstream node) e processor step. Ainda, no contexto de transformações, topologias podem ser divididas em subtopologias, onde cada Node somente interage com seus nodes irmãos, portanto subtopologias podem ser executadas em paralelo pelo Kafka streams por não possuirem dependências entre si. Kafka particiona os dados para armazenamento e replicação. Kafka streams particiona os dados para processá-los. Em ambos os casos, este particionamento proporciona elasticidade, escalabilidade e alta performance sendo entregues em componentes inicialmente construídos com foco em event-streaming applications. Desta forma, stream aplications implementadas utilizando Kafka streams API são aplicações Java tradicionais como qualquer outra. Kafka streams API permite-nos ler dados e escrever dados novamente em tópicos Kafka. Kafka streams API possui três diferentes tipos de interface de processamento: DSL, processor API e KSQL. Uma aplicação que utiliza Kafka streams API, atua como producer e consumer ao mesmo tempo, sendo o principal diferencial do Kafka streams a própria API, mais rica se comparada ao Kafka producer e consumer. Kafka streams é embarcado com diversas features (funcionalidades) como joining, filters, windowing, materialização de tabelas em cima dos event-streams, etc. Assim, Complexidades relacionadas aos requisitos de negócio podem ser abordadas utilizando Kafka streams API através de uma cadeia de serviços assíncronos, conectados por meio de eventos, quebrando grandes processos em componentes menores e mais flexíveis.

Considerações Finais

Os processos stateful (os quais armazenam estado), desenvolvidos a partir do Kafka streams API, possuem seus dados de transformações armazenados em tópicos Kafka ou localmente em memória. Além disso, é possível criar stateless transformations (.map(), .filterMap(), .filter(), etc). Em ambos os casos sendo permitida a construção de joins (left joins), combinados com funcionalidades como windowing (session window) permitindo construção de streams em janelas de tempo predeterminadas. Assim, tornando os fluxos implementados mais inteligentes e robustos. Em uma abordagem event-streaming, podemos materializar localmente o fluxo de dados através do Kafka streams API, aumentando a performance e também a autonomia da execução. Event-streaming applications (aplicações orientadas a eventos) podem necessitar tanto de processos que armazenam estado de execução quanto processos os quais não armazenam estado, sendo em ambos os casos desidratados localmente, seja em memória ou disco. Em termos de arquitetura da aplicação, independente do caminho que escolhermos integrar a comunicação entre nossos microsserviços, seja síncrona ou assíncrona, RESTful ou event-driven, teremos sempre a possibilidade de implementar aplicações event-streaming direcionadas por eventos e compostas por fluxos de streams inteligentes. Desta forma, adquirindo oportunidades de promover verdadeiramente os conceitos relacionados ao EDA. Evidentemente, ao combinarmos poderosas ferramentas como Apache Kafka, em conjunto com Kafka streams API, obtemos diversas vantagens competitivas em relação ao time-to-market (tempo de entrega) de nossas aplicações, encorajando-nos a abraçar desafios cada vez maiores e mais complexos, suportando a execução de processos mais reativos a mudanças, com maior agilidade além de entregarmos aplicações cada vez mais performáticas, independentes, resilientes, inteligentes e consequentemente elevando a qualidade de nossos componentes de software.

Entre para ver ou adicionar um comentário

Outros artigos de Marcelo M. Gonçalves

Outras pessoas também visualizaram

Conferir tópicos