Kafka生产者

生产者就是负责向 Kafka发送消息的应用程序。在 Kafka的历史变迁中,一共有两个大版本的生产者客户端:第一个是于 Kafka开源之初使用Scaa语言编写的客户端,我们可以称之为旧生产者客户端(Old Producer)或 Scala版生产者客户端;第二个是从 Kafka 0.9.x版本开始推出的使用Java语言编写的客户端,我们可以称之为新生产者客户端(New Producer)或Java版生产者客户端,它弥补了旧版客户端中存在的诸多设计缺陷。

1、客户端开发

一个正常的生产逻辑需要具备以下几个步骤
(1)配置生产者客户端参数及创建相应的生产者实例。
(2)构建待发送的消息。
(3)发送消息。
(4)关闭生产者实例。

public class KafkaProducerAnalysis {

    public static final String brokerList="localhost:9092";
    public static final String topic="topic-demo";
    
    /**
     * 配置生产者客户端参数
     * */
    private static Properties initConfig() {
        Properties properties=new Properties();
       // properties.put("bootstrap.servers",brokerList);
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,brokerList);
        // properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        //properties.put("client.id","producer.client.id.demo");//指定客户端ID
        properties.put(ProducerConfig.CLIENT_ID_CONFIG,"producer.client.id.demo");//指定客户端ID
        return properties;
    }

    public static void main(String args[]){
        //配置生产者客户端参数
        Properties properties=initConfig();
        //创建相应的生产者实例
        KafkaProducer<String,String> producer=new KafkaProducer<>(properties);
        //构建待发送的消息  topic 和value 是必填项
        ProducerRecord<String,String> record=new ProducerRecord<>(topic,"hello,Kafka!");
        try {
            //发送消息
            producer.send(record);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭生产者实例
            producer.close();
        }
    }

}

这里有必要单独说明的是构建的消息对象 ProducerRecord,它并不是单纯意义上的消息,它包含了多个属性,原本需要发送的与业务相关的消息体只是其中的一个 value属性,比如“ Hello, Kafka!”只是 Producer Record对象中的一个属性。 ProducerRecord类的定义如下(只截取成员变量):

public class ProducerRecord<K, V> {
    private final String topic;
    private final Integer partition;
    private final Headers headers;
    private final K key;
    private final V value;
    private final Long timestamp;
}

其中 topic和 partition字段分别代表消息要发往的主题和分区号。 headers字段是消息的头部,Kafka 0.11.x版本才引入这个属性,它大多用来设定一些与应用相关的信息,如无需要也可以不用设置。key是用来指定消息的键,它不仅是消息的附加信息,还可以用来计算分区号进而可以让消息发往特定的分区。前面提及消息以主题为单位进行归类,而这个key可以让消息再进行二次归类,同一个key的消息会被划分到同一个分区中。有key的消息还可以支持日志压缩的功能。value是指消息体,一般不为空,如果为空则表示特定的消息——墓碑消息。timestamp是指消息的时间戳,它有 CreateTime和 LogAppendTime两种类型,前者表示消息创建的时间,后者表示消息追加到日志文件的时间。

1.1、必要的参数配置

在创建真正的生产者实例前需要配置相应的参数,比如需要连接的Kafka集群地址。在Kafka生产者客户端 KafkaProducer中有3个参数是必填的。

  • bootstrap.servers:该参数用来指定生产者客户端连接Kafka集群所需的 broker地址清单,具体的内容格式为host1:port1,host2:port2,可以设置一个或多个地址,中间以逗号隔开,此参数的默认值为“”。注意这里并非需要所有的 broker地址,因为生产者会从给定的broker里查找到其他 broker的信息。不过建议至少要设置两个以上的 broker地址信息,当其中任意一个宕机时,生产者仍然可以连接到 Kafka集群上。
  • key.serializer和 value.serializer: broker端接收的消息必须以字节数组(byte[])的形式存在。生产者使用的KafkaProducer<String,String>和ProducerRecord<String,String>中的泛型<String, String>对应的就是消息中key和value的类型,生产者客户端使用这种方式可以让代码具有良好的可读性,不过在发往 broker之前需要将消息中对应的key和value做相应的序列化操作来转换成字节数组。key.seralizer和value.seralizer这两个参数分别用来指定key和value序列化操作的序列化器,这两个参数无默认值。注意这里必须填写序列化器的全限定名。

注意到代码中的 initConfig()方法里还设置了一个参数client.id,这个参数用来设定 KafkaProducer对应的客户端id,默认值为“”。如果客户端不设置,则 KafkaProducer会自动生成一个非空字符串,内容形式如“producer-1“,"producer-2”,即字符串“producer-"与数字的拼接。

KafkaProducer中的参数众多,远非示例 initConfig方法中的那样只有4个,开发人员可以根据业务应用的实际需求来修改这些参数的默认值,以达到灵活调配的目的。一般情况下,普通开发人员无法记住所有的参数名称,只能有个大致的印象。在实际使用过程中,诸如key.serializer,max.request.size,interceptor.classes之类的字符串经常由于人为因素而书写错误。为此,我们可以直接使用客户端中的 org.apache.kafka.clients.producer.ProducerConfig类来做一定程度上的预防措施,每个参数在 ProducerConfig类中都有对应的名称。

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

1.2、消息的发送

在创建完生产者实例之后,接下来的工作就是构建消息,即创建 ProducerRecord对象。我们已经了解了 ProducerRecord的属性结构,其中 topic属性和value属性是必填项,其余属性是选填项,对应的 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)

创建生产者实例和构建消息之后,就可以开始发送消息了。发送消息主要有三种模式:发后即忘(fire-and-forget)、同步(sync)及异步(async)。
代码清单这种发送方式就是发后即忘,它只管往 Kafka中发送消息而并不关心消息是否正确到达。在大多数情况下,这种发送方式没有什么问题,不过在某些时候(比如发生不可重试异常时)会造成消息的丢失。这种发送方式的性能最高,可靠性也最差。
KafkaProducer的 send()方法并非是void类型,而是 Future<RecordMetadata>类型, send()方法有2个重载方法,具体定义如下

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

要实现同步的发送方式,可以利用返回的 Future对象实现,示例如下

producer.send(record).get();

实际上send方法本身就是异步的,send方法返回的 Future对象可以使调用方稍后获得发送的结果。示例中在执行 send()方法之后直接链式调用了get()方法来阻塞等待Kafka的响应,直到消息发送成功,或者发生异常。如果发生异常,那么就需要捕获异常并交由外层逻辑处理。
也可以在执行完 send()方法之后不直接调用get()方法,比如下面的一种同步发送方式的实现:

Future<RecordMetadata> future= producer. send(record);
RecordMetadata metadata = future.get();

这样可以获取一个 RecordMetadata对象,在 RecordMetadata对象里包含了消息的一些元数据信息,比如当前消息的主题、分区号、分区中的偏移量(offset)、时间戳等。如果在应用代码中需要这些信息,则可以使用这个方式。如果不需要,则直接采用producer.send(record).get()的方式更省事。

Future表示一个任务的生命周期,并提供了相应的方法来判断任务是否已经完成或取消,以及获取任务的结果和取消任务等。既然 KafkaProducer.send方法的返回值是一个 Future类型的对象,那么完全可以用Java语言层面的技巧来丰富应用的实现,比如使用 Future中的get(long timeout, TimeUnit unit)方法实现可超时的阻塞。

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

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

props.put(ProducerConfig.RETRIESCONFIG, 10);

示例中配置了10次重试。如果重试了10次之后还没有恢复,那么仍会抛出异常,进而发送的外层逻辑就要处理这些异常了。
同步发送的方式可靠性高,要么消息被发送成功,要么发生异常。如果发生异常,则可以捕获并进行相应的处理,而不会像“发后即忘”的方式直接造成消息的丢失。不过同步发送的方式的性能会差很多,需要阻塞等待一条消息发送完之后才能发送下一条。
我们再来了解一下异步发送的方式,一般是在 send()方法里指定一个 Callback的回调函数Kafka在返回响应时调用该函数来实现异步的发送确认。

send方法的返回值类型就是Future,而 Future本身就可以用作异步的逻辑处理。这样做不是不行,只不过 Future里的get()方法在何时调用,以及怎么调用都是需要面对的问题,消息不停地发送,那么诸多消息对应的 Future对象的处理难免会引起代码处理逻辑的混乱。使用 Callback的方式非常简洁明了, Kafka有响应时就会回调,要么发送成功,要么抛出异常。

public interface Callback {
    void onCompletion(RecordMetadata metadata, Exception exception);
}

onCompletion()方法的两个参数是互斥的,消息发送成功时, metadata不为null而exception为null:消息发送异常时, metadata为null 而exception不为null。

producer.send(record1, callback1);
producer.send(record2, callback2);

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

通常,一个 KafkaProducer不会只负责发送单条消息,更多的是发送多条消息,在发送完这些消息之后,需要调用 KafkaProducer的 close方法来回收资源。

close()方法会阻塞等待之前所有的发送请求完成后再关闭 KafkaProducer。与此同时KafkaProducer还提供了一个带超时时间的 close()方法,具体定义如下:

public void close (long timeout, TimeUnit timeUnit)

如果调用了带超时时间timeout的 close()方法,那么只会在等待 timeout时间内来完成所有尚未完成的请求处理,然后强行退出。在实际应用中,一般使用的都是无参的 close()方法。

1.3、序列化

生产者需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka。而在对侧,消费者需要用反序列化器(Deserializer)把从Kafka中收到的字节数组转换成相应的对象。除了用于String类型的序列化器org.apache.kafka.common.serialization.StringSerializer还有 ByteArray、ByteBuffer、Bytes、Double、Integer、Long这几种类
型,它们都实现了org.apache.kafka.common.serialization.Serializer接口,此接口有4个方法

public interface Serializer<T> extends Closeable {
    default void configure(Map<String, ?> configs, boolean isKey) {
        // intentionally left blank
    }

    byte[] serialize(String topic, T data);

    default byte[] serialize(String topic, Headers headers, T data) {
        return serialize(topic, data);
    }

    @Override
    default void close() {
        // intentionally left blank
    }
}

configure()方法用来配置当前类, serialize()方法用来执行序列化操作。而close()方法用来关闭当前的序列化器,一般情况下 close()是一个空方法,如果实现了此方法,则必须确保此方法的幂等性,因为这个方法很可能会被 KafkaProducer调用多次。生产者使用的序列化器和消费者使用的反序列化器是需要一一对应的,如果生产者使用了某种序列化器,比如 StringSerializer,而消费者使用了另一种序列化器,比如 IntegerSerializer,那么是无法解析出想要的数据的。

1.4、分区器

消息在通过send()方法发往 broker的过程中,有可能需要经过拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)的一系列作用之后才能被真正地发往 broker。拦截器一般不是必需的,而序列化器是必需的。消息经过序列化之后就需要确定它发往的分区,如果消息 ProducerRecord中指定了 partition字段,那么就不需要分区器的作用,因为 partition代表的就是所要发往的分区号。

如果消息 ProducerRecord中没有指定 partition字段,那么就需要依赖分区器,根据key这个字段来计算 partition的值。分区器的作用就是为消息分配分区Kafka中提供的默认分区器是 org.apache.kafka.clients.producer.intemals.DefaultPartitioner,它实现了org.apache.kafka.clients.producer.Partitioner接口,这个接囗中定义了2个方法,具体如下所示。

public interface Partitioner extends Configurable, Closeable {
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
    public void close();
}

其中 partition方法用来计算分区号,返回值为int类型。 partition方法中的参数分别表示主题、键、序列化后的键、值、序列化后的值,以及集群的元数据信息,通过这些信息可以实现功能丰富的分区器。 close()方法在关闭分区器的时候用来回收一些资源。
Partitioner接口还有一个父接口 org.apache.kafka.common.Configurable,这个接口中只有个方法:
void configure(Map<string, ?> configs);

Configurable接口中的 configure()方法主要用来获取配置信息及初始化数据。在默认分区器 DefaultPartitioner的实现中, close是空方法,而在 partition方法中定义了主要的分区分配逻辑。

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

注意:如果key不为null,那么计算得到的分区号会是所有分区中的任意一个;如果key为null,那么计算得到的分区号仅为可用分区中的任意一个,注意两者之间的差别在不改变主题分区数量的情况下,key与分区之间的映射可以保持不变。不过,一旦主题中增加了分区,那么就难以保证key与分区之间的映射关系了。

1.5、生产者拦截器

拦截器(interceptor)是早在Kafka 0.10.0.0中就已经引入的一个功能,Kafka一共有两种拦截器:生产者拦截器和消费者拦截器。
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
生产者拦截器的使用也很方便,主要是自定义实现 org.apache.kafka.clients.produce.ProducerInterceptor接口。 ProducerInterceptor接口中包含3个方法:

public interface ProducerInterceptor<K, V> extends Configurable {
    public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
    public void onAcknowledgement(RecordMetadata metadata, Exception exception);
    public void close();
}

KafkaProducer在将消息序列化和计算分区之前会调用生产者拦截器的 onSend()方法来对消息进行相应的定制化操作。一般来说最好不要修改消息 ProducerRecord的 topic、key和partition等信息,如果要修改,则需确保对其有准确的判断,否则会与预想的效果出现偏差。比如修改κey不仅会影响分区的计算,同样会影响 broker端日志压缩(Log Compaction)的功能。

KafkaProducer会在消息被应答(Acknowledgement)之前或消息发送失败时调用生产者拦截器的onAcknowledgement()方法,优先于用户设定的 Callback之前执行。这个方法运行在Producer的IO线程中,所以这个方法中实现的代码逻辑越简单越好,否则会影响消息的发送速度。
close()方法主要用于在关闭拦截器时执行一些资源的清理工作。在这3个方法中抛出的异常都会被捕获并记录到日志中,但并不会再向上传递。
ProducerInterceptor接口与Partitioner接口一样,它也有一个同样的父接口Configurable,具体的内容可以参见 Partitioner接口的相关介绍。

KafkaProducer中不仅可以指定一个拦截器,还可以指定多个拦截器以形成拦截链。拦截链会按照 interceptor.classes参数配置的拦截器的顺序来一一执行(配置的时候,各个拦截器之间使用逗号隔开)。

2、原理分析

2.1、整体架构

消息在真正发往 Kafka之前,有可能需要经历拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)等一系列的作用,那么在此之后又会发生什么呢?
下面我们来看一下生产者客户端的整体架构,如图2-1所示。

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

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

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

消息在网络上都是以字节(Byte)的形式传输的,在发送之前需要创建一块内存区域来保存对应的消息。在Kafka生产者客户端中,通过 java.io.ByteBuffer实现消息内存的创建和释放。

不过频繁的创建和释放是比较耗费资源的,在 RecordAccumulator的内部还有一个 BufferPool,它主要用来实现 ByteBuffer的复用,以实现缓存的高效利用。不过 BufferPool只针对特定大小的 ByteBuffer进行管理,而其他大小的 ByteBuffer不会缓存进 BufferPool中,这个特定的大小由 batch.size参数来指定,默认值为16384B,即16KB。我们可以适当地调大 batch.size参数以便多缓存一些消息。

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

Sender从 RecordAccumulator中获取缓存的消息之后,会进一步将原本<分区, Deque<ProducerBatch>>的保存形式转变成<Node,List<ProducerBatch>>的形式,其中Node表示 Kafka集群的 broker节点。对于网络连接来说,生产者客户端是与具体的broker节点建立的连接,也就是向具体的 broker节点发送消息,而并不关心消息属于哪一个分区;而对于KafkaProducer的应用逻辑而言,我们只关注向哪个分区中发送哪些消息,所以在这里需要做一个应用逻辑层面到网络IO层面的转换。

在转换成<Node,List<ProducerBatch>>的形式之后, Sender还会进一步封装成<Node,Request>的形式,这样就可以将 Request请求发往各个Node了,这里的 Request是指Kafka的各种协议请求,对于消息发送而言就是指具体的 ProduceRequest。

请求在从 Sender线程发往 Kafka之前还会保存到 InFlightRequests中, InFlightRequests保存对象的具体形式为Map<Nodeld,Deque<Request>>,它的主要作用是缓存了已经发出去但还没有收到响应的请求(Nodeld是一个 String类型,表示节点的id编号)。与此同时,InFlightRequests还提供了许多管理类的方法,并且通过配置参数还可以限制每个连接(也就是客户端与Node之间的连接)最多缓存的请求数这个配置参数为max.in.flight.requests.per.connection,默认值为5,即每个连接最多只能缓存5个未响应的请求,超过该数值之后就不能再向这个连接发送更多的请求了,除非有缓存的请求收到了响应(Response)。通过比较 Deque<Request>的size与这个参数的大小来判断对应的Node中是否已经堆积了很多未响应的消息,如果真是如此,那么说明这个Node节点负载较大或网络连接有问题,再继续向其发送请求会增大请求超时的可能。

2.2、元数据的更新

InFlightRequests还可以获得 leastLoadedNode,即所有Node中负载最小的那一个。这里的负载最小是通过每个Node在 InFlightRequests中还未确认的请求决定的,未确认的请求越多则认为负载越大。对于图2-2中的 InFlightRequests说,图中展示了三个节点Node0、Node1和Node2,很明显 Node1的负载最小。也就是说, Node1为当前的 leastLoadedNode选择 leastLoadedNode发送请求可以使它能够尽快发出,避免因网络拥塞等异常而影响整体的进度。 leastLoadedNode的概念可以用于多个应用场合,比如元数据请求、消费者组播协议的交互。

我们使用如下的方式创建了一条消息 ProducerRecord:

ProducerRecord<String,String> record = new ProducerRecord<>(topic,Hello, Kafka!);

我们只知道主题的名称,对于其他一些必要的信息却一无所知。 KafkaProducer要将此消息追加到指定主题的某个分区所对应的 leader副本之前,首先需要知道主题的分区数量,然后经过计算得出(或者直接指定)目标分区,之后 KafkaProducer需要知道目标分区的 leader副本所在的 broker节点的地址、端口等信息才能建立连接,最终才能将消息发送到 Kafka,在这一过程中所需要的信息都属于元数据信息。

bootstrap.servers参数只需要配置部分 broker节点的地址即可,不需要配置所有 broker节点的地址,因为客户端可以自己发现其他 broker节点的地址,这过程也属于元数据相关的更新操作。与此同时,分区数量及 leader副本的分布都会动态地变化,客户端也需要动态地捕捉这些变化。

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

3、重要的生产者参数

在 KafkaProducer中,除了3个必要的客户端参数,大部分的参数都有合理的默认值,一般不需要修改它们。不过了解这些参数可以让我们更合理地使用生产者客户端,其中还有一些重要的参数涉及程序的可用性和性能,如果能够熟练掌握它们,也可以让我们在编写相关的程序时能够更好地进行性能调优与故障排査。下面挑选一些重要的参数进行讲解。

3.1、acks

这个参数用来指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。acks是生产者客户端中一个非常重要的参数,它涉及消息的可靠性和吞吐量之间的权衡。acks参数有3种类型的值(都是字符串类型)。

  • acks=1。默认值即为1。生产者发送消息之后,只要分区的 leader副本成功写入消息,那么它就会收到来自服务端的成功响应。如果消息无法写入 leader副本,比如在leader副本崩溃、重新选举新的 leader副本的过程中,那么生产者就会收到一个错误的响应,为了避免消息丢失,生产者可以选择重发消息。如果消息写入 leader副本并返回成功响应给生产者,且在被其他 follower副本拉取之前 leader副本崩溃,那么此时消息还是会丢失,因为新选举的 leader副本中并没有这条对应的消息。acks设置为,是消息可靠性和吞吐量之间的折中方案。
  • acks=0。生产者发送消息之后不需要等待任何服务端的响应。如果在消息从发送到写入Kafka的过程中出现某些异常,导致 Kafka并没有收到这条消息,那么生产者也无从得知,消息也就丢失了。在其他配置环境相同的情况下,acks设置为0可以达到最大的吞吐量。
  • acks=-1或acks=all。生产者在消息发送之后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下,acks设置为-1(all)可以达到最强的可靠性。但这并不意味着消息就一定可靠,因为ISR中可能只有 leader副本,这样就退化成了acks=1的情况。要获得更高的消息可靠性需要配合min.insync.replicas等参数的联动。

注意acks参数配置的值是一个字符串类型,而不是整数类型。

3.2、max.request.size

这个参数用来限制生产者客户端能发送的消息的最大值,默认值为1048576B,即1MB。一般情况下,这个默认值就可以满足大多数的应用场景了。这个参数还涉及一些其他参数的联动,比如 broker端的 message.max.bytes参数,如果配置错误可能会引起一些不必要的异常。比如将 broker端的 message.max.bytes参数配置为10,而max.request.size参数配置为20,那么当我们发送一条大小为15B的消息时,生产者客户端就会报出如下的异常:
org.apache.kafka.common.errors.RecordTooLargeException: The request included a message larger than the max message size the server will accept.

3.3、retries 和 retry.backoff.ms

retries参数用来配置生产者重试的次数,默认值为0,即在发生异常的时候不进行任何重试动作。消息在从生产者发出到成功写入服务器之前可能发生一些临时性的异常,比如网络抖动、 leader副本的选举等,这种异常往往是可以自行恢复的,生产者可以通过配置 retries大于0的值,以此通过内部重试来恢复而不是一味地将异常抛给生产者的应用程序。如果重试达到设定的次数,那么生产者就会放弃重试并返回异常。不过并不是所有的异常都是可以通过重试来解决的,比如消息太大,超过max.request.size参数配置的值时,这种方式就不可行了。

重试还和另一个参数 retry.backoff.ms有关,这个参数的默认值为100,它用来设定两次重试之间的时间间隔,避免无效的频繁重试。在配置 retries和 retry.backoff.ms之前,最好先估算一下可能的异常恢复时间,这样可以设定总的重试时间大于这个异常恢复时间,以此来避免生产者过早地放弃重试。

Kafka可以保证同一个分区中的消息是有序的。如果生产者按照一定的顺序发送消息,那么这些消息也会顺序地写入分区,进而消费者也可以按照同样的顺序消费它们。对于某些应用来说,顺序性非常重要,比如 MySQL的 binlog传输,如果出现错误就会造成非常严重的后果。如果将acks参数配置为非零值,并且max.in.flight.requests.per.connection参数配置为大于1的值,那么就会出现错序的现象:如果第一批次消息写入失败,而第二批次消息写入成功,那么生产者会重试发送第一批次的消息,此时如果第一批次的消息写入成功,那么这两个批次的消息就出现了错序。一般而言,在需要保证消息顺序的场合建议把参数max.in.flight.requests.per.connection配置为1,而不是把acks配置为0,不过这样也会影响整体的吞吐。

3.4、compression.type

这个参数用来指定消息的压缩方式,默认值为“none”,即默认情况下,消息不会被压缩。该参数还可以配置为“gzip”,“snappy”和“lz4”。对消息进行压缩可以极大地减少网络传输量、降低网络IO,从而提高整体的性能。消息压缩是一种使用时间换空间的优化方式,如果对时延有一定的要求,则不推荐对消息进行压缩。

3.5、connections.max.idle.ms

这个参数用来指定在多久之后关闭限制的连接,默认值是540000(ms),即9分钟。

3.6、linger.ms

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

3.7、receive.buffer.bytes

这个参数用来设置 Socket接收消息缓冲区(SO_RECBUF)的大小,默认值为32768(B),即32KB。如果设置为-1,则使用操作系统的默认值。如果 Producer与 Kafka处于不同的机房,则可以适地调大这个参数值。

3.8、send.buffer.bytes

这个参数用来设置 Socket发送消息缓冲区(SO_SNDBUF)的大小,默认值为131072(B),即128KB。与 receive.buffer.bytes参数一样,如果设置为-1,则使用操作系统的默认值。

3.9、request.timeout.ms

这个参数用来配置 Producer等待请求响应的最长时间,默认值为30000(ms)。请求超时之后可以选择进行重试。注意这个参数需要比 broker端参数 replica.lag.time.max.ms的值要大,这样可以减少因客户端重试而引起的消息重复的概率。

3.10、其他参数

还有一些生产者客户端的参数在本节中没有提及,这些参数同样非常重要,它们需要单独的章节或场景来描述。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值