Introdução Completa ao Apache Kafka ™

Alguns dos monólitos que usam Apache Kafka

Introdução

Kafka é uma palavra que hoje é ouvida hoje em dia … Muitas das principais empresas digitais parecem usá-la também. Mas o que é realmente?

A Kafka foi originalmente desenvolvida no LinkedIn em 2011 e melhorou desde então. Hoje em dia, é uma plataforma inteira, permitindo que você armazene quantidades de dados absurdas redundantemente, tenha um barramento de mensagens com enorme rendimento (milhões / seg) e use o processamento de fluxo em tempo real nos dados que o atravessam de uma só vez.

Tudo bem e ótimo, mas despojado até o seu núcleo, o Kafka é um registro de confirmação distribuído, horizontalmente escalável, tolerável a falhas.

Essas eram algumas palavras extravagantes, vamos para elas uma a uma e vemos o que elas significam. Depois, vamos mergulhar profundamente em como funciona.

Distribuído

Um sistema distribuído é um que é dividido em várias máquinas executando, que funcionam juntas em um cluster para aparecer como um único nó para o usuário final. Kafka é distribuído no sentido de que armazena, recebe e envia mensagens em diferentes nós (chamados corretores).

Os benefícios para esta abordagem são alta escalabilidade e tolerância a falhas.

Horizontalmente escalável

Vamos definir o termo escalabilidade vertical primeiro. Diga, por exemplo, que você possui um servidor de banco de dados tradicional que está começando a sobrecarregar. A maneira de resolver isso é simplesmente aumentar os recursos (CPU, RAM, SSD) no servidor. Isso é chamado de escala vertical – onde você adiciona mais recursos à máquina. Existem duas grandes desvantagens para escalar para cima:

  1. Existem limites definidos pelo hardware. Você não pode escalar indefinidamente.
  2. Geralmente, requer tempo de inatividade, algo que as grandes corporações não podem pagar.

Escalabilidade horizontal é resolver o mesmo problema, jogando mais máquinas nisso. Adicionar uma nova máquina não requer tempo de inatividade nem há limites para a quantidade de máquinas que você pode ter em seu cluster. A captura é que nem todos os sistemas suportam escalabilidade horizontal, pois não são projetados para funcionar em um cluster e aqueles que são geralmente são mais complexos para trabalhar.

A escala horizontal torna-se muito mais barata após um certo limiar

Tolerante a falhas

Algo que emerge em sistemas não distribuídos é que eles têm um único ponto de falha (SPoF). Se o seu servidor de banco de dados único falhar (como as máquinas) por qualquer motivo, você está ferrado.

Os sistemas distribuídos são projetados de forma a acomodar falhas de forma configurável. Em um cluster Kafka de 5 nós, você pode fazê-lo continuar trabalhando, mesmo que 2 dos nós estejam baixos. Vale ressaltar que a tolerância a falhas está em uma compensação direta com o desempenho, pois quanto mais tolerante a falhas seu sistema é, menos desempenho é.

Registro de compromisso

Um log de confirmação (também conhecido como log de gravação contínua, log de transação) é uma estrutura de dados ordenada persistente que só suporta anexos. Você não pode modificar nem excluir registros dele. É lido da esquerda para a direita e garante o pedido de itens.

Exemplo de ilustração de um log de confirmação, retirado daqui

– Você está me dizendo que a Kafka é uma estrutura de dados tão simples?

De muitas maneiras, sim. Esta estrutura está no coração de Kafka e é inestimável, pois fornece pedidos, o que, por sua vez, fornece processamento determinista. Ambos os quais são problemas não triviais em sistemas distribuídos.

Kafka realmente armazena todas as suas mensagens no disco (mais sobre isso mais tarde) e mandá-las na estrutura permite tirar proveito das leituras de disco seqüenciais.

  • Lê e escreve são um tempo constante O (1) (conhecendo a ID da gravação) , que comparado às operações de O (log N) da estrutura em disco é uma grande vantagem, pois cada busca de disco é dispendiosa.
  • Lê e escreve não afeta outro. A escrita não bloquearia a leitura e vice-versa (em oposição às árvores equilibradas)

Estes dois pontos têm grandes benefícios de desempenho, uma vez que o tamanho dos dados está completamente desacoplado do desempenho. A Kafka tem o mesmo desempenho se você possui 100 KB ou 100 TB de dados em seu servidor.

Como funciona?

As aplicações ( produtores ) enviam mensagens ( registros ) para um nó Kafka ( corretor ) e as referidas mensagens são processadas por outros aplicativos chamados consumidores . Ditas mensagens são armazenadas em um tópico e os consumidores se inscrevem no tópico para receber novas mensagens.

Como os tópicos podem ficar bastante grandes, eles são divididos em partições de tamanho menor para melhor desempenho e escalabilidade. (ex: dizer que você estava armazenando pedidos de login do usuário, você poderia dividi-los pelo primeiro caractere do nome de usuário do usuário)
A Kafka garante que todas as mensagens dentro de uma partição são ordenadas na sequência em que elas vieram. A maneira como você distinta uma mensagem específica é através de seu deslocamento , que você poderia ver como um índice de matriz normal, um número de seqüência que é incrementado para cada nova mensagem em uma partição.

Kafka segue o princípio de um corretor burro e consumidor inteligente. Isso significa que a Kafka não acompanha quais registros são lidos pelo consumidor e excluí-los, mas armazena-os uma quantidade de tempo estabelecida (por ex., Um dia) ou até algum limite de tamanho ser atingido. Os próprios consumidores examinam Kafka para novas mensagens e dizem quais registros eles querem ler. Isso permite que eles incrementem / diminuam o deslocamento no qual desejam, podendo assim reproduzir e reprocessar eventos.

Vale a pena notar que os consumidores são realmente grupos de consumidores que possuem um ou mais processos de consumo dentro. Para evitar dois processos que lêem a mesma mensagem duas vezes, cada partição está vinculada a apenas um processo de consumidor por grupo.

Representação do fluxo de dados

Persistência ao disco

Como mencionei anteriormente, a Kafka atualmente armazena todos os seus registros no disco e não guarda nada na RAM. Você pode estar se perguntando como isso é da maneira mais suave uma escolha sã. Existem muitas otimizações por trás disso que tornam possível:

  1. Kafka possui um protocolo que agrupa mensagens juntas. Isso permite que as solicitações da rede agrupem mensagens juntas e reduzam a sobrecarga de rede; o servidor, por sua vez, persiste um pedaço de mensagens de uma só vez e o consumidor busca grandes volumes lineares ao mesmo tempo
  2. As leituras / escritas em um disco são rápidas. O conceito de que os discos modernos são lentos é devido à procura de disco, algo que não é um problema em grandes operações lineares.
  3. Disseram que as operações lineares são fortemente otimizadas pelo sistema operacional, através de técnicas de read-ahead (prefetch large block multiples) e write-behind (grupamento de pequenas escritas lógicas em grandes escritos físicos).
  4. OS modernos armazenam o disco em memória RAM livre. Isso é chamado de pagecache .
  5. Uma vez que a Kafka armazena mensagens em um formato binário padronizado não modificado em todo o fluxo (produtor-> corretor-> consumidor) , ele pode usar a otimização de cópia zero . Isto é, quando o SO copiar dados da pagecache diretamente para um soquete, ignorando efetivamente o aplicativo Kafka Broker inteiramente

Todas essas otimizações permitem que a Kafka entregue mensagens a uma velocidade próxima da rede.

Distribuição e Replicação de Dados

Vamos falar sobre como Kafka alcança tolerância a falhas e como distribui dados entre nós.

Replicação de dados

Os dados de partição são replicados em vários corretores para preservar os dados no caso de um corretor morrer.

Em todo momento, um corretor "possui" uma partição e é o nó através do qual as aplicações escrevem / lêem a partir da partição. Isso é chamado de líder de partição . Ele replica os dados que recebe para N outros corretores, chamados de seguidores . Eles também armazenam os dados e estão prontos para serem eleitos como líderes no caso de o nó líder morrer.

Isso ajuda você a configurar a garantia de que qualquer mensagem publicada com êxito não será perdida. Ter a opção de alterar o fator de replicação permite que você troque o desempenho por garantias de durabilidade mais fortes, dependendo da criticidade dos dados.

4 corretores da Kafka com um fator de replicação de 3

Desta forma, se um líder nunca falhar, um seguidor pode tomar seu lugar.

Você pode estar perguntando, porém:

– Como um produtor / consumidor sabe quem é o líder de uma partição?

Para que um produtor / consumidor escreva / leia de uma partição, eles precisam conhecer seu líder, certo? Essa informação precisa estar disponível em algum lugar.
Kafka armazena tais metadados em um serviço chamado Zookeeper .

O que é Zookeeper?

O Zookeeper é uma loja de valores-chave distribuídos. É altamente otimizado para ler, mas as gravações são mais lentas. É mais comumente usado para armazenar metadados e lidar com a mecânica de agrupamento (batimentos cardíacos, distribuição de atualizações / configurações, etc.).

Permite que os clientes do serviço (os corretores da Kafka) se inscrevam e as mudanças sejam enviadas para elas uma vez que elas acontecem. É assim que os corretores sabem quando mudar os líderes de partição. Zookeeper também é extremamente tolerante a falhas e deve ser, já que Kafka depende muito disso.

Ele é usado para armazenar todos os tipos de metadados, para mencionar alguns:

  • Deslocamento do grupo do consumidor por partição (embora os clientes modernos armazenem os deslocamentos em um tópico separado da Kafka)
  • ACL (listas de controle de acesso) – usado para limitar o acesso / autorização
  • Quotas do Produtor e do Consumidor – limites máximos de mensagens / segundos
  • Partition Leaders e sua saúde

Como um produtor / consumidor sabe quem é o líder de uma partição?

Produtor e Consumidores costumavam se conectar diretamente e conversar com o Zookeeper para obter essa (e outras) informações. Kafka tem se afastado desse acoplamento e, desde as versões 0.8 e 0.9 respectivamente, os clientes obtêm informações de metadados diretamente dos corretores da Kafka, que eles mesmos conversam com o Zookeeper.

Fluxo de Metadados

Transmissão

Em Kafka, um processador de fluxo é qualquer coisa que leva fluxos contínuos de dados de tópicos de entrada, executa algum processamento nesta entrada e produz um fluxo de dados para produzir tópicos (ou serviços externos, bancos de dados, lixeira, onde quer que esteja realmente …)

É possível fazer um processamento simples diretamente com as APIs produtor / consumidor, no entanto, para transformações mais complexas, como juntar fluxos juntos, a Kafka fornece uma biblioteca de API de Streams integrada.

Esta API destina-se a ser usada em sua própria base de código, ela não está sendo executada em um corretor. Funciona de forma semelhante à API do consumidor e ajuda você a dimensionar o processamento de fluxo em várias aplicações (semelhante aos grupos de consumidores).

Processamento sem estado

Um processamento sem estado de um fluxo é um processamento determinista que não depende de nada externo. Você sabe que, para qualquer dado dado, você sempre produzirá o mesmo resultado independente de qualquer outra coisa. Um exemplo para isso seria simples transformação de dados – acrescentando algo a uma string "Hello" -> "Hello, World!" .

Dualidade de tabela de fluxo

É importante reconhecer que fluxos e tabelas são essencialmente iguais. Um fluxo pode ser interpretado como uma tabela e uma tabela pode ser interpretada como um fluxo.

Stream como uma tabela

Se você olhar para como a replicação de banco de dados síncrono é alcançada, você verá que é através da chamada replicação de transmissão , onde cada alteração em uma tabela é enviada para um servidor de réplica. Um fluxo de Kafka pode ser interpretado da mesma maneira – como um fluxo de atualizações para dados, no qual o agregado é o resultado final da tabela. Tais fluxos são salvos em um RocksDB local (por padrão) e são chamados de KTable.

Cada registro incrementa a contagem agregada

Tabela como um fluxo

Uma tabela pode ser vista como um instantâneo do valor mais recente para cada chave em um fluxo. Da mesma forma, os registros de fluxo podem produzir uma tabela, as atualizações da tabela podem produzir um fluxo de changelog.

Cada atualização produz um registro de instantâneo no fluxo

Processamento estadual

Algumas operações simples como map() ou filter() são apátridas e não exigem que você mantenha dados sobre o processamento. No entanto, na vida real, a maioria das operações que você fará será stateful (por exemplo, count() ) e, como tal, exigirá que você armazene o estado atualmente acumulado.

O problema com a manutenção do estado nos processadores de fluxo é que os processadores de fluxo podem falhar! Onde você precisaria manter esse estado para ser tolerante a falhas?

Uma abordagem ingênua é simplesmente armazenar todos os estados em um banco de dados remoto e juntar-se à rede para aquela loja. O problema com isso é que não há localidade de dados e muitas viagens de ida e volta da rede, o que reduzirá significativamente a sua aplicação. Um problema mais sutil, porém importante, é que o tempo de atividade do seu processamento de fluxo esteja fortemente acoplado ao banco de dados remoto e o trabalho não será autônomo (uma mudança no banco de dados de outra equipe pode quebrar seu processamento) .

Então, o que é uma abordagem melhor?
Lembre-se da dualidade de tabelas e córregos. Isso nos permite converter fluxos em tabelas que são co-localizadas com nosso processamento. Ele também nos fornece um mecanismo para lidar com a tolerância a falhas – armazenando os fluxos em um corretor da Kafka.

Um processador de fluxo pode manter seu estado em uma tabela local (por exemplo, RocksDB), que será atualizado a partir de um fluxo de entrada (após talvez alguma transformação arbitrária). Quando o processo falhar, ele pode restaurar seus dados repetindo o fluxo.

Você poderia até ter um banco de dados remoto, seja o produtor do fluxo, transmitindo efetivamente um changelog com o qual você reconstrói a tabela localmente.

Processamento estadual, juntando um KStream com um KTable

KSQL

Normalmente, você seria forçado a escrever seu processamento de fluxo em uma linguagem JVM, pois é aí que o único cliente oficial da API Kafka Streams é.

Configuração do exemplo KSQL

Atualmente, em uma visualização do desenvolvedor, o KSQL é um novo recurso que permite que você escreva seus trabalhos de transmissão simples em um idioma semelhante ao SQL.

Você configura um servidor KSQL e consulta-lo interativamente através de uma CLI para gerenciar o processamento. Ele funciona com as mesmas abstrações (KStream & KTable), garante os mesmos benefícios da API Streams (escalabilidade, tolerância a falhas) e simplifica muito o trabalho com fluxos.

Isso pode não parecer muito, mas na prática é muito mais útil para testar coisas e até mesmo permite que pessoas fora do desenvolvimento (por exemplo, proprietários de produtos) possam brincar com o processamento de fluxo. Eu encorajo você a dar uma olhada no vídeo de início rápido e ver como é simples .

Alternativas de transmissão

Kafka streams é uma mistura perfeita de poder e simplicidade. Possivelmente, possui as melhores capacidades para trabalhos em fluxo contínuo no mercado e integra-se com o Kafka, mais fácil do que outras alternativas de processamento de fluxo ( Storm , Samza , Spark , Wallaroo ).

O problema com a maioria das outras estruturas de processamento de fluxo é que elas são complexas para trabalhar e implantar. Uma estrutura de processamento em lote como a Spark precisa:

  • Controle uma grande quantidade de trabalhos em um conjunto de máquinas e distribua-os de forma eficiente em todo o cluster.
  • Para conseguir isso, ele deve dinamicamente empacotar seu código e implantá-lo fisicamente nos nós que o executarão. (juntamente com a configuração, bibliotecas, etc.)

Infelizmente, abordar esses problemas torna os frameworks bastante invasivos. Eles querem controlar muitos aspectos de como o código é implantado, configurado, monitorado e empacotado.

Kafka Streams permite que você desenhe sua própria estratégia de implantação quando precisar, seja Kubernetes , Mesos , Nomad , Docker Swarm ou outros.

A motivação subjacente do Kafka Streams é permitir que todas as suas aplicações façam o processamento de fluxo sem a complexidade operacional de executar e manter outro cluster. A única desvantagem potencial é que está fortemente acoplado com a Kafka, mas no mundo moderno, onde a maioria, se não o processamento em tempo real, é alimentado por Kafka, que pode não ser uma grande desvantagem.