共计 7286 个字符,预计需要花费 19 分钟才能阅读完成。
常常有读者后盾跟我说,心愿我可能写一些零碎设计相干的文章,最近我就在钻研罕用音讯队列 kafka 和 pulsar 的架构设计,所以总结了这篇文章,心愿在你做技术选型或浏览源码的时候起到肯定的帮忙。
咱们从一个面试的场景开始好了。
面试官:理解 Kafka 吗?简略介绍下?
我张口就来:Kafka 嘛,作为一款比拟成熟的音讯队列,它必然 YYDS,什么削峰填谷,异步化,解耦,高性能,高可用……
面试官:嗯,那么 Kafka 有什么毛病呢?
我:啊这……
平时背的八股文都是夸 Kafka 的,这从天而降的这波反向操作可把我难住了。
面试官必定一眼就看进去我在背八股文,对音讯队列没有什么深刻的钻研,笑了笑,苦口婆心地教诲我:
正所谓 时势造英雄,任何新技术的呈现都是时代的产物,不存在 YYDS 的技术,Kafka 是很好,但在目前云原生的趋势下它的确老了,不能满足目前很多业务倒退的须要了。你回去好好钻研下 Kafka 可能存在的问题,如果让你来从新设计 Kafka,你会如何做?
我陷入深思:时势造英雄,的确是这样。
想当年刚进大学的时候看着他人的教程本人搭博客平台,要登录 Linux 服务器装置 python mysql nginx 等等一大堆货色,还要相熟 Centos/Ubuntu 等零碎的各种配置细节的差别;大二的时候我去一个云计算公司实习,拼命钻研 OpenStack、Open vSwitch 什么的。现如今各种服务都是容器化一键部署,之前学的那套货色根本忘光了,脑子里只剩 docker + k8s 了。
类比过去,目前 Kafka 的确是支流音讯队列,但这只能证实用它的人多,打的补丁多,并不能代表它是完满的,它的设计不可能跳出时代的局限,随着时代的倒退,它必然也会被新的技术取代。
那我看看新一代的音讯队列是如何设计的,不就能够学到 Kafka 的局限性以及改良思路了吗?
目前最新的音讯队列是 Apache Pulsar,号称是下一代云原生分布式音讯流平台:
这么牛逼,那我必须钻研一波 Pulsar 的设计,看看下一代云原生的音讯平台长啥样。
首先看看咱们为什么须要音讯队列。
如果两个服务之间须要通信,最简略的计划就是间接让它俩之间建设通信就行了。但试想一下,如果所有生产数据的服务和生产数据的服务都要彼此相连,那么零碎多了之后,各个系统之间的依赖关系会极其简单。如果其中的某个零碎进行一点批改,那几乎是噩梦,真堪称牵一发而动全身。
而在生产者和消费者之间引入中间件就是为了解决这个问题:音讯的产生者不关怀消费者是谁,它只须要把音讯一股脑丢到音讯队列外面,消费者会从音讯队列外面生产数据。
这就是零碎设计中一个最简略实用的技巧:加中间层。没有什么问题是加一个中间层服务解决不了的,如果真解决不了,那就加两层。
PS:这不是玩笑,的确有句名言如是说:计算机领域的任何问题都能够通过增加中间层来解决。
细分一下,生产模型又分两种:
1、点对点模式,也叫队列模式。即每条音讯只会被一个消费者生产。
2、公布订阅(Pub/Sub)模式。发送到某个 Topic 的音讯,会分发给所有订阅该 Topic 的消费者进行生产。
一个成熟的音讯队列应该同时反对上述两种生产模型。
另外,你总不能让生产者的音讯「阅后即焚」吧,所以音讯队列应该有本人的长久化存储系统,可能把音讯存储下来,不便后续的回溯、剖析等操作。
综上,音讯队列在整个零碎中的次要作用就是:
1、解耦。使得服务之间的拓扑关系简略了很多。
2、削峰 / 异步化。音讯队列能够把大量不须要实时处理的数据暂存下来,期待消费者缓缓生产。
当然,音讯队列中间件看似打消了服务之间的相互依赖,但说到底其实是让所有服务都依赖音讯队列了,如果音讯队列忽然坏掉,那就全完蛋了。
所以咱们对这个音讯队列自身的要求就十分高,具体来说有几方面:
1、高性能。音讯队列作为整个零碎的枢纽,它的性能必须足够高,否则很可能成为整个零碎的性能瓶颈。
2、高可用。说白了就是要抗揍,如果音讯队列集群中的少部分节点因为种种原因逝世了,也不能影响整个集群的服务。
3、数据可靠性。在各种极其状况下(比方忽然断网、忽然断电),要保障曾经收到的音讯胜利贮存(个别是指落到磁盘中)。
4、可扩大。业务是倒退的,如果音讯队列集群快扛不住计算压力了,就须要更多的计算资源(扩容);如果音讯队列集群压力很小,导致很多节点搁那打酱油,那么须要回收计算资源(缩容)。这就须要音讯队列在设计时就思考如何进行灵便的扩缩容。
Kafka 架构
就我的学习经验,学习新零碎架构设计时的一大难点就是名词太多,很难把「逻辑概念」和「实体」对应起来。
记得我在第一次学习 Kafka 的时候,什么 broker、topic、partition、replica 全副混在一起,差点给我间接劝退。所以上面我除了画一些架构图之外,还会着重形容每个名词背地具体代表的实体。
首先说说 Kafka 的架构设计。
producer 和 customer 能够选定 Kafka 的某些 topic 中投递和生产音讯,但 topic 其实只是个逻辑概念,topic 上面分为多个 partition,音讯是真正存在 partition 中的:
每个 partition 会调配给一个 broker 节点治理:
所谓 broker 节点,就是一个服务过程。简略来说,你把一个 broker 节点了解为一台服务器,把 partition 了解为这台服务器上的一个文件就行了。
发到 topic 的音讯实际上是发给了某个 broker 服务器,而后被长久化存储到一个文件里,咱们个别称这个文件是 log file。
那么为什么要给一个 topic 调配多个 partition 呢?
很显然,如果一个 topic 只有一个 partition,那么也就只能有一台 broker 服务器解决这个 topic 上的音讯,如果划分成很多 partition,就能够把这个 topic 上的音讯调配到多台 broker 的 partition 上,每个 broker 解决音讯并将音讯长久化存储到 log file 中,从而进步单 topic 的数据处理能力。
但问题是怎么保障高可用?如果某个 broker 节点挂了,对应的 partition 上的数据不就就无法访问了吗?
个别都是通过「数据冗余」和「故障主动复原」来保障高可用,Kafka 会对每个 partition 保护若干冗余正本:
若干个 partition 正本中,有一个 leader 正本(图中红色的),其余都是 follower 正本(也可称为 replica,图中橙色的)。
leader 正本负责向生产者和消费者提供订阅公布服务,负责长久化存储音讯,同时还要把最新的音讯同步给所有 follower,让 follower 小弟们和本人存储的数据尽可能雷同。
这样的话,如果 leader 正本挂了,就能从 follower 中选取一个正本作为新的 leader,持续对外提供服务。
这就是 Kafka 的设计,所有看起来很完满,是吧?实际上并不完满。
Kafka 架构的缺点
1、Kafka 把 broker 和 partition 的数据存储牢牢绑定在一起,会产生很多问题。
首先一个问题就是,Kafka 的很多操作都波及 partition 数据的全量复制。
比方说典型的扩容场景,假如有 broker1, broker2
两个节点,它们别离负责若干个 partition,当初我想新退出一个 broker3
节点摊派 broker1
的局部负载,那你得让 broker1
拿一些 partition 给到 broker3
对吧?
那不好意思,得复制 partition 的全量数据,什么时候复制完,broker3
能力上线提供服务。且不说耗费 IO 以及网络资源,如果复制数据的速度小于 partition 的写入速度,那永远都别想复制完了。
再比方说,我想给某个 partition 新增一个 follower 正本,那么这个新增的 follower 正本必须要跟 leader 正本同步全量数据。毕竟 follower 存在的目标就是随时代替 leader,所以复制 leader 的全量数据是必须的。
除此之外,因为 broker 要负责存储,所以整个集群的容量可能局限于存储能力最差的那个 broker 节点。而且如果某些 partition 中的数据特地多(数据歪斜),那么对应 broker 的磁盘可能很快被写满,这又要波及到 partition 的迁徙,数据复制在劫难逃。
尽管 Kafka 提供了现成的脚本来做这些事件,但理论须要思考的问题比拟多,操作也比较复杂,数据迁徙也很耗时,远远做不到集群的「平滑扩容」。
2、Kafka 底层依赖操作系统的 Page Cache,会产生很多问题。
之前说了,只有数据被写到磁盘里能力保障十拿九稳,否则的话都不能保证数据不会丢。所以首先一个问题就是 Kafka 音讯长久化并不牢靠,可能丢音讯。
咱们晓得 Linux 文件系统会利用 Page Cache 机制优化性能。Page Cache 说白了就是读写缓存,Linux 通知你写入胜利,但实际上数据并没有真的写进磁盘里,而是写到了 Page Cache 缓存里,可能要过一会儿才会被真正写入磁盘。
那么这外面就有一个时间差,当数据还在 Page Cache 缓存没有落盘的时候机器忽然断电,缓存中的数据就会永远失落。
而 Kafka 底层齐全依赖 Page Cache,并没有要求 Linux 强制刷盘,所以忽然断电的状况是有可能导致数据失落的。对于大部分场景来说,能够容忍偶然丢点数据,但对于金融领取这类服务场景,是相对不能承受丢数据的。
另外,尽管我看到很多博客都把 Kafka 依赖 page cache 这个个性看做是 Kafka 的长处,理由是能够晋升性能,但实际上 Page Cache 也是有可能呈现性能问题的。
咱们来剖析下消费者生产数据的状况,次要有两种可能:一种叫追尾读(Tailing Reads),一种叫追赶读(Catch-up Reads)。
所谓追尾读,顾名思义,就是消费者的生产速度比拟快,生产者刚生产一条音讯,消费者立即就把它生产了。咱们能够设想一下这种状况 broker 底层是如何解决的:
生产者写入音讯,broker 把音讯写入 Page Cache 写缓存,而后消费者马上就来读音讯,那么 broker 就能够疾速地从 Page Cache 外面读取这条音讯发给消费者,这挺好,没故障。
所谓追赶读的场景,就是消费者的生产速度比较慢,生产者曾经生产了很多新音讯了,但消费者还在读取比拟旧的数据。
这种状况下,Page Cache 缓存里没有消费者想读的老数据,那么 broker 就不得不从磁盘中读取数据并存储在 Page Cache 读缓存。
留神此时读写都依赖 Page Cache,所以读写操作可能会相互影响,对一个 partition 的大量读可能影响到写入性能,大量写也会影响读取性能,而且读写缓存会相互争用内存资源,可能造成 IO 性能抖动。
再进一步剖析,因为每个 partition 都能够了解为 broker 节点上的一个文件,那么如果 partition 的数量特地多,一个 broker 就须要同时对很多文件进行大量读写操作,这性能可就……
那么,Pulsar 是如何解决 Kafka 的这些问题的呢?
存算拆散架构
首先,Kafka broker 的扩容都会波及 partition 数据的迁徙,这是因为 Kafka 应用的是传统的单层架构,broker 须要同时进行计算(向生产者和消费者提供服务)和存储(音讯的长久化)。
怎么解决?很简略,多叠几层呗。
Pulsar 改用多层的存算拆散架构,broker 节点只负责计算,把存储的工作交给业余的存储引擎 Bookkeeper 来做:
有了存算拆散架构,Pulsar 的 partition 在 broker 之间的迁徙齐全不会波及数据复制,所以能够迅速实现。
为什么呢?你想想「文件」和「文件描述符」的区别就明确了。文件,对应磁盘上的一大块数据,挪动起来比拟吃力;而文件描述符能够了解为指向文件的一个指针,传递文件描述符显然要比挪动文件简略得多。
在 Kafka 中,咱们能够把每个 partition 了解成一个存储音讯的大文件,所以在 broker 间转移 partition 须要复制数据,异样麻烦。
而在 Pulsar 中,咱们能够把每个 partition 了解成一个文件描述符,broker 只需持有这个文件描述符即可,把对数据的解决全副甩给存储引擎 Bookkeeper 去做。
如果 Pulsar 的某个 broker 节点的压力特地大,那你减少 broker 节点去分担一些 partition 就行;相似的,如果某个 broker 节点忽然坏了,那你间接把这个 broker 节点治理的 partition 转移到别的 broker 就行了,这些操作齐全不波及数据复制。
进一步,因为 Pulsar 中的 broker 是无状态的,所以很容易借助 k8s 这样的基础设施实现弹性扩缩容。
通过这一波操作,broker 把数据存储的要害工作甩给了存储层,那么 Bookkeeper 是如何提供高可用、数据可靠性、高性能、可扩大的个性呢?
节点对等架构
Bookkeeper 自身就是一个分布式日志存储系统,先说说它是如何实现高可用的。
Kafka 应用主从复制的形式实现高可用;而 Bookkeeper 采纳 Quorum 机制实现高可用。
Bookkeeper 集群是由若干 bookie 节点(运行着 bookie 过程的服务器)组成的,不过和 Kafka 的主从复制机制不同,这些 bookie 节点都是对等的,没有主从的关系。
当 broker 要求 Bookkeeper 集群存储一条音讯(entry)时,这条音讯会被 并发地 同时写入多个 bookie 节点进行存储:
还没完,之后的音讯会以滚动的形式选取不同的 bookie 节点进行写入:
这种写入形式称为「条带化写入」,既实现了数据的冗余存储,又使得数据可能均匀分布在多个 bookie 存储节点上,从而防止数据歪斜某个存储节点压力过大。
因为节点对等,所以 bookie 节点能够进行疾速故障复原和扩容。
比方说 entry0 ~ entry99
都胜利写入到了 bookie1, bookie2, bookie3
中,写 entry100
时bookie2
忽然坏掉了,那么间接退出一个新的 bookie4
节点接替 bookie2
的工作就行了。
那必定有读者纳闷,新增进来的 bookie4
难道不须要先复制 bookie2
的数据吗(像 Kafka broker 节点那样)?
主从复制的架构才须要数据复制,因为从节点必须保障和主节点完全相同,以便随时接替主节点。而节点对等的架构是不须要数据复制的。
Bookkeeper 中保护了相似这样的一组元数据:
[bookie1, bookie2, bookie3], 0
[bookie1, bookie3, bookie4], 100
这组元数据的含意是:entry0 ~ entry99
都写到了 bookie1, bookie2, bookie3
中,entry100
及之后的音讯都写到了 bookie1, bookie3, bookie4
中。
有了这组元数据,咱们就能晓得每条 entry 具体存在那些 bookie 节点外面,即使 bookie2
节点坏了,这不是还有 bookie1, bookie3
节点能够读取嘛。
扩容也是相似的,能够间接增加新的 bookie 节点减少存储能力,齐全不须要数据复制。
比照来看,Kafka 是以 partition 为单位存储在 broker 中的:
Bookkeeper 这边压根没有 partition 的概念,而是以 entry(音讯)为单位进行存储,某个 partition 中的数据会被打散在多个 bookie 节点中:
PS:不必放心,每个 entry 中会存储 partition 的信息,所以咱们可能筛选出属于某个 partition 的所有音讯。
一个叫 Jack Vanlightly 的技术大佬画了张图形象比照了 Kafka 和 Bookkeeper 进行扩容的场景:
接下来看看 Bookkeeper 如何保障高性能和数据可靠性。
读写隔离
bookie 节点实现读写隔离,本人保护缓存,不再依赖操作系统的 Page Cache,保障了数据可靠性和高性能。
之前说到 Kafka 齐全依赖 Page Cache 产生的一些问题,而 Bookkeeper 集群中的 bookie 存储节点采纳了读写隔离的架构:
每个 bookie 节点都领有两块磁盘,其中 Journal 磁盘专门用于写入数据,Entry Log 磁盘专门用于读取数据,而 memtable 是 bookie 节点自行保护的读写缓存。
其中 Journal 盘的写入不依赖 Page Cache,间接强制刷盘(可配置),写入实现后 bookie 节点就会返回 ACK 写入胜利。
写 Journal 盘的同时,数据还会在 memotable 缓存中写一份,memotable 会对数据进行排序,一段时间后刷入 Entry Log 盘。
这样不仅多了一层缓存,而且 Entry Log 盘中的数据有肯定的有序性,在读取数据时能够肯定水平上进步性能。
这样设计的毛病是一份数据要存两次,耗费磁盘空间,但劣势也很显著:
1、保障可靠性,不会丢数据。因为 Journal 落盘后才断定为写入胜利,那么即便机器断电,数据也不会失落。
2、数据读写不依赖操作系统的 Page Cache,即使读写压力较大,也能够保障稳固的性能。
3、能够灵便配置。因为 Journal 盘的数据能够定时迁出,所以能够采纳存储空间较小但写入速度快的存储设备来进步写入性能。
总结
以上介绍了 Kafka 的架构及痛点,并介绍了 Pulsar 是如何在架构层面解决 Kafka 的有余的。当然,以上只是 Pulsar 的大体设计,具体到实现必然有很多细节和难点,不是一篇文章能讲完的,后续我还会分享我的学习教训。
哦,我才想起本文的题目是重构 Kafka,都写到这里了还重构个什么劲,间接用 Pulsar 吧。Pulsar 官网提供了 Kafka 的迁徙计划,可能最小化改变代码实现迁徙。
Pulsar 还有很多优良个性,比方多租户、跨地区复制等企业级个性,比方更灵便的生产模型,比方批流交融的尝试等等,这些个性能够查看 Pulsar 的官网:
https://pulsar.apache.org/
更多硬核内容和工程实际参见 Pulsar 的官网公众号「Apache Pulsar」。
更多高质量干货文章,请关注我的微信公众号:labuladong