文章目录
1. Key和Value
0.10.2.2版本的Kafka的消息字段只有两个:Key和Value。
- Key:消息的标识。
- Value:消息内容。
为了便于追踪,请为消息设置一个唯一的Key。您可以通过Key追踪某消息,打印发送日志和消费日志,了解该消息的发送和消费情况。
2. 失败重试
分布式环境下,由于网络等原因偶尔发送失败是常见的。导致这种失败的原因可能是消息已经发送成功,但是Ack失败,也有可能是确实没发送成功。
消息队列Kafka版是VIP网络架构,会主动断开空闲连接(30秒没活动),因此,不是一直活跃的客户端会经常收到 “connection rest by peer” 错误,建议重试消息发送。
重试参数
您可以根据业务需求,设置以下重试参数:
retries
,重试次数,建议设置为3
。失败重试将会导致数据乱序,如果要保证消息有序,设置max.in.flight.requests.per.connection =1
,这样就能保证消息有序性,但是会影响生产者吞吐量。所以只有在对消息的顺序有严格要求的情况下才能这么做。retry.backoff.ms
,重试间隔,建议设置为1000
。
3. 异步发送
发送接口是异步的,如果您想得到发送的结果,可以调用使用回调函数:
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (recordMetadata == null) {
e.printStackTrace();
}else {
long offset = recordMetadata.offset();
System.out.println("sender success:"+offset);
}
}
});
4. 线程安全
Producer 是线程安全的,且可以往任何 Topic 发送消息。通常情况下,一个应用对应一个 Producer 就足够了。
5. Acks
acks
配置表示 producer 发送消息到 broker 上以后的确认值。有三个可选项
0
:表示 producer 不需要等待 broker 的消息确认。这个选项时延最小但同
时风险最大(因为当 server 宕机时,数据将会丢失)。1
:表示 producer 只需要获得 kafka 集群中的 leader 节点确认即可,这个
选择时延较小同时确保了 leader 节点确认接收成功。all(-1)
:需要 ISR 中所有的 Replica(副本)给予接收确认,速度最慢,安全性最高,
但是由于 ISR 可能会缩小到仅包含一个 Replica,所以设置参数为 all 时并不能一定避免数据丢失。
一般建议选择acks=1
,重要的服务可以设置acks=all
。
6. Batch
-
batch.size
生产者发送多个消息到 broker上的同一个分区时,为了减少网络请求带来的性能开销,通过批量的方式来提交消息,可以通过这个参数来控制批量提交的字节数大小,默认大小是16384byte,也就是16kb,意味着当一批消息大小达到指定的
batch.size
的时候会统一发送。消息超过 16kb,将会动态申请内存,发送消息后回收动态内存。 -
linger.ms
Producer 默认会把两次发送时间间隔内收集到的所有 Requests 进行一次聚合然后再发送,以此提高吞吐量,而
linger.ms
就是为每次发送到 broker 的请求增加一些延迟,以此来聚合更多的消息请求。
batch.size
和 linger.ms
这两个参数是 kafka 性能优化的关键参数,很多同学会发现 batch.size
和linger.ms
这两者的作用是一样的,如果两个都配置了,那么怎么工作的呢?实际上,当二者都配置的时候,只要满足其中一个要求,就会发送请求到 broker上。batch.size
有助于提高吞吐,linger.ms
有助于控制延迟。您可以根据具体业务需求进行调整。
7. 单个请求的最大值
max.request.size
:设置请求的数据的最大字节数,为了防止发生较大的数据包影响到吞吐量,默认值为1MB。超过 1MB 将会报错。
8. OOM
结合 Kafka 的 Batch 设计思路,Kafka 会缓存消息并打包发送,如果缓存太多,则有可能造成OOM(Out of Memory)。
buffer.memory
: 所有缓存消息的总体大小超过这个数值后,就会触发把消息发往服务器。此时会忽略batch.size
和linger.ms
的限制。buffer.memory
的默认数值是32 MB,对于单个 Producer 来说,可以保证足够的性能。 需要注意的是,如果您在同一个JVM中启动多个 Producer,那么每个 Producer 都有可能占用 32 MB缓存空间,此时便有可能触发 OOM。- 在生产时,一般没有必要启动多个 Producer;如果特殊情况需要,则需要考虑
buffer.memory
的大小,避免触发 OOM。
9. 分区顺序
单个分区(Partition)内,消息是按照发送顺序储存的,是基本有序的。
默认情况下,消息队列 Kafka 为了提升可用性,并不保证单个分区内绝对有序,发生消息重试或者宕机时,会产生消息乱序(某个分区挂掉后把消息 Failover 到其它分区)。
10. 顺序保证
Kafka 可以保证同一个分区里的消息是有序的。也就是说,如果生产者按照一定的顺序发送消息,broker 就会按照这个顺序把它们写入分区,消费者也会按照同样的顺序读取它们。
在某些情况下,顺序是非常重要的。例如,往一个账户存入 100 元再取出来,这个与先取钱再存钱是截然不同的。不过,有些场景对顺序不是很敏感。
如果把 retries
设为非零整数,同时把 max.in.flight.requests.per.connection
设为比 1 大的数,那么,如果第一个批次消息写入失败,而第二个批次写入成功,broker 会重试写入第一个批次。如果此时第一个批次也写入成功,那么两个批次的顺序就反过来了。
一般来说,如果某些场景要求消息是有序的,那么消息是否写入成功也是很关键的,所以不建议把 retries
设为 0。可以把 max.in.flight.requests.per.connection
设为 1,这样在生产者尝试发送第一批消息时,就不会有其他的消息发送给 broker。不过这样会严重影响生产者的吞吐量,所以只有在对消息的顺序有严格要求的情况下才能这么做。
11. Producer 幂等性
11.1 Producer 幂等性设置
Producer 的幂等性指的是当发送同一条消息时,数据在 Server 端只会被持久化一次,数据不丟不重,但是这里的幂等性是有条件的:
- 只能保证 Producer 在单个会话内不丟不重,如果 Producer 出现意外挂掉再重启是无法保证的(幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重);
- 幂等性不能跨多个 Topic-Partition,只能保证单个 partition 内的幂等性,当涉及多个 Topic-Partition 时,这中间的状态并没有同步。
如果需要跨会话、跨多个 topic-partition 的情况,需要使用 Kafka 的事务性来实现。
使用方式:props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
当幂等性开启的时候acks即为all。如果显性的将acks设置为0,-1,那么将会报错Must set acks to all in order to use the idempotent producer. Otherwise we cannot guarantee idempotence.
示例如下:
// 保证 producer 的幂等性使用 这2个参数,该幂等性只能保证分区内幂等。producer重启失效。
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
props.put(ProducerConfig.ACKS_CONFIG,"all");
11.2 幂等性原理
幂等性是通过两个关键信息保证的,PID(Producer ID) 和 sequence numbers。
- PID 用来标识每个producer client
- sequence numbers 客户端发送的每条消息都会带相应的 sequence number,Server 端就是根据这个值来判断数据是否重复
producer初始化会由server端生成一个PID,然后发送每条信息都包含该PID和sequence number,在server端,是按照partition同样存放一个sequence numbers 信息,通过判断客户端发送过来的sequence number与server端number+1差值来决定数据是否重复或者漏掉。
通常情况下为了保证数据顺序性,我们可以通过max.in.flight.requests.per.connection=1
来保证,这个也只是针对单实例。在kafka2.0+版本上,只要开启幂等性,不用设置这个参数也能保证发送数据的顺序性。
11.3 原因分析
为什么要求 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 小于等于5
其实这里,要求 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 小于等于 5 的主要原因是:Server 端的 ProducerStateManager 实例会缓存每个 PID 在每个 Topic-Partition 上发送的最近 5 个batch 数据(这个 5 是写死的,至于为什么是 5,可能跟经验有关,当不设置幂等性时,当这个设置为 5 时,性能相对来说较高,社区是有一个相关测试文档),如果超过 5,ProducerStateManager 就会将最旧的 batch 数据清除。
假设应用将 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 设置为 6,假设发送的请求顺序是 1、2、3、4、5、6,这时候 server 端只能缓存 2、3、4、5、6 请求对应的 batch 数据,这时候假设请求 1 发送失败,需要重试,当重试的请求发送过来后,首先先检查是否为重复的 batch,这时候检查的结果是否,之后会开始 check 其 sequence number 值,这时候只会返回一个 OutOfOrderSequenceException 异常,client 在收到这个异常后,会再次进行重试,直到超过最大重试次数或者超时,这样不但会影响 Producer 性能,还可能给 Server 带来压力(相当于client 狂发错误请求)。
12. Producer 开启事务
12.1 Producer 事务示例
package com.mock.data.stream.kafka;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* https://www.cnblogs.com/fnlingnzb-learner/p/13646390.html
* producer 启用事务
*/
public class ProducerSenderTransactional {
public ProducerSenderTransactional() {
producer = new KafkaProducer<String, String>(producerConfig());
// 初始化事务
producer.initTransactions();
}
public Map<String, Object> producerConfig(){
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092,hadoop102:9094,hadoop103:9092");
props.put(ProducerConfig.CLIENT_ID_CONFIG,"flink_id");
props.put(ProducerConfig.RETRIES_CONFIG, "3");
props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG,1048576);// 1M
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);// 16kb
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);//默认为0
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432L);// 32M
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 保证 producer 的幂等性使用 这2个参数,该幂等性只能保证分区内幂等。producer重启失效。
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
props.put(ProducerConfig.ACKS_CONFIG,"all");
// 事务ID
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transactionalId_0");
// 保证消息顺序性
props.put("max.in.flight.requests.per.connection",1);
return props;
}
KafkaProducer<String, String> producer;
public void send(String topic, String key, String value) {
ProducerRecord<String, String> record = new ProducerRecord<>(topic, key,value);
producer.beginTransaction();
boolean success = true;
try {
RecordMetadata recordMetadata = producer.send(record).get(3, TimeUnit.SECONDS);
long offset = recordMetadata.offset();
System.out.println("发送成功:"+offset);
if (key.contains("1")) {
throw new RuntimeException("发送失败");
}
}catch (Exception e) {
e.printStackTrace();
success = false;
} finally {
if (success) {
// 提交事务
producer.commitTransaction();
} else {
// 中断事务
producer.abortTransaction();
}
}
}
public static void main(String[] args) throws Exception{
ProducerSenderTransactional sender2 = new ProducerSenderTransactional();
String topic1 = "topic1";
for (int i = 0; i < 10; i++) {
String key = "key_"+i;
String value = "value"+i;
sender2.send(topic1,key,value);
}
Thread.sleep(600000);
}
}
12.1.2 查找TransactionCoordinator事务实现原理
通过transaction_id 找到TransactionCoordinator,具体算法是Utils.abs(transaction_id.hashCode %transactionTopicPartitionCount )
,获取到partition,再找到该partition的leader,即为TransactionCoordinator。
12.1.3 获取PID
凡是开启幂等性都是需要生成PID(Producer ID),只不过未开启事务的PID可以在任意broker生成,而开启事务只能在TransactionCoordinator节点生成。这里只讲开启事务的情况,Producer Client的initTransactions()
方法会向TransactionCoordinator发起InitPidRequest ,这样就能获取PID。这里面还有一些细节问题,这里不探讨,例如transaction_id 之前的事务状态什么的。但需要说明的一点是这里会将 transaction_id 与相应的 TransactionMetadata 持久化到事务日志(_transaction_state)中。
12.1.4 开启事务
Producer调用beginTransaction
开始一个事务状态,这里只是在客户端将本地事务状态转移成 IN_TRANSACTION,只有在发送第一条信息后,TransactionCoordinator才会认为该事务已经开启。
12.1.5 Consume-Porcess-Produce Loop
这里说的是一个典型的consume-process-produce
场景:
while (true) {
ConsumerRecords records = consumer.poll(Duration.ofMillis(1000));
producer.beginTransaction();
//start
for (ConsumerRecord record : records){
producer.send(producerRecord(“outputTopic1”, record));
producer.send(producerRecord(“outputTopic2”, record));
}
producer.sendOffsetsToTransaction(currentOffsets(consumer), group);
//end
producer.commitTransaction();
}
AddPartitionsToTxnRequest该阶段主要经历以下几个步骤:
- ProduceRequest
- AddOffsetsToTxnRequest
- TxnOffsetsCommitRequest
关于这里的详细介绍可以查看官网文档!
12.1.6 提交或者中断事务
Producer 调用 commitTransaction()
或者 abortTransaction()
方法来 commit 或者 abort 这个事务操作。
基本上经历以下三个步骤,才真正结束事务。
- EndTxnRequest
- WriteTxnMarkerRquest
- Writing the Final Commit or Abort Message
其中EndTxnRequest是在Producer发起的请求,其他阶段都是在TransactionCoordinator端发起完成的。WriteTxnMarkerRquest是发送请求到partition的leader上写入事务结果信息(ControlBatch),第三步主要是在_transaction_state
中标记事务的结束。