二、kafka生产者

1. 客户端

kafka生产者是线程安全的。

直接上代码

import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class ProducerDemo {

    public static void main(String[] args) {
        String server = "127.0.0.1:9092";
        String topic = "demo";
        String value = "message";

        // 参数配置
        Properties properties = new Properties();
        properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, server);
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        // 初始化
        Producer<String, String> producer = new KafkaProducer<>(properties);

        // 构造消息
        ProducerRecord<String, String> record = new ProducerRecord<>(topic, value);

        // 发送消息
        producer.send(record);

        // 回收资源
        producer.close();
    }

}

1.1 必要参数

可以直接使用CommonClientConfigsProducerConfig定义的参数变量名。

  • bootstrap.servers:用来指定kafka集群地址,格式为:host1:port1,host2:port2,多个用",“隔开,默认值为”"。
  • key.serializer和value.serializer:消息发送的时候,需要将消息转换成字节数组(byte[])。KafkaProducer<String, String>ProducerRecord<String, String>中的泛型<String,String>对应的就是消息的key、value,通过这两个参数执行消息的序列化器。
  • client.id:用来指定生产者的客户端id,默认"",不设置KafkaProducer会自动生成一个非空字符串,内容为:“producer-1”、“producer-2”

1.2 消息发送

生产者实例创建完成之后,接下来就是构建消息,即创建ProducerRecord对象了,ProducerRecord有很多的构造方法。

public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers);
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value);
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers);
public ProducerRecord(String topic, Integer partition, K key, V value);
public ProducerRecord(String topic, K key, V value);
public ProducerRecord(String topic, V value);

上面的实例使用的是最后的那个构造方法,最简单的。需要注意的是:ProducerRecord的创建是一个很频繁的动作。

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

上面的示例就是使用的发后即忘的模式,发只管往kafka发送消息, 并不关心消息是否正确送达。通常来讲,这种方式没有问题,不过在kafka发生了不可重试异常的时候会造成数据的丢失。这种方式是性能最高的,也是可靠性最差的。

KafkaProducer的send()方法井非是void类型,它有两个重载。

Future<RecordMetadata> send(ProducerRecord<K, V> record);
Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback);

同步发送代码如下:

try {
    producer.send(record).get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

或者

Future<RecordMetadata> future = producer.send(record);                                                            
try {                                                                                                             
    RecordMetadata recordMetadata = future.get();                                                                 
    System.out.println(recordMetadata.topic() + ":" + recordMetadata.partition() + ":" + recordMetadata.offset());
} catch (InterruptedException | ExecutionException e) {                                                           
    e.printStackTrace();                                                                                          
}                                                                                                                 

实际上send()方法本身就是异步的send()返回的Futureget()方法则会阻塞kafka的响应。get()返回的RecordMetadata对象包含了一些消息的元数据信息,如:主题、分区、偏移量等。

KafkaProducer中一般会发生两种类型的异常:可重试的异常和不可重试的异常。常见的可重试异常有:NetworkException、LeaderNotAvailableException、UnknownTopicOrPartitionException、NotEnoughReplicasException、NotCoordinatorException等。比如NetworkException表示网络异常,这个有可能是由于网络瞬时故障而导致的异常,可以通过重试解决;又比如LeaderNotAvailableException表示分区的leader副本不可用,这个异常通常发生在leader副本下线而新的leader副本选举完成之前,重试之后可以重新恢复。不可重试的异常,比如1.4节中提及的RecordTooLargeException异常,暗示了所发送的消息太大,KafkaProducer对此不会进行任何重试,直接抛出异常。

对于可重试的异常,如果配置了retries参数,那么只要在规定的重试次数内自行恢复了,就不会抛出异常。retries参数的默认值为0,配置方式参考如下:

properties.put(ProducerConfig.RETRIES_CONFIG, 3);

同步发送的方式可靠性高,要么消息发送成功,要么消息异常。发生异常,可以捕获并作出相应的处理,不会像“发后即忘”那样造成消息丢失。同步发送性能差很多,需要阻塞等待一条消息发送完成后才能发送下一条。

再来看一下异步消息发送,使用Callback回调的方式。kafka有响应时就有回调,要么发送成功,要么异常。

producer.send(record, (recordMetadata, e) -> {                                                                                                      
    if (e != null) {                                                                                                                                
        e.printStackTrace();                                                                                                                        
    } else {                                                                                                                                        
        System.out.println(recordMetadata.topic() + ":" + recordMetadata.partition() + ":" + recordMetadata.offset());                              
    }                                                                                                                                               
});                                                                                                                                                 

对于同一分区而言,如果消息1在消息2之前发送,那么callback1在callback2之前调用,回调函数的调用也可以保证分区有效。

1.3 序列化

生产者发送消息到Kafka时,需要使用序列化器将消息序列化为字节数组(byte[]),而消费者需要使用反序列化器将消息还原。这里使用的序列化器为:org.apache.kafka.common.serialization.StringSerializer,除了String类型的序列化器之外,还有ByteArray、ByteBuffer、Bytes、Double、Integer、Long这几种类型,都实现了org.apache.kafka.common.serialization.Serializer接口,这个接口有三个方法。

void configure(Map<String, ?> configs, boolean isKey);
byte[] serialize(String topic, T data);
void close();

configure()用来配置当前类,serialize()用来执行序列化操作,close()一般是个空方法,如果实现了该方法,需要保证方法的幂等性,因为这个方法可能会被KafkaProducer调用多次。

生产者序列化器和消费者反序列化器需要一一对应。如果Kafka提供的几种序列化器都无法满足要求,可以选择使用Avro、JSON、Thrift、ProtoBuff等。

1.4 分区器

消息在发往kafka的时候,可能需要经过拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)的一系列作用后才能真正的发往kafka。消息序列化后就会确定它发往的分区,如果在ProducerRecord里面指定了partition字段,那么就不需要分区器的作用了,partition代表的就是需要发往的分区号。

如果partition不指定,那么就需要分区器的参与了,根据key在计算partition值。

kafka默认的分区器是org.apache.kafka.clients.producer.internals.DefaultPartitioner,它实现了org.apache.kafka.clients.producer.Partitioner接口

int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
void close();

partition()方法用来计算分区号,返回值为int类型。参数分别表示主题、键、序列化键、值、序列化值、以及集群的元数据信息。close()方法用来在分区器关闭的时候回收一些资源。

在默认分区器DefaultPartitioner中,分区号的计算规则为:key不为空,key进行hash(采用MurmurHash2算法,高效率、低碰撞),根据得到的hash值计算分区号,拥有相同key的消息会被写入同一分区。key为空,消息则以轮训的方式发往各分区。

注意:如果key不为空,分区号是所有分区中的任意一个;key为空,分区号是可用分区中的一个。

1.5 生产者拦截器

生产者拦截器可以用来在消息发送前做一些准备工作,如果过滤不合规的消息、修改消息内容等。

生产者拦截器使用比较方便,主要是实现org.apache.kafka.clients.producer.ProducerInterceptor接口。

ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
onAcknowledgement(RecordMetadata metadata, Exception exception);
void close();

onSend()用来对消息进行一些定制化操作,一般来说不要修改消息的topic、key、partition等。onAcknowledgement()消息应答之前或者消息发送失败时调用,在Callback之前执行。这个方法在Producer的I/O线程中,所以这个方法实现逻辑越简单越好,否则会影响消息的发送速度。close()执行资源清理操作等。

自定义拦截器,用来给消息内容加一个前缀

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

/**
 * 给消息内容加上前缀
 */
public class PrefixInterceptor implements ProducerInterceptor<String, String> {

    String prefix = "prefix-";
    private volatile int success;
    private volatile int error;

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

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

    @Override
    public void close() {
        if (success + error > 0) {
            double successRate = (double) success / (success + error);
            System.out.printf("成功率: %f%%\n", successRate * 100);
        }
    }

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

    }

}

然后需要在properties指定拦截器

properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, PrefixInterceptor.class.getName());

发送的原始消息内容为message,消费时会发现这个消息变成了prefix-message

KafkaProducer还可以指定多个拦截器形成拦截器链。配置ProducerConfig.INTERCEPTOR_CLASSES_CONFIG的时候,多个拦截器使用,隔开。

注意:如果拦截器的执行需要依赖于前一个拦截器的输出,可能会产生“副作用”,上一个拦截器执行失败,那么这个拦截器也无法争取执行。

2. 原理分析

2.1 整体架构

生产者客户端整体架构如下:
生产者客户端整体架构图
生产者客户端由两个线程协调运行,主线程+Sender线程(发送线程)。主线程中,KafkaProducer创建消息,然后消息经过拦截器、序列化器、分区器后,缓存到消息累加器RecordAccumulator(也叫消息收集器中)。Sender线程从RecordAccumulator获取消息并发送到kafka。

RecordAccumulator缓存消息以便Sender线程可以批量发送,减少网络传输的资源以提升性能。RecordAccumulator缓存大小可以通过buffer.memory配置,默认32MB。生产者发送消息的速度大于发送到服务器的速度,导致生产者空间不足,send()方法要么阻塞,要么抛出异常,取决于max.block.ms,默认60秒。

RecordAccumulator为每一个分区维护了一个双端队列,队列内容是ProducerBatch,主线程发送的消息会追加到队尾,Sender线程从队首读取消息。注意是ProducerBatch而不是ProducerRecord,ProducerBatch指一个消息批次,可以理解为多个ProducerRecord组成了一个ProducerBatch。

消息在网络上都是以字节(Byte)形式传输的,在发送之前创建一块内存区域保存对应的消息。Kafka生产者客户端中,通过java.io.ByteBuffer实现消息的创建和释放。频繁的创建和释放比较耗费系统资源的,在RecordAccumulator内部有一个BufferPool,用来实现ByteBuffer的复用。BytePool只针对特定大小ByteBuffer进行管理,而其它大小的ByteBuffer不会缓存到BufferPool中,这个大小可以通过参数batch.size指定,默认16KB。

Sender从RecordAccumulator获取消息后,将原本的<分区, Deque<ProducerBatch>>转换成<Node, List<ProducerBatch>>,Node表示kafka集群broker节点。进一步封装为<Node, Request>的形式,这样就可以将Request请求发往各个Node了。

Sender线程发往Kafka之前还会保存到InFlightRequests中,格式为Map<NodeId, Deque<Request>>,它的作用是缓存已经发出去但是没有收到响应的请求。可以通过配置max.in.flight.requests.per.connection限制每个连接,默认是5,表示最多缓存5个未响应的请求,超过后就不能再向这个连接发送请求了,除非缓存的请求收到了响应。可以通过Deque<Request>的size判断Node是否已经堆积了很多未响应的请求,如果未响应请求很多,说明这个Node结点的负载较大或者网络有问题,在向其发送请求会增大超时的可能。

2.2 元数据更新

可以通过InFlightRequests获得LeastLoadedNode,即Node中负载最小的那个。负载最小是通过每个Node在InFlightRequests中还未相应的请求决定的,未确定的请求越多,负载越大。下图,Node1的负载最小。

在这里插入图片描述

KafkaProducer要将消息追加到给定主题的某个分区对应的leader副本之前,需要知道主题的分区数量,然后计算目标分区,之后KafkaProducer需要知道leader副本所在的broker结点地址、端口才能建立连接,最终才能叫消息发送到Kafka,这一过程中需要的信息都属于元数据信息。

元数据是指Kafka集群的元数据,这些元数据具体记录了集群中有哪些主题,这些主题有哪些分区,每个分区的 leader副本分配在哪个节点上,follower副本分配在哪些节点上,哪些副本在AR、ISR等集合中,集群中有哪些节点,控制器节点又是哪一个等信息。

当客户端中没有需要使用的元数据信息时,比如没有指定的主题信息,或者超过metadata.max.age.ms时间没有更新元数据都会引起元数据的更新操作。客户端参数metadata.max.age.ms的默认值为300000,即5分钟。元数据的更新操作是在客户端内部进行的,对客户端的外部使用者不可见。当需要更新元数据时,会先挑选出leastLoadedNode,然后向这个Node发送MetadataRequest请求来获取具体的元数据信息。这个更新操作是由Sender线程发起的,在创建完MetadataRequest之后同样会存入InFlightRequests,之后的步骤就和发送消息时的类似。元数据虽然由Sender线程负责更新,但是主线程也需要读取这些信息,这里的数据同步通过synchronized和final关键字来保障。

3. 重要的生产者参数

  • acks
    这个参数用来指定分区中必须要有多少个副本收到这条消息之后,生产者才会认为这条消息是写入成功的。这个参数在生产者客户端中非常重要,它涉及到可靠性和吞吐量之间的权衡。注意:取值是字符串。
    acks = 1。默认为1。生产者发送消息后,只要leader副本成功写入消息,那么它就会收到来自服务端的响应。leader崩溃,重新选举leader副本的过程中,导致消息无法写入,生产者会收到一个错误的响应,为了避免消息丢失,生产者可以选择重发消息。写入leader并返回成功响应给生产者,但是其他follower副本拉取之前leader崩溃了,那么消息还是会丢失,因为新的leader中并没有这条消息。acks = 1是消息可靠性和吞吐量之间的这种方案。
    acks = 0。生产者发送消息之后不需要等待服务端的响应。消息写入Kafka的过程中出现了异常,导致kafka没有收到这条消息,生产者也无法得知。acks = 0可以达到最大的吞吐量。
    acks = -1 或者acks = all。生产者在发送消息后,需要等待ISR所有副本都成功写入消息之后才能收到服务端的响应。acks = -1 或者acks = all可以达到最高的可靠性,但是并不意味着消息一定可靠,因为ISR中可能只有leader副本,这样就退化成了acks = 1的情况。要保证更高的可靠性还需要配合参数min.insync.replicas

  • max.request.size
    这个参数用来限制生产者客户端能够发送消息的最大值,默认为1MB。

  • retries和retries.backoff.ms
    retries参数用来配置生产者重试次数,默认值0,即在发生异常的时候不进行重试操作。在生产者发出消息到写入服务器之前,可能会发生一些异常,网络抖动、leader选举等,这些异常是可以通过内部重试机制而成功发送消息的,如果重试达到设置的次数,生产者放弃重试并返回异常。不是所有的异常都可以通过重试来解决的,比如消息太大。
    重试相关的还有一个参数retries.backoff.ms,默认值为100ms,用来设置两次重试的时间间隔。

  • compression.type
    这个参数用来设置消息的压缩方式,默认为"none",即不压缩。还可以配置为"gzip"、“snappy”、“lz4”。压缩消息可以减少网络传输量、降低网络I/O,从而提高性能。消息压缩是一种使用时间换空间的优化方式,如果对延迟有一定的要求,则不建议消息压缩。

  • connections.max.idel.ms
    这个参数用来指定在多久后关闭限制的连接,默认9分钟。

  • linger.ms
    这个参数用来指定生产者发送ProducerBatch之前等待更多消息(ProducerRecord)加入ProducerBatch的时间,默认值为0。生产者客户端会在ProducerBatch被填满或者等待时间超过linger.ms时发送出去。增大这个参数值会增加消息的延迟,同时能提升一定的吞吐量。

  • receive.buffer.bytes
    这个参数用来设置Scoket接受消息缓冲区(SO_RECBUF)的大小,默认值为32KB。设置为-1,则使用操作系统的默认值。如果Producer与Kafka处于不同机房,可以适当的增大这个参数值。

  • send.buffer.bytes
    这个参数用来设置Scoket发送消息缓冲区(SO_SNDBUF)的大小,默认128KB。-1则使用操作系统默认值。

  • request.timeout.ms
    配置Producer等待请求的最长时间,默认30000ms。请求超时可以选择重试。

部分生产者客户端参数如下:
参数名称默认值参数解释
bootstrap.servers“”kafka集群链接地址
key.serializer“”key对应的序列化类
value.serializer“”value对应的序列化类
buffer.memory33554432(32MB)生产者客户端缓存消息的缓冲区大小
batch.size16384(16KB)ProducerBatch可复用内存区域大小
client.id“”客户端ID
max.block.ms6000生产者缓冲区已满,或者没有可用元数据时,发送消息的阻塞时间
partitioner.classorg.apache.kafka.clients.producer.internals.DefaultPartitioner分区器
enable.idempotencefalse是否开启幂等功能
interceptor.classes“”生产者拦截器,多个使用,隔开
max.in.flight.requests.per.connection5最多缓存未响应的请求数
metadata.max.age.ms30000(5分钟)这个时间内元数据没有更新的话会强制更新
transactional.idnull设置事务的ID,唯一
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流年ln

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值