CategoryResourceRepost/极客时间专栏/消息队列高手课/进阶篇/27 | Pulsar的存储计算分离设计:全新的消息队列设计思路.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

92 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="27 | Pulsar的存储计算分离设计全新的消息队列设计思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/76/8d/766246b97bff902fb602bcdecc95c68d.mp3"></audio>
你好,我是李玥。
之前的课程我们大部分时间都在以RocketMQ、Kafka和RabbitMQ为例通过分析源码的方式来讲解消息队列的实现原理。原因是这三种消息队列在国内的受众群体非常庞大大家在工作中会经常用到。这节课我给你介绍一个不太一样的开源消息队列产品Apache Pulsar。
Pulsar也是一个开源的分布式消息队列产品最早是由Yahoo开发现在是Apache基金会旗下的开源项目。你可能会觉得好奇我们的课程中为什么要花一节课来讲Pulsar这个产品呢原因是Pulsar在架构设计上和其他的消息队列产品有非常显著的区别。我个人的观点是Pulsar的这种全新的架构设计很可能是消息队列这类中间件产品未来架构的发展方向。
接下来我们一起看一下Pulsar到底有什么不同
## Pulsar的架构和其他消息队列有什么不同
我们知道无论是RocketMQ、RabbitMQ还是Kafka消息都是存储在Broker的磁盘或者内存中。客户端在访问某个主题分区之前必须先找到这个分区所在Broker然后连接到这个Broker上进行生产和消费。
在集群模式下为了避免单点故障导致丢消息Broker在保存消息的时候必须也把消息复制到其他的Broker上。当某个Broker节点故障的时候并不是集群中任意一个节点都能替代这个故障的节点只有那些“和这个故障节点拥有相同数据的节点”才能替代这个故障的节点。原因就是每一个Broker存储的消息数据是不一样的或者说每个节点上都存储了状态数据。这种节点称为“有状态的节点Stateful Node”。
Pulsar与其他消息队列在架构上最大的不同在于它的Broker是无状态的Stateless。也就是说**在Pulsar的Broker中既不保存元数据也不存储消息**。那Pulsar的消息存储在哪儿呢我们来看一下Pulsar的架构是什么样的。
<img src="https://static001.geekbang.org/resource/image/c6/39/c6d87dbd3ef911f3581b8e51681d3339.png" alt="">
这张Pulsar的架构图来自Pulsar的官方文档如果你想了解这张架构图的细节可以去看官方文档中的[Architecture Overview](https://pulsar.apache.org/docs/en/concepts-architecture-overview/)。我来给你解读一下这张图中我们感兴趣的重点内容。
先来看图中右侧的Bookie和ZK这两个方框这两个方框分别代表了BookKeeper集群和ZooKeeper集群。ZooKeeper集群的作用我在《[24 | Kafka的协调服务ZooKeeper实现分布式系统的“瑞士军刀](https://time.geekbang.org/column/article/137655)》这节课中专门讲过在Pulsar中ZooKeeper集群的作用和在Kafka中是一样的都是被用来存储元数据。BookKeeper集群则被用来存储消息数据。
那这个BookKeeper又是什么呢BookKeeper有点儿类似HDFS是一个分布式的存储集群只不过它的存储单元和HDFS不一样在HDFS中存储单元就是文件这个很好理解。而BookKeeper的存储单元是Ledger。这个Ledger又是什么呢
这里再次吐槽一下国外程序员喜欢发明概念、增加学习成本这个坏习惯。其实Ledger就是一段WALWrite Ahead Log或者你可以简单地理解为某个主题队列的一段它包含了连续的若干条消息消息在Ledger中称为Entry。为了保证Ledger中的Entry的严格顺序Pulsar为Ledger增加一次性的写入限制Broker创建一个Ledger后只有这个Broker可以往Ledger中写入Entry一旦Ledger关闭后无论是Broker主动关闭还是因为Broker宕机异常关闭这个Ledger就永远只能读取不能写入了。如果需要继续写入Entry只能新建另外一个Ledger。
请你注意一下,这种“一次性写入”的设计,它的主要目的是为了解决并发写入控制的问题,我在之前课程中讲过,对于共享资源数据的并发写一般都是需要加锁的,否则很难保证数据的一致性。对于分布式存储来说,就需要加“分布式锁”。
但我们知道分布式锁本身就很难实现使用分布式锁对性能也会有比较大的损失。这种“一次性写入”的设计只有创建Ledger的进程可以写入数据Ledger这个资源不共享也就不需要加锁是一种很巧妙的设计你在遇到类似场景的时候可以借鉴。
消息数据由BookKeeper集群负责存储元数据由ZooKeeper集群负责存储Pulsar的Broker上就不需要存储任何数据了这样Broker就成为了无状态的节点。
虽然Broker是无状态的不存储任何的数据但是在一个特定的时刻每一个主题的分区还是要落在某个具体的Broker上。不能说多个Broker同时读写同一个分区因为这样是没有办法保证消息的顺序的也没有办法来管理消费位置。
再来看图中左侧最大的那个Broker方框在Broker中包含了几个重要的模块。Load Balancer负责动态的分配哪些Broker管理哪些主题分区。Managed Ledger这个模块负责管理本节点需要用到的那些Ledger当然这些Ledger都是保存在BookKeeper集群中的。为了提升性能Pulsar同样采用用了一个Cache模块来缓存一部分Ledger。
Pulsar的客户端要读写某个主题分区上的数据之前依然要在元数据中找到分区当前所在的那个Broker这一点是和其他消息队列的实现是一样的。不一样的地方是其他的消息队列分区与Broker的对应关系是相对稳定的只要不发生故障这个关系是不会变的。而在Pulsar中这个对应关系是动态的它可以根据Broker的负载情况进行动态调整而且由于Broker是无状态的分区可以调整到集群中任意一个Broker上这个负载均衡策略就可以做得非常简单并且灵活。如果某一个Broker发生故障可以立即用任何一个Broker来替代它。
那在这种架构下Pulsar又是如何来完成消息收发的呢客户端在收发消息之前需要先连接Service Discovery模块获取当前主题分区与Broker的对应关系然后再连接到相应Broker上进行消息收发。客户端收发消息的整体流程和其他的消息队列是差不多的。比较显著的一个区别就是消息是保存在BookKeeper集群中的而不是本机上。数据的可靠性保证也是BookKeeper集群提供的所以Broker就不需要再往其他的Broker上复制消息了。
图中的Global replicators模块虽然也会复制消息但是复制的目的是为了在不同的集群之间共享数据而不是为了保证数据的可靠性。集群间数据复制是Pulsar提供的一个特色功能具体可以看一下Pulsar文档中的[geo-replication](https://pulsar.apache.org/docs/en/administration-geo/)这部分。
## 存储计算分离的设计有哪些优点?
在Pulsar这种架构下消息数据保存在BookKeeper中元数据保存在ZooKeeper中Broker的数据存储的职责被完全被剥离出去只保留了处理收发消息等计算的职责这就是一个非常典型的“存储计算分离”的设计。
什么是存储计算分离呢?顾名思义,就是将系统的存储职责和计算职责分离开,存储节点只负责数据存储,而计算节点只负责计算,也就是执行业务逻辑。这样一种设计,称为存储计算分离。存储计算分离设计并不新鲜,它的应用其实是非常广泛的。
比如说所有的大数据系统包括Map Reduce这种传统的批量计算和现在比较流行的Spark、Flink这种流计算它们都采用的存储计算分离设计。数据保存在HDFS中也就是说HDFS负责存储而负责计算的节点无论是用YARN调度还是Kubernetes调度都只负责“读取-计算-写入”这样一种通用的计算逻辑,不保存任何数据。
更普遍的,**我们每天都在开发的各种Web应用和微服务应用绝大多数也采用的是存储计算分离的设计**。数据保存在数据库中,微服务节点只负责响应请求,执行业务逻辑。也就是说,数据库负责存储,微服务节点负责计算。
那存储计算分离有什么优点呢?我们分两方面来看。
对于计算节点来说它不需要存储数据节点就变成了无状态的Stateless节点。一个由无状态节点组成的集群管理、调度都变得非常简单了。集群中每个节点都是一样的天然就支持水平扩展。任意一个请求都可以路由到集群中任意一个节点上负载均衡策略可以做得非常灵活可以随机分配可以轮询也可以根据节点负载动态分配等等。故障转移Failover也更加简单快速如果某个节点故障了直接把请求分配给其他节点就可以了。
对比一下像ZooKeeper这样存储计算不分离的系统它们的故障转移就非常麻烦一般需要用复杂的选举算法选出新的leader提供服务之前可能还需要进行数据同步确保新的节点上的数据和故障节点是完全一致之后才可以继续提供服务。这个过程是非常复杂而且漫长的。
对于计算节点的开发者来说,可以专注于计算业务逻辑开发,而不需要关注像数据一致性、数据可靠性、故障恢复和数据读写性能等等这些比较麻烦的存储问题,极大地降低了开发难度,提升了开发效率。
而对于存储系统来说它需要实现的功能就很简单系统的开发者只需要专注于解决一件事就可以了那就是“如何安全高效地存储数据”并且存储系统的功能是非常稳定的比如像ZooKeeper、HDFS、MySQL这些存储系统从它们诞生到现在功能几乎就没有变过。每次升级都是在优化存储引擎提升性能、数据可靠性、可用性等等。
接下来说存储计算分离这种设计的缺点。
俗话说背着抱着一样沉。对于一个系统来说无论存储和计算是不是分离的它需要完成的功能和解决的问题是一样的。就像我刚刚讲到的Pulsar的Broker相比于其他消息队列的Broker各方面都变的很简单。这并不是说存储计算分离的设计能把系统面临的各种复杂的问题都解决了其实一个问题都没解决只是把这些问题转移到了BookKeeper这个存储集群上了而已。
**BookKeeper依然要解决数据一致性、节点故障转移、选举、数据复制等等这些问题。**并且,存储计算分离之后,原来一个集群变成了两个集群,整个系统其实变得更加复杂了。
另外存储计算分离之后系统的性能也会有一些损失。比如从Pulsar的Broker上消费一条消息Broker还需要去请求BookKeeper集群读取数据然后返回给客户端这个过程至少增加了一次网络传输和n次内存拷贝。相比于直接读本地磁盘性能肯定是要差一些的。
不过对于业务系统来说采用存储计算分离的设计它并不需要自己开发一个数据库或者HDFS只要用现有的成熟的存储系统就可以了所以相当于系统的复杂度还是降低了。相比于存储计算分离带来的各种优点损失一些性能也是可以接受的。
因此,对于大部分业务系统来说,采用存储计算分离设计,都是非常划算的。
## 小结
这节课我们一起分析了Apache Pulsar的架构然后一起学习了一下存储计算分离的这种设计思想。
Pulsar和其他消息队列最大的区别是它采用了存储计算分离的设计。存储消息的职责从Broker中分离出来交给专门的BookKeeper存储集群。这样Broker就变成了无状态的节点在集群调度和故障恢复方面更加简单灵活。
存储计算分离是一种设计思想,它将系统的存储职责和计算职责分离开,存储节点只负责数据存储,而计算节点只负责计算,计算节点是无状态的。无状态的计算节点,具有易于开发、调度灵活的优点,故障转移和恢复也更加简单快速。这种设计的缺点是,系统总体的复杂度更高,性能也更差。不过对于大部分分布式的业务系统来说,由于它不需要自己开发存储系统,采用存储计算分离的设计,既可以充分利用这种设计的优点,整个系统也不会因此变得过于复杂,综合评估优缺点,利大于弊,更加划算。
## 思考题
课后请你想一下既然存储计算分离这种设计有这么多的优点那为什么除了Pulsar以外大多数的消息队列都没有采用存储计算分离的设计呢欢迎在评论区留言写下你的想法。
感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。