Kafka 笔记
1概念
- Producer:消息生产者,向Kafka中发布消息的角色。
- Consumer:消息消费者,即从Kafka中拉取消息消费的客户端。
- Consumer Group:消费者组,消费者组则是一组中存在多个消费者,消费者消费Broker中当前Topic的不同分区中的消息,消费者组之间互不影响,所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。某一个分区中的消息只能够一个消费者组中的一个消费者所消费
- Broker:经纪人,一台Kafka服务器就是一个Broker,一个集群由多个Broker组成,一个Broker可以容纳多个Topic。
- Topic:主题,可以理解为一个队列,生产者和消费者都是面向一个Topic
- Partition:分区,为了实现扩展性,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition,每个Partition是一个有序的队列(分区有序,不能保证全局有序)
- Replica:副本Replication,为保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常的工作,Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader和多个Follower
- Leader:每个分区多个副本的主角色,生产者发送数据的对象,以及消费者消费数据的对象都是Leader。
- Follower:每个分区多个副本的从角色,实时的从Leader中同步数据,保持和Leader数据的同步,Leader发生故障的时候,某个Follower会成为新的Leader。
上述一个Topic会产生多个分区Partition,分区中分为Leader和Follower,消息一般发送到Leader,Follower通过数据的同步与Leader保持同步,消费的话也是在Leader中发生消费,如果多个消费者,则分别消费Leader和各个Follower中的消息,当Leader发生故障的时候,某个Follower会成为主节点,此时会对齐消息的偏移量
工作流程
2 .1工作流程
Kafka中消息是以topic进行分类的,Producer生产消息,Consumer消费消息,都是面向topic的。
Topic是逻辑上的改变,Partition是物理上的概念,每个Partition对应着一个log文件,该log文件中存储的就是producer生产的数据
Producer生产的数据会被不断的追加到该log文件的末端,且每条数据都有自己的offset,consumer组中的每个consumer,都会实时记录自己消费到了哪个offset,以便出错恢复的时候,可以从上次的位置继续消费。
2.2 文件存储
Kafka文件存储也是通过本地落盘的方式存储的,主要是通过相应的log与index等文件保存具体的消息文件。
生产者不断的向log文件追加消息文件,为了防止log文件过大导致定位效率低下,Kafka的log文件以1G为一个分界点,当.log
文件大小超过1G的时候,此时会创建一个新的.log
文件,同时为了快速定位大文件中消息位置,Kafka采取了分片和索引的机制来加速定位。
在kafka的存储log的地方,即文件的地方,会存在消费的偏移量以及具体的分区信息,分区信息的话主要包括.index
和.log
文件组成,
2.3 生产者分区策略
分区的原因
-
方便在集群中扩展:每个partition通过调整以适应它所在的机器,而一个Topic又可以有多个partition组成,因此整个集群可以适应适合的数据
-
可以提高并发:以Partition为单位进行读写。类似于多路。
分区的原则
- 指明partition(这里的指明是指第几个分区)的情况下,直接将指明的值作为partition的值
- 没有指明partition的情况下,但是存在值key,此时将key的hash值与topic的partition总数进行取余得到partition值
- 值与partition均无的情况下,第一次调用时随机生成一个整数,后面每次调用在这个整数上自增,将这个值与topic可用的partition总数取余得到partition值,即round-robin算法。
2.4 生产者ISR
为保证producer发送的数据能够可靠的发送到指定的topic中,topic的每个partition收到producer发送的数据后,都需要向producer发送ackacknowledgement
,如果producer收到ack就会进行下一轮的发送,否则重新发送数据。
发送ack的时机
确保有follower与leader同步完成,leader在发送ack,这样可以保证在leader挂掉之后,follower中可以选出新的leader(主要是确保follower中数据不丢失)
follower同步完成多少才发送ack
- 半数以上的follower同步完成,即可发送ack
- 全部的follower同步完成,才可以发送ack
2.4.1 副本数据同步策略
半数follower同步完成即发送ack
优点是延迟低
缺点是选举新的leader的时候,容忍n台节点的故障,需要2n+1个副本(因为需要半数同意,所以故障的时候,能够选举的前提是剩下的副本超过半数),容错率为1/2
全部follower同步完成完成发送ack
优点是容错率搞,选举新的leader的时候,容忍n台节点的故障只需要n+1个副本即可,因为只需要剩下的一个人同意即可发送ack了
缺点是延迟高,因为需要全部副本同步完成才可
kafka选择的是第二种,因为在容错率上面更加有优势,同时对于分区的数据而言,每个分区都有大量的数据,第一种方案会造成大量数据的冗余。虽然第二种网络延迟较高,但是网络延迟对于Kafka的影响较小。
2.4.2 ISR(同步副本集)
猜想
采用了第二种方案进行同步ack之后,如果leader收到数据,所有的follower开始同步数据,但有一个follower因为某种故障,迟迟不能够与leader进行同步,那么leader就要一直等待下去,直到它同步完成,才可以发送ack,此时需要如何解决这个问题呢?
解决
leader中维护了一个动态的ISR(in-sync replica set),即与leader保持同步的follower集合,当ISR中的follower完成数据的同步之后,给leader发送ack,如果follower长时间没有向leader同步数据,则该follower将从ISR中被踢出,该之间阈值由replica.lag.time.max.ms
参数设定。当leader发生故障之后,会从ISR中选举出新的leader。
2.5 生产者ack机制
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没有必要等到ISR中所有的follower全部接受成功。
Kafka为用户提供了三种可靠性级别,用户根据可靠性和延迟的要求进行权衡选择不同的配置。
ack参数配置
0
:producer不等待broker的ack,这一操作提供了最低的延迟,broker接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据1
:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将丢失数据。(只是leader落盘--1(all)
:producer等待broker的ack,partition的leader和ISR的follower全部落盘成功才返回ack,但是如果在follower同步完成后,broker发送ack之前,如果leader发生故障,会造成数据重复。(这里的数据重复是因为没有收到,所以继续重发导致的数据重复)
producer返ack,0无落盘直接返,1只leader落盘然后返,-1全部落盘然后返
2.6 数据一致性问题
- LEO(Log End Offset):每个副本最后的一个offset
- HW(High Watermark):高水位,指代消费者能见到的最大的offset,ISR队列中最小的LEO。
follower故障和leader故障
- follower故障:follower发生故障后会被临时提出ISR(InSyncRepli),等待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步,等待该follower的LEO大于等于该partition的HW,即follower追上leader之后,就可以重新加入ISR了。
- leader故障:leader发生故障之后,会从ISR中选出一个新的leader,为了保证多个副本之间的数据的一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader中同步数据。
这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复
2.7 ExactlyOnce
将服务器的ACK级别设置为-1(all),可以保证producer到Server之间不会丢失数据,即At Least Once至少一次语义。将服务器ACK级别设置为0,可以保证生产者每条消息只会被发送一次,即At Most Once至多一次。
At Least Once可以保证数据不丢失,但是不能保证数据不重复,而At Most Once可以保证数据不重复,但是不能保证数据不丢失,对于重要的数据,则要求数据不重复也不丢失,即Exactly Once即精确的一次。
在0.11
版本的Kafka之前,只能保证数据不丢失,在下游对数据的重复进行去重操作,多余多个下游应用的情况,则分别进行全局去重,对性能有很大影响。
0.11
版本的kafka,引入了一项重大特性:幂等性,幂等性指代Producer不论向Server发送了多少次重复数据,Server端都只会持久化一条数据。幂等性结合At Least Once语义就构成了Kafka的Exactly Once语义。
启用幂等性,即在Producer的参数中设置enable.idempotence=true
即可,Kafka的幂等性实现实际是将之前的去重操作放在了数据上游来做,开启幂等性的Producer在初始化的时候会被分配一个PID,发往同一个Partition的消息会附带Sequence Number,而Broker端会对<PID,Partition,SeqNumber>
做缓存,当具有相同主键的消息的时候,Broker只会持久化一条。
但PID在重启之后会发生变化,同时不同的Partition也具有不同的主键,所以幂等性无法保证跨分区跨会话的Exactly Once。
3.Kafka 与 Zookeeper
Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线、所有topic的分区副本分配和leader的选举等工作。Controller的工作管理是依赖于zookeeper的。
3.1 Zookeeper 协调控制
-
管理broker与consumer的动态加入与离开。(Producer不需要管理,随便一台计算机都可以作为Producer向Kakfa Broker发消息)
-
触发负载均衡,当broker或consumer加入或离开时会触发负载均衡算法,使得一
个consumer group内的多个consumer的消费负载平衡。(因为一个comsumer消费一个或多个partition,一个partition只能被一个consumer消费)
-
维护消费关系及每个partition的消费信息。
3.2 Zookeeper上的细节:
-
每个broker启动后会在zookeeper上注册一个临时的broker registry,包含broker的ip地址和端口号,所存储的topics和partitions信息。
-
每个consumer启动后会在zookeeper上注册一个临时的consumer registry:包含consumer所属的consumer group以及订阅的topics。
-
每个consumer group关联一个临时的owner registry和一个持久的offset registry。对于被订阅的每个partition包含一个owner registry,内容为订阅这个partition的consumer id;同时包含一个offset registry,内容为上一次订阅的offset。
用了磁盘,还速度快
没错,kafka就是速度无敌,本文将探究kafka无敌性能背后的秘密。
首先要有个概念,kafka高性能的背后,是多方面协同后、最终的结果,kafka从宏观架构、分布式partition存储、ISR数据同步、以及“无孔不入”的高效利用磁盘/操作系统特性,这些多方面的协同,是kafka成为性能之王的必然结果。
本文将从kafka零拷贝,探究其是如何“无孔不入”的高效利用磁盘/操作系统特性的。
先说说零拷贝
零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在IO读写过程中。
实际上,零拷贝是有广义和狭义之分,目前我们通常听到的零拷贝,包括上面这个定义减少不必要的拷贝次数
都是广义上的零拷贝。其实了解到这点就足够了。
我们知道,减少不必要的拷贝次数,就是为了提高效率。那零拷贝之前,是怎样的呢?
聊聊传统IO流程
比如:读取文件,再用socket发送出去
传统方式实现:
先读取、再发送,实际经过1~4四次copy。
buffer = File.read
Socket.send(buffer)
1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。
重新思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的意义。
这种场景
:是指读取磁盘文件后,不需要做其他处理,直接用网络发送出去。试想,如果读取磁盘的数据需要用程序进一步处理的话,必须要经过第二次和第三次数据copy,让应用程序在内存缓冲区处理。
为什么Kafka这么快
kafka作为MQ也好,作为存储层也好,无非是两个重要功能,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据;我们把它简化成如下两个过程:
1、网络数据持久化到磁盘 (Producer 到 Broker)
2、磁盘文件通过网络发送(Broker 到 Consumer)
下面,先给出“kafka用了磁盘,还速度快”的结论
1、顺序读写
磁盘顺序读或写的速度400M/s,能够发挥磁盘最大的速度。
随机读写,磁盘速度慢的时候十几到几百K/s。这就看出了差距。
kafka将来自Producer的数据,顺序追加在partition,partition就是一个文件,以此实现顺序写入。
Consumer从broker读取数据时,因为自带了偏移量,接着上次读取的位置继续读,以此实现顺序读。
顺序读写,是kafka利用磁盘特性的一个重要体现。
2、零拷贝 sendfile(in,out)
数据直接在内核完成输入和输出,不需要拷贝到用户空间再写出去。
kafka数据写入磁盘前,数据先写到进程的内存空间。
3、mmap文件映射
虚拟映射只支持文件;
在进程 的非堆内存开辟一块内存空间,和OS内核空间的一块内存进行映射,
kafka数据写入、是写入这块内存空间,但实际这块内存和OS内核内存有映射,也就是相当于写在内核内存空间了,且这块内核空间、内核直接能够访问到,直接落入磁盘。
这里,我们需要清楚的是:内核缓冲区的数据,flush就能完成落盘。
我们来重点探究 kafka两个重要过程、以及是如何利用两个零拷贝技术sendfile和mmap的。
网络数据持久化到磁盘 (Producer 到 Broker)
传统方式实现:
data = socket.read()// 读取网络数据
File file = new File()
file.write(data)// 持久化到磁盘
file.flush()
先接收生产者发来的消息,再落入磁盘。
实际会经过四次copy,如下图的四个箭头。
数据落盘通常都是非实时的,kafka生产者数据持久化也是如此。Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。
对于kafka来说,Producer生产的数据存到broker,这个过程读取到socket buffer的网络数据,其实可以直接在OS内核缓冲区,完成落盘。并没有必要将socket buffer的网络数据,读取到应用进程缓冲区;在这里应用进程缓冲区其实就是broker,broker收到生产者的数据,就是为了持久化。
在此特殊场景
下:接收来自socket buffer的网络数据,应用进程不需要中间处理、直接进行持久化时。——可以使用mmap内存文件映射。
Memory Mapped Files
简称mmap,简单描述其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。
mmap也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数——producer.type来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。
磁盘文件通过网络发送(Broker 到 Consumer)
传统方式实现:
先读取磁盘、再用socket发送,实际也是进过四次copy。
buffer = File.read
Socket.send(buffer)
而 Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示。
相比于文章开始,对传统IO 4步拷贝的分析,sendfile将第二次、第三次拷贝,一步完成。
其实这项零拷贝技术,直接从内核空间(DMA的)到内核空间(Socket的)、然后发送网卡。
应用的场景非常多,如Tomcat、Nginx、Apache等web服务器返回静态资源等,将数据用网络发送出去,都运用了sendfile。
简单理解 sendfile(in,out)就是,磁盘文件读取到操作系统内核缓冲区后、直接扔给网卡,发送网络数据。
注: transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。
Kafka总结
- partition顺序读写,充分利用磁盘特性,这是基础,顺序读写是指的文件的顺序追加,减少了磁盘寻址的开销,相比随机写速度提升很多;
- Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;
- Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。
- Kafka天生的分布式架构
- 使用了零拷贝技术,不需要切换到用户态,在内核态即可完成读写操作,且数据的拷贝次数也更少。
- 对log文件进行了分segment,并对segment建立了索引
mmap 和 sendfile总结
1、都是Linux内核提供、实现零拷贝的API;
2、sendfile 是将读到内核空间的数据,转到socket buffer,进行网络发送;
3、mmap将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
RocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。
Kafka 和 RabbitMQ
有个 xx 需求,我应该用 Kafka 还是 RabbitMQ ?
这个问题很常见,而且很多人对二者的选择也把握不好。
所以我决定写篇文章来详细说一下:Kafka 和 RabbitMQ 的区别,适用于什么场景?
同时,这个问题在面试中也经常问到。
下面我会通过 6 个场景,来对比分析一下 Kafka 和 RabbitMQ 的优劣。
一、消息的顺序
有这样一个需求:当订单状态变化的时候,把订单状态变化的消息发送给所有关心订单变化的系统。
订单会有创建成功、待付款、已支付、已发货的状态,状态之间是单向流动的。
好,现在我们把订单状态变化消息要发送给所有关心订单状态的系统上去,实现方式就是用消息队列。
在这种业务下,我们最想要的是什么?
- 消息的顺序:对于同一笔订单来说,状态的变化都是有严格的先后顺序的。
- 吞吐量:像订单的业务,我们自然希望订单越多越好。订单越多,吞吐量就越大。
在这种情况下,我们先看看 RabbitMQ 是怎么做的。
首先,对于发消息,并广播给多个消费者这种情况,RabbitMQ 会为每个消费者建立一个对应的队列。也就是说,如果有 10 个消费者,RabbitMQ 会建立 10 个对应的队列。然后,当一条消息被发出后,RabbitMQ 会把这条消息复制 10 份放到这 10 个队列里。
当 RabbitMQ 把消息放入到对应的队列后,我们紧接着面临的问题就是,我们应该在系统内部启动多少线程去从消息队列中获取消息。
如果只是单线程去获取消息,那自然没有什么好说的。但是多线程情况,可能就会有问题了……
RabbitMQ 有这么个特性,它在官方文档就声明了自己是不保证多线程消费同一个队列的消息,一定保证顺序的。而不保证的原因,是因为多线程时,当一个线程消费消息报错的时候,RabbitMQ 会把消费失败的消息再入队,此时就可能出现乱序的情况。
T0 时刻,队列中有四条消息 A1、B1、B2、A2。其中 A1、A2 表示订单 A 的两个状态:待付款、已付款。B1、B2 也同理,是订单 B 的待付款、已付款。
到了 T1 时刻,消息 A1 被线程 1 收到,消息 B1 被线程 2 收到。此时,一切都还正常。
到了 T3 时刻,B1 消费出错了,同时呢,由于线程 1 处理速度快,又从消息队列中获取到了 B2。此时,问题开始出现。
到了 T4 时刻,由于 RabbitMQ 线程消费出错,可以把消息重新入队的特性,此时 B1 会被重新放到队列头部。所以,如果不凑巧,线程 1 获取到了 B1,就出现了乱序情况,B2 状态明明是 B1 的后续状态,却被提前处理了。
所以,可以看到了,这个场景用 RabbitMQ,出现了三个问题:
- 为了实现发布订阅功能,从而使用的消息复制,会降低性能并耗费更多资源
- 多个消费者无法严格保证消息顺序
- 大量的订单集中在一个队列,吞吐量受到了限制
那么 Kafka 怎么样呢?Kafka 正好在这三个问题上,表现的要比 RabbitMQ 要好得多。
首先,Kafka 的发布订阅并不会复制消息,因为 Kafka 的发布订阅就是消费者直接去获取被 Kafka 保存在日志文件中的消息就好。无论是多少消费者,他们只需要主动去找到消息在文件中的位置即可。
其次,Kafka 不会出现消费者出错后,把消息重新入队的现象。
最后,Kafka 可以对订单进行分区,把不同订单分到多个分区中保存,这样,吞吐量能更好。
所以,对于这个需求 Kafka 更合适。
二、消息的匹配
我曾经做过一套营销系统。这套系统中有个非常显著的特点,就是非常复杂非常灵活地匹配规则。
比如,要根据推广内容去匹配不同的方式做宣传。又比如,要根据不同的活动去匹配不同的渠道去做分发。
总之,数不清的匹配规则是这套系统中非常重要的一个特点。
首先,先看看 RabbitMQ 的,你会发现 RabbitMQ 是允许在消息中添加 routing_key 或者自定义消息头,然后通过一些特殊的 Exchange,很简单的就实现了消息匹配分发。开发几乎不用成本。
而 Kafka 呢?如果你要实现消息匹配,开发成本高多了。
首先,通过简单的配置去自动匹配和分发到合适的消费者端这件事是不可能的。
其次,消费者端必须先把所有消息不管需要不需要,都取出来。然后,再根据业务需求,自己去实现各种精准和模糊匹配。可能因为过度的复杂性,还要引入规则引擎。
这个场景下 RabbitMQ 扳回一分。
三、消息的超时
在电商业务里,有个需求:下单之后,如果用户在 15 分钟内未支付,则自动取消订单。
你可能奇怪,这种怎么也会用到消息队列的?
我来先简单解释一下,在单一服务的系统,可以起个定时任务就搞定了。
但是,在 SOA 或者微服务架构下,这样做就不行了。因为很多个服务都关心是否支付这件事,如果每种服务,都自己实现一套定时任务的逻辑,既重复,又难以维护。
在这种情况下,我们往往会做一层抽象:把要执行的任务封装成消息。当时间到了,直接扔到消息队列里,消息的订阅者们获取到消息后,直接执行即可。
希望把消息延迟一定时间再处理的,被称为延迟队列。
对于订单取消的这种业务,我们就会在创建订单的时候,同时扔一个包含了执行任务信息的消息到延迟队列,指定 15 分钟后,让订阅这个队列的各个消费者,可以收到这个消息。随后,各个消费者所在的系统就可以去执行相关的扫描订单的任务了。
RabbitMQ 和 Kafka 消息队列如何选?
先看下 RabbitMQ 的。
RabbitMQ 的消息自带手表,消息中有个 TTL 字段,可以设置消息在 RabbitMQ 中的存放的时间,超时了会被移送到一个叫死信队列的地方。
所以,延迟队列 RabbitMQ 最简单的实现方式就是设置 TTL,然后一个消费者去监听死信队列。当消息超时了,监听死信队列的消费者就收到消息了。
不过,这样做有个大问题:假设,我们先往队列放入一条过期时间是 10 秒的 A 消息,再放入一条过期时间是 5 秒的 B 消息。 那么问题来了,B 消息会先于 A 消息进入死信队列吗?
答案是否定的。B 消息会优先遵守队列的先进先出规则,在 A 消息过期后,和其一起进入死信队列被消费者消费。
在 RabbitMQ 的 3.5.8 版本以后,官方推荐的 rabbitmq delayed message exchange 插件可以解决这个问题。
- 用了这个插件,我们在发送消息的时候,把消息发往一个特殊的 Exchange。
- 同时,在消息头里指定要延迟的时间。
- 收到消息的 Exchange 并不会立即把消息放到队列里,而是在消息延迟时间到达后,才会把消息放入。
再看下 Kafka 的:
Kafka 要实现延迟队列就很麻烦了。
- 你先需要把消息先放入一个临时的 topic。
- 然后得自己开发一个做中转的消费者。让这个中间的消费者先去把消息从这个临时的 topic 取出来。
- 取出来,这消息还不能马上处理啊,因为没到时间呢。也没法保存在自己的内存里,怕崩溃了,消息没了。所以,就得把没有到时间的消息存入到数据库里。
- 存入数据库中的消息需要在时间到了之后再放入到 Kafka 里,以便真正的消费者去执行真正的业务逻辑。
- ……
想想就已经头大了,这都快搞成调度平台了。再高级点,还要用时间轮算法才能更好更准确。
这次,RabbitMQ 上那一条条戴手表的消息,才是最好的选择。
四、消息的保持
在微服务里,事件溯源模式是经常用到的。如果想用消息队列实现,一般是把事件当成消息,依次发送到消息队列中。
事件溯源有个最经典的场景,就是事件的重放。简单来讲就是把系统中某段时间发生的事件依次取出来再处理。而且,根据业务场景不同,这些事件重放很可能不是一次,更可能是重复 N 次。
假设,我们现在需要一批在线事件重放,去排查一些问题。
RabbitMQ 此时就真的不行了,因为消息被人取出来就被删除了。想再次被重复消费?对不起。
而 Kafka 呢,消息会被持久化一个专门的日志文件里。不会因为被消费了就被删除。
所以,对消息不离不弃的 Kafka 相对用过就抛的 RabbitMQ,请选择 Kafka。
五、消息的错误处理
很多时候,在做记录数据相关业务的时候,Kafka 一般是不二选择。不过,有时候在记录数据吞吐量不大时,我自己倒是更喜欢用 RabbitMQ。
原因就是 Kafka 有一个我很不喜欢的设计原则:
当单个分区中的消息一旦出现消费失败,就只能停止而不是跳过这条失败的消息继续消费后面的消息。即不允许消息空洞。
只要消息出现失败,不管是 Kafka 自身消息格式的损坏,还是消费者处理出现异常,是不允许跳过消费失败的消息继续往后消费的。
所以,在数据统计不要求十分精确的场景下选了 Kafka,一旦出现了消息消费问题,就会发生项目不可用的情况。这真是徒增烦恼。
而 RabbitMQ 呢,它由于会在消息出问题或者消费错误的时候,可以重新入队或者移动消息到死信队列,继续消费后面的,会省心很多。
坏消息就像群众中的坏蛋那样,Kafka 处理这种坏蛋太过残暴,非得把坏蛋揪出来不行。相对来说,RabbitMQ 就温柔多了,群众是群众,坏蛋是坏蛋,分开处理嘛。
六、消息的吞吐量
Kafka 是每秒几十万条消息吞吐,而 RabbitMQ 的吞吐量是每秒几万条消息。
其实,在一家公司内部,有必须用到 Kafka 那么大吞吐量的项目真的很少。大部分项目,像 RabbitMQ 那样每秒几万的消息吞吐,已经非常够了。
在一些没那么大吞吐量的项目中引入 Kafka,我觉得就不如引入 RabbitMQ。
为什么呢?
因为 Kafka 为了更好的吞吐量,很大程度上增加了自己的复杂度。而这些复杂度对项目来说,就是麻烦,主要体现在两个方面:
1、配置复杂、维护复杂
Kafka 的参数配置相对 RabbitMQ 是很复杂的。比如:磁盘管理相关参数,集群管理相关参数,ZooKeeper 交互相关参数,Topic 级别相关参数等,都需要一些思考和调优。
另外,Kafka 本身集群和参与管理集群的 ZooKeeper,这就带来了更多的维护成本。Kafka 要用好,你要考虑 JVM,消息持久化,集群本身交互,以及 ZooKeeper 本身和它与 Kafka 之间的可靠和效率。
2、用好,用对存在门槛
Kafka 的 Producer 和 Consumer 本身要用好用对也存在很高的门槛。
比如,Producer 消息可靠性保障、幂等性、事务消息等,都需要对 KafkaProducer 有深入的了解。
而 Consumer 更不用说了,光是一个日志偏移管理就让一大堆人掉了不少头发。
相对来说,RabbitMQ 就简单得多。你可能都不用配置什么,直接启动起来就能很稳定可靠地使用了。就算配置,也是寥寥几个参数设置即可。
所以,大家在项目中引入消息队列的时候,真的要好好考虑下,不要因为大家都鼓吹 Kafka 好,就无脑引入。
总结
可以看到,如果我们要做消息队列选型,有两件事是必须要做好的:
- 列出业务最重要的几个特点
- 深入到消息队列的细节中去比较
等我们对这些中间件的特点非常熟悉之后,甚至可以把业务分解成不同的子业务,再根据不同的子业务的特征,引入不同的消息队列,即消息队列混用。这样,我们就可能会最大化我们的获益,最小化我们的成本。
说了这么多,其实还有很多 Kafka 和 RabbitMQ 的比较没有说,比如二者集群的区别,占用资源多少的比较等。以后有机会可以再提提。
总之,期待大家看完这篇文章后,能对 Kafka 和 RabbitMQ 的区别有了更细节性的了解。
最后,分享一个网上的比较全的对比图: