Kafka学习--- Kafka Producer生产者

本文详细介绍了Kafka Producer的基本概念,包括执行过程、重要参数和消息发送方式。重点讨论了Kafka Producer的配置参数,如batch.num.messages、request.required.acks等,并解释了同步与异步发送的区别。此外,还深入解析了消息发送原理,涉及拦截器、序列化、分区器以及Sender线程的工作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里主要是记录学习使用,介绍 Kafka Producer基本使用和基本原理。

从编程的角度而言, 生产者就是负责向 Kafka发送消息的应用程序。

一、基本概念

1.1、Kafka Producer 的执行过程

Kafka producer 的正常生产逻辑包含以下几个步骤:

  1. 配置生产者客户端参数常见生产者实例。

  2. 构建待发送的消息。

  3. 发送消息。

  4. 关闭生产者实例。

代码实现:

public class Producer {
    public static void main(String[] args) {
        //配置信息
        Properties props = new Properties();
        //kafka服务器地址
        props.put("bootstrap.servers", "localhost:9092");
        //设置数据key和value的序列化处理类
        props.put("key.serializer", StringSerializer.class);
        props.put("value.serializer", StringSerializer.class);
        //创建生产者实例
        KafkaProducer<String,String> producer = new KafkaProducer<>(props);
         String msg="dukun kafka practice msg";
        //异步回调通知
        producer.send(new ProducerRecord<>("topicName", msg), (metadata, exception) -> {

                    System.out.println(metadata.offset()+"->"+metadata.partition()+"->"+metadata.topic());
                });
        //发送记录
        producer.send(record);
        producer.close();
    }
}

KafkaProducer是线程安全的,可以在多个线程中共享单个KafkaProducer实例,也可以将 KafkaProducer实例进行池化来供其他线程调用。 

1.2、重要参数

  • batch.num.messages

    默认值:200,每次批量消息的数量,只对 asyc 起作用。

  • request.required.acks

    默认值:0,0 表示 producer 无须等待 leader 的确认,1 代表需要 leader 确认写入它的本地 log 并立即确认,-1 代表所有的副本都完成后确认。只对 async 模式起作用,这个参数的调整是数据不丢失和发送效率的 tradeoff,如果对数据丢失不敏感而在乎效率的场景可以考虑设置为 0,这样可以大大提高 producer 发送数据的效率。

  • request.timeout.ms

    默认值:10000,确认超时时间。

  • partitioner.class

    默认值:kafka.producer.DefaultPartitioner,必须实现 kafka.producer.Partitioner,根据 Key 提供一个分区策略。有时候我们需要相同类型的消息必须顺序处理,这样我们就必须自定义分配策略,从而将相同类型的数据分配到同一个分区中。

  • producer.type

    默认值:sync,指定消息发送是同步还是异步。异步 asyc 成批发送用 kafka.producer.AyncProducer, 同步 sync 用 kafka.producer.SyncProducer。同步和异步发送也会影响消息生产的效率。

  • compression.topic

    默认值:none,消息压缩,默认不压缩。其余压缩方式还有,"gzip"、"snappy"和"lz4"。对消息的压缩可以极大地减少网络传输量、降低网络 IO,从而提高整体性能。

  • compressed.topics

    默认值:null,在设置了压缩的情况下,可以指定特定的 topic 压缩,未指定则全部压缩。

  • message.send.max.retries

    默认值:3,消息发送最大尝试次数。

  • retry.backoff.ms

    默认值:300,每次尝试增加的额外的间隔时间。

  • topic.metadata.refresh.interval.ms

    默认值:600000,定期的获取元数据的时间。当分区丢失,leader 不可用时 producer 也会主动获取元数据,如果为 0,则每次发送完消息就获取元数据,不推荐。如果为负值,则只有在失败的情况下获取元数据。

  • queue.buffering.max.ms

    默认值:5000,在 producer queue 的缓存的数据最大时间,仅仅 for asyc。

  • queue.buffering.max.message

    默认值:10000,producer 缓存的消息的最大数量,仅仅 for asyc。

  • queue.enqueue.timeout.ms

    默认值:-1,0 当 queue 满时丢掉,负值是 queue 满时 block, 正值是 queue 满时 block 相应的时间,仅仅 for asyc。

1.3、消息的发送

    在创建完生产者实例之后,接下来的工作就是构建消息,即创建ProducerRecord对象。 通过上面代码了解了ProducerRecord的属性结构, 其中topic属性和value属 性是必填项, 其余属性是选填项。

创建生产者实例和构建消息之后, 就可以开始发送消息了。 发送消息主要有三种模式: 发后即忘(fire-and-forget)、 同步(sync)及异步Casync)。 

KafkaProducer的 send()方法并非是void类型,而是Future<RecordMetadata>类型,实际上send()方法本身就是异步的,send()方法返回的Future对象可以使调用方稍后获得发 送的结果。 同步发送执行send()方法之后直接链式调用了get()方法来阻塞等待Kaflca的响应, 直到消息发送成功, 或者发生异常。 如果发生异常,那么就需要捕获异常并交由外层逻辑处理

send() 方法有2个重载方法,具体定义如下: 

//
 public Future<RecordMetadata> send(ProducerRecord<K, V> record) {
        return this.send(record, (Callback)null);
    }
//异步发送带有回调函数的
 public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
        return this.doSend(interceptedRecord, callback);
    }

同步发送消息

//同步阻塞发送
 ProducerRecord recordTobng=new ProducerRecord<String, String>(topic, "userName", "测试");
  Future<RecordMetadata> sendResult = producer.send(recordTobng);
  RecordMetadata metadata =sendResult.get();
  System.out.println(metadata.topic() + "-" + metadata.partition() + ":" + metadata.offset());

异步回调方法发送消息

producer.send(new ProducerRecord<>(topic, msg), new Callback() {
	//
	@Override
	public void onCompletion(RecordMetadata recordMetadata, Exception e) {
		//TODO 发送异常可以在这里处理
		if(null!=e) {
			e.printStackTrace();
		}else {
			System.out.println(recordMetadata.offset()+"->"+recordMetadata.partition()+"->"+recordMetadata.topic());
		}
	}
});

      示例代码中遇到异常时(exception!=null)只是做了简单的打印操作,在实际应用中应该使用更加稳妥的方式来处理,比如可以将异常记录以便日后分析,也可以做一定的处理来进行消息重发。 onCompletion()方法的两个参数是互斥的, 消息发送成功时, metadata不为null而 exception为null: 消息发送异常时, metadata为null而exception不为null。 

    对于同一个分区而言,如果消息record1于record2之前先发送, 那么KafkaProducer 就可以保证对应的callback1在callback2之前调用 ,也就是说, 回调函数的调用也可以保证分区有序。 

二、发送原理分析

     消息在真正发往 Kafka 之前,需要经历拦截器(lnterceptor)、 序列化器 (Serializer)和分区器(Partitioner)等一系列的作用,最终由累加器批量发送至 Broker。 下面我们来看一下生产者客户端的整体架构 。

              

     整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和 Sender 线程(发送线 程)。在主线程中KafkaProducer 创建消息,然后通过可能的拦截器、序列化器和分区器的作 用之后缓存到消息累加器RecordAccumulator,也称为消息收集器〉中。 Sender 线程负责从 RecordAccumulator 中获取消息并将其发送到 Kafka 中 。

2.1、主线程 过程

1、生产者拦截器

    拦截器 (Interceptor)是早在 Kafka 0.10.0.0 中就已经引入的一个功能, Kafka 一共有两种拦 截器: 生产者拦截器和消费者拦截器。

    生产者拦截器既可以用来在消息发送前做一些准备工作, 比如按照某个规则过滤不符合要求的消息、修改消息的内容等, 也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。

     生产者拦截器的使用也很方便,主要是自定义实现org.apache.kafka.clients.producer.ProducerInterceptor接口。 ProducerInterceptor 接口中包含 3 个方法:

public interface ProducerInterceptor<K, V> extends Configurable {
    //发消息之前
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> var1);

    //KafkaProducer 会在消息被应答(Acknowledgement)之前或消息发送失败时调用生产者拦 截器的
    void onAcknowledgement(RecordMetadata var1, Exception var2);
    //close()方法主要用于在关闭拦截器时执行一些资源的清理工作。在这 3 个方法中抛出的异 常都会被捕获并记录到日志中,但并不会再向上传递。
    void close();
}

下面通过一个示例来演示生产者拦截器的具体用法 , ProducerlnterceptorPrefix 中通过 onSend()方法来为每条消息添加一个前缀“prefixl-”,井且通过 onAcknowledgement()方法来计 算发送消息的成功率。 ProducerlnterceptorPrefix 类的具体实现如代码清单所示。
 

public class ProducerinterceptorPrefix implements ProducerInterceptor {

    private volatile long sendSuccess = 0;
    private volatile long sendFailure = 0;

    @Override
    public ProducerRecord onSend(ProducerRecord record) {
        String modifiedValue = "prefixl-" + record.value();
        return new ProducerRecord<>(record.topic(),record.partition(), record.timestamp(),
                record.key() , modifiedValue, record.headers()) ;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception == null) {
            sendSuccess++;
          } else sendFailure++;
        }

    @Override
    public void close() {
        double successRat工O = (double)sendSuccess /(sendFailure + sendSuccess);
        System.out.println("[ INFO]发送成功率="+String.format("%f", successRat工O * 100)+"%") ;

    }

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

    }
}

2、序列化

         生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka。  在 消费者需要用反序列化器(Deserializer)把从Kafka 中收到的字节数组转换成相应的 对象。 

3、分区器

      消息经过序列化之后就需要确 定它发往的分区 ,如果消息ProducerRecord中指定了partition字段, 那么就不需要分区器 的作用, 因为partition代表的就是所要发往的分区号。 如果消息ProducerRecord中没有指定partition字段,那么就需要依赖分区器,根据key 这个字段来计算partition的值。 分区器的作用 就是为消息分配分区。 Kafka 中提供的默认分区器是org.apache.kafka.clients.producer.intemals.DefaultPart山oner, 它 实现了org.apache.kafka.clients.producer.Partitioner 接口

     在默认分区器 DefaultPartitioner 的实现中, close()是空方法,而在 partition()方法中定义了 主要的分区分配逻辑。 如果 key 不为 null,那么默认的分区器会对 key 进行哈希(采用 MurmurHash2 算法,具备高运算性能及低碰撞率),最终根据得到的哈希值来计算分区号, 拥 有相同 key 的消息会被写入同一个分区。 如果 key 为 null,那么消息将会以轮询的方式发往主 题内的各个可用分区。 

自定义分区器:

public class MyPartition implements Partitioner {

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        System.out.println("enter");
        List<PartitionInfo> list=cluster.partitionsForTopic(topic);
        int leng=list.size();
        if(key==null){
            Random random=new Random();
            return random.nextInt(leng);
        }
        return Math.abs(key.hashCode())%leng;
    }

    @Override
    public void close() {

    }

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

    }

    public static void main(String[] args) {
        System.out.println(Math.abs("gp-gid1".hashCode())%50);
    }
}

二、Sender 线程 (发送线程)

      RecordAccumulator 主要用来缓存消息以便 Sender 线程可以批量发送,进而减少网络传输 的资源消耗以提升性能 。 RecordAccumulator 缓存的大 小可以通过生产者客户端参数 buffer.memory 配置,默认值为 33554432B,即 32MB。 如果生产者发送消息的速度超过发 送到服务器的速度,则会导致生产者空间不足,这个时候 KafkaProducer 的 send()方法调用要么 被阻塞,要么抛出异常,这个取决于参数 max.block .ms 的配置,此参数的默认值为 60000, 即 60 秒。 

   主线程中发送过来的消息都会被迫加到 RecordAccumulator 的某个双端队列(Deque)中, 在 RecordAccumulator 的内部为每个分区都维护了 一 个双端队列,队列中的内容就是 Producer Batch,即 Deque<ProducerBatch>。消息写入缓存时,追加到双端队列的尾部: Sender读取消息时,从双端队列的头部读取。注意 ProducerBatch 不是 ProducerRecord, ProducerBatch 中可以包含一至多个 ProducerRecord。 通俗地说, ProducerRecord 是生产者中创建的消息,而 Producer Batch 是指一个消息批次, ProducerRecord 会被包含在 ProducerBatch 中,这样可以使宇 节的使用更加紧凑。与此同时,将较小的 ProducerRecord 拼凑成一个较大的 ProducerBatch,也 可以减少网络请求的次数以提升整体的吞吐量。 ProducerBatch 和消息的具体格式有关。如果生产者客户端需要向很多分区发送消息, 则可以 将 buffer.memory 参数适当调大以增加整体的吞吐量

     消息在网络上都是以字节 (Byte)的形式传输的,在发送之前需要创建一块内存区域来保存对应的消息。在 Kafka 生产者客户端中,通过java.io.ByteBuffer 实现消息内存的创建和释放。 不过频繁的创建和释放是比较耗费资源的,在 RecordAccumulator 的内部还有一个 BufferPool, 它主要用来实现 ByteBuffer 的复用,以实现缓存的高效利用 。不过 BufferPool 只针对特定大小 的 ByteBuffer 进行管理,而其他大小的 ByteBuffer 不会缓存进 Bu他rPool 中,这个特定的大小 由 batch.size 参数来指定,默认值为 16384B,即 16KB。 我们可以适当地调大 batch.size 参数以便多缓存一些消息。

   ProducerBatch 的大小和 batch.size 参数也有着密切的关系。当一条消息(ProducerRecord) 流入 RecordAccumulator 时,会先寻找与消息分区所对应的双端队列(如果没有则新建),再从 这个双端队列的尾部获取一个 ProducerBatch (如果没有则新建),查看 ProducerBatch 中是否 还可以写入这个 ProducerRecord ,如 果可以 则 写入,如果不可 以则 需要创 建一个新 的 Producer Batch。在新建 ProducerBatch 时评估这条消息的大小是否超过 batch.size 参数的大小,如果不超过,那么就以 batch . size 参数的大小来创建 ProducerBatch,这样在使用完这 段内存区域之后,可以通过 BufferPool 的管理来进行复用;如果超过,那么就以评估的大小来 创建 ProducerBatch, 这段内存区域不会被复用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值