Esse post é um resumo sobre Apache Kafka e Schemas.
Apache Kafka e Schemas
Antes de começar a debater precisamos resolver um mito: Kafka não é um banco de dados. Apesar de ser usado como Fonte Única de Verdade (Single Source of Truth), mas muitas das funcionalidades dos bancos SQL (DBMS) ou mesmo dos bancos NoSQL não estarão presente. Martin Kleppman, autor do livro Designing Data-Intensive Applications, defende nessa apresentação de 2019 que podemos sim considerar o Kafka uma base de dados, mas isso vai depender da forma como ele é usado, não sendo a função principal da plataforma.
Todo bando de dados, mesmo os não relacionais, possuem alguma forma de gerenciamento de schemas para os dados do mesmo tipo lógico (tabelas, collections, tópicos, canais, ou qualquer dados que podem ser agrupados conjuntamente). Mas Apache Kafka é totalmente agnóstico em relação ao conteúdo das mensagens e das chaves também. Não conhecer o conteúdo das mensagens é o Trade-off necessário para que o throughput do broker seja maior, essa é uma desisão de design, pois funcionalidades como serialização e desserialização foram delegadas para os clientes (produtores, consumidores, streams e connectors). Ao broker cabe apenas receber e enviar mensagens binárias.
Talvez a afirmação que o Kafka é agnóstico em relação ao conteúdo da chave possa parecer estranha. Se considerarmos o Kafka como uma plataforma ele não é agnóstico, mas o Broker Kafka não conhece o conteúdo da chave, o tipo e nem o algoritmo usado para associar chave e partição. Mesmo a chave é usada pelo producer para decidir para qual partição a mensagem será enviada a mensagem e essa decisão pode ser alterada pela com o uso da configuração partitioner.class
.
Então ao se implementar um produtor devemos definir quais classes farão a serialização das mensagens pois essa não é uma configuração para o qual existe um valor padrão. Os formatos mais comuns de serialização são JSON, AVRO e Protocol Buffers. O formato JSON, tão usado no protocolo HTTP, não é o formato recomendado irá demandar mais espaço em disco pois as mensagens não são compactadas. Já o AVRO ou o Protocol Buffers são otimizadas para reduzir o espaço em disco armazenando as mensagens em formato binário, mas cada um desses formatos terá suas peculiaridades que não discuteremos aqui.
Como o Kafka não lida com Schemas, é um padrão usar um serviço para registro de schemas. A solução mais conhecida é o Schema Registry da Confluent, ao usar o serializador compatível com o Schema Registry a cada nova mensagem é verificada se houve mudanças de schema e se elas são compatíveis com o já schema registrado. Esse padrão pode adicionar um pouco de latência, pois no melhor caso há uma verificação de Schema e no pior caso há o registro do schema em através de uma requisição HTTP. Mas usar um serviço de registro de schemas pode ser útil para alcança um baixo acoplamento entre times, pois o time que desenvolve um consumidor não precisa conversar com o time que desenvolve o produto. Outra vantagem é que as classes necessárias para implementar o objeto de negócio pode ser geradas automaticamente.
Evolução de Schemas
Mensagens armazenadas no Apache Kafka são imutáveis, mas os serviços que produzem e consomem mensagens não são imutáveis o que nos leva ao seguinte questionamento: como lidar com a mudança de schema na evolução de um sistema?
O paper An empirical characterization of event sourced systems and their schema evolution — Lessons from industry vai apresentar cinco processos de evolução, mas como no Apache Kafka não é permitido se alterar o conteúdo das mensagens, apenas quatro são aplicavéis.
- Eventos versionados: Todos os schemas devem ser declarados no código e cabe ao consumidor decidir qual schema usar.
- Schema fraco: o método de serialização permite que campos sejam adicionados e removidos sem prejuízo para a leitura, cabendo ao consumidor verificar se o valor que se deseja ler ainda existe.
- Up Casting: Ao receber um evento, o consumidor irá transformar o schema dele no mais recente. O consumidor deve conhecer todos os schemas e a função de transformação que atualizará cada schema na versão mais recente.
- Transformação In-Place: o schema de todos os eventos são alterados na Fonte Única de Verdade. Esse padrão é impossível de ser utilizado no Apache Kafka
- Copia-e-Transforma: um novo agrupamento é criado contendo mensagens usando o novo schema. O agrupamento anterior pode ser mantido.
Para se evoluir um schema AVRO, deve-se manter o nome dos campos e dos records, pois eles são usados na serialização para se associar o respectivo campo ao seu valor. Já uma mensagem Protocol Buffer tem sempre associado um identificador para cada campo, podendo assim serem renomeados. Deve-se sempre manter em mente que ao se atualizar um produtor, todos os consumidores devem ser compatíveis com esse novo schema.
Compatibilidade de Schemas
O Schema Registry da Confluente define alguns tipos de compatibilidade assim como uma biblioteca para checar se o schema atual é compatível com o anterior. Esse nível de compatibilidade pode se validar de uma alteração de Schema pode ser feita ou não.
Tipo de compatibilidade | Alterações permitidas | Nível de verificação | Quem atualizar primeiro |
---|---|---|---|
Backward |
|
Última versão | Consumidores |
Backward Transitive |
|
Todas as versões anteriores | Consumidores |
Forward |
|
Última versão | Produtores |
Forward Transitive |
|
Todas as versões anteriores | Produtores |
Full |
|
Última versão | Qualquer |
Full Transitive |
|
Todas as versões anteriores | Qualquer |
None |
|
Não é verificada compatibilidade | Depende |
De todas essas compatibilidades, somente a nenhuma (None) é preocupante, pois vai requerer que exista um plano de migração dos dados conforme já vimos.