"Coluna Kafka" - 002 Explicação detalhada dos produtores Kafka

1. Fluxo de trabalho do produtor Kafka

No processo de envio de uma mensagem, existem dois threads trabalhando juntos - o thread principal e o thread do remetente . O encadeamento principal é responsável por processar os dados, especificando o local de envio e armazenando-os temporariamente.O encadeamento do remetente é responsável por transmitir os dados armazenados temporariamente para o Kafka Broker.

Foto de: Shang Vale do Silício

imagem-20220713114149675

1. Detalhes da rosca principal

1) Criar acumulador de registros

A thread principal criará um container, que por enquanto chamaremos de Record Accumulator . Seu tamanho padrão é 32m. Este é um buffer . Existe um container no buffer ConcurrentMap<TopicPartition, Deque<ProducerBatch>> batchespara armazenar os dados a serem enviados. A chave está apontando para a partição, Value armazena os dados a serem enviados, que ProducerBatché uma fila de duas extremidades.

imagem-20220713145905229

2) Processe os dados

Antes que os dados sejam armazenados no buffer, eles precisam ser processados ​​por interceptores, serializadores e particionadores :

  1. Os interceptores raramente são usados ​​no lado da produção, podemos personalizar as regras de interceptação para interceptar dados
  2. Serializadores são bem compreendidos, key.serializerpodemos value.serializerespecificar como serializar dados especificando e parâmetros
  3. O particionador determina a partição para a qual os dados precisam ser enviados analisando os parâmetros e os envia para o ProducerBatch correspondente

Fluxograma de trabalho do segmento principal, enviei alguns:

imagem-20220714150802855

2. Explicação detalhada do tópico do remetente

O encadeamento do remetente puxará os dados no ProduceBatch e, em seguida, enviará os dados para o Kafka Broker por meio da solicitação Http. Em seguida, o thread do remetente envolverá vários problemas:

  1. Quando extrair dados?
  2. Como posso confirmar que a mensagem foi enviada com sucesso?
  3. Quantas requisições simultâneas podem ser feitas?

1) Quando extrair os dados no ProducerBatch?

在生产者参数中,有一个 batch.size 项,默认是 16k,这个配置项就是控制 ProducerBatch 这个双端队列的大小,当数据累计到配置的值时,Sender 线程就会将里面的数据拉走。

但如果数据一直达不到配置的大小呢?总不能一直不拉取数据吧,这样在使用者看起来,消费者迟迟收不到生产的数据,这是不合理的,因此有另一个配置项 linger.ms,当数据迟迟达不到 batch.size 时,Sender 线程等待了超过 linger.ms 设置的时间,也会拉取数据,linger.ms 的默认值是 0ms,也就是说有数据就会被立即拉走。

2)如何确认消息发送成功?

在生产环境下,消息的发送往往都不是一帆风顺,如网络波动、Kafka Broker 挂掉,等情况都有可能导致消息持久化失败,这就涉及一个问题,在什么情况下 Producer 会认为消息已经发送成功了呢?这里引入一个参数 acks,它有三个可配置的值:

  1. acks=0:生产者将不会等待来自服务器的任何确认,该记录将立即添加到缓冲区并视为已发送
  2. acks=1(默认值):Leader 会将记录写入其本地日志,但无需等待所有副本服务器的完全确认即可做出回应,在这种情况下,如果 Leader 在确认记录后立即失败,则记录将会丢失
  3. acks=all:相当于 acks=-1,Leader 将等待完整的同步副本集以确认记录,这保证了只要至少一个同步副本服务器仍然存活,记录就不会丢失,这是最强有力的保证

如果害怕消息发送失败,还可以通过配置 retries 参数来激活重试机制,发送失败 Sender 线程会自动重试

3)可以同时进行多少个请求?

生产端的 Sender 线程会缓存一个请求队列,默认每个 Broker 最多可以缓存 5 个请求,可以通过配置 max.in.flight.requests.per.connection 值来改变。

由于在 Kafka 1.X 以后,Kafka 服务端可以缓存生产者发来的最近的五个请求元数据,所以在五个请求内,都能保证数据的顺序。

Sender 线程工作流程图,来自我寄几:

imagem-20220714152333176

二、生产者常用参数

这个小节列举一些生产者常用的配置项,有印象、了解即可

参数名 作用
key.serializervalue.serializer 指定发送消息的 key 和 value 的序列化类型,一定要写类的全限定名
buffer.memory 缓冲区 RecordAccumulator 总大小,默认 32m
batch.size 缓冲区内的批次队列 ProducerBatch 大小,默认 16k
linger.ms 如果数据迟迟未达到 batch.size,Sender 等待 linger.time
之后就会发送数据。默认值是 0ms,表示没
有延迟。生产环境一般设置为 50ms
acks 0:生产者发送过来的数据,不需要等数据落盘应答
1:生产者发送过来的数据,Leader 收到数据后应答
-1(默认值):生产者发送过来的数据,Leader 和 ISR 队列里面的所有节点收齐数据后应答
max.in.flight.requests.per.connection Sender 线程缓存的请求数,也是就是允许没有 ack 的请求次数,默认为 5
retries 消息发送失败后重试的次数,如果需要保证数据的顺序性
应该把 Sender 线程缓存的请求数设置为1,否则其他消息可能先发送成功
retry.backoff.ms 两次重试之间的时间间隔,默认是 100ms
compression.type 生者者发送数据的时候是否压缩,默认是 none,支持
gzip、snappy、lz4 和 zstd,生产环境一般使用 snappy

三、示例:使用 API 向 Kafka 发送消息

版本:

  • Kafka 3.2
  • kafka-client 3.2

引入依赖:

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.2.0</version>
</dependency>

1. 同步发送

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class BaseProducer {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
        //配置生产者
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "http://localhost:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);    
        
        // 往指定的 Topic 里面发送 数据 hello,同步发送
        RecordMetadata hello = producer.send(new ProducerRecord<>("topic-test", "hello")).get();
        System.out.println(hello);
        producer.close();
        }
}

2. 异步发送,带会回调函数

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

public class BaseProducer {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
        //配置生产者
        Properties properties = new Properties();
        properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "http://localhost:9092");
        properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);    
        
        // 往指定的 Topic 里面发送 数据 hello,同步发送
        producer.send(new ProducerRecord<>("topic-test", "hello"), new Callback() {
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
                if (exception != null) {
                    System.out.println("消息发送失败,原因:" + exception.getMessage());
                }
                System.out.println("消息发送成功," + metadata.offset() + metadata.hasOffset() + metadata.topic() + metadata.hasTimestamp() + metadata.timestamp() + metadata.partition());
            }
        });
        producer.close();
        }
}

四、生产者分区策略

**Kafka 为什么要分区?**分区有两个好处:

  1. Facilitar o uso racional dos recursos de armazenamento : um tópico pode ter várias partições, as partições podem ser distribuídas em diferentes brokers, os dados massivos são divididos em partes e armazenados em diferentes servidores e as tarefas das partições podem ser razoavelmente controladas para obter o efeito de carga equilibrando
  2. Melhorar o paralelismo : O produtor pode especificar uma partição para enviar dados e o consumidor pode especificar uma partição para consumo, que atinge o efeito semelhante ao multi-threading

1. Particionador padrão

O particionador padrão DefaultPartitionerdescreve a estratégia de particionamento padrão e seus comentários podem ser encontrados no código-fonte.

imagem-20220714231423531

Vou traduzir a tradução:

  1. Com partição : armazene dados diretamente na partição correspondente
  2. Não há partição com chave : um valor de partição Key的Hash值 % 主题的分区数será
  3. Sem partição e sem chave : Usando Sticky Partition (particionador fixo), uma partição será selecionada aleatoriamente e esta partição será usada o maior tempo possível. Quando o ProducerBatch da partição estiver cheio ou concluído, outras partições serão selecionadas aleatoriamente ( sem repetição use a última partição)

2. Particionador Personalizado

Em primeiro lugar, temos que criar um particionador. Ao implementar a org.apache.kafka.clients.producer.Partitionerinterface , podemos obter um particionador personalizado. Ao implementar o método, podemos personalizar as regras de particionamento:

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        int partition = 0;
        if (key.equals("1")) {
            partition = 1;
        }
        return partition;
    }
    
    @Override
    public void close() {}

    @Override
    public void configure(Map<String, ?> configs) {}
}

Ao criar um produtor, podemos configurar o particionador personalizado nos parâmetros:

Properties properties = new Properties();
properties.setProperty(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

Acho que você gosta

Origin juejin.im/post/7120511144703295502
Recomendado
Clasificación