kafka原理剖析(一)

序列化

我们知道,基于网络流传播的数据,大多传的都不是原始数据,而是经过序列化后的数据,在到达目的地后,在反序列化成原始数据。kafka也不例外

生产者需要用序列化器把对象转换为字节数组进行传播,消费者则用反序列化器把字节数组还原成对象。

生产者使用的序列化器和消费者使用的反序列化器是需要一一对应的,如果生产者使用了某种序列化器,而消费者使用了另一种序列化器,那么是无法解析出正确的数据的。

分区器

我们知道kafka中的消息是按照分区存储的,如果消息中指定了分区号,那么就不需要分区器的作用,因为指定的分区号就是该消息要发往的分区。

如果消息中没有指定分区号,根据Key这个字段来计算分区号,然后把消息发往对应的分区。分区器的作用就是为消息分配分区。

如果Key不为null,那么默认的分区器会对key进行哈希计算,最终根据得到的哈希值来计算分区号,拥有相同key的消息会被放入同一个分区中。如果key为null,那么消息将会以轮询的方式发往主题内的各个分区。

注意:如果key不为null,那么计算得到的分区号会是所有分区中的任意一个,如果key不为null,那么计算得到的分区号仅为可用分区中的任意一个

接下来解释分区不可用的情况

kafka分区不可用的情况:

  1. Leader副本不可用,这是最常见的情况,每个分区都有一个Leader副本处理读写请求,当Leader所在的broker宕机,网络分区无法连接到Leader,Leader选举正在进行中时,Leader副本会处于不可用的状态
  2. 分区没有ISR,ISR是与Leader保持同步的副本集和
  3. 分区处于离线状态
  4. 分区正在重新平衡

拦截器

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

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

kafkaProducer会在消息被应答之前或消息发送失败时调用生产者拦截器的onAcknowledgement()方法,优先于用户设定的Callback之前执行。这个方法运行在Producer的I/O线程中,所以这个方法中的代码逻辑越简单越好,否则会影响消息的发送速度

KafkaProducer中不仅可以指定一个拦截器,还可以指定多个拦截器以形成拦截器链。拦截链会按照intercepetor.classes参数配置的拦截器的顺序来一一执行,

整体架构

我们已经知道,消息在真正发往kafka之前,有可能需要经历拦截器,序列化器和分区器等一系列的作用,那么在此之后又会发生什么呢?

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

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

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

消息在网络上都是以字节的形式传输的,在发送之前需要创建一块内存区域来保存对应的消息。在RecordAccumulator的内部有一个Bufferpool,它主要用来实现ByteBuffer的复用,以实现缓存的高效利用。不过BufferPool只针对于特定大小的ByteBuffer进行管理,而其它大小的ByteBuffer不会缓存进BufferPool中,这个特定的大小由batch.size参数指定

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

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

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

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

元数据的更新

上面我们提到了InFlightRequests,InFlightRequests还可以用来获得leastLoadedNode,即所有Node中负载最小的那一个,这里的负载最小是通过每个Node再InFlightRequests中还未确认的请求决定的,未确认的请求越多则认为负载越大。选择leastLoadedNode发送请求可以使它能够尽快发出,避免因网络拥塞等异常而影响整体的进度。leastLoadedNode的概念可以用于多个应用场合,比如元数据请求,消费组播协议的交互。

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

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

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

重要的生产者参数

acks

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

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

max.request.size

这个参数用来限制生产者客户端能发送的消息的最大值,默认值为1048576B,即1MB。一般情况下,这个默认值就可以满足大多数的应用场景了。并不建议盲目地增大这个参数的配置值。

retries和retry.backoff.ms

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

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

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

compression.type

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

connections.max.idle.ms

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

linger.ms

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

相关推荐
码农水水2 小时前
京东Java面试被问:分布式会话的一致性和容灾方案
java·开发语言·数据库·分布式·mysql·面试·职场和发展
【赫兹威客】浩哥2 小时前
【赫兹威客】完全分布式Spark测试教程
大数据·分布式·spark
成为你的宁宁2 小时前
【RabbitMQ 集群企业级实战:RabbitMQ 特性、存储、工作模式解析与普通集群搭建及仲裁队列搭建企业级配置】
分布式·rabbitmq
【赫兹威客】浩哥3 小时前
【赫兹威客】伪分布式Hadoop测试教程
大数据·hadoop·分布式
a努力。3 小时前
国家电网Java面试被问:分布式Top K问题的解决方案
java·开发语言·分布式·oracle·面试·职场和发展·kafka
_ziva_3 小时前
分布式(三)深入浅出理解PyTorch分布式训练:nn.parallel.DistributedDataParallel详解
人工智能·pytorch·分布式
是垚不是土3 小时前
基于OpenTelemetry实现分布式链路追踪
java·运维·分布式·目标跟踪·系统架构
组合缺一3 小时前
Solon AI Remote Skills:开启分布式技能的“感知”时代
java·人工智能·分布式·agent·langgraph·mcp
jiunian_cn3 小时前
【Redis】Redis入门——分布式架构演进及Redis基本特性初识
redis·分布式·架构