Kafka 实战指南——Kafka 生产者配置

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 上以后的确认值。有三个可选项

  1. 0:表示 producer 不需要等待 broker 的消息确认。这个选项时延最小但同
    时风险最大(因为当 server 宕机时,数据将会丢失)。
  2. 1:表示 producer 只需要获得 kafka 集群中的 leader 节点确认即可,这个
    选择时延较小同时确保了 leader 节点确认接收成功。
  3. all(-1):需要 ISR 中所有的 Replica(副本)给予接收确认,速度最慢,安全性最高,
    但是由于 ISR 可能会缩小到仅包含一个 Replica,所以设置参数为 all 时并不能一定避免数据丢失。

一般建议选择acks=1,重要的服务可以设置acks=all

6. Batch

  1. batch.size

    生产者发送多个消息到 broker上的同一个分区时,为了减少网络请求带来的性能开销,通过批量的方式来提交消息,可以通过这个参数来控制批量提交的字节数大小,默认大小是16384byte,也就是16kb,意味着当一批消息大小达到指定的 batch.size的时候会统一发送。消息超过 16kb,将会动态申请内存,发送消息后回收动态内存。

  2. linger.ms

    Producer 默认会把两次发送时间间隔内收集到的所有 Requests 进行一次聚合然后再发送,以此提高吞吐量,而 linger.ms就是为每次发送到 broker 的请求增加一些延迟,以此来聚合更多的消息请求。

batch.sizelinger.ms这两个参数是 kafka 性能优化的关键参数,很多同学会发现 batch.sizelinger.ms这两者的作用是一样的,如果两个都配置了,那么怎么工作的呢?实际上,当二者都配置的时候,只要满足其中一个要求,就会发送请求到 broker上。batch.size有助于提高吞吐,linger.ms有助于控制延迟。您可以根据具体业务需求进行调整。

7. 单个请求的最大值

max.request.size:设置请求的数据的最大字节数,为了防止发生较大的数据包影响到吞吐量,默认值为1MB。超过 1MB 将会报错。

8. OOM

结合 Kafka 的 Batch 设计思路,Kafka 会缓存消息并打包发送,如果缓存太多,则有可能造成OOM(Out of Memory)。

  • buffer.memory: 所有缓存消息的总体大小超过这个数值后,就会触发把消息发往服务器。此时会忽略batch.sizelinger.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该阶段主要经历以下几个步骤:

  1. ProduceRequest
  2. AddOffsetsToTxnRequest
  3. TxnOffsetsCommitRequest

关于这里的详细介绍可以查看官网文档!

12.1.6 提交或者中断事务

Producer 调用 commitTransaction() 或者 abortTransaction() 方法来 commit 或者 abort 这个事务操作。

基本上经历以下三个步骤,才真正结束事务。

  1. EndTxnRequest
  2. WriteTxnMarkerRquest
  3. Writing the Final Commit or Abort Message

其中EndTxnRequest是在Producer发起的请求,其他阶段都是在TransactionCoordinator端发起完成的。WriteTxnMarkerRquest是发送请求到partition的leader上写入事务结果信息(ControlBatch),第三步主要是在_transaction_state中标记事务的结束。

猜你喜欢

转载自blog.csdn.net/dwjf321/article/details/114279581