写在前面
这几天拜读了郭斯杰的《Messaging,Storage,or both?》一文,原文地址在这里,大有感触,作者分享了自己过去几年时间里在工作中使用Apache Pulsar、DistributedLog,以及BookKeeper的实际经验。
郭斯杰7年前作为雅虎北京的推送消息团队成员开始使用BookKeeper,大约5年前,也就是2012年,郭斯杰转战到了位于旧金山的Twitter公司,开始致力于利用BookKeeper解决分布式数据库的一致性问题,根据他的描述,这一工作内容最终促成了Apache DistriutedLog的诞生。
郭斯杰的这篇文章主要是围绕Pulsar、DistriutedLog两者如何与BookKeeper协作完成实时处理的经验分享。本文我将会解读他的这篇文章,并且加入我自己的实际理解和使用经验。
预备知识
郭斯杰最早开始接触的是BookKeeper,从后面的文章介绍中我们可以知道,BookKeeper是很多组件的基础,可以帮助进行分布式环境的信息协同管理,正是由于拥有BookKeeper的实际工作经验,郭斯杰也有了接触Pulsar和DistributedLog的机会。我先谈谈自己整理的一些相关知识,介绍这三个东西究竟是什么?
Apache Pulsar
Pulsar是分布式订阅发布消息传输系统,最早有由Yahoo公司开发的,并在2016年正式开源。
Pulsar提供了灵活消息传输、多租户、跨地理位置数据复制等特性。Pulsar的创始人Joe和Matteo等人认为需求是Pulsar项目启动的原因,如果应用程序提供实时服务,需要保证平均5ms以内的发布延迟,99%的请求不会超过15ms的延迟,同时满足分类、强持久性以及传输保证等特征的消息传输系统,这个系统必须满足提交到各个磁盘或者节点达到99.99%的准确性。
Pulsar对于消息的相关概念和角色定义与Kafka很相近,它们都把数据的接入方叫做生产者,都把数据的接收方叫做消费者(订阅者),如下图所示。
Pulsar是如何实现对于多租户用例的支持的?通过属性(Property)和命名空间(NameSpace)。属性表示系统中的租户,在Pulsar集群内部,一个属性可以包含多个命名空间,如下图所示。
命名空间是Pulsar集群的最基本管理单元,在命名空间级别,你可以设置权限、调优复制策略、管理跨集群的消息数据复制、控制消息过期,以及其他关键操作。同一个命名空间里的主题共享相同的配置。
在Pulsar内部存在几个一对多的关系。一个命名空间对应多个主题(Topic),一个主题对应多个订阅者(Subsribes),一个订阅者可以接收主题上的所有消息。为了提供更加灵活的订阅方式,Pulsar提供了三种不同的订阅类型:
- 独占式订阅:每个主题有且仅有一个消费者;
- 共享式订阅:多个消费者可以共享一个订阅/主题,每个消费者可以收到订阅的某一部分内容;
- 失败切换模式:多个消费者可以连接到一个主题,但是同时有且仅有一个消费者可以收到消息,其他的消费者只有在当前在线的那个消费者离线时才能竞争上岗(同样只会有一个竞争获胜者,其它竞争失败者还是需要继续守候下一次竞争)。
除此之外,Pulsar支持将主题进行分区,一旦分区,数据也会被自动分区,如下图所示,和Kafka类似,Pulsar也引入了“Broker”概念,每个Broker管理多个主题。
Apache DistributedLog
DistributedLog的出现是数据层面抽象的必然结果。2012年底这个时间段正好是Twitter公司内部的实时消息基础设施的杂乱无章阶段。Kestrel是一款队列系统,被设计用来处理在线服务的关键消息,Kafka则被用于进行离线服务的日志收集和分析,郭斯杰的团队则使用BookKeeper进行数据库备份。
DistributedLog也被称为“共享日志基础设施”。日志存储是几乎所有分布式系统都需要解决的问题,而DistributedLog被设计来解决这一共有需求,也可以统一分歧,逐渐变成其他服务的基础组件,包括键值对数据库、订阅发布消息,以及跨数据中心的复制机制等等。
下面这张图诠释了DistributedLog和BookKeeper的使用案例。
DistributedLog最初的定义就是共享的日志组件,它会跟踪分布式交易日志的改变。DistributedLog是基于BookKeeper的,它利用了BookKeeper的低延时存储、并行复制、简单重复读一致性、快速多对多复制修复、I/O隔离,以及简单操作性,实现了无缝连接历史和未来数据的设计目标。
Kafka与DistributedLog有一些不同,Kafka最初就是作为一个日志收集系统,进而形成了一个消息系统,但是DistributedLog开始是为了解决数据库一致性问题,逐渐形成了流式的存储系统。这就意味着两者有较大的设计和技术实现差异,因为Kafka的设计初衷就是为了实现数据的交换,它是作为消息中间件设计的,不会仅仅考虑数据一致性问题,两者看待问题的格局不一样。现在DistributedLog已经被作为BookKeeper的一个子项目在运行了。
Apache BookKeeper
BookKeeper是一款可扩展、容错、低延时的日志存储服务,尤其针对实时任务流设计。
BookKeeper最初是由Yahoo公司开发的,2011年被合并到了Apache ZooKeeper,作为它的子项目。2015年单独成为Apache顶级项目。
BookKeeper集群由以下两个组件组成:
- bookies:代表一系列独立的存储服务;
- metadata存储:服务发现和元数据管理。
BookKeeper客户端使用DistributedLog的API或者原生API提供对于bookies的访问方式,架构图如下所示。
BookKeeper支持以下需求:
- 客户端写入和读取数据需要低于5ms的延时,同时确保数据不丢失(强持久性);
- 数据存储应该是持久化的、一致性的、容错的;
- 客户端写入时可以在数据的尾部写入,并支持快速存取数据;
- 支持存储、访问历史和实时数据。
BookKeeper针对每一份数据需要复制和存储多份副本,对于复制算法,这一点和其他的分布式系统有所不同,BookKeeper使用的是一种被称为“仲裁并行复制算法(quorum-vote parallel replication algorithm)”的算法,该算法确保了低延时复制数据,这一点有别于HDFS、Ceph、Kafka的Master/Slave模式,BookKeeper更加偏向于无中心化。
如上图所示,可以理解到以下观点:
- bookies是自动从BookKeeper集群里选择的,ledger作为一组顺序记录的抽象,类似一个文件名;
- 被存储在ledger上的数据记录会被同时写入到集群上的其他bookies节点;
- 当客户端写入数据时,客户端需要等待一定数量的副本确认写入后才能收到成功消息。这一点和Cassandra类似,都是需要遵从R+W>N的公式约束,以确保返回的数据是最新的且数据不丢失;
- 集群内部支持bookie的容错处理。
深入理解三者关系
Pulsar的出现是为了解决当前开源的消息系统存在的一些弱点。据郭斯杰介绍,最开始的时候使用BookKeeper作为针对ActiveMQ的持久化消息存储,随后引入了统一灵活消息传输模型(Unified Flexible Messaging Model)概念,为什么这里称之为统一呢?我们需要先来了解一下传统的消息模式。郭斯杰介绍,一般来说,我们有两种传统的消息模型:队列和发布订阅。队列是点对点的通信模式,通常是无序消息传输,用于无状态应用比较多一些。对于队列的消费设计,通常存在一个消费池概念,消费者可以从服务端读取消息,每条消息仅仅能被传递给一个消费者。这种方式允许你对跨越多个消费者实例的处理数据进行分段处理,并且也容易对处理过程通过横向扩展方式提升性能。订阅发布模式则是一种广播通信模式,消息可以被广播给所有的消费者。当前几乎所有的消息组件都会分割对于这两种消息传输模式的支持方案,即形成两套方案,分别对应队列和主题(Topic)。郭斯杰也举例了确实存在一些消息组件尝试去合并这两种不同的通信模型,例如Twitter的Kestrel,但是最终这样做的性价比不高。就我个人的使用经验来看,当前比较主流的Kafka采用的是发布订阅模式,当然,采用哪种方式本质上没有对错之分,重要的是和你的业务需求契合。
郭斯杰将Pulsar的消息模型方式概括为“生产者-主题-订阅-消费者”,或者简称PTSC。通过PTSC模式,每一条消息都可以与主题相绑定,然后通过BookKeeper复制到不同的机器上,接着可以被消费者任意消费。消费者存在组的概念,每一组内的消费者可以决定自己的消费方式(独占式、共享式,或故障转移方式)。下面这张图解释了Pulsar支持的订阅模式。
郭斯杰提出了一种观点,他认为一些基于分区的订阅发布消息组件误导了人们对于队列和流的概念,这些组件强迫消费者按照同一种模式进行消息处理。举个例子,对于队列用例,用户不得不增加分区以用于匹配消费者并行需求,这种方式降低处理的效率,特别是对于任务派发这样的功能。我主持研发的系统的一个功能就包含任务派发,对于任务派发功能,我们可以有很多种不同的实现方式,多数情况是有多个接收端,这个时候如果需要通过增加队列方式,确实很不方便,所以郭斯杰提出的用例问题确实存在。
Pulsar是怎么满足队列需求的?郭斯杰解释它是通过基于轮询算法的共享订阅模式,允许应用程序在一个订阅过程中进行分割处理,这样我们就可以实现在一个主题内部通过增加消费者数量的方式扩展并行处理能力。Pulsar通过将发布并行性和消费并行性扩展分离方式,允许活动的发布和消费两者独立扩展,互不约束对方。
正是由于Pulsar是构建于BookKeeper之上的,所以可以提供高性能的队列和流式处理。BookKeeper不仅仅有助于队列、流式处理的高性能,也帮助Pulsar有效地实现消息删除功能,而这一项恰恰是其他主流消息组件的弱项。
Pulsar利用游标系统实现高效的消息移除。我插一句话,对于游标,熟悉C语言的同学应该有点映像,它对于数据的移动非常高效。据郭斯杰介绍游标是Pulsar用来为每个订阅者记录消息消费的重要状态信息。对于一个独占式/失败切换订阅模式,游标是一个偏移量,标记哪一个消费者已经消费了;而对于共享订阅模式,游标则不仅仅是一个偏移量了,它被用于跟踪每个消息的消费情况。Pulsar利用BookKeeper特有的ledgers(ledgers支持应用程序创建很多独立的日志,一个ledger是一个递增的数据结构,通过单一写入模式保持数据写入,并且通过复制机制被复制到其他节点上)设计用于记录游标的更新情况,这样就相当于通过BookKeeper实现了游标信息的高可用性保障,减少了消费者出现异常之后的消息重新传递几率。正因为有了游标,Pulsar可以针对消息的消费进行跟踪,也可以进行消息的删除。据郭斯杰介绍,他们在Streamlio使用游标系统解决Pulsar发布过程中的性能问题。
总结
从郭斯杰的文章里我们可以知道Pulsar是一种支持灵活的消息模式(队列和订阅发布)的分布式订阅系统,背后是通过BookKeeper支持高度扩展、持久化流式存储。Pulsar聚焦于消息传输和消费,允许快速删除不需要的消息。我认为正是由于背后有可扩展的流式存储支持,也相应获得了强一致性保障,并且允许流式计算中的回调数据和重新处理。
BookKeeper与DistributedLog的结合形成了高度可扩展的日志流存储系统,对外提供实时的日志流存储服务,可以被用来为其他消息组件提供存储消息服务,例如Pulsar。但是我们也应该看到Pulsar和DistributedLog之间确实存在一些技术上的重叠,更多应该从它们所分别对应的应用场景出发进行考虑。如果你正在寻找针对流式数据的实时存储组件,BookKeeper+DistributedLog的组合适合你的需求(高吞吐、低延时、持久化复制功能等等)。我认为,如果你需要一个快速、灵活的消息系统,Pulsar提供了灵活的消息模式,同时支持队列和发布订阅两种模式。
从全文分析来看,我认为Pulsar和DistributedLog/BookKeeper的组合,即消息+存储的组合可以提供快速、持久、灵活的消息传输模式以及高可扩展的流式存储。
郭斯杰认为现实世界我们不能单纯谈论消息或者存储,两者应该是并存的。对于这个观点,我也是持支持态度,因为现实的需求往往是需要多种组件协同支持的,即便是实时分析引擎,其实它内部也存在保存任务状态的持久化组件,只不过看你如何看待保存的数据了。如果没有持久化、低延时的存储,消息系统就有可能丢失数据,也就不能满足计算引擎重复处理数据的要求;如果没有消息系统,那么也就无法为实时流、微服务、事件驱动架构等提供灵活的消息传输、日志流存储等功能,消息和存储是针对两个不同技术领域的概念,消息聚焦于传输和消费,存储则聚焦于一致性、持久性和低延时等条件下的存储和复制数据,同时也支持快速传播数据的功能。就郭斯杰的这篇文章来看,他告诉了我们BookKeeper、Pulsar、DistributedLog三者的关系。我认为,这三个框架的组合已经发展到方案层面上的组合,不再仅仅能够单独工作。开源社区应该就三者搭建更好的生态链路。相信未来这三者的组合可以为实时消息处理提供更强大的能力支撑。
对本文感兴趣的读者可以访问以下三个网址了解更多信息:
http://bookkeeper.apache.org/
http://bookkeeper.apache.org/distributedlog/
https://pulsar.incubator.apache.org/
作者介绍
周明耀,2004年毕业于浙江大学,工学硕士。13年软件研发经验,近10年技术团队管理经验,4年分布式计算、大数据技术经验。出版书籍包括《大话Java性能优化》、《深入理解JVM&G1 GC》、《技术领导力-码农如何才能带团队》,个人公众号“麦克叔叔每晚10点说”出品人。个人微信号michael_tec。
转自 http://www.infoq.com/cn/articles/messaging-storage-or-both