本文会对 Pravega 进行性能评估,重点关注读写性能。
为了对比不同的设计选择,我们还额外展示了来自其它系统的性能结果:Apache Kafka 和 Apache Pulsar。Pulsar 和 Kafka 最初都被作为优秀的消息系统而为人熟知,但它们最近都做出了很大努力向存储系统方向发展,这两个系统最近都新增了分层存储的特性。然而,它们的设计选择具有根本性的不同,并导致了不同的行为以及性能特点。我们将会在本文探索这些不同点。
本文的主要的实验参数和结果亮点如下:
整体注入性能。 单个 Pravega 的 Writer 可以每秒产生超过一百万小型事件(100 字节),以及为大型事件(10,000 字节)保持 350MB/s 的吞吐量,并且在这两个场景下都能保持个位数毫秒数级的延迟(95% 分位数,95th percentile)。
持久性。 Pravega 总是在发回确认时保证数据的持久性。对于单 Segment(类比 Kafka partition)的 Stream,Kafka 的写吞吐量至少比 Pravega 低 40%,无论是否对每个消息进行刷新(flush)操作。对于 16 Segment 的 Stream,Pravega 的 Writer 与每消息进行刷新的 Kafka Writer 的吞吐量相仿,但 Pravega 的延迟 更低(几毫秒相比于 Kafka 的超过 1 秒)。
能够动态调整的批处理。 Pravega 并不要求为客户端的批处理进行任何复杂配置。Pulsar 可以达到低延迟或者高吞吐,但不能二者兼具。对于 Kafka,当应用程序使用路由键(Routing Key)分区的时候,较大的批对吞吐量的影响很大(对于 16 Segment 的 Stream,吞吐量降低了 80%)。在这两种情况下,强制要求用户静态配置批处理都不是理想的解决方案。
当存在大事件时,注重高吞吐量工作负载的行为。 对于 16 Segment 的 Stream 和 10kB 大小的事件,Pravega 取得了高达 350MB/s 的吞吐量。这一吞吐量比 Pulsar 在同配置下高 40%,并与 Kafka 旗鼓相当(约有 6% 的差异)。然而,Kafka 在这一配置下的延迟高达 800 毫秒,而 Pravega 却仅有几毫秒。
端到端的延迟。 当处于 Stream 的尾部时,Pravega 的 Reader 同样提供仅数毫秒的端到端延迟,同时保持相当高的吞吐量。对于单 Segment 的 Stream,它比 Kafka 的吞吐量更高(大约高 80%)。对于 Pulsar,增加 Partition 和 Reader 使得性能下降(单 Partition 用例所取得的吞吐量是 16 Partition 用例的 3.6 倍)。
路由键(Routing Key)的使用。 无论使用还是不使用路由键,Pravega 并未表现出显著的性能差异。对于中高吞吐量的用例,当使用路由键时,Kafka 和 Pulsar 表现出超过两倍的端到端延迟,尤其是对于 Kafka,最大读取吞吐量降低了超过 37%。
追赶(Catch-up)读取和历史读取的分层存储。 Pravega 可以在具有 100GB 历史数据并且同时进行 100MB/s 新数据注入的情况下动态进行追赶读取。在相同场景下,Pulsar 开启了分层存储后不能很好地进行追赶读取,表现为积压无限增长。
自动扩展的性能。 自动扩展是 Pravega 独有的特性,扩展一个 Stream 可以提供更高的注入性能。我们可以看到,在使用 100MB/s 恒定注入速率的情况下, Stream 自动扩展使得写入延迟下降。
除了在展示时间序列的时候,我们都绘制了延迟随吞吐量的变化曲线。在现有的公开文章中,我们发现了一个普遍问题:它们要么只绘制延迟曲线,要么只绘制吞吐量曲线,然而对于流式负载,这二者其实是相互关联的。例如,当某个配置所得到的最大吞吐量看起来很优秀时,它的延迟很可能已经达到了几百毫秒甚至数秒的数量级。我们通过绘制延迟 - 吞吐量图像来避免这种具有误导性的结论。此外,我们还提供图表数据点所对应的表格数据作为补充。从完备性方面说,表格数据比图提供了更多信息(例如不同的百分等级)。
Pravega 是一个复杂的系统,因此我们推荐读者深入阅读我们的文档,包括 先前的博客文章 (http://www.infoq.cn/theme/56)。在这里,我们仅提供关于读写路径的简要概括,同时还有 Kafka 和 Pulsar 的一些要点。
Pravega 提供不同的 API 实现将数据加入 Stream。在本小节,我们主要关注 事件流 API。
事件流 Writer 将事件写入 Stream。一个 Stream 可以有数个并行的 Segment,并且根据自动扩展策略,这些 Segment 还可以动态变化。当一个应用提供了路由键,Pravega 客户端就以此将事件映射到 Segment。如果在事件写入时没有提供路由键,那么客户端就随机选取一个 Segment。
客户端会适时地批处理事件数据,将事件成批写入某个 Segment。客户端控制着何时新开一个批以及何时关闭一个已有的批,但是同一批数据还是会在服务器端积累。在 Pravega 中,管理 Segment 的组件称作 Segment Store。Segment Store 接收对 Segment 的写请求,同时把这些数据加入一个缓存和追加到一个持久化日志中,该日志目前由 Apache BookKeeper 实现。当追加到持久日志时,Segment Store 会进行第二轮批处理。对于那些消息尺寸很小并且消息很不频繁的场景,我们通过这种二层聚合来保持高吞吐量。
日志保证了持久性:一旦事件在日志中被持久化,Pravega 便对事件进行确认,而这些日志仅在故障恢复时才使用。由于 Segment Store 在缓存中对写入数据保留了一份副本,它需要异步地将这些数据从缓存刷入底层存储,称作长期存储(Long-Term Storage,LTS)。
BookKeeper 将自身的存储划分为日志(Journal)、条目记录器(Entry Logger)和索引(Index)三部分。其中,日志是一种只追加的数据结构,并且它是写路径上的重要组件。条目记录器和索引则在 BookKeeper 的读取路径上使用。对于 Pravega 来说,BookKeeper 的读取路径仅在 Segment Store 的故障恢复阶段才发生。因此,如果排除那些偶发性的干扰,写路径的容量主要依赖于日志而不是其它两个组件。
应用程序使用事件流 Reader 的实例独立进行事件读取。事件流 Reader 是 Reader Group 的组成部分之一,而 Reader Group 则在内部负责协调 Segment 的分配。Reader 从被分配到的 Segment 中读取事件,并且它们是从 Segment Store 主动拉取数据。Reader 每次拉取 Segment 数据时,都会尽可能多地获取可用数据,最大上限为 1MB。
Segment Store 总是从缓存中进行数据服务。如果缓存命中,那么响应读请求不再需要额外的 IO 操作。否则,Segment Store 从长期存储中以 1MB 为单位拉取数据,填充缓存,然后响应客户端。Segment Store 并不是从持久化日志中读取数据然后响应读请求的。
Pulsar 和 Kafka 都将自己定义为流式平台。Pulsar 基于 Apache BookKeeper 实现了一个消息代理。这一代理是建立在一个托管分类账簿(Ledger)的抽象之上,具体而言,是由一系列顺序组织的 BookKeeper ledger 组成的无边界日志。BookKeeper 是 Pulsar 的主要数据存储,尽管最近的一个新特性允许用户可选地 将数据分层存储到长期存储中去。
Pulsar 对外暴露 Topic 抽象,并允许 Partition。Pulsar 的生产者向 Topic 中生产消息。Pulsar 为消息的收取和消费提供了不同选项,例如不同的订阅模式以及从 Topic 中手动读取等。
Kafka 同样实现了消息代理,并且暴露 Topic 与 Partition。Kafka 并不像 Pravega 和 Pulsar 那样使用外部的存储依赖,它使用本地存储代理作为系统的主要存储。的确有人 提议为 Kafka 增加开源的分层存储,但据我们所知,该特性目前仅是 Confluent platform 的预览特性。因此,我们只与 Pulsar 做分层存储的对比。
Pulsar 和 Kafka 都实现了客户端批处理,并允许对这种批处理进行配置。对于二者来说,有两个主要参数控制着客户端批处理:
最大批尺寸和最大等待时间之间存在一种内在的权衡关系:较大的批对吞吐量有利而对延迟不利,而较小的等待时间则有着相反的效果。如果批能够积累得足够快,那么是有可能同时做到高吞吐与低延