Guia do Spring Cloud Stream com Kafka, Apache Avro e Registro de Esquema Confluent

Guia do Spring Cloud Stream com Kafka, Apache Avro e Registro de Esquema Confluent

1. Introdução

Apache Kafka is a messaging platform. Com ele, podemos trocar dados entre diferentes aplicativos em escala.

O Spring Cloud Stream é uma estrutura para criar aplicativos orientados a mensagens. It can simplify the integration of Kafka into our services.

Convencionalmente, o Kafka é usado com o formato de mensagem Avro, suportado por um registro de esquema. Neste tutorial, usaremos o Confluent Schema Registry. Tentaremos a implementação de integração do Spring com o Confluent Schema Registry e também com as bibliotecas nativas do Confluent.

2. Registro de esquema confluente

Kafka representa todos os dados como bytes, então é comum ause an external schema and serialize and deserialize into bytes de acordo com esse esquema. Em vez de fornecer uma cópia desse esquema com cada mensagem, o que seria uma sobrecarga cara, também é comum manter o esquema em um registro e fornecer apenas um id para cada mensagem.

O Confluent Schema Registry fornece uma maneira fácil de armazenar, recuperar e gerenciar esquemas. Ele expõe váriosRESTful APIs úteis.

Os esquemas são armazenados por assunto e, por padrão, o registro faz uma verificação de compatibilidade antes de permitir que um novo esquema seja carregado em um assunto.

Cada produtor saberá o esquema com o qual está produzindo e cada consumidor deve ser capaz de consumir dados em QUALQUER formato ou deve ter um esquema específico que prefere ler. The producer consults the registry to establish the correct ID to use when sending a message. The consumer uses the registry to fetch the sender’s schema. 

Quando o consumidor conhece o esquema do remetente e seu próprio formato de mensagem desejado, a biblioteca Avro pode converter os dados para o formato desejado pelo consumidor.

3. Apache Avro

Apache Avro is a data serialization system.

Ele usa uma estrutura JSON para definir o esquema, fornecendo serialização entre bytes e dados estruturados.

Um dos pontos fortes da Avro é o suporte à evolução de mensagens gravadas em uma versão de um esquema para o formato definido por um esquema alternativo compatível.

O conjunto de ferramentas Avro também é capaz de gerar classes para representar as estruturas de dados desses esquemas, facilitando a serialização dentro e fora dos POJOs.

4. Configurando o Projeto

Para usar um registro de esquema comSpring Cloud Stream, precisamos das dependências MavenSpring Cloud Kafka Bindereschema registry:


    org.springframework.cloud
    spring-cloud-stream-binder-kafka



    org.springframework.cloud
    spring-cloud-stream-schema

ParaConfluent’s serializer, precisamos:


    io.confluent
    kafka-avro-serializer
    4.0.0

E o Serializer do Confluent está em seu repo:


    
        confluent
        https://packages.confluent.io/maven/
    

Além disso, vamos usar umMaven plugin para gerar as classes Avro:


    
        
            org.apache.avro
            avro-maven-plugin
            1.8.2
            
                
                    schemas
                    generate-sources
                    
                        schema
                        protocol
                        idl-protocol
                    
                    
                        ${project.basedir}/src/main/resources/
                        ${project.basedir}/src/main/java/
                    
                
            
        
    

Para testar, podemos usar um Kafka existente e um Schema Registry configurado ou usar umdockerized Confluent and Kafka.

5. Spring Cloud Stream

Agora que configuramos nosso projeto, vamos escrever um produtor usandoSpring Cloud Stream. Ele publicará detalhes do funcionário sobre um tópico.

Em seguida, criaremos um consumidor que lerá eventos do tópico e os escreverá em uma declaração de log.

5.1. Esquema

Primeiro, vamos definir um esquema para os detalhes do funcionário. Podemos chamá-lo deemployee-schema.avsc.

Podemos manter o arquivo de esquema emsrc/main/resources:

{
    "type": "record",
    "name": "Employee",
    "namespace": "com.example.schema",
    "fields": [
    {
        "name": "id",
        "type": "int"
    },
    {
        "name": "firstName",
        "type": "string"
    },
    {
        "name": "lastName",
        "type": "string"
    }]
}

Depois de criar o esquema acima, precisamos construir o projeto. Em seguida, o gerador de código Apache Avro criará um POJO chamadoEmployee sob o pacotecom.example.schema.

5.2. Produtor

O Spring Cloud Stream fornece a interfaceProcessor. Isso nos fornece um canal de saída e entrada.

Vamos usar isso para fazer um produtor que envia objetosEmployee para o tópicoemployee-details Kafka:

@Autowired
private Processor processor;

public void produceEmployeeDetails(int empId, String firstName, String lastName) {

    // creating employee details
    Employee employee = new Employee();
    employee.setId(empId);
    employee.setFirstName(firstName);
    employee.setLastName(lastName);

    Message message = MessageBuilder.withPayload(employee)
                .build();

    processor.output()
        .send(message);
}

5.2. Consumidor

Agora, vamos escrever ao nosso consumidor:

@StreamListener(Processor.INPUT)
public void consumeEmployeeDetails(Employee employeeDetails) {
    logger.info("Let's process employee details: {}", employeeDetails);
}

Este consumidor lerá os eventos publicados no tópicoemployee-details. Vamos direcionar sua saída para o log para ver o que ele faz.

5.3. Kafka Bindings

Até agora, só trabalhamos contra os canaisinputeoutput de nosso objetoProcessor. Esses canais precisam ser configurados com os destinos corretos.

Vamos usarapplication.yml para fornecer as ligações Kafka:

spring:
  cloud:
    stream:
      bindings:
        input:
          destination: employee-details
          content-type: application/*+avro
        output:
          destination: employee-details
          content-type: application/*+avro

Devemos notar que, neste caso,destination means o tópico Kafka. Pode ser um pouco confuso que seja chamado dedestination, uma vez que é a fonte de entrada neste caso, mas é um termo consistente entre consumidores e produtores.

5.4. Ponto de entrada

Agora que temos nosso produtor e consumidor, vamos expor uma API para obter entradas de um usuário e passá-las para o produtor:

@Autowired
private AvroProducer avroProducer;

@PostMapping("/employees/{id}/{firstName}/{lastName}")
public String producerAvroMessage(@PathVariable int id, @PathVariable String firstName,
  @PathVariable String lastName) {
    avroProducer.produceEmployeeDetails(id, firstName, lastName);
    return "Sent employee details to consumer";
}

5.5. Habilitar o registro e as ligações do esquema confluente

Finalmente, para fazer nosso aplicativo aplicar as ligações de registro Kafka e de esquema, precisaremos adicionar@EnableBindinge@EnableSchemaRegistryClient em uma de nossas classes de configuração:

@SpringBootApplication
@EnableBinding(Processor.class)
@EnableSchemaRegistryClient
public class AvroKafkaApplication {

    public static void main(String[] args) {
        SpringApplication.run(AvroKafkaApplication.class, args);
    }

}

E devemos fornecer um beanConfluentSchemaRegistryClient:

@Value("${spring.cloud.stream.kafka.binder.producer-properties.schema.registry.url}")
private String endPoint;

@Bean
public SchemaRegistryClient schemaRegistryClient() {
    ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient();
    client.setEndpoint(endPoint);
    return client;
}

OendPoint é a URL para o Confluent Schema Registry.

5.6. Testando nosso serviço

Vamos testar o serviço com uma solicitação POST:

curl -X POST localhost:8080/employees/1001/Harry/Potter

Os logs nos dizem que isso funcionou:

2019-06-11 18:45:45.343  INFO 17036 --- [container-0-C-1] com.example.consumer.AvroConsumer       : Let's process employee details: {"id": 1001, "firstName": "Harry", "lastName": "Potter"}

5.7. O que aconteceu durante o processamento?

Vamos tentar entender o que exatamente aconteceu com nosso aplicativo de exemplo:

  1. O produtor construiu a mensagem Kafka usando o objetoEmployee

  2. O produtor registrou o esquema do funcionário no registro do esquema para obter um ID da versão do esquema, isso cria um novo ID ou reutiliza o existente para o esquema exato

  3. Avro serializou o objetoEmployee usando o esquema

  4. O Spring Cloud coloca o ID do esquema nos cabeçalhos da mensagem

  5. A mensagem foi publicada no tópico

  6. Quando a mensagem chegou ao consumidor, ela leu o ID do esquema no cabeçalho

  7. O consumidor usou schema-id para obter o esquemaEmployee do registro

  8. O consumidor encontrou uma classe local que poderia representar esse objeto e desserializou a mensagem para ele

6. Serialization/Deserialization Using Native Kafka Libraries

O Spring Boot fornece alguns conversores prontos para uso. By default, Spring Boot uses the Content-Type header to select an appropriate message converter.

Em nosso exemplo,Content-Type éapplication/+avro, Portanto, ele usouAvroSchemaMessageConverter para ler e gravar formatos Avro. Porém, a Confluent recomenda * usarKafkaAvroSerializereKafkaAvroDeserializer para conversão de mensagem.

Embora o próprio formato do Spring funcione bem, ele tem algumas desvantagens em termos de particionamento e não é interoperável com os padrões do Confluent, o que alguns serviços não Spring em nossa instância Kafka podem precisar ser.

Vamos atualizar nossoapplication.yml para usar os conversores Confluent:

spring:
  cloud:
    stream:
      default:
        producer:
          useNativeEncoding: true
        consumer:
          useNativeEncoding: true
      bindings:
        input:
          destination: employee-details
          content-type: application/*+avro
        output:
          destination: employee-details
          content-type: application/*+avro
      kafka:
         binder:
           producer-properties:
             key.serializer: io.confluent.kafka.serializers.KafkaAvroSerializer
             value.serializer: io.confluent.kafka.serializers.KafkaAvroSerializer
             schema.registry.url: http://localhost:8081
           consumer-properties:
             key.deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
             value.deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
             schema.registry.url: http://localhost:8081
             specific.avro.reader: true

Habilitamos ouseNativeEncoding. Força o Spring Cloud Stream a delegar a serialização nas classes fornecidas.

Também devemos saber como podemos fornecer propriedades de configurações nativas para Kafka dentro do Spring Cloud usandokafka.binder.producer-propertiesekafka.binder.consumer-properties.

7. Grupos de consumidores e partições

The consumer groups are the set of consumers belonging to the same application. Os consumidores do mesmo grupo de consumidores compartilham o mesmo nome do grupo.

Vamos atualizarapplication.yml para adicionar um nome de grupo de consumidores:

spring:
  cloud:
    stream:
      // ...
      bindings:
        input:
          destination: employee-details
          content-type: application/*+avro
          group: group-1
      // ...

Todos os consumidores distribuem as partições de tópicos entre eles uniformemente. Mensagens em diferentes partições serão processadas em paralelo.

In a consumer group, the max number of consumers reading messages at a time is equal to the number of partitions. Assim, podemos configurar o número de partições e consumidores para obter o paralelismo desejado. Em geral, devemos ter mais partições que o número total de consumidores em todas as réplicas do nosso serviço.

7.1. Chave de Partição

Ao processar nossas mensagens, a ordem em que elas são processadas pode ser importante. Quando nossas mensagens são processadas em paralelo, a sequência de processamento seria difícil de controlar.

Kafka fornece a regra dein a given partition, the messages are always processed in the sequence they arrived. Portanto, onde é importante que determinadas mensagens sejam processadas na ordem correta, garantimos que elas caiam na mesma partição uma da outra.

Podemos fornecer uma chave de partição ao enviar uma mensagem para um tópico. The messages with the same partition key will always go to the same partition. Se a chave da partição não estiver presente, as mensagens serão particionadas de maneira round-robin.

Vamos tentar entender isso com um exemplo. Imagine que estamos recebendo várias mensagens para um funcionário e queremos processar todas as mensagens de um funcionário na sequência. O nome do departamento e a identificação do funcionário podem identificar um funcionário exclusivamente.

Então, vamos definir a chave de partição com a id do funcionário e o nome do departamento:

{
    "type": "record",
    "name": "EmployeeKey",
    "namespace": "com.example.schema",
    "fields": [
     {
        "name": "id",
        "type": "int"
    },
    {
        "name": "departmentName",
        "type": "string"
    }]
}

Depois de construir o projeto, o POJOEmployeeKey será gerado no pacotecom.example.schema.

Vamos atualizar nosso produtor para usarEmployeeKey como uma chave de partição:

public void produceEmployeeDetails(int empId, String firstName, String lastName) {

    // creating employee details
    Employee employee = new Employee();
    employee.setId(empId);
    // ...

    // creating partition key for kafka topic
    EmployeeKey employeeKey = new EmployeeKey();
    employeeKey.setId(empId);
    employeeKey.setDepartmentName("IT");

    Message message = MessageBuilder.withPayload(employee)
        .setHeader(KafkaHeaders.MESSAGE_KEY, employeeKey)
        .build();

    processor.output()
        .send(message);
}

Aqui, estamos colocando a chave de partição no cabeçalho da mensagem.

Agora, a mesma partição receberá as mensagens com a mesma identificação de funcionário e nome do departamento.

7.2 Consumer Concurrency

O Spring Cloud Stream nos permite definir a simultaneidade para um consumidor emapplication.yml:

spring:
  cloud:
    stream:
      // ...
      bindings:
        input:
          destination: employee-details
          content-type: application/*+avro
          group: group-1
          concurrency: 3

Agora, nossos consumidores lerão três mensagens do tópico simultaneamente. Em outras palavras, o Spring gerará três threads diferentes para consumir independentemente.

8. Conclusão

Neste artigo, integramos um produtor e um consumidor contraApache Kafka with Avro schemas and the Confluent Schema Registry.

Fizemos isso em um único aplicativo, mas o produtor e o consumidor poderiam ter sido implantados em diferentes aplicativos e poderiam ter suas próprias versões dos esquemas, mantidas em sincronia pelo registro.

Vimos como usarSpring’s implementation of Avro and Schema Registry client,e vimos como alternar paraConfluent standard implementation de serialização e desserialização para fins de interoperabilidade.

Finalmente, vimos como particionar nosso tópico e garantir que temos as chaves de mensagem corretas para permitir o processamento paralelo seguro de nossas mensagens.

O código completo usado para este artigo pode ser encontrado emGitHub.