Kafka 2.2.0 Java客户端开发——消息生产者

快速入门

Kafka消息生产者逻辑应当具备以下步骤:

  1. 配置生产者参数,创造生产者实例KafkaProducer
  2. 构建待发送的消息ProducerRecord
  3. 发送消息
  4. 程序退出或者无需生产消息时关闭生产者实例KafkaProducer

示例代码:

public class Producer {
    public static void main(String[] _args) throws Exception {
        Properties p = new Properties();
        p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.142.128:9092");  //Broker的IP地址和端口
        p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());  //key序列化器
        p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//value序列化器
        p.put(ProducerConfig.CLIENT_ID_CONFIG, "producer.client.id.demo");
        KafkaProducer<String, String> producer = new KafkaProducer<>(p);
        for(int i = 0; i < 100; i++) {  //发送100条消息
            ProducerRecord<String, String> record = new ProducerRecord<>("topic-test", "message-" + i);
            RecordMetadata meta = producer.send(record).get();
            System.out.println(meta.topic() + " " + meta.partition() + " " + meta.offset());
        }
        producer.close();
    }
}

上述代码向地址为192.168.142.128:9092的Broker主题topic-test发送了100条消息

生产者实例的创建

创建生产者实例KafkaProducer前需要指定相应的配置参数,配置参数需要存储在Properties中,有3个参数是必须要指定的:

  • bootstrap.serversProducerConfig.BOOTSTRAP_SERVERS_CONFIG):该参数指定了生产者客户端连接Kafka集群所需的Broker列表,格式为host:port,用逗号分隔。这里并非需要所有的Broker,生产者可以根据一个Broker来自动获取其它Broker的信息。建议设置为2个以上,当客户端连接的Broker因为网络故障或者该Broker宕机时可以根据该配置连接至其它Broker
  • key.serializervalue.serializerProducerConfig.KEY_SERIALIZER_CLASS_CONFIGProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG):在传递给Broker时必须将消息ProducerRecord中的keyvalue转换为字节数组(byte[])的形式,其转换逻辑由key.serializervalue.serializer实现,这两个参数必须是一个实现了Serializer接口的类的全限定名,且具备一个无参构造方法。

将上述参数存入Properties对象后,就可以构造生产者实例KafkaProducer了。

消息的发送

创建完生产者实例后,接下来的工作就是构建消息ProducerRecord,一个ProducerRecord实例代表一个独立的消息,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;      //消息的时间戳
}

ProducerRecord具备了多个构造方法,其中topic必须不为null,其它参数均可为null。指定分区编号partition后可以将消息发送到该主题下的指定分区。消息头一般用来设定一些与应用相关的信息。键key可以让消息进行一个二次分类,相同key的消息可以被发送到同一个分区,同时也会影响到Broker端的日志压缩。value为消息体,一般不为空。timestamp指消息时间戳,有CreateTimeLogAppendTime两种类型,前者为消息创建时间,后者为消息追加到服务器日志的时间。

构造好ProducerRecord后,就可以通过KafkaProducer将这个消息发送给Broker了,发送消息主要有两种模式:同步发送、异步发送。具体可以通过KafkaProducersend方法来实现:

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

可以看到send方法都会返回一个Future对象,因为这个方法本身就是异步的,如果不需要关心这个消息是否成功送达,那么只需要调用其send方法就好了。如果应用程序需要同步发送,那么只需要调用get方法阻塞等待Kafka响应就可以了:

ProducerRecord<String, String> record = ...;
try {
	RecordMetadata meta = kafkaProducer.send(record).get();
	System.out.println(meta.topic() + "-" + meta.partition() + ":" + meta.offset()); 
} catch(ExecutionException | InterruptedException e) {
	//Do something...
}

RecordMetadata包含了消息的一些元数据,例如消息的主题、分区、分区中的偏移量、时间戳。

KafkaProducer在发送消息时产生的异常分为两种类型:可重试异常和不可重试异常,这些异常都属于KafkaException的子类,也都继承自RuntimeException
常见的可重试异常有:LeaderNotAvailableExceptionNetworkExceptionUnknownTopicOrPartitionExceptionNotEnoughReplicasExceptionNotCoordinatorException等。例如NetworkException表示网络异常,有可能是因为网络故障导致消息发送失败或者没有收到Broker的响应,可以通过重试来解决。LeaderNotAvailableException则表示分区的leader副本不可用,即发生在leader副本下线而新的leader正在进行选举,重试后可能恢复。不可重试的异常有RecordTooLargeException,即消息过大,KafkaProducer不会对其进行重试。

可以设置参数retriesProducerConfig.RETRIES_CONFIG)来让KafkaProducer自动进行重试操作而不抛出异常,该参数值的类型为数字,即最大重试次数,如果超出该次数依然没有成功发出消息,那么会直接抛出异常。

send方法还有一个重载的方法,也就是可以添加一个回调Callback。使用Callback很简单,Kafka有响应就会进行回调,不论是消息发送成功或者抛出异常都会进行回调。
Callback是一个接口,定义了一个方法:

void onCompletion(RecordMetadata metadata, Exception exception);

onCompletion两个参数是互斥的:即要么metadatanull,要么exceptionnull。当消息发送成功时,metadata不为null,消息发送失败抛出异常时,exception不为nullmetadatanull

序列化器Serializer

序列化器负责将Java对象转换为字节数组的形式以便通过网络的形式发送给Broker。Serializer本身是个接口,它定义了以下方法:

public interface Serializer<T> extends Closeable {
    void configure(Map<String, ?> configs, boolean isKey);
    
    byte[] serialize(String topic, T data);
    
    default byte[] serialize(String topic, Headers headers, T data) {
        return serialize(topic, data);
    }
    
    @Override 
    void close();
}

configure方法用来配置当前类,serialize两个重载方法都用于执行序列号操作,close方法用来关闭当前序列化器,一般来说close方法是空实现,如果有相应的逻辑实现则需要保证close方法具有幂等性,因为同一个对象close方法可能会被KafkaProducer调用多次。

configure方法传入的configs参数和构造KafkaProducer时传入的Properties参数是等价的。

Kafka自带了几个基本的Serializer实现类:StringSerializerUUIDSerializerIntegerSerializerLongSerializerByteArraySerializerByteBufferSerializerShortSerializerFloatSerializerDoubleSerializer。如果用户需要将自定义的对象序列化为字节流,那么需要自己定义对应的Serializer实现类。
我们以比较常用的StringSerializer为例:

public class StringSerializer implements Serializer<String> {
    private String encoding = "UTF8";

    @Override
    public void configure(Map<String, ?> configs, boolean isKey) {
        String propertyName = isKey ? "key.serializer.encoding" : "value.serializer.encoding";
        Object encodingValue = configs.get(propertyName);
        if (encodingValue == null)
            encodingValue = configs.get("serializer.encoding");
        if (encodingValue instanceof String)
            encoding = (String) encodingValue;
    }

    @Override
    public byte[] serialize(String topic, String data) {
        try {
            if (data == null)
                return null;
            else
                return data.getBytes(encoding);
        } catch (UnsupportedEncodingException e) {
            throw new SerializationException("Error when serializing string to byte[] due to unsupported encoding " + encoding);
        }
    }

    @Override
    public void close() {
        // nothing to do
    }
}

configure方法会尝试获取两个参数:key.serializer.encodingvalue.serializer.encoding,即ProducerRecordkeyvalue字符串编码格式。如果没有指定,则默认为UTF-8serialize方法也很直观,直接将字符串通过编码将其转换为byte[]

生产者拦截器ProducerInterceptor

拦截器分为两种:生产者拦截器和消费者拦截器,这里我们讨论生产者拦截器ProducerInterceptor
ProducerInterceptor可以在消息发送前做一些准备工作,比如过滤掉一些不符合规则的消息、修改消息的内容等,也可以在执行回调逻辑前做一些工作。ProducerInterceptor接口包含以下方法:

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();
}

public interface Configurable {
    void configure(Map<String, ?> configs);
}

KafkaProducer会在执行序列化器和分区器之前调用onSend方法并传入待发送的ProducerRecord,在这里可以对ProducerRecord进行一些修改。

发送完消息后,如果消息被Broker应答或者消息发送失败时就会调用到onAcknowledgement方法,优先于用户的Callback执行,类似于Callback接口,metadataexception两个参数互斥。还需要注意的是onAcknowledgement方法逻辑应尽量简短,因为该方法由IO线程执行,否则会影响到消息发送的速度。

生产者可以通过指定参数interceptor.classes来确定拦截器。

分区器Partitioner

KafkaProducer发送完消息前,分区器在调用完序列化器的逻辑后执行,其主要任务是决策消息发往的分区编号。
Partitioner接口定义了以下方法:

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值就是该消息的最终分区号

如果没有在构造KafkaProducer前指定partitioner.class参数,那么默认的分区器为DefaultPartitioner,它实现了Partitioner接口:

public class DefaultPartitioner implements Partitioner {
    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();
    
    public void configure(Map<String, ?> configs) {}
    
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); //获取主题topic的分区信息
        int numPartitions = partitions.size();  //获取该主题一共有多少个分区
        if (keyBytes == null) { //如果该消息没有指定key
            int nextValue = nextValue(topic); //通过AtomicInteger获取一个数
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) { 
                int part = Utils.toPositive(nextValue) % availablePartitions.size();  //将该数与分区数量进行模运算
                return availablePartitions.get(part).partition();  //返回分区列表集合中第part的分区号
            } else {  //如果分区列表为空
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {  //如果指定了key,以该key的hash取模的得出分区为准
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    private int nextValue(String topic) {
        AtomicInteger counter = topicCounterMap.get(topic);
        if (null == counter) {
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());  //获取一个随机数作为初始值
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter); //存入Map
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();  //原子加1
    }

    public void close() {}
}

其它重要的生产者参数

了解完上述生产者的相关概念后,我们再来关注一些比较重要的生产者参数。

  • acksProducerConfig.ACKS_CONFIG):这个参数用来指定分区中必须要有多少个副本收到这条消息,生产者客户端才认定这个消息是成功发送到了Kafka服务器。这个参数可以取三种值
  1. ack = 1,如果没有显式指定该参数那么就以这个数为默认值。具体策略是:生产者发送消息后,只要分区的leader副本成功写入消息,那么就会收到来自服务器的成功响应。如果该leader副本宕机,或者正在重新选举新的leader副本,那么生产者就会收到一个错误的响应,之前我们提到过,生产者可以通过自定义参数来实现消息的自动重发。需要注意的是这种策略依然有可能造成消息的丢失:如果消息成功写入leader副本,且在被其它follower副本拉取之前leader副本崩溃,那么此时消息还是会丢失,因为新选举的leader副本不会存在该消息。该值是消息可靠性与吞吐量之间的折中方案,如果不是需要严格保证消息不丢失,那么建议还是使用该参数的。
  2. ack = 0,这个策略是生产者发送消息后不等待服务端响应,如果消息在发送到Broker之间发生了异常,那么生产者也是无从知晓的,消息自然也就丢失了。此策略能够达到最大的吞吐量,也最容易造成消息的丢失。
  3. ack = -1ack = all,生产者在发送消息后,需要等待ISR中所有的副本都成功写入消息后才能够收到服务端成功的响应,这种配置带来的消息可靠性是最强的。但是也不一定是绝对可靠,如果ISR中只有leader副本不就成了ack = 1的情形?
  • max.request.size:该参数可以限制生产者所能发送的消息的最大值,默认值为1048576B,即1MB。这个默认值可以满足绝大多数场景了,此外也并不建议盲目增大该值。因为该参数还涉及到了其它参数的联动,例如Broker端的message.max.bytes参数,如果该参数小于客户端指定的max.request.size,当发送的消息大于Broker端的message.max.bytes参数时,就会抛出RecordTooLargeException,之前也讲到了这是一个不可恢复的异常。
  • retriesretry.backoff.msretries作用不再赘述了。retry.backoff.ms用来设定两次重试时间的间隔时间,单位为毫秒,默认值100
  • compression.type,消息压缩方式,默认值为none,即不压缩。也可以配置为gzipsnappylz4。如果对消息时延有严格要求,则不建议进行压缩。
  • connections.max.idle.ms,用来指定多久之后关闭空闲的连接,单位为毫秒,默认为540000,即9分钟。
  • linger.ms,该参数用来指定生产者发送ProducerBatch(由多个ProducerRecord序列化后的字节数组组成的集合)之前等待更多消息加入ProducerBatch的时间,生产者会在ProducerBatch被填满或者未被填满时等待时长超出linger.msProducerBatch发送出去,默认为0。增大该值会增加消息的时延,但是可以增加Broker的吞吐量。
  • receive.buffer.bytes,Socket接收缓冲区(SO_RECBUF)大小,默认为32768字节,即32KB。设置为-1时采用操作系统默认值。
  • send.buffer.bytes,Socket发送缓冲区(SO_SNDBUF)大小,默认为131072字节,设置为-1时采用操作系统默认值。
  • request.timeout.ms,生产者等待Broker响应的最长时间,默认为30000ms,超过该时间后可以选择重试。

生产者客户端整体结构

在这里插入图片描述
整个生产者客户端需要至少两个线程运行:一个主线程,由用户自己定义,还有一个Sender线程,该线程由KafkaProducer负责构造。
从由主线程构造ProducerRecord并调用KafkaProducersend方法开始,发送流程如下:

  1. 如果指定了对应的拦截器,那么调用拦截器的onSend方法并传入ProducerRecord
  2. 依次调用keyvalue对应的序列化器的serialize方法,并传入ProducerRecord中的keyvalue,获取返回的字节数组
  3. 调用分区器的partition方法,获取分区编号
  4. 根据分区编号,找到RecordAccmulator对应的双端队列。之后,将序列化后的结果存入ProducerBatch,如果队尾的ProducerBatch已满则构建一个新的ProducerBatch并存入。
  5. 接下来,由SenderRecordAccmulator获取ProducerBatch,由之前的【分区,Deque<ProducerBatch>】转换为【Node,Request】,Node表示Kafka集群中的单个Broker。转换完成后将其暂存在InFlightRequests中,表示这些消息还没有收到服务端的响应。
  6. 根据NodeRequest的映射关系,通过选择器Selector发送到Kafka集群,等待响应
  7. 收到成功响应后,通过响应内容从InFlightRequests删除缓存。如果超时或者发生可重复发送异常,那么根据retries参数策略进行重试
  8. 如果存在拦截器,则执行拦截器的onAcknowledgement方法。
  9. 如果在调用KafkaProducersender方法传入了Callback,那么执行Callback定义的逻辑。
发布了117 篇原创文章 · 获赞 96 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/100547605
今日推荐