Kafka e microsserviços: uma visão geral


Olá pessoal. Neste artigo, explicarei por que escolhemos Kafka há nove meses em Avito e o que é. Compartilharei um dos casos de uso - um intermediário de mensagens. E, finalmente, vamos falar sobre as vantagens que temos ao aplicar a abordagem Kafka como serviço.


O problema



Primeiro, um pouco de contexto. Há algum tempo, começamos a nos afastar da arquitetura monolítica e agora em Avito já existem várias centenas de serviços diferentes. Eles têm seus próprios repositórios, sua própria pilha de tecnologia e são responsáveis ​​por sua parte da lógica de negócios.


Um dos problemas com um grande número de serviços é a comunicação. O serviço A geralmente deseja conhecer as informações do serviço B. Nesse caso, o serviço A acessa o serviço B por meio de uma API síncrona. O serviço B quer saber o que acontece com os serviços G e D, e esses, por sua vez, estão interessados ​​nos serviços A e B. Quando existem muitos desses serviços "curiosos", as conexões entre eles se transformam em uma bola emaranhada.


Além disso, a qualquer momento, o serviço A pode ficar indisponível. E o que fazer nesse caso, o serviço B e todos os outros serviços vinculados a ele? E se você precisar fazer uma cadeia de chamadas síncronas consecutivas para concluir uma operação de negócios, a probabilidade de falha de toda a operação se tornará ainda maior (e, quanto maior, mais longa será essa cadeia).


Seleção de tecnologia


imagem


OK, os problemas são claros. Você pode eliminá-los criando um sistema de mensagens centralizado entre os serviços. Agora, cada um dos serviços é suficiente para conhecer apenas esse sistema de mensagens. Além disso, o próprio sistema deve ser tolerante a falhas e escalável horizontalmente, bem como em caso de acidentes, acumular um buffer de chamada para o processamento subsequente.


Vamos agora escolher a tecnologia na qual a entrega de mensagens será implementada. Para fazer isso, primeiro entenda o que esperamos dela:


  • Mensagens entre serviços não devem ser perdidas;
  • As mensagens podem ser duplicadas
  • as mensagens podem ser armazenadas e lidas com profundidade de vários dias (buffer persistente);
  • os serviços podem assinar dados de seu interesse;
  • vários serviços podem ler os mesmos dados;
  • As mensagens podem conter carga útil em massa detalhada (transferência de estado transportada por evento);
  • às vezes você precisa de uma garantia de pedido de mensagem.

Também foi crucial escolhermos o sistema mais escalável e confiável com alto rendimento (pelo menos 100 mil mensagens a alguns kilobytes por segundo).


Nesse estágio, nos despedimos do RabbitMQ (é difícil manter a estabilidade em altas rps), do SkyTools PGQ (não é rápido o suficiente e é pouco escalável) e do NSQ (não persistente). Todas essas tecnologias são usadas em nossa empresa, mas não se ajustaram à tarefa em questão.


Então começamos a procurar novas tecnologias para nós - Apache Kafka, Apache Pulsar e NATS Streaming.


O primeiro a largar o Pulsar. Decidimos que Kafka e Pulsar são soluções bastante semelhantes. E, apesar do Pulsar ser testado por grandes empresas, é mais recente e oferece menor latência (em teoria), decidimos deixar Kafka fora dos dois, como o padrão de fato para essas tarefas. Provavelmente retornaremos ao Apache Pulsar no futuro.


E havia dois candidatos restantes: NATS Streaming e Apache Kafka. Estudamos as duas soluções com mais detalhes e as duas chegaram à tarefa. Mas, no final, tínhamos medo da juventude relativa do NATS Streaming (e do fato de um dos principais desenvolvedores, Tyler Treat, decidir deixar o projeto e começar o seu próprio - Liftbridge). Ao mesmo tempo, o modo de clustering do NATS Streaming não permitia uma escala horizontal forte (isso provavelmente não é mais um problema após a adição do modo de particionamento em 2017).


No entanto, o NATS Streaming é uma tecnologia interessante escrita em Go e suportada pela Cloud Native Computing Foundation. Ao contrário do Apache Kafka, ele não precisa do Zookeeper para funcionar ( pode ser possível dizer o mesmo sobre o Kafka em breve ), já que dentro dele implementa o RAFT. Ao mesmo tempo, o NATS Streaming é mais fácil de administrar. Não excluímos que, no futuro, retornaremos a essa tecnologia.


No entanto, o Apache Kafka se tornou nosso vencedor hoje. Em nossos testes, provou ser bastante rápido (mais de um milhão de mensagens por segundo para leitura e gravação com um volume de mensagens de 1 kilobyte), bastante confiável, experiência bem dimensionável e comprovada na venda por grandes empresas. Além disso, o Kafka suporta pelo menos várias grandes empresas comerciais (por exemplo, usamos a versão Confluent) e o Kafka possui um ecossistema desenvolvido.


Revisão Kafka


Antes de começar, recomendo imediatamente um excelente livro - "Kafka: O Guia Definitivo" (também está na tradução para o russo, mas os termos quebram um pouco o cérebro). Nele você encontra as informações necessárias para um entendimento básico de Kafka e até um pouco mais. A documentação do Apache em si e o blog Confluent também são bem escritos e fáceis de ler.


Então, vamos dar uma olhada em como Kafka é uma visão panorâmica. A topologia básica do Kafka consiste em produtor, consumidor, corretor e tratador.


Corretor



Um corretor é responsável por armazenar seus dados. Todos os dados são armazenados em formato binário, e o broker sabe pouco sobre o que são e qual é a sua estrutura.


Cada tipo lógico de evento geralmente está localizado em seu próprio tópico separado (tópico). Por exemplo, um evento de criação de anúncio pode cair no tópico item.created e um evento de sua alteração pode cair em item.changed. Os tópicos podem ser considerados como classificadores de eventos. No nível do tópico, você pode definir parâmetros de configuração como:


  • volume de dados armazenados e / ou idade (retention.bytes, retention.ms);
  • fator de redundância de dados (fator de replicação);
  • tamanho máximo de uma mensagem (max.message.bytes);
  • o número mínimo de réplicas consistentes nas quais os dados podem ser gravados no tópico (min.insync.replicas);
  • a capacidade de realizar failover para uma réplica de atraso não síncrona com possível perda de dados (unclean.leader.election.enable);
  • e muito mais ( https://kafka.apache.org/documentation/#topicconfigs ).

Por sua vez, cada tópico é dividido em uma ou mais partições (partição). É na partição que os eventos acabam caindo. Se houver mais de um intermediário no cluster, as partições serão distribuídas igualmente entre todos os intermediários (na medida do possível), o que permitirá escalar a carga na escrita e leitura em um tópico para vários intermediários ao mesmo tempo.


No disco, os dados de cada partição são armazenados como arquivos de segmento, por padrão iguais a um gigabyte (controlado por log.segment.bytes). Um recurso importante é a exclusão de dados de partições (quando a retenção é acionada) apenas por segmentos (não é possível excluir um evento de uma partição, é possível excluir apenas o segmento inteiro e apenas inativo).


Zookeeper


O Zookeeper atua como um repositório e coordenador de metadados. É ele quem é capaz de dizer se os corretores estão vivos (você pode vê-lo através dos olhos de um tratador de zoológicos através do comando ls /brokers/ids do zookeeper-shell), qual dos corretores é o controlador ( get /controller ), se as partições estão em estado síncrono com suas réplicas ( get /brokers/topics/topic_name/partitions/partition_number/state ). Além disso, o produtor e o consumidor irão primeiro ao tratador para descobrir em qual intermediário quais tópicos e partições estão armazenados. Nos casos em que um fator de replicação maior que 1 for especificado para o tópico, o tratador indicará quais partições são líderes (elas serão gravadas e lidas a partir). No caso de uma falha do broker, é no tratador que as informações sobre as novas partições de líderes serão registradas (a partir da versão 1.1.0 de forma assíncrona, e isso é importante ).


Nas versões anteriores do Kafka, o zookeeper também era responsável pelo armazenamento de compensações, mas agora elas são armazenadas em um tópico especial __consumer_offsets no broker (embora você ainda possa usar o zookeeper para esses fins).


A maneira mais fácil de transformar seus dados em abóbora é apenas a perda de informações com o tratador. Nesse cenário, será muito difícil entender o que e onde ler.


Produtor


O Producer geralmente é um serviço que grava dados diretamente no Apache Kafka. O produtor seleciona um tópico no qual suas mensagens temáticas serão armazenadas e começa a escrever informações nele. Por exemplo, um produtor pode ser um serviço de anúncios. Nesse caso, ele enviará eventos como "anúncio criado", "anúncio atualizado", "anúncio excluído" etc. para tópicos temáticos. Cada evento é um par de valores-chave.


Por padrão, todos os eventos são distribuídos pelas partições da partição com round-robin se a chave não estiver configurada (ordem perdida) e através de MurmurHash (chave) se a chave estiver presente (ordenando na mesma partição).


É imediatamente interessante notar aqui que o Kafka garante a ordem dos eventos em apenas uma partição. Mas, de fato, muitas vezes isso não é um problema. Por exemplo, você pode adicionar todas as alterações do mesmo comunicado a uma partição (garantindo assim a ordem dessas alterações no comunicado). Você também pode passar um número de sequência em um dos campos do evento.


Consumidor



O consumidor é responsável por recuperar dados do Apache Kafka. Se você voltar ao exemplo acima, o consumidor pode ser um serviço de moderação. Este serviço será inscrito no tópico do serviço de anúncio e, quando um novo anúncio aparecer, ele será recebido e analisado para conformidade com algumas políticas especificadas.


O Apache Kafka lembra quais eventos recentes o consumidor recebeu (o tópico do serviço __consumer__offsets é usado para isso), garantindo assim que, após uma leitura bem-sucedida, o consumidor não receberá a mesma mensagem duas vezes. No entanto, se você usar a opção enable.auto.commit = true e executar completamente o trabalho de rastrear a posição do consumidor no tópico para Kafka, poderá perder dados . No código de produção, a posição do consumidor geralmente é controlada manualmente (o desenvolvedor controla o momento em que a confirmação do evento de leitura deve ocorrer).


Nos casos em que um consumidor não é suficiente (por exemplo, o fluxo de novos eventos é muito grande), você pode adicionar mais alguns consumidores vinculando-os no grupo de consumidores. O grupo de consumidores logicamente é exatamente o mesmo consumidor, mas com a distribuição de dados entre os membros do grupo. Isso permite que cada um dos participantes receba sua parcela de mensagens, aumentando assim a velocidade de leitura.


Resultados do teste


imagem


Aqui não vou escrever muito texto explicativo, apenas compartilhar os resultados. Os testes foram realizados em três máquinas físicas (12 CPU, 384 GB de RAM, 15k SAS DISK, 10 GBit / s Net), corretores e tratador foram implantados no lxc.


Teste de desempenho


Durante o teste, os seguintes resultados foram obtidos.


  • A velocidade de gravação de mensagens de 1 KB de tamanho ao mesmo tempo por 9 produtores - 1300000 eventos por segundo.
  • Velocidade de leitura de mensagens de 1 KB ao mesmo tempo por 9 consumidores - 1.500.000 eventos por segundo.

Teste de tolerância a falhas


Durante o teste, foram obtidos os seguintes resultados (3 corretores, 3 tratador).


  • Um término anormal de um dos intermediários não leva à suspensão ou inacessibilidade do cluster. O trabalho continua como de costume, mas os demais corretores têm uma grande carga.
  • O encerramento anormal de dois intermediários no caso de um cluster de três intermediários e min.isr = 2 leva à inacessibilidade do cluster para gravar, mas é legível. Caso min.isr = 1, o cluster continuará disponível para leitura e gravação. No entanto, esse modo contradiz o requisito de alta segurança de dados.
  • O encerramento anormal de um dos servidores do Zookeeper não leva ao encerramento ou inacessibilidade do cluster. O trabalho continua normalmente.
  • Um encerramento anormal de dois servidores Zookeeper leva a uma inacessibilidade de cluster até que pelo menos um dos servidores Zookeeper seja restaurado. Esta afirmação é verdadeira para um cluster do Zookeeper de 3 servidores. Como resultado, após a pesquisa, foi decidido aumentar o cluster do Zookeeper para 5 servidores para aumentar a tolerância a falhas.

Kafka como um serviço


imagem


Garantimos que o Kafka é uma excelente tecnologia que nos permite resolver a tarefa definida para nós (implementando um intermediário de mensagens). No entanto, decidimos proibir os serviços de acessar diretamente o Kafka e fechá-lo no topo com o serviço de barramento de dados. Por que fizemos isso? Na verdade, existem várias razões.


  • O barramento de dados assumiu todas as tarefas relacionadas à integração com o Kafka (implementação e configuração de consumidores e produtores, monitoramento, alerta, registro, dimensionamento, etc.). Portanto, a integração com o intermediário de mensagens é o mais simples possível.


  • Barramento de dados permitido abstrair de um idioma ou biblioteca específica para trabalhar com Kafka.


  • O barramento de dados permitiu que outros serviços abstraíssem da camada de armazenamento. Talvez em algum momento alteremos o Kafka para Pulsar, e ninguém notará nada (todos os serviços conhecem apenas a API do barramento de dados).


  • O barramento de dados assumiu a validação dos esquemas de eventos.


  • O uso da autenticação de barramento de dados é implementado.


  • Sob a cobertura do barramento de dados, podemos, sem tempo de inatividade, atualizar discretamente as versões Kafka, realizar centralmente configurações de produtores, consumidores, corretores, etc.


  • O barramento de dados nos permitiu adicionar recursos de que precisamos que não estão no Kafka (como auditoria de tópicos, monitoramento de anomalias no cluster, criação de DLQ etc.).


  • O barramento de dados permite que o failover seja implementado centralmente para todos os serviços.



No momento, para começar a enviar eventos para o intermediário de mensagens, basta conectar uma pequena biblioteca ao seu código de serviço. Isso é tudo. Você tem a oportunidade de escrever, ler e escalar com uma linha de código. Toda a implementação está oculta, apenas algumas varas como o tamanho do lote. Sob o capô, o serviço de barramento de dados aumenta o número necessário de instâncias de produtor e consumidor no Kubernetes e adiciona a configuração necessária a elas, mas tudo isso é transparente para o seu serviço.


Obviamente, não existe uma bala de prata, e essa abordagem tem suas limitações.


  • O barramento de dados precisa ser suportado por si só, diferente das bibliotecas de terceiros.
  • O barramento de dados aumenta o número de interações entre serviços e o intermediário de mensagens, o que leva a um desempenho mais baixo comparado ao Kafka simples.
  • Nem tudo pode ser tão oculto dos serviços, que não queremos duplicar a funcionalidade do KSQL ou do Kafka Streams no barramento de dados; portanto, às vezes, é necessário permitir que os serviços sigam diretamente.

No nosso caso, os profissionais superaram os contras, e a decisão de cobrir o mediador de mensagens com um serviço separado foi justificada. Durante o ano de operação, não tivemos acidentes e problemas graves.


PS Obrigado à minha namorada, Ekaterina Oblyalyaeva, pelas fotos legais deste artigo. Se você gostou, ainda mais ilustrações.

Source: https://habr.com/ru/post/pt465315/


All Articles