持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情
前言
本文主要介绍kafka生产者客户端,包含以下内容:
- kafka消息发送流程,包含消息拦截器、序列化器、分区器等。
- kafka消息发送原理与客户端整体架构,包含主线程与发送Sender线程的作用。
客户端开发流程
一般来说,一个生产逻辑需要一下几个步骤:
- 配置生产者客户端参数及创建相应的生产者实例。
- 构建待发送的消息。
- 发送消息。
- 关闭生产者实例。
kafka的消息一般为以下结构:
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 fianl Long timestamp // 消息的时间戳
// 其他信息
}
其中topic与partition字段分别代表了消息要发往的主题和区号,headers字段为消息的头部,大多用来设定一些应用的相关信息,比如消息所属host、服务等。key为指定消息的键,可以用来结算消息的分区号,消息以主题为单位进行归类,除此之外,业务也可以通过key将消息进行二次归类,比如简单将同一个key的消息划分到统一分区中。另外有key的消息可以支持日志压缩功能,此处不展开详细讲解,可自行查阅资料。value则是实际消息的值。一般不为空,如果为空则表示特定的消息——墓碑消息。timestamp是指消息的时间戳,一般有两类,创建时间CreateTime与消息追加到日志文件的时间LogAppendTime。
发送消息需要创建生产者实例KafkaProducer,KakfaProducer是线程安全的,可以在多个线程中共享单个KafkaProducer实例,也可以将KafkaProducer实例进行池化来供其他线程调用。KafkaProducer实例发送消息有三种模式:
- 发后即忘(fire-and-forgrt):只负责往kafka发送消息,不用关心消息是否正确到达,此种方式性能最高,但可能丢失消息。
- 同步(sync):消息发送时同步的,通过发送接口返回的Future对象的get()方法获取发送结果,根据结果进行其他业务处理,比如发生异常时重试等。
- 异步(async):通过在发送send方法中指定一个Callback回调函数实现。在kafka返回相应时通过回调函数实现异步消息发送的确认。
小问题:发送消息的方法返回为Future对象,为什么不用Future对象做异步处理而通过指定回调函数来实现? 答:通过Future对象调用需要考虑get()方法在什么地方什么时候调用,消息持续发送时,多个Future对象的处理逻辑过于混乱负责,而通过回调Callback的方式比较简单明了,kafka有响应时就会回调,要么成功,要么异常,只需分别出列这两种情况即可。
由于kafka可以保证分区的有序性,因此回调函数也可以保证分区的回调有序性。另外在发送消息后一般建议通过close方法回收对应资源,该方法调用时会阻塞等待之前所有的发送请求完成后再关闭回收KafkaProducer实例,并且支持指定超时时间进行关闭。
序列化器
生产者发送消息需要用序列化器(Serializer)把对象转换成字节数组才能通过网络发送给Kafka,而对策消费者端,则需要使用反序列化器(Deserializer)包Kafka种收到的字节数组转换为相应的对象。显然生产者与消费者使用的序列化器与反序列化器需要一一对应,否则二者就像中国人与外国人交流时都用本土化交流,根本无法正常沟通。 Kafka自身提供了几种序列化器如StringSerialzer、IntegerSerialzer等。也可以使用市面常见的JSON、Thrift、ProtoBuf和Protostuff等,并且也支持使用自定义的序列化器。
分区器
消息发送到broker的过程中,有可能经过拦截器(Interceptor)、序列化器(Serializer)、分区器(Partitioner)的一系列的处理才能到达broker。拦截器使用较少一般不是必须的,而序列化器是必须的,通过序列化器后消息会发往分区,此若消息未指定具体的分区partition,那么就需要使用分区器来计算消息应该发往哪个分区,简而言之,分区器的作用就是来为消息分配分区。
Kafka的默认分区器通过对消息的key进行哈希(采用MurmurHash2算法,具备高运算性能与低碰撞率的特点)计算分区,但如果key为null,消息会以轮询的方式发送到主题内的各个可用的分区。 注:key为null时,消息会被发送到可用分区的任意一个,key不为null时,key会发往所有分区中通过计算获取的一个分区,二者是存在差别的。
除了默认的分区器外,kafak也可以支持我们实现自定义的分区器。
拦截器
Kafak对生产者拦截器提供了接口扩展,在消息序列化与分区计算前会调用接口的onSend()方法来对消息进行定制化的操作,但一般不建议修改消息的topic、partition、key等信息,这些信息在分区器以及后面操作中会用到,避免造成非预期的应对象。另外该接口提供了onAcknowledgement()方法用于在消息被应答或发送失败前调用,并且此方法在用户异步发送时指定的Callback方法之前调用,此方法运行在KafkaProducer的I/O线程中,因此实现逻辑越简单越好,否则会影响消息的发送速度。
生产则KafkaProducer原理分析
如上图,整个Kafka生产者客户端由两个线程协调运行,分别是主线程与Sender发送线程。在主线程中消息经过拦截器、序列化器、分区器的作用后会被缓存到消息累加器RecordAccumulator中,然后由Sender线程负责将从RecordAccumulator中获取并发送消息大Kafka中。RecordAccumulator主要用于缓存消息以便Sender线程可以批量发送,从而减少网络传输的资源消耗以提升性能,RecordAccumulator包含多个双端队列,主线程发送的消息即ProducerBatch(一条或多条消息)都会被添加到某个双端队列中(尾插法追加),Sender线程会从双端队列的头部读取消息,然后将消息发送到kafka具体的broker。发送过程涉及将消息分区转换为对应的broker节点等过程,此步骤依赖逻辑层partition与网络I/O层broker的转换,需要依赖kafka的集群元数据信息,袁术元数据信息经常是动态变化的,客户端也会定时对元数据进行更新,这些内部细节大家可查阅资料自行将进行了解。
总结
本文简单介绍的Kafka生产者整体架构与作用,以及发送消息的流程和内部实现,相信读者对于kafka消息发送的过程有了清晰的认识。