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
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>> batches
para 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.
2) Processe os dados
Antes que os dados sejam armazenados no buffer, eles precisam ser processados por interceptores, serializadores e particionadores :
- Os interceptores raramente são usados no lado da produção, podemos personalizar as regras de interceptação para interceptar dados
- Serializadores são bem compreendidos,
key.serializer
podemosvalue.serializer
especificar como serializar dados especificando e parâmetros - 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:
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:
- Quando extrair dados?
- Como posso confirmar que a mensagem foi enviada com sucesso?
- 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
,它有三个可配置的值:
acks=0
:生产者将不会等待来自服务器的任何确认,该记录将立即添加到缓冲区并视为已发送acks=1
(默认值):Leader 会将记录写入其本地日志,但无需等待所有副本服务器的完全确认即可做出回应,在这种情况下,如果 Leader 在确认记录后立即失败,则记录将会丢失acks=all
:相当于acks=-1
,Leader 将等待完整的同步副本集以确认记录,这保证了只要至少一个同步副本服务器仍然存活,记录就不会丢失,这是最强有力的保证
如果害怕消息发送失败,还可以通过配置
retries
参数来激活重试机制,发送失败 Sender 线程会自动重试
3)可以同时进行多少个请求?
生产端的 Sender 线程会缓存一个请求队列,默认每个 Broker 最多可以缓存 5 个请求,可以通过配置 max.in.flight.requests.per.connection
值来改变。
由于在 Kafka 1.X 以后,Kafka 服务端可以缓存生产者发来的最近的五个请求元数据,所以在五个请求内,都能保证数据的顺序。
Sender 线程工作流程图,来自我寄几:
二、生产者常用参数
这个小节列举一些生产者常用的配置项,有印象、了解即可
参数名 | 作用 |
---|---|
key.serializer 和 value.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 为什么要分区?**分区有两个好处:
- 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
- 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 DefaultPartitioner
descreve a estratégia de particionamento padrão e seus comentários podem ser encontrados no código-fonte.
Vou traduzir a tradução:
- Com partição : armazene dados diretamente na partição correspondente
- Não há partição com chave : um valor de partição
Key的Hash值 % 主题的分区数
será - 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.Partitioner
interface , 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);