Kafka Producer全流程分析和思考

1,323 阅读6分钟

Kafka Producer大家也许都比较熟悉,但是如果深究的话,估计有些细节还有模棱两可的地方,本文将结合工作上遇到的问题和源码分析来尽量说清楚Kafka Producer,以便大家在高并发的情况下清除如何去优化。

目录

一、示例代码

二、Producer流程分析

2.1消息发送到共享变量
2.2 共享变量的消息发送到borker
三、Producer 关键参数

四、总结

4.1 如何保证数据不丢失?
4.2 如何保证数据发送的实时性

4.3 一个ProducerBatch的大小

一、示例代码
首先我们看一下Kafka Producer同步发送和异步发送的测试代码

public class ProducerTest {
private Properties properties = new Properties();
private String key = "key";
private String value = "value";

    @Before
    public void before() {
        properties.put("client.id", "DemoProducer");

        // 以下三个参数必须指定
        // 用于创建与Kafka broker服务器的连接,集群的话则用逗号分隔
        properties.put("bootstrap.servers", "ec2-3-86-37-86.compute-1.amazonaws.com:9093");
        // 消息的key序列化方式
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // 消息的value序列化方式
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 以下参数为可配置选项
        properties.put("acks", "-1");
        properties.put("retries", "3");
        properties.put("batch.size", "323840");
        properties.put("linger.ms", "10");
        properties.put("buffer.memory", "33554432");
        properties.put("max.block.ms", "3000");
    }

    //异步发送
    @Test
    public void testAsyncSend() throws InterruptedException {
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);

        try {
            // 异步发送,发送过程不需要等待上一次的结果。当有结果返回时,会调用callback方法。
            producer.send(new ProducerRecord<>("logsystem_output", key, value), new WrapperCallback(value));
        } finally {
            producer.close();
        }
        Thread.sleep(10000);
    }

    //同步发送
    @Test
    public void testSyncSend() {
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        try {
            producer.send(new ProducerRecord<>("logsystem_output", key, value)).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            producer.close();
        }
     }
}

异步发送的Callback实现

public class WrapperCallback implements Callback {

    private String value;

    public WrapperCallback(String value) {
      this.value = value;
    }

    @Override
    public void onCompletion(RecordMetadata metadata, Exception e) {
      // 发送成功
      if (e == null) {
            System.out.println("success! ");
        // 发送失败
        } else {
            if (e instanceof RetriableException) {
                // RetriableException是可重试的异常,例如partition leader副本不可用
                // retry 逻辑
                System.out.println("retry! value:" + value);
            } else {
                  // 不可重试业务逻辑
                System.out.println("don't retry! value:" + value);
            }
        }
    }
}

二、Producer流程分析
通过如上的示例代码,我们可以看出Kafka 的 Producer本质上是通过异步的方式发送消息,想要同步的发送,只能通过get方法将线程阻塞达到同步发送的效果。

在消息发送的过程中,涉及到了 两个线程---主线程和Sender线程,以及一个线程共享变量——RecordAccumulator。主线程将消息发送给 RecordAccumulator 中的Deque(双端队列)的数据结构中,然后Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker。

kafka producer中配置的 buffer.memory (参数在文末有详细说明)参数是缓冲区的大小,这个缓存区大家也就是RecordAccmulator所用的内存大小。默认是32MB。

2.1 消息发送到共享变量
通过调用producer.send(),最终调用doSend()方法,源码如下

@Override
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
    ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
return doSend(interceptedRecord, callback);
}

doSend() 方法将消息append到RecordAccmulator对象的 batches 变量中。注意:此时并没有发送到kafka broker。batches 是RecordAccumulator对象的核心参数:

private final ConcurrentMap<TopicPartition, Deque> batches;
下面是doSend()方法最最重要的点:将数据append到RecordAccmulator对象。

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
        TopicPartition tp = null;
          try {
            ......
            RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs);

                 if (result.abortForNewBatch) {
                ......
                result = accumulator.append(tp, timestamp, serializedKey,
                    serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs);
            }
            ......
             return result.future;
          // handling exceptions and record the errors;
          // for API exceptions return them in the future,
          // for other exceptions throw directly
        } catch (ApiException e) {
            ......
        }
    }

accumulator.append的流程如下图:

2.2 共享变量的消息发送到borker
Sender和RecordAccumulator是在创建KafkaProducer的时候同时创建。Sender是一个线程,主要功能是将RecordAccumulator中的消息发送kakfa borker中。

Sender线程是从RecordAccumulator对象的batches变量中获取消息的,batches变量的数据结构如下图:

batches是一个Map结构,Key是:TopicPartition,即:表示Topic和Partition,Value这是要发送到该Topic和Partition的消息。由于Deque是一个双端队列,Sender线程不停的从队头获取数据发送到kafka borker。

三、Producer 关键参数

batch.size: kafka producer不是一条一条消息发送到broker的,而且是将多个数据组合成一个ProducerBatch,然后Sender一次性批量发送,相同分区多条消息集合叫batch,当batch满了则发送给broker。所以只要数据大小凑够batch.size的大小后就会发送。默认是16KB

linger.ms: 难道batch没满就不发了么?当然不是,不满则等linger.ms时间再发。

buffer.memory: producer启动会创建一个内存缓冲区保存待发送的消息,这部分的内存大小就是这个参数来控制的

max.request.size:client发送消息到broker的最大值,默认值:1MB,如果单条消息的数据量超过这个大小则报错。该参数和其他的参数有联动,例如:broker端的message.max.bytes参数,如果broker的message.max.bytes参数设置为10KB,而max.request.size设置为20KB,当发送一条大小为15B的消息时,生产者参数就会报错。

buffer.memory:producer启动会创建一个内存缓冲区保存待发送的消息,这部分的内存大小就是这个参数来控制的

commpression.type:压缩算法的选择,目前有GZIP、Snappy和LZ4。目前结合LZ4性能最好

request.timeout.ms:超过时间则会在回调函数抛出TimeoutException异常

partitioner.class:分区机制,可自定义,默认分区器的处理是:有key则用murmur2算法计算key的哈希值,对总分区取模算出分区号,无key则轮询

retries:重试次数,0.11.0.0版本之前可能导致消息重发

acks:有3个值,0、1和all(-1)

0:produce不关心broker端的处理结果,吞吐量最高

1:produce发送消息给leader broker端,broker端写入本地日志返回结果,折中方案

all(-1):配合min.insync.replicas使用,控制写入isr中的多少副本才算成功

四、总结

4.1 如何保证数据不丢失?
acks参数设置为 0 和 1 无法保证数据不丢失,设置为 0 的吞吐量是最高的,设置为 1 能保证数据已经发到leader broker端,如果一条消息发完后这台leader broker 宕机,consumer又正好没来得及消费,其他broker被选举为新的leader时,这条数据就丢失了。

如果acks设置为all(-1) ,这是可以保证数据不丢失的。我们可以如开头的示例中实现自己的callback,在callback中将失败的消息重新发送。重试实现的源代码将在之后的文章中贡献出来,更 多 内 容 关 注:xixiqiuqiu8。

4.2 如何保证数据发送的实时性
之前有个同事在测试过程中,发送一条数据后,要等10ms之后才能consumer到这条数据,后来发现是将linger.ms设置为10了。如果你对数据实时性要求,需要将linger.ms设置成可接受的范围内。

4.3 一个ProducerBatch的大小
文中提到的ProducerBatch大小就是batch.size参数的值,超过batch.size就会新建一个ProducerBatch