目录
了解Kafka
Kafka是一个分布式流处理平台,最初由LinkedIn开发,后来成为Apache软件基金会的一部分。它被设计为一个高吞吐量、可扩展、耐用的消息队列系统,通常用于实时数据管道和流处理。Kafka本质是日志消息代理,特点是append-only和不可变,它强大的局部性在内存中可以抽象为buffer,内核态page cache磁盘上它会集中在同一磁道,因此从上至下利于软件和操作系统进行快速写入。
假设你是一个程序员,假设你维护了两个服务 A 和 B。
B 服务每秒最多可以处理 100 个消息,但 A 服务每秒发出消息的方式峰值可以到达200个,B 服务哪里顶得住,分分钟被压垮。
那么问题就来了,有没有办法让 B 在不被压垮的同时,还能处理掉 A 的消息?
当然有,没有什么是加一层中间层不能解决的,如果有,那就再加一层。
这次我们要加的中间层是 消息队列 Kafka。
什么是消息和消息队列
消息
消息(Message):是队列系统中的基本数据单元,可以包含文本、二进制数据或序列化的对象。
消息队列
为了保护 B 服务,我们很容易想到可以在 B 服务的内存中加入一个队列。
说白了,它其实是个链表,链表的每个节点就是一个消息。每个节点有一个序号,我们叫它 Offset,记录消息的位置。
B 服务依据自己的处理能力,消费链表里的消息。能处理多少是多少,不断更新已处理 Offset 的值。
但这有个问题,来不及处理的消息会堆积在内存里,如果 B 服务更新重启,这些消息就都丢了。这个好解决,将队列挪出来,变成一个单独的进程。就算 B 服务重启,也不会影响到了队列里的消息。
这样一个简陋的队列进程,其实就是所谓的消息队列。
而像 A 服务这样负责发数据到消息队列的角色,就是生产者,像 B 服务这样处理消息的角色,就是消费者。
但这个消息队列属实过于简陋,像什么高性能,高扩展性,高可用,它是一个都不沾。
我们来看下怎么优化它。
高性能
B 服务由于性能较差,消息队列里会不断堆积数据,为了提升性能,我们可以扩展更多的消费者, 这样消费速度就上去了,相对的我们就可以增加更多生产者,提升消息队列的吞吐量。
增加生产者和消费者
随着生产者和消费者都变多,我们会发现它们会同时争抢同一个消息队列,抢不到的一方就得等待,这不纯纯浪费时间,那么有解决方案吗?
首先是对消息进行分类,每一类是一个 topic,然后根据 topic 新增队列的数量,生产者将数据按 topic 投递到不同的队列中,消费者则根据需要订阅不同的 topic。这就大大降低了 topic 队列的压力。
但单个 topic 的消息还是可能过多,我们可以将单个队列,拆成好几段,每段就是一个 partition 分区,每个消费者负责一个 partition。这就大大降低了争抢,提升了消息队列的性能。
Patiotion的调度
至此每一个传入到topic的消息记录,就由Partition来接收了,也可以认为Patition是kafka处理消息的最小基本单位。那么假如一个消息记录到来,我们怎么在topic中选择Partition呢?
首先我们来看看消息记录Record,它是由Key和Value构成的。
当没有指定Record的Key时,采用轮转(Round-Robin)的调度方式,在RR方式下,加入有3个Patition和a b c d e f g h i 九个消息,那么就会顺序轮着放入每个Patition。
当指定了Record的Key时,那么就会保证相同Key值的消息在同一个Partition中,这样做保证了数据的连续性,读取时效率更高。
拓展性
随着 partition 变多,如果 partition 都在同一台机器上的话,就会导致单机 cpu 和内存过高,影响整体系统性能。
于是我们可以申请更多的机器,将 partition 分散部署在多台机器上,这每一台机器,就代表一个 broker。我们可以通过增加 broker 缓解机器 cpu 过高带来的性能问题。这样许许多多的Broker就构成了我们的Kafka Cluster。
高可用
到这里,其实还有个问题,如果其中一个 partition 所在的 broker 挂了,那 broker 里所有 partition 的消息就都没了。这高可用还从何谈起,那怎么办才好呢?
如果你拿这个问题去问小学生,都可以一个简单直接的回答就是:备份呗。
我们可以给 partition 多加几个副本,也就是 replicas,将它们分为 Leader 和 Follower。Leader 负责应付生产者和消费者的读写请求,而 Follower 只管同步 Leader 的消息。不同broker之间的Leader和Follower关系由ISR List维系。
将 Leader 和 Follower 分散到不同的 broker 上,这样 Leader 所在的 broker 挂了,也不会影响到 Follower 所在的 broker, 并且还能从 Follower 中选举出一个新的 Leader partition 顶上。这样就保证了消息队列的高可用。
持久化和过期策略
刚刚提到的是几个 broker 挂掉的情况,那往夸张点说,假设所有 broker 都挂了,那岂不是数据全丢了?
为了解决这个问题,我们不能光把数据放内存里,还要持久化到磁盘中,这样哪怕全部 broker 都挂了,数据也不会全丢,重启服务后,也能从磁盘里读出数据,继续工作。
但问题又来了,磁盘总是有限的,这一直往里写数据迟早有一天得炸。所以我们还可以给数据加上保留策略,也就是所谓的 retention policy
,比如磁盘数据超过一定大小或消息放置超过一定时间就会被清理掉。
Kafka的底层存储
Kafka 的 partition 分区,其实在底层由很多段(segment)组成,每个 segment 可以认为就是个小文件。将消息数据写入到 partition 分区,本质上就是将数据写入到某个 segment 文件下。
我们知道,操作系统的机械磁盘,顺序写的性能会比随机写快很多,差距高达几十倍。为了提升性能,Kafka 对每个小文件都是顺序写。
如果只有一个 segment 文件,那写文件的性能会很好。
但当 topic 变多之后,topic 底下的 partition 分区也会变多,对应的 partition 底下的 segment 文件也会变多。同时写多个 topic 底下的 partition,就是同时写多个文件,虽然每个文件内部都是顺序写,但多个文件存放在磁盘的不同地方,原本顺序写磁盘就可能劣化变成了随机写。于是写性能就降低了。
那问题又又来了,究竟多少 topic 才算多?这个看实际情况,仅供参考,8 个分区的情况下,超过 64 topic, Kafka 性能就会开始下降。
Consumer Group
到这里,这个消息队列好像就挺完美了。但其实还有个问题,按现在的消费方式,每次新增的消费者只能跟着最新的消费 Offset 接着消费。如果我想让新增的消费者从某个 Offset 开始消费呢?
听起来这个需求很刁钻?我举个例子你就明白了。
哪怕 B 服务有多个实例,但本质上,它只有一个消费业务方,新增实例一般也是接着之前的 offset 继续消费。
假设现在来了个新的业务方,C 服务,它想从头开始消费消息队列里的数据,这时候就不能跟在 B 服务的 offset 后边继续消费了。
所以我们还可以给消息队列加入消费者组(consumer group)的概念,B 和 C 服务各自是一个独立的消费者组,消费者组相互独立,不同消费者组维护自己的消费进度,互不打搅。
消费顺序
那么关于Customer Group是怎么消费Patition里的消息的呢?我们遵循下面四点:
1.分区是最小的并行单位
2.一个消费者可以消费多个分区
3.一个分区可以被多个消费者组里的消费者消费
4.但是,一个分区不能同时被同一个消费者组里的多个消费者消费
于是我们就有了发布-订阅模式和点对点模式
同时,同一个生产者发送到同一分区的消息,先发送的offset比后发送的offset小,同一个生产者发送到不同分区的消息,消息顺序无法保证。消费者按照消息在分区内的消息顺序进行消费的,Kafka只能保证分区内的消息顺序,不能保证分区间的消息顺序。
意思就是如果C1和C2都拿M1到M4,C1可能拿到M1,M2,M3,M4,而C2可能拿到M1,M2,M4,M3,这样我们可以发现,同一P1的M1,M2,M3顺序是一致的(按offset),而P1和P2不同分区间消息顺序是乱序的。
Zookeeper
相信你也发现了,组件太多了,而且每个组件都有自己的数据和状态,所以还需要有个组件去统一维护这些组件的状态信息,于是我们引入 ZooKeeper 组件。它会定期和 broker 通信,获取 整个 kafka 集群的状态,以此判断 某些 broker 是不是跪了,某些消费组消费到哪了。
Zookeeper 在 Kafka 架构中会和 broker 通信,维护 Kafka 集群信息。一个新的 broker 连上 Zookeeper 后,其他 broker 就能立马感知到它的加入,像这种能在分布式环境下,让多个实例同时获取到同一份信息的服务,就是所谓的分布式协调服务。
好了,到这里,当初那个简陋的消息队列,就成了一个高性能,高扩展性,高可用,支持持久化的超强消息队列,没错,它就是我们常说的消息队列 Kafka,上面涉及到各种概念,比如 partition 和 broker 什么的,都出自它。
但 Zookeeper 作为一个通用的分布式协调服务,它不仅可以用于服务注册与发现,还可以用于分布式锁、配置管理等场景。 Kafka 其实只用到了它的部分功能,多少有点杀鸡用牛刀的味道。太重了。
当然,开发 Kafka 的大佬们后来也意识到了 Zookeeper 过重的问题,所以从 2.8.0 版本就支持将 Zookeeper 移除,通过 在 broker 之间加入一致性算法 raft 实现同样的效果,这就是所谓的 KRaft 或 Quorum 模式。
Kafka的应用场景
消息队列是架构中最常见的中间件之一,使用场景之多,堪称万金油!
比如上游流量忽高忽低,想要削峰填谷,提升 cpu/gpu 利用率,用它。又比如系统过大,消息流向盘根错节,想要拆解组件,降低系统耦合,还是用它。再比如秒杀活动,请求激增,想要保护服务的同时又尽量不影响用户,还得用它。
当然,凡事无绝对,方案还得根据实际情况来定,做架构做到最后,都是在做折中。
总结
- kafka 是消息队列,像消息队列投递消息的是生产者,消费消息的是消费者。增加生产者和消费者的实例个数可以提升系统吞吐。多个消费者可以组成一个消费者组,不同消费者组维护自己的消费进度,互不打搅。
- kafka 将消息分为多个 topic,每个 topic 内部拆分为多个 partition,每个 partition 又有自己的副本,不同的 partition 会分布在不同的 broker 上,提升性能的同时,还增加了系统可用性和可扩展性。