kafka rabbitMq rocketMq

精华整理

kafka简介

Kafka已经定位为一个分布式流式处理平台。
Kafka 还提供了大多数消息系统难以实现的消息顺序性保障及回溯消费的功能。
高可靠-Kafka 把消息持久化到磁盘,有效地降低了数据丢失的风险。

分区

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

重要参数

客户端参数

  • max.in.flight.requests.per.connection
    默认值为 5,即每个连接(也就是客户端与kafka node的连接)最多只能缓存 5个未响应的请求。也就是说如果客户端发送的消息的key不同,客户端会与多个kafka node维持连接,每个连接都可以缓存5个请求。
    设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求。注意:设置此参数为1,可以避免消息乱序。
  • acks
    这个参数用来指定分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。acks 是生产者客户端中一个非常重要的参数,它涉及消息的可靠性和吞吐量之间的权衡。
    • acks=1。默认值即为1。生产者发送消息之后,只要分区的leader副本成功写入消息,那么它就会收到来自服务端的成功响应。如果消息写入leader副本并返回成功响应给生产者,且在被其他follower副本拉取之前leader副本崩溃,那么此时消息会丢失。acks设置为1,是消息可靠性和吞吐量之间的折中方案。
    • acks=0。生产者发送消息之后不需要等待任何服务端的响应。在其他配置环境相同的情况下,acks 设置为 0 可以达到最大的吞吐量。
    • acks=-1或acks=all。生产者在消息发送之后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下,acks 设置为-1(all)可以达到最强的可靠性。但这并不意味着消息就一定可靠,因为ISR中可能只有leader副本,这样就退化成了acks=1的情况。要获得更高的消息可靠性需要配合 min.insync.replicas 等参数的联动。

如果将acks参数配置为非零值,并且max.in.flight.requests.per.connection参数配置为大于1的值,那么就会出现错序的现象:如果第一批次消息写入失败,而第二批次消息写入成功,那么生产者会重试发送第一批次的消息,此时如果第一批次的消息写入成功,那么这两个批次的消息就出现了错序。
一般而言,在需要保证消息顺序的场合建议把参数max.in.flight.requests.per.connection配置为1

  • enable.auto.commit
    在Kafka 中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数enable.auto.commit 配置,默认值为 true 。当然这个默认的自动提交不是每消费一条消息就提交一次,而是定期提交,这个定期的周期时间由客户端参数 auto.commit.interval.ms配置,默认值为5秒。

broker端参数

  • min. insync. replicas
    该主题端参数对应的brokker端参数为min.insync.replicas。
    分区 ISR 集合中至少要有多少个副本,默认值为1。

日志

日志文件

查找日志分段使用跳跃表,在日志分段里查找对应的偏移量使用二分查找。

页缓存

页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘 I/O的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问转变为对内存的访问。
当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page )是否在页缓存( pagecache )中,如果存在(命中〉 直接返回数据,从 避免了对物理磁盘的 操作;
如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存,之后再将数据返回给进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页 。
被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。

Kafka 中大量使用了页缓存 ,这是 Kafka 实现高吞吐的重要因素之一。 虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务的,但在 Kafka 中同样提供了同步刷盘及间断性强制刷盘( fsync )的功能,这些功能可以通过 log.flush.interval.messages、log.flush .interval.ms 等参数来控制

零拷贝

所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。
采用了零拷贝技术,那么应用程序可以直接请求内核把磁盘中的数据传输给 Socket,零拷贝技术通过 DMA (Direct Memory Access )技术将文件内容复制到内核模式下的 Read Buffer 。DMA 擎直接将数据从内核模式中传递到网卡设备(协议引擎)。这里数据只经历了2次复制就从磁盘中传送出去了,并且上下文切换也变成,2次。
零拷贝是针对内核模式而言的 数据在内核模式下实现了零拷贝。

服务端

Kafka 中存在大量的延时操作,比如延时生产、延时拉取和延时删除等。

Kafka 中的时间轮( TimingWheel )是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表( TimerTaskList )。 TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项( TimerTaskEntry ),其中封装了真正的定时任务 TimerTask。

时间轮由多个时间格组成, 每个时间格代表当前时间轮的基本时间跨度( tickMs) 。时间轮的时间格个数是固定的,可用 wheelSize来表示,那么整个时间轮的总体时间跨度( interval)可以通过公式 tickMs × wheelSize 计算得出。

时间轮还有一个表盘指针( currentTime ),用来表示时间轮当前所处的时间,currentTime是tickMs 的整数倍。currentTime 可以将整个时间轮划分为到期部分和未到期部分。 currentTime 当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的 TimerTaskList 中的所有任务。

Kafka 的定时器只需持有TimingWheel 的第一层时间轮的引用,并不会直接持有其他高层的时间轮,但每一层时间轮都会有一个引用( overflowWhee )指向更高一层的应用,以此层级调用可以实现定时器间接持有各个层级时间轮的引用。

Kafka 引入了层级时间轮的概念,当任务的到期时间超过了当前时间轮所表示的时间范围时,就会尝试添加到上层时间轮中。
随着时间的流逝,这里就有一个时间轮降级的操作。

“随着时间的流逝”或“随着时间的推移”,那么在 Kafka中到底是怎么推进时间的呢?Kafka 的定时器借了JDK中的DelayQueue来协助推进时间轮 具体做法是对于所有的TimerTaskList 都加入DelayQueue 。

可靠性分析

怎样可以确保 Kafka 完全可靠?如果这样做就可以确保消息不丢失了吗?

  • 副本
    就Kafka而言,越多的副本数越能够保证数据的可靠性,副本数可以在创建主题时配置,也可以在后期修改,不过副本数越多也会引起磁盘、网络带宽的浪费,同时会引起性能的下降。一般而言,设置副本数为3即可满足绝大多数场景对可靠性的要求,而对可靠性要求更高的场景下,可以适当增大这个数值,比如国内部分银行在使用 Kafka 时就会设置副本数为5。与此同时,如果能够在分配分区副本的时候引入基架信息 broker.rack 参数) ,那么还要应对机架整体岩机的风险。
  • acks
    acks=-1,可以最大程度地提高消息的可靠性。
    对于ack=-1配置,生产者将消息发送到 leader 副本, leader 副本在成功写入本地日志之后还要等待 ISR中的 follower 副本全部同步完成才能够告知生产者已经成功提交,即使此时leader 本宕机,消息也不会丢失。
  • 消息发送模式
    生产者可以采用同步或异步的模式,在出现异常情况时可以及时获得通知,以便可以做相应的补救措施,比如选择重试发送(可能会引起消息重复)。
  • min.insync.replicas
    最小ISR集合。
    对于acks =-1,它要求ISR中所有的副本都收到相关的消息之后才能够告知生产者己经成功提交。试想一下这样的情形,leader副本的消息流入速度很快,而follower副本的同步速度很慢,在某个临界点时所有的 follower副本都被剔除出了ISR集合,那ISR只有一个 leader 副本,最终 acks =-1演变为 acks =1的情形,如此也就加大了消息丢失的风险。
    Kafka考虑到了这种情况,并为此提供了 min.insync.replicas参数(默认值1)来作为辅助(配合acks = -1来使用),这个参数指定了ISR集合中最小的副本数,如果不满足条件就会抛出 NotEnoughReplicasException或notEnoughReplicasAfterAppendException。
    注意min.insync.replicas参数在提升可靠性的时候会从侧面影响可用性。试想如果ISR中只有一个 leader副本,那么最起码还可以使用,而此时如果配置 min.insync.replicas>1,则会使消息无法写入。
  • unclean.leader.election.enable
    非ISR副本是否可成为leader。
    这个参数的默认值为false,如果设置为 true,就意味着当 leader 下线时候可以从非 ISR 集合中选举出新的 leader,这样有可能造成数据的丢失。如果这个参数设置为false,那么也会影响可用性,非ISR集合中的副本虽然没能及时同步所有的消息,但最起码还是存活的可用副本。
  • 刷盘策略
    在broker端还有两个参数log.flush.interval.messages和log.flush.interval.ms,用来调整同步刷盘的策略,默认是不做控制而交由操作系统本身来进行处理。

消费端的可靠性

  • 手动提交
    将 enable.auto.commit 参数设置为 false 来执行手动位移提交
  • 死信队列
    有时候,由于应用解析消息的异常,可能导致部分消息一直不能够成功被消费,那么这个时候为了不影响整体消费的进度,可以将这类消息暂存到死信队列中,以便后续的故障排除。
  • 回溯消费
    对于消费端, Kafka 还提供了一个可以兜底的功能,即回溯消费,通过这个功能可以让我们能够有机会对漏掉的消息相应地进行回补,进而可以进一步提高可靠性。

高级应用

过期时间(TTL)

我们可以使用消息的timestamp字段和拦截器Consumerlnterceptor接口的onConsume()方法,并将消息的TTL的设定值以键值对的形式保存在消息的 headers字段中,这样消费者消费到这条消息的时候可以在拦截器中根据headers 字段设定的超时时间来判断此条消息是否超时。

延时队列

通过增加一层有时间轮延迟功能的微服务

消息中间件选型

1.功能维度
优先级队列
延时队列
重试队列
死信队列
消费模式
广播消费
回溯消费
消息堆积+持久化
消息顺序性
2.性能维度
3 可靠性和可用性

RabbitMQ

Queue: 队列,是 RabbitMQ 的内部对象,用于存储消息。RabbitMQ 中消息都只能存储在队列中。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊 (Round-Robin ,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息井处理。

RabbitMQ 不支持队列层面的广播消费。可以通过其他配置来实现广播消费。即通过使用多个队列和绑定,实现支持多个消费者接收相同的消息。(也即是每个消费者对应一个队列,相当于实现了exchange层面的发布订阅)

rabbittMQ 的消费模式分两种:推Push模式和拉 Pull 模式。如果要实现高吞吐量,消费者理应使用 Basic.Consume 方法。(因为拉是每次只能拉单条消息,所以生产上没法用)

持久化:
rabbittMQ可以将所有的消息都设直为持久化,但是这样会严重影响 RabbitMQ 的性能(随机)。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吐吞量之间做一个权衡。

提高可靠性的方式:

  • RabbitMQ 提供了两种提高生产者发送消息的可靠性的方式:
    通过事务机制实现:
    通过发送方确认 publisher confirm 机制实现。
  • 从消费者来说,如果在订阅消费队列时将 autoAck 参数设置为 true ,那么
    当消费者接收到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。这种情况很好解决,将autoAck 参数设置为 false 并进行手动确认。
  • 其次,在持久化的消息正确存入 RabbitMQ 之后,还需要有一段时间(虽然很短,但是不可忽视〉才能存入磁盘之中。 RabbitMQ 并不会为每条消息都进行同步存盘(调用内核的 fsync方法)的处理,可能仅仅保存到操作系统缓存之中而不是物理磁盘之中。如果在这段时间内RabbitMQ 服务节点发生了岩机、重启等异常情况,消息保存还没来得及落盘,那么这些消息将会丢失。
    这个问题怎么解决呢?这里可以引入 RabbitMQ 镜像队列机制,相当
    于配置了副本,这样有效地保证了高可用性。

存储机制
不管是持久化的消息还是非持久化的消息都可以被写入到磁盘。持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,当内存吃紧的时候会从内存中清除。非持久化的消息一般只保存在内存中 ,在内存吃紧的时候会被换入到磁盘中,以节省内存空间。这两种类型的消息的落盘处理都在RabbitMQ 的"持久层"中完成。

深入理解Kafka:核心设计与实践原理

1 初识Kafka

目前Kafka已经定位为一个分布式流式处理平台,它以高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛使用。

Kafka之所以受到越来越多的青睐,与它所“扮演”的三大角色是分不开的:

  • 消息系统:Kafka 和传统的消息系统(也称作消息中间件)都具备系统解耦、冗余存储、流量削峰、缓冲、异步通信、扩展性、可恢复性等功能。与此同时,Kafka 还提供了大多数消息系统难以实现的消息顺序性保障及回溯消费的功能。
  • 存储系统:Kafka 把消息持久化到磁盘,相比于其他基于内存存储的系统而言,有效地降低了数据丢失的风险。也正是得益于Kafka 的消息持久化功能和多副本机制,我们可以把Kafka作为长期的数据存储系统来使用,只需要把对应的数据保留策略设置为“永久”或启用主题的日志压缩功能即可。
  • 流式处理平台:Kafka 不仅为每个流行的流式处理框架提供了可靠的数据来源,还提供了一个完整的流式处理类库,比如窗口、连接、变换和聚合等各类操作。

1.1 基本概念

Kafka体系结构
一个典型的 Kafka 体系架构包括若干 Producer、若干 Broker 、若干 Consumer ,以及ZooKeeper 集群,如图 所示 其中 ZooKeeper Kafka 用来负责集群元数据的管理、控制器的选举 操作的 Producer 将消息发送到 Broker, Broker 负责将收到的消息存储到磁盘中,而Consumer 负责从 Broker 订阅并消 费消息。

Broker :服务代理节点
Broke 可以简单地看作一个独立的 Kafka服务节点或 Kafka 服务实例。一个或多个 Broker 组成了 一个 Kafka 集群。
主题( Topic )
Kafka 中的消息以主题为单位进行归类,主题是一个逻辑上的概念,它还可以细分为多个分区。

分区( Partition )
同一主题下的不同分区包含的消息是不同的,分区在存储层面可以看作一个可追加的日志( Log )文件,消息在被追加到分区日志、文件的时候都会分配一个特定的偏移量( offset )。 offset 是消息在分区中的唯一标识, Kafka 通过它来保证消息在分区内的顺序性,不过 offset 并不跨越分区,也就是说, Kafka 保证的是分区有序而不是主题有序。,通过增加分区的数量可以实现水平扩展。
Kafka 为分区引入了多副本 Replica 机制, 通过增加副本数量可以提升容灾能力。副本之间是“一主多从”的关系,其中 leader 副本负责处理读写请求 follower 副本只负责与 lead er 副本的消息同步。副本处于不同的 broker ,当 leader 副本出现故障时,从 fo llow er 副本中重新选举新的 leader 本对外提供服务。 Kafka 通过多副本机制实现了故障的自动转移,当 Kafka 集群中某个 broker 失效时仍然能保证服务可用。
多副本架构
Kafka 消费端也具备 容灾能力。 Consumer 使用拉 Pull )模式从服务端拉取消息,并且保存消费 位置 当消费者看机后恢复上线时可以根据之前保存的消费位置重新拉取需要的消息进行消 ,这样就不会造成消息丢失。

分区中 的所有副本统称为 AR ( Assigned Replicas 所有与 leader 副本保持一定程度同步的副本(包括 leader 副本在内〕组成 ISR On-Sync Replicas) , ISR 集合是 AR 集合中 个子集。前面所说的“ 一定程度的同步”是指可忍受的滞后范围,这个范围可以通过参数进行配置。与 leader 副本同步滞后过多的副本(不包 leader 副本)组成 OSR (Out-of-Sync Replicas ),由此可见, AR=ISR+OSR。在正常情况下, follower 副本都应该与 leader 副本保持 定程度 的同步,即 AR=ISR,OSR 集合为空。默认情况下, leader副本发生故障时,只 有在 ISR 集合中的副本才有资格被选举为新的 leader,而在 OSR 集合中的副本没有任何机会(不过这个原则也可以通过修改相应的参数配置来改变)。

ISR HW LEO 也有紧密的关系 HW High Watermark 的缩写,俗称高水位,它标识一个特定的消息偏移量( offset ),消费者只能拉取到这个 offset 前的消息。
分区中各种偏移量的说明
如图 -4 示,它代表 个日志文件,这个日志文件中有 条消息,第一条消息的 offset( LogStartOffset )为 ,最后 条消息的 offset 8, offset 的消息用虚线框表示,代表下一条待写入的消息。日志文件的 HW ,表示消费者只能拉取到 offset 在0至5之间的消息,而offset 为6的消息对消 费者而言是不可见的。

LEO Log End Offset 缩写,它标识当前日志文件中下一条待写入消息 offset ,图 1-4offset 的位置即为当前日志文件的 LEO, LEO 的大小相 当于当前日 志分区中最后 条消息的 offse 值加1 。分区 ISR 集合中的每个副本都会维护自身的 LEO,而 IS 集合中最小的 LEO即为分区的 HW ,对消费者而言只能消费 HW 之前的消息。

由此可见, kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的 fo ll ower 副本都复制完,这条消息才会被确认为已成功提交,这种复制方式极大地影响了性能。而在异步复制方式下, fo llower 副本异步地从 leader 副本中 复制数据,数据只要被 leader 副本写入就被认为已经成功提交。在这种情况下,如果 fo ll ower 副本都还没有复制完而落后于 leader 副本,突然 leader 副本着机,则会造成数据丢失。 Kafka 使用的这种ISR 的方式则有效地权衡了数据可靠性和性能之间的关系。

2 生产者

2.1 客户端开发

2.1.1 消息对象 ProducerRecord
public class ProducerRecord<K, V> {
    
	private final String topic; // 主题
	private final Integer partit on //分区号
	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 的消息会被划分到同一个分区中,value 是指消息体, 一般不为空,如果为空则表示特定的消息一一墓碑消息,timestamp 是指消息的时间戳,
它有 CreateTime和LogAppendTime 两种类型,前者表示消息创建的时间,后者表示消息追加到日志文件的时间。

2.1.2 消息的发送

发送消息主要有三种模式:发后即忘(fire-and-forget)、同步(sync)及异步(async)。

发后即忘
性能最高,可靠性也最差。

同步
KafkaProducer 的 send()方法并非是 void 类型,而是 Future<RecordMetadata>类型,send()方法有2个重载方法,具体定义如下:
在这里插入图片描述
实现同步的发送方式,可以利用返回的Future对象实现。示例中在执行send()方法之后直接链式调用了get()方法来阻塞等待Kafka的响应,直到消息发送成功,或者发生异常。如果发生异常,那么就需要捕获异常并交由外层逻辑处理。

在这里插入图片描述
也可以在执行完send()方法之后不直接调用get()方法,比如下面的一种同步发送方式的实现:
在这里插入图片描述
这样可以获取一个RecordMetadata对象,在RecordMetadata对象里包含了消息的一些元数据信息,比如当前消息的主题、分区号、分区中的偏移量(offset)、时间戳等。
Future 表示一个任务的生命周期,并提供了相应的方法来判断任务是否已经完成或取消,以及获取任务的结果和取消任务等。

同步发送的方式可靠性高,要么消息被发送成功,要么发生异常。如果发生异常,则可以捕获并进行相应的处理,而不会像“发后即忘”的方式直接造成消息的丢失。

异步
异步发送的方式,一般是在send()方法里指定一个Callback的回调函数,Kafka在返回响应时调用该函数来实现异步的发送确认。异步发送方式的示例如下:
在这里插入图片描述
对于同一个分区而言,如果消息record1于record2之前先发送(参考上面的示例代码),那么KafkaProducer就可以保证对应的callback1在callback2之前调用,也就是说,回调函数的调用也可以保证分区有序。

2.1.3 分区器

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

注意:如果 key 不为 null,那么计算得到的分区号会是所有分区中的任意一个;如果 key为null,那么计算得到的分区号仅为可用分区中的任意一个,注意两者之间的差别。

在不改变主题分区数量的情况下,key与分区之间的映射可以保持不变。不过,一旦主题中增加了分区,那么就难以保证key与分区之间的映射关系了。

2.2 原理分析

2.2.1 整体架构

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

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

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

消息在网络上都是以字节(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节点。

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

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

2.2.2 元数据的更新

2.2.1节中提及的InFlightRequests还可以获得leastLoadedNode,即所有Node中负载最小的那一个。这里的负载最小是通过每个Node在InFlightRequests中还未确认的请求决定的,未确认的请求越多则认为负载越大。

我们只知道主题的名称,对于其他一些必要的信息却一无所知。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的默认值为300000,即5分钟。元数据的更新操作是在客户端内部进行的,对客户端的外部使用者不可见。当需要更新元数据时,会先挑选出leastLoadedNode,然后向这个Node发送MetadataRequest请求来获取具体的元数据信息。这个更新操作是由Sender线程发起的,在创建完MetadataRequest之后同样会存入InFlightRequests,之后的步骤就和发送消息时的类似。元数据虽然由Sender线程负责更新,但是主线程也需要读取这些信息,这里的数据同步通过synchronized和final关键字来保障。

2.3 重要的生产者参数

acks

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

  • acks=1。默认值即为1。生产者发送消息之后,只要分区的leader副本成功写入消息,那么它就会收到来自服务端的成功响应。如果消息写入leader副本并返回成功响应给生产者,且在被其他follower副本拉取之前leader副本崩溃,那么此时消息会丢失。acks设置为1,是消息可靠性和吞吐量之间的折中方案。
  • acks=0。生产者发送消息之后不需要等待任何服务端的响应。在其他配置环境相同的情况下,acks 设置为 0 可以达到最大的吞吐量。
  • · acks=-1或acks=all。生产者在消息发送之后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应。在其他配置环境相同的情况下,acks 设置为-1(all)可以达到最强的可靠性。但这并不意味着消息就一定可靠,因为ISR中可能只有leader副本,这样就退化成了acks=1的情况。要获得更高的消息可靠性需要配合 min.insync.replicas 等参数的联动。
max.in.flight.requests.per.connection

默认值是5,该参数指定了生产者在收到服务器响应之前可以发送多少个消息。设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求。注意:设置此参数是为了避免消息乱序。

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

参考:Kafka 常见问题

compression.type

对消息进行压缩可以极大地减少网络传输量、降低网络I/O,从而提高整体的性能。消息压缩是一种使用时间换空间的优化方式,如果对时延有一定的要求,则不推荐对消息进行压缩。

enable.idempotence

是否开启幂等功能。开启 enable.idempotence 后, kafka 就会自动帮你做好消息去重的一系列工作。 底层具体实现原理很简单,就是用空间换时间的优化思路,即在 broker 端多存一些字段来标识数据的唯一性。 当 Producer 发送了具有相同字段值的消息后, broker 会进行匹配去重,丢弃重复的数据。

3 消费者

3.1 消费者与消费者组

每一个消费者都有一个对应的消费者组。当消息发布到主题后,只会被投递给订阅它的每一个消费组中的一个消费者。
消费者和消费者组模型,使得kafka可以同时支持点对点模式和发布/订阅模式。

  • 如果所有消费者都隶属于同一个消费组,那么所有的消息都会被均衡地投递寄给每一个消费者,这就相当于点对点模式的应用
  • 如果所有的消费者都隶属于不同的消费组,那么每条休息都会被所有的消费者处理,这就相当于发布/订阅模式的应用

kafka中消费者宕机,分区数据会被其他消费者消费么?
是的。

3.2 客户端开发

3.2.1 消息消费

kafka中的消费是基于拉模式的。

public ConsumerRecords<K,V> poll(final Duration timeout)

消费者消费到的每条消息的类型为ConsumerRecord(注意与ConsumerRecords的区别),这个和生产者发送的消息类型producerRecord相对应,不过ConsumerRecord的内容更加丰富。

public class ConsumerRecord<K, V> {
    
private final String topic;
private final int partition;
private final long offset;
private final long timestamp;
private final TimestampType timestampType;
private final int serializedKeySize;
private final int serializedValueSize; 
private final Headers headers; 
private final K key ; 
private final V value ; 
private volatile Long checksum; 
}
3.2.2 位移提交

对于 Kafka 中的分区而言,它的每条消息都有唯 offset ,用来表示消息在分区中对应的位置。对于消费者而,它也有 offset 的概念,消费者使用 offse 来表示消费到分区中某个消息所在的位置。

在旧消费者客户端中,消费位移是存储在 ZooKeeper中的。而在新消费者客户端中,消费位移存储在 Kafka 部的主题 _consumer_offsets中。

在Kafka 中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数
enable.auto.commit 配置,默认值为 true 。当然这个默认的自动提交不是每消费一条消息就提交一次,而是定期提交,这个定期的周期时间由客户端参数 auto.commit.interval.ms配置,默认值为5秒,此参数生效的前提是 enable.auto.commit 参数为 true 。
在默认的方式下,消费者每隔5秒会将拉取到的每个分区中最大的消息位移进行提交。自动位移提交的动作是在 poll ()方法的逻辑里完成的,在每次真正向服务端发起拉取请求之前会检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移。

3.2.3 指定位移消费

在kafka 中,每当消费者查找不到所记录的消费位移时,就会根据消费者客户端参数auto. offset.reset 的配置来决定从何处开始进行消费,这个参数的默认值为“latest ”,表示从分区末尾开始消费消息。如果将 auto.offset.reset
参数配置为“earlie ”,那么消费者会从起始处,也就是 开始消费。auto.offset.reset参数还有 个可配置的值一一“none ”,配置为此值就意味着出现查不到消费位移的时候,既不从最新的消息位置处开始消费,也不从最早的消息位置处开始消费,此时会报出 NoOffsetForPartitionException 异常。

KafkaConsumer 中的 seek()方法,可以从特定的位移处开始拉取消息。

public void seek(TopicPartit on part tion long offset)
3.2.4再均衡

再均衡是指分区的所属权从一个消费者转移到另一消费者的行为,它为消费组具备高可用性和伸缩性提供保障,使我们可以既方便又安全地删除消费组内的消费者或往消费组内添加消费者。

不过在再均衡发生期间,消费组内的消费者是无法读取消息的。 就是说,在再均衡发生期间的这一小段时间内,消费组会变得不可用 。另外,当一个分区被重新分配给另一个消费者时, 消费者当前的状态也会丢失。。比如消费者消费完某个分区中的一部分消息时还没有来得及提交消费位移就发生了再均衡操作 之后这个分区又被分配给了消费组 的另一个消费者,原来被消费完的那部分消息又被重新消费一遍,也就是发生了重复消费。一般情况下,应尽量避免不必要的再均衡的发生。

3.2.5 消费者拦截器

消费者拦截器需要自定义实现 org.apache.kafka.clients.consumer.
Consumerlnterceptor 接口。 ConsumerInterceptor 接口包含3个方法:

public ConsumerRecords<K, V> onConsume(ConsumerRecords<K , V> records); 
public void onCommit(M ap<TopicPartition, OffsetAndMetadata> offsets); 
public void close()

KafkaConsumer 会在 Poll()方法返回之前调用拦截器的 Consumer() 方法来对消息进行相应的定制 操作,比如修改返回的消息内容、按照某种规则过滤消息(可能会减少 poll () 方法返回的消息的个数〉。
KafkaConsumer 会在提交完消费位移之后调用拦截器的 Commit() 方法,可以使用这个方法来记录跟踪所提交的位移信息,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值