本文可能是全网最好的对比 Kafka 与 Pulsar 的文章之一。
翻译自 https://learning.oreilly.com/library/view/apache-pulsar-versus/9781492076551/ch01.html#what_is_apache_pulsar ,原作者 Chris Bartholomew。
如有错漏,欢迎提 PR 至 https://github.com/AlphaWang/Translation-Apache-Pulsar-Versus-Apache-Kafka
Apache Kafka 是一种广泛使用的发布订阅(pub-sub)消息系统,起源于 LinkedIn,并于 2011 年成为 Apache 软件基金会(ASF)项目。而近年来,Apache Pulsar 逐渐成为 Kafka 的重要替代品,原本被 Kafka 占据的使用场景正越来越多地转向 Pulsar。在本报告中,我们将回顾 Kafka 与 Pulsar 之间的主要区别,并深入了解 Pulsar 为何势头如此强劲。
与 Kafka 类似,Apache Pulsar 也是起源于一家互联网公司内部,用于解决自己特有的问题。2015 年,雅虎的工程师们需要一个可以在商业硬件上提供低延迟的 pub-sub 消息系统,并且需要支持扩展到数百万个主题,并为其处理的所有消息提供强持久性保证。
雅虎的工程师们评估了当时已有的解决方案,但无一能满足所有需求。于是他们决定着手构建一个全新的 pub-sub 消息系统,使之可以支持他们的全球应用程序,例如邮箱、金融、体育以及广告。他们的解决方案后来演化成 Apache Pulsar,自 2016 年就开始在雅虎的生产环境中运行。
让我们先从架构角度对比 Kafka 和 Pulsar 这两个系统。由于在开发 Pulsar 的时候 Kafka 已经广为人知,所以 Pulsar 的作者对其架构了如指掌。你将看到这两个系统有相似之处,也有不同之处。如您所料,这是因为 Pulsar 的作者参考了 Kafka 架构中的可取之处,同时改进了其短板。既然一切都源于 Kafka,那我们就先从 Kafka 的架构开始讲起吧。
Kafka 有两个主要组件:Apache ZooKeeper 和 Kafka Broker,如图 1 所示。ZooKeeper 用于服务发现、领导者选举以及元数据存储。在旧版本中,ZooKeeper 也用来存储消费者组信息,包括主题消费偏移量;但新版本不再这样了。
图 1. Kafka 架构图
Kafka Broker 承包了 Kafka 的所有消息功能,包括终止生产者和消费者连接、接受来自生产者的新消息并将消息发送给消费者。为了保证消息持久化,Broker 还为消息提供持久化存储功能。每个 Kafka Broker 负责一组主题。
Kafka Broker 是有状态的。每个 Broker 都存储了相关主题的完整状态,有了这些信息 Broker 才能正常运行。如果一个 Broker 发生故障,并不是任何 Broker 都可以接管它,而是必须拥有相关主题副本的 Broker 才能接管。如果一个 Broker 负载太高,也不能简单地通过增加 Broker 来分担负载,还需要移动主题(状态)才能平衡集群中的负载。虽然 Kafka 提供了用来帮助重平衡的工具,但是要用它来运维 Kafka 集群的话,你必须了解 Kafka Broker 与其磁盘上存储的消息状态的关系才行。
消息计算(Serving)是指消息在生产者和消费者之间的流动,在 Kafka Broker 中消息计算与消息存储是相互耦合的。如果你的使用场景中所有消息都能被快速地消费掉,那么对消息存储的要就可能较低,而对消息计算的要求则较高。相反,如果你的使用场景中消息消费得很慢,则需要存储大量消息。在这种情况下,对消息计算的要求可能较低,而对消息存储的要求则较高。
由于消息的计算和存储都封装在单个 Kafka Broker 中,所以无法独立地扩展这两个维度。即便你的集群只对消息计算有较高要求,你还是得通过添加 Broker 实现扩展,也就是说不得不同时扩展消息计算和消息存储。而如果你对消息存储有较高要求,而对消息计算的要求较低,最简单的方案也是添加 Kafka Broker,也就是说还是必须同时扩展消息计算和消息存储。
在扩展存储的场景中,你可以在现有的 Broker 上添加更多磁盘或者增加磁盘容量,但需要小心不要创建出一些具有不同存储配置和容量的独特 Kafka Broker。这种“雪花”(Snowflake)服务器环境比具有统一配置的服务器环境要复杂得多,更难以管理。
Pulsar 架构中主要有三个组件:ZooKeeper、Pulsar Broker 和 Apache BookKeeper Bookie,如图 2 所示。与 Kafka 一样,ZooKeeper 提供服务发现、领导者选举和元数据存储。与 Kafka 不同的是,Pulsar 通过 Broker 和 BookKeeper Bookie 组件分离了消息处理计算与消息存储功能。
图 2. Pulsar 架构图
Pulsar Broker 负责消息计算,而 BookKeeper Bookie 负责消息存储。这是一种分层架构,Pulsar Broker 处理生产者和消费者之间的消息流动,而将消息存储交给 BookKeeper 层处理。
得益于这种分层架构,Pulsar Broker 是无状态的,这一点与 Kafka 不同。这意味着任何 Broker 均可接管失效的 Broker。也意味着新的 Broker 上线后可以立即开始处理生产者和消费者的消息流动。为了确保 Broker 之间的负载均衡,Pulsar Broker 内置了一套负载均衡器,不断监视每个 Broker 的 CPU、内存以及网络使用情况,并据此在 Broker 之间转移主题归属以保持负载均衡。这个过程会让 Latency 小幅增加,但最终能让集群的负载达到均衡。
BookKeeper 作为数据存储层当然是有状态的。提供可靠消息投递保证的消息系统必须为消费者保留消息,所以消息必须持久化存储到某个地方。BookKeeper 旨在构建跨服务器的分布式日志,它是一个独立的 Apache 项目,用于多种应用中,而非仅仅是 Puslar 中。
BookKeeper 将日志切分成一个一个被称为 Ledger 的分片(Segment),这样就很容易在 BookKeeper Bookie 节点之间保持均衡。如果 Bookie 节点故障,一些主题会变成小于复制因子(Under Replicated)。发生这种情况后 BookKeeper 会自动从存储与其他 Bookie 中的副本复制 Ledger,从而让其恢复到复制因子,而无需等待故障 Bookie 恢复或等待其他 Bookie 上线。如果添加一个新 Bookie,它能立即开始存储已有主题的新 Ledger。由于主题或分区并不从属于某个 Bookie,所以故障恢复过程无需将主题或分区移动到新服务器。
为了保证消息持久性,Kafka 和 Pulsar 都对每个消息存储多个拷贝或副本。但是他们各自使用了不同的复制模型。
Kafka 使用的是 Leader-Follower 复制模型。对每个主题(确切说是主题分区,稍后我们会详细解释)都会选出一个 Broker 作为 Leader。所有消息最初都写入到 Leader,然后 Follower 从 Leader 读取并复制消息,如图 3 所示。这种关系是静态的,除非 Broker 发生故障。同一主题的消息总是被写入同一组 Leader 和 Follower Broker。引入新的 Broker 并不会改变现有主题的关系。
图 3. Kafka leader–follower 复制模型
Pulsar 使用的则是法定人数投票复制模型(quorum-vote)。Pulsar 并行写入消息的多个副本(Write Quorum)。一旦一定数量的副本被确认写入成功,则该消息被确认(Ack Quorum)。与 Leader-Follower 模型不同,Pulsar 将副本分散(或称为条带化写入)到一组存储节点(Ensemble)中,这能改善读写性能。这也意味着新的节点添加成功后,即可立即成为可写入集合的一部分,用于存储消息。
如图 4 所示,消息被发往 Broker,然后被切分成分片(Segment)并写入多个 Bookie 节点。这些 Bookie 节点存储分片并发送确认给 Broker。一旦 Broker 从足够多的 Bookie 节点收到足够多的分片确认,则向生产者发送消息确认。
图 4. Pulsar quorum–vote 复制模型
由于 Broker 层是无状态的、存储层是分布式的、并且使用了法定人数投票复制模型(quorum-vote),所以与 Kafka 相比 Puslar 能更容易地处理服务器故障。只需替换掉故障服务器,Pulsar 即可自动恢复。增加新容量也更容易,只需简单的水平扩展即可。
而且由于计算层和存储层是分离的,所以你可以独立地扩展它们。如果对计算要求较高而对存储要求较低,那么在集群中加入更多的 Puslar Broker 即可扩展计算层。如果对存储要求很高而对计算要求很低,那么加入更多 BookKeeper Bookie 即可扩展存储层。这种独立的可扩展性意味着你可以更好地优化集群资源,避免在仅需要扩展计算能力时不得不浪费额外的存储,反之亦然。
Kafka 和 Pulsar 支持的基础消息模式都是 pub-sub,又称发布订阅。在 pub-sub系统中,消息的发送方和接收方是解耦的,因此彼此透明。发送方(生产者)将消息发送到一个主题,而无需知道谁将接收到这些消息;接收方(消费者)订阅要接收消息的主题。发送方和接收方并不互相连接,且随时间推移可能变化。
图 5. Pub–sub 消息模式:每个订阅者都能收到生产者发送的一条消息拷贝
Pub-sub 消息模式的一个关键特性是单个主题上可能有多个生产者与订阅者。如图 5 所示,多个发布应用可以发送消息到一个主题,多个订阅应用可以接收这些消息。重要的是,每个订阅应用都会收到自己的消息拷贝。所以如果发布了一条消息并且有 10 个订阅者,那么就会发送 10 条消息拷贝,每个订阅者收到一条消息拷贝。
Pub-sub 消息模式并不是什么新鲜事物,且被多种消息系统支持:RabbitMQ、ActiveMQ、IBM MQ,数不胜数。Kafka 与这些传统消息系统的区别在于,它有能力扩展到支持海量消息,同时保持一致的消息延迟。
与 Kafka 类似,Pulsar 也支持 pub-sub 消息模式,且也能支持海量消息且具有一致延迟。Kafka 使用消费者组来实现多个消费者接收同一消息的不同拷贝。Kafka 会与主题关联的每个消费者组发送一条消息。Pulsar 使用订阅(Subscription)来实现相同的行为, 向与主题关联的每个订阅发送一条消息。
Kafka 与传统消息系统的另一个主要区别是将日志作为处理消息的基本抽象。生产者写入主题,即写入日志;而消费者独立地读取日志。然而与传统消息系统不同,消息被读取后并不会从日志中删除。消息被持久化到日志中直至配置的时间到期。Kafka 消费者确认消息后并不会删除消息,而是提交一个偏移量值来表示它已读取了多少日志。此操作不会从日志中删除消息或以任何方式修改日志。总之,日志是不可变的。
为了防止日志变得无限长,日志中的消息在一段时间(保留周期)后会过期。过期的消息会从日志中删除。Kafka 默认的保留周期是七天。图 6 展示了发布的消息是如何附加到日志中,消费者如何以不同的偏移量读取它。日志中的消息到期后会过期并被删除。
图 6. 日志抽象
利用日志抽象可以允许多个消费者独立地读取同一个主题,同时还能支持消息重放。由于消费者只是从日志中读取消息并提交日志偏移量,因此只要将偏移量移动到较早位置就能很容易地让消费者重放已消费过的消息。支持消息重放有很多优势。例如,有 bug 的应用程序修复后可以重放之前消费过的消息以纠正其状态。在测试应用程序或开发新应用程序时,消息重放也很有用。
与 Kafka 类似,Pulsar 也使用日志来抽象其主题,只不过具体实现有所不同。这意味着 Puslar 也能支持消息重放。在 Puslar 中,每个订阅都有一个游标来跟踪其在主题日志中的消费位置。创建订阅的时候可以指定游标从主题的最早或最新消息开始读取。你可以将订阅游标倒回到特定消息或特定时间(例如倒回 24 小时)。
到目前为止,我们看到 Kafka 与 Pulsar 有许多相似之处。他们都是能处理海量消息的 pub-sub 消息系统,都使用日志来抽象主题,并支持消息回放。不同之处在于对传统消息模型的支持。
在传统消息模型中,消息系统负责确保将消息投递给消费者。消息系统会跟踪消费者是否已经确认消息,并周期性地将未被确认的消息重新投递给消费者,直至被确认为止。一旦消息被确认,即可被删除(或标记为将来删除)。而未被确认的消息永远不会被删除,它将永远存在。而已确认的消息永远不会再发送给消费者。
Pulsar 利用订阅充分支持上述模型。 由于这种能力,Puslar 能够支持额外的消息模式,专注于消息如何被消费。
我们首先要分析的消息模式是传统队列模型。这种模型中队列消息代表一系列将要完成的工作(工作队列)。你可以使用单个消费者从队列中读取消息并执行工作,但更常见的做法是在多个消费者中分配工作。这种模式被称为竞争消费者模式,如图 7 所示。
在竞争消费者模式中,队列用于存储需要很长时间来处理的消息,例如转换视频。一条消息被发布到队列中后,被消费者读取并处理。当消息被处理完成后,消费者发回确认,然后消息从队列中删除。如果是单个消费者,则队列中的所有消息会被阻塞,直到消息被处理并确认。
图 7. 竞争消费者:每条消息被一个消费者处理一次
为了改善整个流程并保持队列不被填满,你可以往队列中添加多个消费者。然后多个消费者会“竞争”从队列中获取消息并处理它们。如果上述视频转换的例子中我们有两个消费者,则相同时间内能处理的视频量会增加到两倍。如果这还不够快,我们可以添加更多消费者来提高吞吐量。
为了更有效,工作队列需要始终将消息分发给那些有能力处理队列消息的消费者。如果消费者有能力处理消息,则队列就将消息发给它。
Kafka 使用消费者组和多分区来实现竞争消费者模式。Kafka 主题由一个或多个分区组成,消息被发布后通过 round-robin 或者消息 key 分布到主题分区中,随后被消费者组从主题分区中读取。
需要注意的是,Kafka 的一个分区一次只能被一个消费者消费。要实现竞争消费者模式,则每个消费者要有对应的分区。如果消费者数目多于分区数目,则多出来的消费者就会被闲置。举个例子,假设你的主题有两个分区,则消费者组中最多有两个活跃消费者。如果增加第三个消费者,则该消费者没有分区可以读取,所以不会竞争读取队列中的工作(消息)。
这意味着在创建主题时就需要明确有多少竞争消费者。当然你可以增加主题分区数,但这是相当重的变动,尤其当根据 key 分配分区时。除了 Kafka 消费者与主题的对应关系外,往消费者组中添加消费者会重平衡该主题的所有消费者,这种重平衡会暂停对所有消费者的消息投递。
所以说 Kafka 虽然确实支持竞争消费者消息模式,但是需要你仔细管理主题的分区数,确保添加新消费者时能真的处理消息。另外,与传统消息系统不同,Kafka 不会周期性地重新投递消息以便可以再次处理这些消息。如果想要有消息重试机制,你需要自己实现。
Kafka 确实比传统消息系统有个优势。竞争消费者模式的一个弊端是消息可能被乱序处理。因为竞争消费消息的多个消费者可能处理速率不同,很有可能消息会乱序处理。如果消息代表独立的工作,那么这不是什么问题。但如果消息代表像金融交易这样的事件,那有序性就很重要了。
由于 Kafka 分区一次只能被一个消费者消费,所以 Kafka 可以在竞争消费者模式下保证相同 key 的消息被顺序投递。如果消息是按照 key 路由到分区,则每个分区中的消息是按照发布的顺序存储的。消费者可以消费该分区并按顺序获取消息。这使得你可以扩展消费者以并行处理同时保持消息顺序,当然这一切都需要仔细的规划才行。
Pulsar 中的竞争消费者模式很容易实现,只需在主题上创建共享订阅即可。之后消费者使用此共享订阅连接到主题,消息以 round-robin 方式被连接到该订阅上的消费者消费。消费者的上线下线并不会像 Kafka 那样触发重平衡。当新的消费者上线后即开始参与 round-robin 消息接收。这是因为与 Kafka 不同,Pulsar 并不使用分区来在消费者之间分发消息,而完全通过订阅来控制。Pulsar 当然也支持分区,这一点我们稍后将讨论,但消息的消费主要是由订阅控制,而不受分区控制。
Pulsar 订阅会周期性地将未确认消息重新投递给消费者。不仅如此,Pulsar 还支持高级确认语义,例如单条消息确认(选择性确认)和否定确认(negative acknowledgment),这一点对工作队列场景很有用。单条消息确认允许消息不按顺序确认,所以慢速消费者不会阻塞对其他消费者投递消息,而累积确认是可能发生这种阻塞的。否定确认允许消费者将消息放回主题中,之后可以被其他消费者处理。
Pulsar 支持按 key 将消息路由到分区,所以也可以使用与 Kafka 一样的方案来实现竞争消费者。共享订阅这种实现方式更加简单,但是如果你想在横向扩展消费者并行处理能力的同时也保证按 key 有序,Pulsar 也是可以实现的。
共享订阅是 Pulsar 中实现工作队列的一种简单方式。Puslar 还支持其他订阅模型来支持多种消息消费模式:独占、 灾备、共享、键共享,如图 8 所示。
独占订阅模型中,不允许超过一个消费者消费主题消息。如果其他消费者尝试消费消息,则会被拒绝。如果你需要保证消息被单个消费者按顺序消费,那就使用独占订阅模型。
灾备订阅模型中,允许多个消费者连接到一个主题,但是在任何时间都只有一个消费者可以消费主题。这就建立了一种主备关系,一个消费者处于活跃状态,其他的处于备用状态,当活跃消费者故障时进行接管。当活跃消费者断开连接或失败,所有未确认消息会被重新投递到某个备用消费者。
图 8. Pulsar 订阅模型:独占、灾备、共享、键共享
前文已提到,基于共享订阅模型实现的竞争消费者模式的一个弱点是消息可能被乱序处理。在 Kafka 和 Pulsar 中,都可以通过将消息按 key 路由到分区来解决。Puslar 最近推出了一种新的名为 key_shared 的订阅模型,可以更简单地解决这个问题。这种订阅模式的优点是可以按 key 有序投递消息而无需关心分区。消息可以发布到单个主题并分发给多个消费者,这跟共享订阅模型一样。不一样的是,单个消费者只会接受对应某个 key 的消息。这种订阅模型可以通过 key 按顺序投递消息而无需对主题进行分区。
如我们所见,Kafka 和 Pulsar 都支持 pub-sub 消息投递。它们都使用日志来抽象主题,所以可以支持重放已被消费者处理过的消息。但是 Kafka 只能有限地支持按不同方式来消费消息,不会自动重新投递消息,也不能保证未确认的消息不会丢失。实际上,保留周期之外的所有消息无论是否被消费过都会被删除。Kafka 可以实现工作队列,但有很多事项需要注意和考虑。
由于这些限制,如果企业需要高性能 pub-sub 消息系统、同时需要可靠性投递保证以及传统消息模式,他们通常会在 Kafka 之外使用传统的消息系统,例如 RabbitMQ。将 Kafka 用于高性能 pub-sub 场景,而将 RabbitMQ 用于要求可靠性投递保证的场景,例如工作队列。
Pulsar 在单个消息系统中同时支持高性能 pub-sub 以及保证可靠性投递的传统消息模式。在 Pulsar 中实现工作队列非常简单——实际上这也是 Puslar 最开始设计时就想解决的场景。如果你正同时使用多个消息系统——使用 Kafka 处理高流量 pub-sub场景、使用 RabbitMQ 处理工作队列场景——那么可以考虑使用 Puslar 把它们整合成单个消息系统。即便最初只有一种消息场景需求,也可以直接使用 Pulsar 以应对未来可能出现的新的消息场景。
运维单个消息系统显然要比运维两个要更加简单、所需的 IT 和人力资源也更少。
现在我们介绍了 Kafka 与 Puslar 的高层次架构,也了解了这两个系统能实现的各种消息模式,接下来让我们更详细地了解这两个系统的底层模块。首先我们来看看日志抽象。
Kafka 团队的设计思路值得称赞,日志的确是实时数据交换系统的一个很好的抽象。因为日志只能追加,所以数据可以快速写入;因为日志中的数据是连续的,所以可以按照写入顺序快速读取。数据的顺序读写是很快的,而随机读写则不然。在提供数据保证的系统中,持久化存储交互都是瓶颈,而日志抽象则让这一点变得尽可能高效。Kafka 和 Pulsar 都使用日志作为其底层模块。
为了简单起见,下文假设 Kafka 主题是单分区的,因此下文中主题和分区是同义词。
在 Kafka 中,每个主题都是一个日志。日志作为单个存储单元存储在 Kafka Broker 上。虽然日志由一系列文件组成,但日志并不能拆分到多个 Broker 上,也不能拆分到同一个 Broker 的多个磁盘上。这种将整个日志作为最小存储单元的方式通常运行良好,但是当规模增大或在维护期间会很麻烦。
比方说日志的最大大小会受其所在磁盘容量的限制。因此,存储日志的 Broker 磁盘大小限制了主题的大小。在 Broker 上添加磁盘并不能解决问题,因为日志是最小存储单元,并不能跨磁盘拆分。唯一的选择是增加磁盘大小。这在云环境中是可行的,但如果你在物理硬件上运行 Kafka,那么增加现有磁盘的容量不是一件容易的事。
还有另一件麻烦的事,由于日志与其底层文件是一对一绑定的,所以在实时系统上执行维护操作是很麻烦的。如果 Broker 服务器出现故障,或者需要增加新的 Broker 来分担高负载,都需要在服务器之间拷贝大量日志文件。在保持数据实时性的同时执行大量文件拷贝会给 Kafka 集群带来很大压力。
与 Kafka 一样,Apache Pulsar 也使用日志抽象作为其实时消息系统的基础,每个主题在 Pulsar 中也是一个日志。然而 Pulsar 采用不一样的方式将日志写入存储。Pulsar 不是将日志作为最小存储单元存储到单个服务器,而是将日志分解为分片(或称为 Ledger),然后将 Ledger 分布到多个服务器。通过这种方式,Pulsar 创建的分布式日志驻留在多个服务器上。
分布式日志有许多优点。日志的最大大小不再受限于单个服务器的磁盘容量。由于分片是跨服务器分布的,所以日志可以增长到所有服务器的总存储容量一样大。增加分布式日志的容量就像往集群添加服务器一样简单。一旦新服务器上线,分布式日志即可开始使用新上线的容量来写入新的日志分片。也无需调整磁盘大小或重平衡分区来分配负载了。一旦服务器出现故障,故障恢复也很简单。故障丢失的分片可以从多个不同的服务器恢复出来,从而缩短恢复时间。
显而易见,让分布式日志可靠地工作起来是很困难的。这也是为什么 Puslar 要使用另一个 Apache 项目(BookKeeper)来实现分布式日志的原因。要运行 Pulsar 的话必须同时运行 Apache BookKeeper 集群。尽管这会引入运维复杂度,但是 BookKeeper 这个分布式日志的底层组件已经过验证且被广泛应用。BookKeeper 专为健壮的、低延迟的读写而设计。举个例子,BookKeeper 从架构上将写入和读取分离到单独的磁盘,这样一来慢速消费者就不会影响生产者发布新消息的性能。
BookKeeper 还为 Puslar 提供高持久性保证。当消息存储到 BookKeeper 时,会先刷到磁盘再给生产者发回确认;即便 BookKeeper 服务器故障,所有已确认的消息仍然能保证永久存储在磁盘上。BookKeeper 能够在保持低延迟的同时提供这种高持久性保证。
反观 Kafka,默认情况下定期将消息刷到磁盘。这意味着 Kafka Broker 发生故障后几乎总会导致消息丢失,因为这些消息尚未被刷到磁盘。当然,通过配置在线副本数,这些丢失的消息可以恢复;但是 BookKeeper 服务器发生类似故障的情况下,不会有数据丢失,所以也就不需要数据恢复。Kafka 也可以配置为将每条消息即时刷到磁盘,但这会带来性能损失。
Pulsar 存储计算分离的另一个优点是允许在架构中引入第三层,即长期存储,又称冷存储。Pulsar 和 BookKeeper 针对快速访问主题中的消息进行了优化;然而,如果你的消息量非常大但不需要快速访问,或者只需要快速访问最新的消息即可,那么 Pulsar 允许你将这些消息推送到云对象存储,例如 AWS S3 或者 Google Cloud Storage。Pulsar 是这样实现该功能的:将主题中的老分片卸载(offload)到云提供商,然后从 bookie 本地存储中删除这些消息。
云对象存储比起构建高性能消息系统常用的高速 SSD 磁盘要便宜得多,因此运营成本也更低。由于云存储提供了几乎无限的存储容量,所以你不必担心超出你集群的存储容量。非常大的主题可能主要驻留在云存储中,而其他较小的主题则驻留在 bookie 节点的高速磁盘。
这种三层架构可以很好地适应需要永久存储消息的场景,比方说事件溯源。事件溯源是将所有状态变化都记录为事件,存储为 Pulsar 中的消息。应用的当前状态是由直到当前时间为止的整个事件历史记录确定。为了确保可以重建当前状态,你必须保存完整的事件历史。得益于持久性保证、使用分层存储实现近乎无限的存储容量,以及重放主题中所有消息的能力,Pulsar 非常适合事件溯源应用架构。
如果你用过 Kafka,那么对分区一定很熟悉。本文中已经多次提及分区,因为这是绕不过去的。分区是 Kafka 中的一个基本概念,非常有用。Pulsar 也支持分区,但是是可选的。
Kafka 的所有主题都是分区的。一个主题可能只有一个分区,但必须至少有一个分区。分区在 Kafka 中是很重要的,因为分区是 Kafka 并行度的基本单元。将负载分散到多个分区即可分散到多个 Broker,单个主题的处理速度就能提高。Kafka 旨在处理高吞吐量,特别是要使用商用硬件来达到这个目的,分区在其中扮演着不可或缺的角色。
自 Kafka 诞生以来,商用硬件的容量不断提升。此外运行 Kafka 的 Java 虚拟机性能也不断提升。 这种硬件和软件的提升意味着现在在商用硬件上使用单分区也可以获得良好的性能。从性能角度来看,单分区主题也足以满足很多使用场景。
然而,正如前文所述,如果你想用多个消费者读取 Kafka 主题,就不能使用单分区。因为分区是 Kafka 生产和消费并行度的基本单元。因此即便单个分区足以满足主题的输入消息速度,你也希望使用多分区,以便将来可以选择增加多个消费者。当然,你也可以在创建主题之后再增加分区,但如果使用基于 key 的分区,这将会改变哪些 key 分配给哪些分区,从而影响分区中消息的处理顺序;而且分区会消耗资源(例如 Broker 上的文件句柄、客户端的内存占用),所以增加分区绝非一个轻量操作;另外虽然可以增加主题分区,但永远不能减少主题分区数。
正因为分区是 Kafka 的基础,所以要想用好 Kafka 就必须理解分区的工作原理。在创建主题时,你就需要考虑需要(或将来可能需要)多少分区数;在连接消费者时,你需要理解消费者如何与消费者组中的分区进行交互;如果你运维一个 Kafka 集群,一切都以分区级别运行,在维护和维修时,你需要以分区为中心。
Pulsar 也支持分区,但是它们完全是可选的。事实上运行 Pulsar 时完全可以不使用分区。不分区的主题即可支持发布海量消息并支持多个消费者读取。如果你需要额外的性能,或需要基于 key 的有序消息消费,那么可以创建 Pulsar 分区主题。Pulsar 完全支持分区,其功能与 Kafka 大体相同。
Pulsar 分区被实现为一组主题的集合,用后缀来表示分区编号。例如创建一个包含三个分区的主题 mytopic
,则会自动创建三个主题,分别名为 mytopic-parition-1
、mytopic-partition-2
和 mytopic-partition-3
。生产者可以连接到主主题 mytopic
,根据生产者定义的路由规则将消息分发到分区主题。也可以直接发布到分区主题。同样地,消费者可以连接到主主题,也可以连接到一个分区主题。与 Kafka 一样,可以增加主题的分区数,但永远不能减少分区数。
由于分区在 Pulsar 中是可选的,所以 Pulsar 使用起来更加简单,尤其对于初学者来说。在 Pulsar 中你可以放心地忽略分区,除非你的使用场景需要用到分区提供的功能。这不仅简化了 Pulsar 集群的运维,也使得 Pulsar 客户端 API 更容易使用。分区是个有用的概念,不过如果你无需处理分区即可满足需求,那就有助于简化固有的复杂技术。
Kafka 以其性能而闻名,以能够在实时环境中支持海量消息而著称。比较消息系统之间的性能有点棘手,每个系统都有性能最佳点和性能盲点,很难进行公平的比较。
OpenMessaging 项目 是一个旨在公平比较消息系统之间性能的项目,它是一个 Linux 软件基金会协作项目。OpenMessaging 项目由多个消息系统供应商支持,其目标是为消息和流系统提供供应商中立和语言独立的标准。该项目包含一个性能测试框架,支持多种消息系统,包括 Kafka 和 Pulsar。
其思想是利用标准的测试框架和方法,在评估中引入一定程度的公平性。OpenMessaging 项目的所有代码都是开源的,任何人都可以运行基准测试并输出自己的结果。
对 Kafka 和 Pulsar 进行详细的性能分析已经超出了本文的范围。不过一些基于 OpenMessaging 基准测试框架的测试结果表明 Pulsar 的性能要优于 Kafka。
GigaOm 发布的一份报告显示:
为了验证其中一些结果,我使用 OpenMessaging 项目的基准框架对 Kafka 和 Pulsar 的延迟进行了一个 详细对比。在这次对比中,我得出的结论是 Pulsar 能提供更加可预测的延迟。在许多情况下,Pulsar 的延迟比 Kafka 更低,尤其是在需要强持久性保证场景下,或需要大量分区的场景下。
租户是指可以独立使用系统的用户或用户组数量。在单租户系统中,所有的资源都是共享的,因此系统用户需要知道系统的其他用户在做什么。由于资源是共享的,必然引入争用和可能的冲突。如果多个用户组使用单租户系统,那么通常需要为系统提供多个拷贝,每个用户组使用一个拷贝,以提供隔离性和隐私。
在多租户系统中,不同的用户组或租户可以独立地使用系统。每个租户都是与其他租户隔离的。系统资源被各租户割据,所以每个用户都有自己的系统私有实例。我们只需提供一套系统,但每个租户都有自己的虚拟隔离环境。多租户系统可以支持多个用户组。
消息系统是一种核心基础设施,它最终会被多个不同的团队用于不同的项目。如果为每个团队或项目都创建一个新集群,那么运维复杂度会很高,而且也不能有效地利用资源。因此,多租户在消息系统中是一个令人向往的特性。
多租户是 Pulsar 的关键设计要求。因此 Pulsar 有多种多租户特性,让单个 Puslar 系统可以支持多个团队以及多个项目。
在 Pulsar 中,每个租户有自己的虚拟消息环境,与其他租户隔离开。一个租户创建的主题也与其他租户创建的主题隔离。通常,一个租户可以被一个团队或部门的所有成员使用。每个租户可以有多个命名空间。命名空间包含一组主题。不同命名空间可以包含同名的主题。命名空间可以便捷地将特定项目中的所有主题组织到一起。
命名空间也是一种在主题之间共享策略配置的机制。举个例子,所有需要 14 天保留周期的主题可以归到同一命名空间。在命名空间上配置该保留周期策略后,该命名空间内的所有主题都将继承这个策略。
当多个租户共享同一资源时,很重要的一点是要有某种机制确保所有租户都能公平地访问。需要确保一个租户不会消耗掉所有资源,导致其他租户饥饿。
Pulsar 有多种策略确保单个租户不至于消耗掉集群里的所有资源,例如限制消息出站速率、限制未确认消息存储以及限制消息保留期。可以在命名空间级别设置这些策略,这样各个主题组可以有不同的策略。
为了让多租户更好地工作,Pulsar 支持命名空间级别的授权。这意味着你可以限制对命名空间中主题的访问,可以控制谁有权限在命名空间中创建主题,以及谁有权限生产和消费这些主题。
Kafka 是单租户系统,所有主题都属于一个全局命名空间。诸于保留周期等策略可以设置全局默认值,或者在单个主题上进行覆盖。但无法将相关主题组织到一起,也无法将策略应用到一组主题上。
关于授权,Kafka 支持访问控制列表(ACL),允许限制谁可以从主题上生产和消费。ACL 允许对集群中的授权进行细粒度的控制,可以对各种资源设置策略,比如集群、主题和消费者组;还可以指定各种特定的操作,比如创建、描述、更改和删除。除了基于用户(主体)的授权之外,还支持基于主机的授权。例如你可以允许 User:Bob
读写某个主题,但限制只能从 IP 地址 198.51.100.0 进行读写。而 Pulsar 没有这种细粒度的授权以及基于主机的限制,只支持少数几个操作(管理、生产、消费),并且不提供基于主机的授权。
尽管 Kafka 在授权控制上有更大的灵活性,但它本质上仍然是一个单租户系统。如果多个用户组使用同一个 Kafka 集群,他们需要保证主题名称不要冲突,并且 ACL 被正确应用。而多租户在 Pulsar 中是内置的,因此在不同团队和项目之间共享集群是非常简单的。
Kafka 和 Pulsar 这类系统要实现高性能,重要一点是让其中的组件互相靠近以便有较低的互相通讯时延。这意味着 Kafka 和 Pulsar 要部署在单个数据中心,组件之间由高速网络互联。当集群内一个或多个组件(计算、存储、网络)发生故障时,集群内的消息复制机制保证免受消息丢失和服务宕机之苦。在云环境中,组件可以分布到一个数据中心(区域)内的多个可用区,以防止一个可用区发生故障。
但如果整个数据中心发生故障或被隔离,那么消息系统则会发生宕机(或发生灾难时丢失数据)。如果这对你来说不可接受,那么你可以使用跨地域复制。跨地域复制是指将消息复制到远端的另一个集群,发布到数据中心的每条消息都会被自动且可靠地复制到另一个数据中心。这可以防止整个数据中心发生故障。
跨地域复制对于全球应用程序来说也非常有用,消息从世界上某个位置生产出来,并被世界上其他地方的消费者消费。通过将消息复制到远程数据中心,可以分散负载,并提高客户端响应能力。
雅虎的团队在构建 Apache Pulsar 之初,一个关键需求就是要支持在跨地域的数据中心之间复制消息,需要确保即便整个数据中心发生故障消息仍然可用。因此对于 Pulsar 来说跨地域复制是一项核心功能,完全集成到管理界面中。可以在命名空间级别开启或关闭跨地域复制功能。管理员可以轻松配置哪些主题需要复制,哪些不需要复制。甚至生产者在发布消息时可以排除某些数据中心让它不接收消息复制。
图 9. Active–standby 复制
Pulsar 的跨地域复制支持多种拓扑结构,例如主备(active-standby)、双活(active-active)、全网格(full mesh)以及边缘聚合(edge aggregation)。图 9 展示的是 active-standby 复制。所有消息都被发布到主数据中心(Data Center 1),然后被复制到备用数据中心(Data Center 2),如果主数据中心发生故障,客户端可以切换到备用数据中心。对于主备复制拓扑,Pulsar 新近引入了复制订阅(replicated subscription)功能,该功能在主备集群之间同步订阅状态,以便应用程序可以切换到备用数据中心并从中断的地方继续消费。
在主备(active–standby)复制中,客户端一次只连接到一个数据中心。而在双活(active-active)复制中,客户端连接到多个数据中心。图 10 所示的事一个全网格配置的双活复制拓扑。发布到一个数据中心的消息会被同步到其他多个数据中心。
图 11 所示的是边缘聚合拓扑(edge aggregation)。在此拓扑中,客户端连接到多个数据中心,这些数据中心将消息复制到中央数据中心进行处理。如果边缘数据中心处于客户端附近,那么即使中央数据中心离得很远,已发布的消息也能快速被确认。
图 10. Active–active, full-mesh replication
图 11. Edge aggregation
Pulsar 也可以进行同步跨地域复制。在典型的跨地域复制配置中,消息复制是异步完成的。生产者将消息发送到主数据中心后,消息即被持久化并确认回生产者;然后再被可靠地复制到远端数据中心。整个过程是异步的,因为消息在被复制到远端数据中心之前已向生产者确认。只要远端数据中心可用并且可以通过网络访问,这种异步复制就没有任何问题。然而,如果远端数据中心出现问题,或者网络连接变慢,那么已确认的消息就可能不能马上被复制到远端数据中心。如果主数据中心在消息被复制到远端数据中心之前发生故障,那么消息可能会丢失。
如果这种消息丢失对你来说不可接受,那么可以配置 Pulsar 进行同步复制。在同步复制时,消息直到被安全地存储到多个数据中心之后才会确认回生产者。由于消息要发到多数距离分散的数据中心,而数据中心之间有网络延迟,因此同步复制确认消息的时间会更长一些。不过这保证了即便整个数据中心故障也不会发生消息丢失。
Pulsar 有着丰富的跨地域复制功能,能支持几乎所有你能想到的配置。跨地域复制的配置和管理完全集成到 Pulsar 中,无需外部包也无需扩展。
Kafka 中有多种方式可以实现跨地域复制,或者像 Kafka 文档那样称之为 mirroring。Kafka 提供了一个 MirrorMaker 工具,用来在消息生产后将其从一个集群复制到其他集群。这个工具很简单,只是将一个数据中心的 Kafka 消费者与另一个数据中心的 Kafka 生产者连接起来。它不能动态配置(改变配置后需要重启),且不支持在本地和远端集群机制同步配置信息或同步订阅信息。
另一个跨地域方案是由 Uber 开发并开源的 uReplicator。Uber 之所以开发 uReplicator 是为了解决 MirrorMaker 的许多缺点,提高其性能、可扩展性和可运维性。无疑 uReplicator 是更好的 Kafka 跨地域复制方案。然而它是一个独立的分布式系统,有控制器节点和工作节点,需要与 Kafka 集群并行运维。
Kafka 中还有用于跨地域复制的其他商业解决方案,例如 Confluent Replicator。它支持双活(active-active)复制,支持在集群间同步配置,并且比 MirrorMaker 更容易运维。它依赖于 Kafka Connect,需要与 Kafka 集群并行运维。
在 Kafka 中是可以实现跨地域复制的,但做起来并不简单。必须在多个方案中做出选择,需要并行运维各种工具,甚至并行运维整个分布式系统;所以说 Kafka 跨地域复制是很复杂的,尤其与 Pulsar 内置的跨地域复制能力相比。
我们花了大量篇幅研究 Kafka 与 Pulsar 的核心技术。现在让我们放宽视野,看看围绕他们的生态系统。
Kafka 于 2011 年开源,而 Pulsar 于 2016 年开源。因此 Kafka 在社区构建和周边产品这方面具有五年的领先优势。Kafka 被广泛应用,已构建出了许多开源和商业产品。现在有多个商业 Kafka 发行版本可用,也有许多云提供商提供托管 Kafka 服务。
不仅有许多运行 Kafka 的选项,还有许多开源项目为 Kafka 提供各种客户端、工具、集成和连接器。由于 Kafka 被大型互联网公司使用,因此其中许多项目来自 Salesforce、LinkedIn、Uber 和 Shopify 这类公司。当然,Kafka 同时还有许多商业补充项目。
Kafka 知识也广为人知,因此很容易找到有关 Kafka 问题的答案。有很多博客文章、在线课程、超过 15,000 条 StackOverflow 问题、超过 500 位 GitHub 贡献者,以及有着丰富使用经验的大量专家。
Pulsar 成为开源项目的时间相对要短一些,其生态系统和社区显然还无法与 Kafka 匹敌。然而,Pulsar 从 Apache 孵化项目迅速发展为顶级项目,并且在许多社区指标上都呈现出稳步增长,例如 GitHub 贡献者、Slack 工作区成员数等。虽然 Pulsar 社区相对较小,但却热情活跃。
尽管如此,Kafka 在社区和相关项目上还是具有明显优势。
Kafka 与 Pulsar 都是 ASF 开源项目。最近有很多关于开源许可证的讨论,一些开源软件供应商已经修改了他们的许可证,以防止云提供商在某些应用里使用他们的开源项目。这种做法是开源项目之间的一个重要区别。
一些开源项目由商业公司控制,另一些由软件基金会控制,例如 ASF。开源项目可以自由更改其软件许可证。今天他们可能会使用像 Apache 2.0 或 MIT 这样的宽松许可证,但明天就可能转向使用更加严格的许可方案。如果你正在使用由商业公司控制的开源项目,就要面对该公司出于特定商业原因更改许可证的风险。如果发生这种情况,并且你的使用方式违反了新的许可,而你又想继续获得新的更新(例如安全补丁),那么你就需要找到一个友好的项目分支,或者自己维护一个分支,或者向商业公司支付许可证费用。
由软件基金会控制的开源项目不太可能更改许可。使用广泛的 Apache 2.0 许可自 2004 年就已存在。即便软件基金会确实要更改其开源项目的许可证,也不太可能改成更严格,因为大多数基金会都有授权以免费提供软件且不受限制。
当评估开源软件时,必须牢记这一区别。Kafka 是 Apache 下的一个开源项目,Kafka 生态中的许多组件虽然是开源的,但并不受 Apache 控制,例如:
Apache Pulsar 开源项目将更广泛的生态系统包含在项目之中。它将 Java、Python、Go 以及 C++ 客户端包含在主项目之中。许多连接器也是 Pulsar IO 包的一部分,例如 Aerospike、Apache Cassandra 以及 AWS Kinesis。Pulsar 自带模式注册表以及名为 Pulsar SQL 的基于 SQL 的主题查询机制。还包含仪表盘应用程序以及基于 Prometheus 的指标和告警功能。
由于所有这些组件都在 Pulsar 主项目中,并受 Apache 管理,其许可证不太可能变得更加严格。此外,只要项目整体得到积极维护,这些组件也会得到维护。社区对这些组件会定期进行测试,并在发布 Puslar 新版本之前修复不兼容性。
作为 Apache Kafka 替代品,Apache Pulsar 发展势头正劲。在本文中,我们从多个维度对比了 Kafka 和 Pulsar,总结如 [表 1]。
对比维度 | Kafka | Pulsar |
---|---|---|
架构组件 | ZooKeeper、Kafka broker | ZooKeeper、Pulsar broker、BookKeeper |
复制模型 | Leader–follower | Quorum-vote |
高性能 pub-sub 消息系统 | 支持 | 支持 |
消息重放 | 支持 | 支持 |
竞争消费者 | 有限支持 | 支持 |
传统消费模式 | 不支持 | 支持 |
日志抽象 | 单节点 | 分布式 |
多级存储 | 不支持 | 支持 |
分区 | 必选 | 可选 |
性能 | 高 | 更高 |
跨地域复制 | 由额外工具或外部系统实现 | 内置支持 |
社区及相关项目 | 大而成熟 | 小而成长 |
开源 | ASF 与其他混合 | 纯 ASF |
我们对比了这两个系统的架构以及不同的复制模型。二者都使用 Apache ZooKeeper 以及 Broker,但 Pulsar 将 Broker 分为两层:消息计算层以及消息存储层。Pulsar 使用 Apache BookKeeper 作为其存储层。这种计算和存储分离的架构,以及 Apache BookKeeper 本身的水平扩展性,使得在 Kebernetes 等云原生环境中运行 Pulsar 变得自然而然。
Kafka 和 Pulsar 都使用消息复制来实现持久性。Kafka 使用 leader-follower 复制模型,而 Pulsar 使用 quorum-vote 复制模型。
我们分析了 Kafka 和 Pulsar 都能支持的消息模式,以及只有 Pulsar 能支持的传统消息系统(例如 RabbitMQ)的消息模式。由于 Pulsar 支持 pub-sub、流式消息模式、以及传统消息系统的基于队列的模式,因此在同时运行 Kafka 和 RabbitMQ 的组织中,可以将这些系统整合为单个 Pulsar 消息系统。如果企业想要为流式系统或传统队列部署一套消息系统,那么也可以选用 Pulsar,将来如果要支持新消息模式也能完美适配。
Kafka 与 Pulsar 都建立在日志抽象之上,消息被附加到不可变日志中。在 Kafka 中,日志与 Broker 节点绑定;而在 Puslar 中,日志分布在多个 Bookie 节点中。
分区是 Kafka 中的基础概念,但对 Pulsar 来说是可选的。这意味着 Pulsar 在处理客户端 API 以及运维上比 Kafka 更简单。
Pulsar 提供 Kafka 所不具备的功能,例如多级存储、内置跨地域复制、多租户等。报告表明 Pulsar 在延迟和吞吐量方面都比 Kafka 更具性能优势。Pulsar 绝大多数开源组件都由 ASF 控制,而不受商业公司控制。
虽然 Pulsar 的生态和社区尚不能与 Kafka 匹敌,但它在很多方面比 Kafka 更有优势。鉴于这些优势,Pulsar 作为 Kafka 替代品如此势头强劲就不足为奇了。一旦更多的人意识到它的优势,Pulsar 有望继续取得发展。
感谢 Sijie Guo 给予的技术评审,感谢 Jeff Bleiel 的洞察及耐心,感谢 Jess Haberman 的热情和支持。
]]>本文翻译自 StreamNative 博客《Achieving Broker Load Balancing with Apache Pulsar》 - 译文发表于 Apache Pulsar 公众号:https://mp.weixin.qq.com/s/p9nWE_cyzYENNxEzXGXcew
In this blog, we talk about the importance of load balancing in distributed computing systems and provide a deep dive on how Pulsar handles broker load balancing. First, we’ll cover Pulsar’s topic-bundle grouping, bundle-broker ownership, and load data models. Then, we’ll walk through Pulsar’s load balancing logic with sequence diagrams that demonstrate bundle assignment, split, and shedding. By the end of this blog, you’ll understand how Pulsar dynamically balances brokers.
本文将探讨负载均衡在分布式计算系统中的重要性,并深入分析 Pulsar 处理 Broker 负载均衡的方式。首先我们介绍 Pulsar 中的 Topic-Bundle 分组、Bundle-Broker 归属关系以及负载数据模型。然后讲解 Pulsar 的负载均衡逻辑,通过时序图来展示 Bundle 的分配、拆分和缩减过程。通过本文,你将了解 Pulsar Broker 是如何做到动态均衡的。
Before we dive into the details of Pulsar’s broker load balancing, we’ll briefly discuss the challenges of distributed computing, and specifically, systems with monolithic architectures.
在深入探讨 Pulsar Broker 负载均衡的细节之前,我们先简要讨论分布式计算的挑战,特别是单体架构系统的挑战。
A key challenge of distributed computing is load balancing. Distributed systems need to evenly distribute message loads among servers to avoid overloaded servers that can malfunction and harm the performance of the cluster. Topics are naturally a good choice to partition messages because messages under the same topic (or topic partition) can be grouped and served by a single logical server. In most distributed streaming systems, including Pulsar, topics or groups of topics are considered a load-balance entity, where the systems need to evenly distribute the message load among the servers.
负载均衡是分布式计算中的一大关键挑战。分布式系统需要在服务器之间平均分配消息负载,以避免出现服务器过载,从而导致故障并损害集群性能。一个自然而然的合理选择是根据主题来对消息进行拆分,因为同一主题(或主题分区)下的消息可以组织到一起并分配给单个逻辑服务器处理。包括 Pulsar 在内的多数分布式流系统将主题或一组主题视为负载均衡的实体,系统需要在服务器之间平均分配主题或一组主题的消息负载。
Topic load balancing can be challenging when topic loads are unpredictable. When there is a load increase in certain topics, these topics must offload directly or repartition to redistribute the load to other machines. Alternatively, when machines receive low traffic or become idle, the cluster needs to rebalance to avoid wasting server resources.
当主题负载不可预测时,如何做好主题负载均衡可能会形成挑战。当某些主题的负载增加时,这些主题必须直接卸载或者重新分区,以便将负载重新分配到其他机器。另一种情况是,某些机器流量非常低,甚至空闲,集群需要重平衡来避免服务器资源浪费。
Dynamic rebalancing can be difficult in monolithic architectures, where messages are both served and persisted in the same stateful server. In monolithic streaming systems, rebalancing often involves copying messages from one server to another. Admins must carefully compute the initial topic distribution to avoid future rebalancing as much as possible. In many cases, they need careful orchestration to execute topic rebalancing.
动态重平衡在单体架构中可能会很困难,因为消息在同一个有状态的服务器中处理以及持久化。在单体流式系统中,重平衡通常涉及将消息从一台服务器复制到另一台服务器。管理员必须仔细计算初始主题分布,尽可能避免将来发生重平衡。在许多情况下,管理员需要仔细编排才能执行主题重平衡操作。
By contrast, Apache Pulsar is equipped with automatic broker load balancing that requires no admin intervention. Pulsar’s architecture separates storage and compute, making the broker-topic assignment more flexible. Pulsar brokers persist messages in the storage servers, which removes the need for Pulsar to copy messages from one broker to another when rebalancing topics among brokers. In this scenario, the new broker simply looks up the metadata store to point to the correct storage servers where the topic messages are located.
相比之下,Apache Pulsar 则实现了 Broker 的动态负载均衡,无需管理员手工干预。Pulsar 从架构上分离了存储层和计算层,可以更加灵活地分配 Broker 与主题的映射关系。Pulsar Broker 将消息持久化保存到存储服务器,当在 Broker 之间重平衡主题时,无需将消息从一个 Broker 复制到另一个 Broker。在这种情况下,新加入的 Broker 只需要查找 Metadata Store 并指向主题消息所在的正确存储服务器即可。
Let’s briefly talk about the Pulsar storage architecture to have the complete Pulsar’s scaling context here. On the storage side, topic messages are segmented into Ledgers, and these Ledgers are distributed to multiple BookKeeper servers, known as bookies. Pulsar horizontally scales its bookies to distribute as many Ledger (Segment) entities as possible.
这里简要讨论一下 Pulsar 的存储架构,以便全面地了解 Pulsar 的扩展能力。在存储层,主题消息被分割成多个 Ledger,这些 Ledger 分布到多个 BookKeeper 服务器,即 Bookie。Pulsar 通过水平扩展 Bookie,即可存储尽可能多的 Ledger(Segment)条目。
For a high write load, if all bookies are full, you could add more bookies, and the new message entries (new ledgers) will be placed on the new bookies. With this segmentation, during the storage scaling, Pulsar does not involve recopying old messages from bookies. For a high read load, because Pulsar caches messages in the brokers' memory, the read load on the bookies significantly offloads to the brokers, which are load-balanced. You can read more about Pulsar Storage architecture and scaling information in the blog post Comparing Pulsar and Kafka.
对于高写入负载,如果所有 Bookie 都已满,只需增加更多的 Bookie,新的消息条目(即新的 Ledger)即可存储到这些新的 Bookie 上。通过这种分段设计,在存储扩展期间 Pulsar 无需从 Bookie 中重新复制旧消息。对于高读取负载,Pulsar 将消息缓存在 Broker 内存中,所以 Bookie 的读负载会显著卸载到 Broker 上,而 Broker 是负载均衡的。你可以在这篇关于对比 Pulsar 与 Kafka 的博文中了解更多关于 Pulsar 存储架构以及扩展能力的信息。
From the client perspective, Pulsar topics are the basic units in which clients publish and consume messages. On the broker side, a single broker will serve all the messages for a topic from all clients. A topic can be partitioned, and partitions will be distributed to multiple brokers. You could regard a topic partition as a topic and a partitioned topic as a group of topics.
从客户端的角度来看,Pulsar 主题是客户端发布和消费消息的基本单元。在 Broker 端,单个 Broker 处理所有客户端对某个主题的所有消息请求。主题可以被分区,而分区可以分布在多个 Broker 上。你可以将主题分区视为一个主题,而将被分区的主题视为一组主题。
Because it would be inefficient for each broker to serve only one topic, brokers need to serve multiple topics simultaneously. For this multi-topic ownership, the concept of a bundle was introduced in Pulsar to represent a middle-layer group.
由于每个 Broker 只处理一个主题效率较低,所以一般 Broker 需要同时处理多个主题。对于这种多主题归属关系,Pulsar 引入了 Bundle 的概念来作为一种中间层组。
Related topics are logically grouped into a namespace, which is the administrative unit. For instance, you can set configuration policies that apply to all the topics in a namespace. Internally, a namespace is divided into shards, aka the bundles. Each of these bundles becomes an assignment unit.
在 Pulsar 中,相关的主题可以在逻辑上归到一个命名空间中。命名空间是一个管理单元,例如可以设置一套配置策略,应用到命名空间中的所有主题上。命名空间内部被分成多个分片,即 Bundle,每个 Bundle 是负载均衡的分配单元。
Pulsar uses bundles to shard topics, which will help reduce the amount of information to track. For example, Pulsar LoadManger aggregates topic load statistics, such as message rates at the bundle layer, which helps reduce the number of load samples to monitor. Also, Pulsar needs to track which broker currently serves a particular topic. With bundles, Pulsar can reduce the space needed for this ownership mapping.
Pulsar 使用 Bundle 来对主题进行分片,这有助于减少要跟踪的信息量。例如,Pulsar LoadManager 聚合了主题负载统计信息,比如 Bundle 层的消息速率,这有助于减少要监控的负载样本数量。此外,Pulsar 需要跟踪当前是哪个 Broker 服务于特定主题。得益于 Bundle,Pulsar 可以减少维护这种归属关系所需的存储空间。
Pulsar uses a hash to map topics to bundles. Here’s an example of two bundles in a namespace.
Pulsar 使用哈希算法将主题映射到 Bundle。如下是一个命名空间包含两个 Bundle 的示例。
1 2 3 |
|
Pulsar computes the hashcode given topic name by Long hashcode = hash(topicName)
. Let’s say hash(“my-topic”) = 0x0000000F
. Then Pulsar could do a binary search by NamespaceBundle getBundle(hashCode)
to which bundle the topic belongs given the bundle key ranges. In this example, “Bundle1” is the one to which “my-topic” belongs.
Pulsar 通过 Long hashcode = hash(topicName)
来计算给定主题名的哈希码。假设 hash(“my-topic”) = 0x0000000F
,在已知 Bundle Key 范围的情况下,Pulsar 可通过 NamespaceBundle getBundle(hashCode)
进行二分搜索,找到主题所属的 Bundle。在此示例中,“my-topic” 属于 “Bundle1”。
One of the advantages of Pulsar’s compute (brokers) and storage (bookies) separation is that Pulsar brokers can be stateless and horizontally scalable with dynamic bundle ownership. When brokers are overloaded, more brokers can be easily added to a cluster and redistribute bundle ownerships.
Pulsar 计算层(Broker)和存储层(Bookie)分离的一大优势是 Pulsar Broker 是无状态的,基于动态 Bundle 归属可以实现良好的水平扩展性。当 Broker 过载后,可以轻松地将更多 Broker 加入集群并重新分配 Bundle 归属关系。
To discover the current bundle-broker ownership in a given topic, Pulsar uses a server-side discovery mechanism that redirects clients to the owner brokers’ URLs. This discovery logic requires:
Pulsar 使用服务端发现机制来发现给定主题当前的 Bundle-Broker 归属关系,将客户端重定向到 Owner Broker 的 URL。这种发现逻辑需要知道:
Pulsar stores bundle ranges and ownership mapping in the metadata store, such as ZooKeeper or etcd, and the information is also cached by each broker.
Pulsar 将 Bundle 范围和归属关系存储在 Metadata Store 中,例如 ZooKeeper 或 etcd,这些信息也会缓存在每个 Broker 中。
Collecting up-to-date load information from brokers is crucial to load balancing decisions. Pulsar constantly updates the following load data in the memory cache and metadata store and replicates it to the leader broker. Based on this load data, the leader broker runs topic-broker assignment, bundle split, and unload logic:
负载均衡决策中至关重要的一点是从 Broker 端收集最新的负载信息。Pulsar 不断将以下负载数据更新到内存缓存和 Metadata Store 中,并将其复制到 Leader Broker。基于这些负载数据,Leader Broker 执行 Topic-Broker 分配、Bundle 拆分以及卸载逻辑:
In this section, we’ll walk through load balancing logic with sequence diagrams:
本节将通过时序图来展示负载均衡逻辑: - 将主题动态分配到 Broker(阅读完整文档)。 - 拆分过载的 Bundle(阅读完整文档)。 - 从过载的 Broker 中卸载 Bundle(阅读完整文档)。
Imagine a client trying to connect to a broker for a topic. The client connects to a random broker, and the broker first searches the matching bundle by the hash of the topic and its namespace bundle ranges. Then the broker checks if any broker already owns the bundle in the metadata store. If already owned, the broker redirects the client to the owner URL. Otherwise, the broker redirects the client to the leader for a broker assignment. For the assignment, the leader first filters out available brokers by the configured rules and then randomly selects one of the least loaded brokers to the bundle, as shown in Section 1 below, and returns its URL. The leader redirects the client to the returned URL, and the client connects to the assigned broker. This new broker-bundle ownership creates an ephemeral lock in the metadata store, and the lock is automatically released if the owner becomes unavailable.
假设某客户端想要读写主题,现在试图连接到一个 Broker。该客户端先会连接到一个随机的 Broker,该 Broker 首先根据主题的哈希码以及命名空间的 Bundle 范围搜索匹配的 Bundle。然后,该 Broker 会查询 Metadata Store,检查所匹配的 Bundle 是否属于某 Broker。如果已经归属,该 Broker 会将客户端重定向到 Owner URL。否则,会将客户端重定向到 Leader 以进行 Broker 分配。Broker 分配逻辑如下:Leader 首先基于配置好的规则过滤出可用的 Broker 列表,然后随机选择一个负载最少的 Broker 分配给 Bundle(如下文第一步所示),并返回该 Broker 的 URL;Leader 将客户端重定向到该 URL,客户端即可连接到分配的 Broker。新的 Broker-Bundle 归属关系会在 Metadata Store 中创建一个临时锁,一旦 Owner 不可用之后该锁会自动释放。
This step selects a broker from the filtered broker list. As a tie-breaker strategy, it uses ModularLoadManagerStrategy
(LeastLongTermMessageRate
by default). LeastLongTermMessageRate
computes brokers’ load scores and randomly selects one among the minimal scores by the following logic:
LoadBalancerBrokerOverloadedThresholdPercentage
(default 85%), then score=INF
.score = longTermMsgIn
rate and longTermMsgOut
rate.这一步从已过滤的可用 Broker 列表中选定一个 Broker,使用 ModularLoadManagerStrategy
(默认为 LeastLongTermMessageRate
)。LeastLongTermMessageRate
策略计算 Broker 的负载分数,并从分数最小的 Broker 中随机选择一个,计分规则如下:
LoadBalancerBrokerOverloadedThresholdPercentage
(默认 85%),则设置 score=INF
。longTermMsgIn
消息输入速率加上 longTermMsgOut
消息输出速率。
With the bundle load data, the leader broker identifies which bundles are overloaded beyond the threshold as shown in Section 2 below and asks the owner broker to split them. For the split, the owner broker first computes split positions, as shown in Section 3 below, and repartition the target bundles at them, as shown in Section 4 below. After the split, the owner broker updates the bundle ownerships and ranges in the metadata store. The newly split bundles can be automatically unloaded from the owner broker, configurable by the LoadBalancerAutoUnloadSplitBundlesEnabled
flag.
Leader Broker 根据 Bundle 负载数据判断哪些 Bundle 的负载超过阈值(见第二步),并要求 Owner Broker 进行 Bundle 拆分。具体的拆分逻辑如下:Owner Broker 首先计算拆分位置(见第三步),然后据此重新拆分目标 Bundle(见第四步);完成拆分之后,Owner Broker 将最新的 Bundle 归属关系和范围更新到 Metadata Store 中。如果启用了 LoadBalancerAutoUnloadSplitBundlesEnabled
,新拆分的 Bundle 可以从 Owner Broker 中自动卸载。
If the auto bundle split is enabled by loadBalancerAutoBundleSplitEnabled
(default true) configuration, the leader broker checks if any bundle’s load is beyond LoadBalancerNamespaceBundle
thresholds.
如果启用了 loadBalancerAutoBundleSplitEnabled
(默认为 true),则启用自动拆分 Bundle 功能,Leader Broker 会判断是否有 Bundle 的负载超过 LoadBalancerNamespaceBundle
配置的阈值。
1 2 3 4 5 6 |
|
If the number of bundles in the namespace is already larger than or equal to MaximumBundles
, it skips the split logic.
如果命名空间中的 Bundle 个数已经达到或超过 MaximumBundles
,则会跳过拆分逻辑。
Split operations compute the target bundle’s range boundaries to split. The bundle split boundary algorithm is configurable by supportedNamespaceBundleSplitAlgorithms
.If we have two bundle ranges in a namespace with range partitions (0x0000, 0X8000, 0xFFFF), and we are currently targeting the first bundle range (0x0000, 0x8000) to split:
接下来计算目标 Bundle 的拆分边界。Bundle 拆分边界算法可通过 supportedNamespaceBundleSplitAlgorithms
配置。假设某个命名空间有两个 Bundle 范围,范围分布是 (0x0000, 0X8000, 0xFFFF),现在要拆分第一个 Bundle 范围 (0x0000, 0x8000),可使用如下拆分算法:
RANGE_EQUALLY_DIVIDE_NAME (default): This algorithm divides the bundle into two parts with the same hash range size, for example target bundle to split=(0x0000, 0x8000) => bundle split boundary=[0x4000].
RANGE_EQUALLY_DIVIDE_NAME(默认算法):该算法将目标 Bundle 拆分为具有相同哈希范围大小的两个部分,例如要拆分的目标 Bundle 为 (0x0000, 0x8000),则拆分边界为 [0x4000]。
TOPIC_COUNT_EQUALLY_DIVIDE: It divides the bundle into two parts with the same topic count. Let’s say there are 6 topics in the target bundle [0x0000, 0x8000):hash(topic1) = 0x0000hash(topic2) = 0x0005hash(topic3) = 0x0010hash(topic4) = 0x0015hash(topic5) = 0x0020hash(topic6) = 0x0025
Here we want to split at 0x0012 to make the left and right sides of the number of topics the same. E.g. target bundle to split [0x0000, 0x8000) => bundle split boundary=[0x0012].
TOPIC_COUNT_EQUALLY_DIVIDE:该算法将目标 Bundle 拆分为具有相同主题数的两个部分。假设在目标 Bundle [0x0000, 0x8000) 中有 6 个主题:hash(topic1) = 0x0000hash(topic2) = 0x0005hash(topic3) = 0x0010hash(topic4) = 0x0015hash(topic5) = 0x0020hash(topic6) = 0x0025
这种情况会在 0x0012 处进行拆分,使左右两边的主题数相同。如果要拆分的目标 Bundle 为 [0x0000, 0x8000),则拆分边界为 [0x0012]。
Example:Given bundle partitions [0x0000, 0x8000, 0xFFFF], splitBoundaries: [0x4000]Bundle partitions after split = [0x0000, 0x4000, 0x8000, 0xFFFF]Bundles ranges after split = [[0x0000, 0x4000),[0x4000, 0x8000), [0x8000, 0xFFFF]]
示例:给定 Bundle 分区为 [0x0000, 0x8000, 0xFFFF],拆分边界为 [0x4000]。拆分后的 Bundle 分布为 [0x0000, 0x4000, 0x8000, 0xFFFF]。拆分后的 Bundle 范围为 [[0x0000, 0x4000), [0x4000, 0x8000), [0x8000, 0xFFFF]]。
With the broker load information collected from all brokers, the leader broker identifies which brokers are overloaded and triggers bundle unload operations, with the objective of rebalancing the traffic throughout the cluster.
Leader Broker 根据从所有 Broker 中收集的负载信息,识别出哪些 Broker 已经过载,并触发 Bundle 卸载操作,目的是为了重平衡整个集群的流量。
Using the default ThresholdShedder
strategy, the leader broker computes the average of the maximal resource usage among CPU, memory, and network IO. After that, the leader finds brokers whose load is higher than the average-based threshold, as shown in Section 5 below. If identified, the leader asks the overloaded brokers to unload some bundles of topics, starting from the high throughput ones, enough to bring the broker load to below the critical threshold.For the unloading request, the owner broker removes the target bundles’ ownerships in the metadata store and closes the client topic connections. Then the clients reinitiate the broker discovery mechanism. Eventually, the leader assigns less-loaded brokers to the unloaded bundles and the clients connect to them.
Leader Broker 默认使用 ThresholdShedder
策略,计算 CPU、内存以及网络 IO 之间最大资源使用率的平均值。之后,Leader 找到那些负载高于此基于平均值阈值的 Broker(见第五步)。找到过载的 Broker 之后,Leader 要求它们从高吞吐量的主题开始卸载一些主题 Bundle,直至将 Broker 负载降低到临界阈值以下。收到卸载请求后,Owner Broker 从 Metadata Store 中移除目标 Bundle 的归属信息,并关闭客户端的主题连接。然后客户端重新启动 Broker 发现机制。最终,Leader 将负载较少的 Broker 分配给被卸载的 Bundle,客户端则连接到新的 Broker。
It first computes the average resource usage of all brokers using the following formula.
ThresholdShedder 首先使用如下公式计算出所有 Broker 的平均资源使用率。
1 2 3 4 5 6 7 8 9 10 11 |
|
If any broker’s usage is bigger than avgUsage + y, it is considered an overloaded broker.
loadBalancerResourceWeight
configurations.loadBalancerHistoryResourcePercentage
. By default, it is 0.9, which weighs the previous usage more than the latest.avgUsage
buffer y is configurable by loadBalancerBrokerThresholdShedderPercentage
, which is 10% by default.如果 Broker 的资源使用率大于 avgUsage + y,则被认为过载。
loadBalancerResourceWeight
进行配置。loadBalancerHistoryResourcePercentage
进行配置。其默认值是 0.9,历史使用率比最近使用率的权重更大。avgUsage
缓冲值 y 可通过 loadBalancerBrokerThresholdShedderPercentage
进行配置,默认值是 10%。In this blog, we reviewed the Pulsar broker load balance logic focusing on its sequence. Here are the broker load balance behaviors that I found important in this review.
在本博文中,我们回顾了 Pulsar Broker 的负载均衡逻辑,重点关注其时序图。我认为 Broker 负载均衡行为有如下几个要点。
本文翻译自 StreamNative 博客《What the FLiP is the FLiP Stack?》
- 原文链接:https://streamnative.io/blog/engineering/2022-04-14-what-the-flip-is-the-flip-stack/
- 译文发表于 Apache Pulsar 公众号:https://mp.weixin.qq.com/s/KoigahiZ2sTG-GhD2fG0Fw
In this article on the FLiP Stack, we will explain how to build a real-time event driven application using the latest open source frameworks. We will walk through building a Python IoT application utilizing Apache Pulsar, Apache Flink, Apache Spark, and Apache NiFi. You will see how quickly we can build applications for a plethora of use cases. The easy, fast, scalable way: The FLiP Way.
本文将介绍 FLiP 技术栈,我们将解释如何使用最新的开源框架构建实时事件驱动应用程序,并介绍如何通过 Apache Pulsar、Apache Flink、Apache Spark 和 Apache NiFi 构建一个 Python IoT 应用。得益于 FLiP 的简单、快速、可扩展的特性,使用 FLiP 可以快速地为各种场景构建应用程序。
The FLiP Stack is a number of open source technologies that work well together. FLiP is a best practice pattern for building a variety of streaming data applications. The projects in the stack are dictated by the needs of that use case; the available technologies in the builder’s current stack; and the desired end results. As we shall see, there are several variations of the FLiP stack built upon the base of Apache Flink and Apache Pulsar.
FLiP 技术栈由许多可协同工作的开源技术组成,是构建各种流数据应用程序的最佳实践模式。FLiP 技术栈包含哪些项目并不是固定的,而是由特定场景的需求、团队当前掌握的技术栈、以及期望的最终结果决定。建立在 Apache Flink 和 Apache Pulsar 基础上的 FLiP 技术栈有很多变体。
For some use cases like log analytics, you will need a nice dashboard for visualizing, aggregating, and querying your log data. For that one you would most likely want something like FLiPEN, as an enhancement to the ELK Stack. As you can tell, FLiP+ is a moving list of acronyms for open source projects that are commonly used together.
例如对于日志分析这种场景,通常需要清晰直观的仪表板来可视化、聚合并查询日志数据。对于这种场景,你可能需要像 FLiPEN 这样的技术,作为对 ELK 技术栈 的增强。由此可以看出,FLiP+ 是一个可变的缩写,表示多种配合使用的开源项目。
With so many variations of the FLiP stack, it might be difficult to figure out which one is right for you. Therefore, we have provided some general guidelines for selecting the proper FLiP+ stack to use based on your use case. We already mentioned Log Analytics, which is a common use case. There are many more, driven usually by data sources and data sinks.
由于 FLiP 技术栈的变体非常多,所以可能很难确定哪一种适合你的场景。因此,我们提供了一些通用指南,你可以根据不同的场景选择合适的 FLiP+ 技术栈。上文提到的日志分析是一种常用场景,当然还有其他更多的场景,通常由数据 source 和 sink 驱动。
场景(数据) | 技术栈 | 演示 |
---|---|---|
点击流 | FLiP-C | https://github.com/tspannhw/FLiP-Stream2Clickhouse |
IoT | FLiP-I | https://github.com/tspannhw/FLiP-InfluxDB |
CDC | FLiPS | https://github.com/tspannhw/FLiPS-SparkOnPulsar |
统一消息 | FLiP* | https://github.com/tspannhw/FLiPN-Demos |
Lakehouse,云数据湖 | FLiP | https://github.com/tspannhw/FLiP-CloudIngest |
移动应用 | FLiP-M | https://github.com/tspannhw/FLiP-Mobile |
微服务 | FLiP | https://streamnative.io/blog/engineering/2021-12-14-developing-event-driven-microservices-with-apache-pulsar/ |
SQL on Topics | FLiPiT | https://github.com/tspannhw/FLiP-Into-Trino |
边缘 AI | FLip-EdgeAI | https://github.com/tspannhw/FLiP-EdgeAI |
ETL | FLiPS | https://github.com/tspannhw/FLiPS-SparkOnPulsar |
搜索 | FLiPEN | https://github.com/tspannhw/FLiP-Elastic |
Python 应用 | FLiP-Py | https://github.com/tspannhw/FLiP-Py-Pi-GasThermal |
A critical component of the FLiP stack is utilizing Apache Flink as a stream processing engine against Apache Pulsar data. This is enabled by the Pulsar-Flink Connector that enables developers to build Flink applications natively and stream in events from Pulsar at scale as they happen. This allows for use cases such as streaming ELT and continuous SQL on joined topic streams. SQL is the language of business that can drive event-driven, real-time applications by writing simple SQL queries against Pulsar streams with Flink SQL, including aggregation and joins.
FLiP 技术栈的一个关键组件是利用 Apache Flink 作为流式处理引擎来处理 Apache Pulsar 数据。这是基于 Pulsar-Flink 连接器实现的,开发人员可以构建原生的 Flink 应用,并在事件发生时从 Pulsar 大规模地流式传输事件,适用于流式 ELT 以及在主题流上持续执行 SQL 等场景。SQL 是一种业务语言,可以通过使用 Flink SQL 编写针对 Pulsar 流的简单 SQL 查询(包括聚合和连接)来实现事件驱动的实时应用程序。
The connector builds an elastic data processing platform enabled by Apache Pulsar and Apache Flink that is seamlessly integrated to allow full read and write access to all Pulsar messages at any scale. As a citizen data engineer or analyst you can focus on building business logic without concern about where the data is coming from or how it is stored.Check out the resources below to learn more about this connector:
Pulsar-Flink 连接器构建了一个弹性数据处理平台,通过无缝集成 Apache Pulsar 和 Apache Flink 允许以任何规模对 Pulsar 消息进行完全读写访问。作为数据工程师或数据分析师,你可以专注于业务逻辑,而无需担心数据来源以及存储。可以通过如下资源学习更多关于 Pulsar-Flink 连接器的知识:
If you have been following our blog, you have seen the recent formal announcement of the Apache Pulsar processor for Apache NiFi release. We now have an official way to consume and produce messages from any Pulsar topic with the low code streaming tool that is Apache NiFi.
近期,StreamNative 与 Cloudera 宣布推出 Apache Pulsar + Apache NiFi 联合解决方案。现在我们官方支持利用 Apache NiFi 这种低代码流式工具从任何 Pulsar 主题中消费和生产消息。
This integration allows us to build a real-time data processing and analytics platform for all types of rich data pipelines. This is the keystone connector for the democratization of streaming application development.
利用 NiFi-Pulsar 集成,我们可以为任何数据管道构建实时数据处理和分析平台。这是流式应用程序开发平民化的关键连接器。
Read the articles below to learn more:
若要了解更多信息,可以阅读如下文章:
[Blog] Cloudera and StreamNative Announce the Integration of Apache NiFi and Apache Pulsar [博客] StreamNative 与 Cloudera 宣布推出 Apache Pulsar + Apache NiFi 联合解决方案
[Blog] Producing and Consuming Pulsar messages with Apache NiFi [博客] Producing and Consuming Pulsar messages with Apache NiFi
[Datanami Article] Code for Pulsar, NiFi Tie-Up Now Open Source [Datanami 文章] Code for Pulsar, NiFi Tie-Up Now Open Source
[Resources] Pulsar and NiFi Integration Resources [资源] Pulsar and NiFi Integration Resources
Now that you have seen the combinations, use cases, and the basic integration, we can walk through an example FLiP Stack application. In this example, we will be ingesting sensor data from a device running a Python Pulsar application.
上文介绍了 FLiP 的技术组合、使用场景以及基本集成,现在我们来看一个 FLiP 技术栈应用的示例。在此示例中,我们从一个运行 Python Pulsar 程序的设备中采集传感数据。
2GB 内存的 Raspberry Pi 4
Pimoroni BreakoutGarden Hat
Sensiron SGP30 TVOC 及 eCO2 传感器
In this application, we want to monitor the air quality in an office continuously and then hand off a large amount of data to a data scientist to make predictions. Once that model is done, we will add that model to a Pulsar function for live anomaly detection to alert office occupants of the situation. We will also want dashboards to monitor trends, aggregates and advanced analytics.
在这个示例程序中,我们希望持续监测办公室的空气质量,然后将大量数据交给数据科学家进行预测。一旦该模型完成,我们会将其添加到 Puslar Function 中进行实时异常检测,发送告警给办公室人员。我们还需要仪表盘来监控趋势、进行聚合和高级分析。
Once the initial prototype proves itself, we will deploy it to all the remote offices for monitoring internal air quality. For future enhancements, we will ingest outside air quality data as well local weather conditions.
一旦初始的原型证明可用,我们将部署到所有远程办公室以监测内部空气质量。未来我们将持续改进,采集外部空气质量数据以及当地天气状况。
On our edge devices, we will perform the following three steps to collect the sensor readings, format the data into the desired schema, and forward the records to Pulsar.
我们的客户端设备执行如下三个步骤来收集传感器读数,将数据格式化为期望的 schema,并将记录发送到 Pulsar。
result = sgp30.get_air_quality()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1
|
|
Now that we have built the edge-to-pulsar ingestion pipeline, let’s do something interesting with the sensor data that we have published to Pulsar.
现在我们构建了从边缘设备到 Pulsar 的数据采集管道,接下来我们对发布到 Pulsar 的传感器数据做一些有意思的处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 |
|
1
|
|
We could have used a Pulsar Function and Pulsar IO Sink for MongoDB instead, but you may want to do other data enrichment with Apache NiFi without coding.
我们本可以为 MongoDB 使用 Pulsar Function 和 Pulsar IO Sink,但使用 Apache NiFi 无需编码就能完成数据丰富。
1 2 |
|
1
|
|
(https://streamnative.io/blog/engineering/2022-04-14-what-the-flip-is-the-flip-stack/)
In this blog, we explained how to build real-time event driven applications utilizing the latest open source frameworks together as FLiP Stack applications. So now you know what we are talking about when we say “FLiPN Stack”. By using the latest and greatest open source Apache streaming and big data projects together, we can build applications faster, easier, and with known scalable results.
本文介绍了如何利用最新的开源框架组成 FLiP 技术栈,来构建实时事件驱动应用程序。通过使用最新的优秀的开源 Apache 流处理和大数据项目,我们可以更快、更轻松、更可扩展地构建应用程序。
Join us in building scalable applications today with Pulsar and its awesome friends. Start with data, route it through Pulsar, transform it to meet your analytic needs, and stream it to every corner of your enterprise. Dashboards, live reports, applications, and machine learning analytics driven by fast data at scale built by citizen data engineers in hours, not months. Let’s get these FLiPN applications built now.
欢迎大家使用 Pulsar 及其他上下游生态中出色的工具来构建可扩展的应用程序。从数据开始,通过 Pulsar 进行路由,对其进行转换以满足分析需求,并将其流式传输到企业的每个角落。无需数月的时间,数据工程师即可在数小时内构建出大规模快速数据驱动的仪表板、实时报告、应用程序以及机器学习分析数据。现在就开始构建这些 FLiPN 应用程序吧。
本文翻译自 《Apache BookKeeper Insights Part 1 — External Consensus and Dynamic Membership》,作者 Jack Vanlightly
- 原文链接:https://medium.com/splunk-maas/apache-bookkeeper-insights-part-1-external-consensus-and-dynamic-membership-c259f388da21
- 译文发表于 Apache Pulsar 公众号:https://mp.weixin.qq.com/s/i2CzmL8k2EKbjxNlW0OG6w
The BookKeeper replication protocol is pretty interesting, it’s quite different to other replication protocols that people are used to in the messaging space such as Raft (RabbitMQ Quorum Queues, Red Panda, NATS Streaming) or the Apache Kafka replication protocol. But being different means that people often don’t understand it fully and can either get tripped up when it behaves in a way they don’t expect or not use it to its full potential.
BookKeeper 复制协议非常有趣,它与人们在消息领域习惯使用的其他复制协议大不相同,例如 RabbitMQ Quorum Queues、Red Panda 及 NATS Streaming 使用的 Raft 协议和 Apache Kafka 使用的复制协议。但与众不同意味着人们往往无法完全掌握 BookKeeper 的各项玩法,例如当 BookKeeper 的运行方式不符合预期时不知如何解决,又或是没能充分利用 BookKeeper 的各项优势功能。
This series aims to help people understand some fundamental insights into what makes BookKeeper different and also dig into some of the nuances of the protocol. We’ll dig into the “why” the protocol is the way it is and also some of the ramifications of those design decisions.
本系列文章旨在帮助大家了解那些使 BookKeeper 与众不同的一些基本见解,详细分析该协议的一些细微差别。我们将深入研究 BookKeeper 复制协议背后的设计考量,以及这些设计决策所带来的结果。
One of the best ways I know of how to describe design decisions is via comparison. Comparing one thing against another is a great way to discuss trade-offs, weak/strong points and many other aspects. I’m going to use both Raft and Apache Kafka as comparison points. I am not going to try to persuade you that BookKeeper is better than other protocols, this is not a thinly veiled marketing piece. This post is about teaching the mechanics of the BookKeeper protocol and ramifications.
对比是描述设计决策的最好方法之一,可以让我们很好地讨论权衡、优缺点等等方面。本系列文章将同时使用 Raft 和 Apache Kafka 作为比较对象,但本系列并不旨在宣传或说服用户 BookKeeper 比其他协议更好,而是要探讨 BookKeeper 协议的机制及其衍生的结果。
Also note that this is not an in-depth look at Raft or Kafka. I will be describing enough of those protocols for my aims, but will be glossing over large amounts of complexity. If you want to understand Raft and Apache Kafka more, the protocols are well documented elsewhere.
另外请注意,本系列文章旨在通过对比更好地帮助大家理解 BookKeeper,而不是对 Raft 或 Kafka 进行深入研究。我们将有的放矢地尽量多地介绍这些协议,但也会跳过大量复杂的细节。如果想深入学习 Raft 和 Apache Kafka,可以参考其协议文档。
This first post describes the biggest difference between BookKeeper and other replication protocols. This difference informs most of the later posts on the nuances of the protocol also.
本系列的第一篇文章会为大家介绍 BookKeeper 和其他复制协议之间的最大区别,帮助大家理解本系列后续文章中会谈及的协议间的细微差别。
Raft is an “integrated” protocol. What I mean by that is that the control plane and data plane are both integrated into the same protocol and that protocol is exercised by the storage nodes which are all equal peers. Each node has all the data locally stored in persistent storage.
Raft 是一个“集成”协议。这里的集成是指控制平面和数据平面都集成到同一个协议中,并且该协议由所有对等的存储节点执行。每个节点都将所有数据存储在本地持久化存储中。
The same is true of Apache Kafka albeit with the use of ZooKeeper for metadata, though this will be removed soon (KIP-500).
Apache Kafka 也是如此。之前通过 ZooKeeper 存储 Kafka 的元数据,尽管新版本可在不需要 ZooKeeper 的情况下运行 Kafka (KIP-500),但也将依赖于 ZooKeeper 的控制器改造成了基于 Kafka Raft 的 Quorum 控制器。
With Raft, we have a steady state where replication is being performed, then periods of perturbation which trigger elections. Once a leader is elected, the leader node handles all client requests and replication of entries to followers.
Raft 在稳定状态时会执行数据复制,一旦出现扰动会触发选举。选举出 Leader 后,Leader 节点会处理所有客户端请求并将 Entry 复制到 Follower。
With Raft the leader learns where in the log each follower is and starts replicating data to each follower according to their position. Because the leader has all the state locally, it can retrieve that state and transmit it, no matter how far behind a follower is.
在 Raft 中,Leader 可以了解每个 Follower 在日志中的位置,并根据这些位置将数据复制到每个 Follower。由于 Leader 在本地拥有所有状态,因此无论 Follower 落后多远,它都可以检索到 Follower 当前的状态并将其后的数据传输给 Follower。
Fig 1. Integrated replication protocols where the replication is performed by stateful nodes that host the data.
图 1. 集成复制协议,存储数据的有状态节点执行数据复制
With Kafka, the followers send fetch requests to the leader, the requests include their current position. The leader, having all the state locally, simply retrieves the next data and sends it back to the follower.
在 Kafka 中,Follower 向 Leader 发送 fetch 请求,请求中包含它们的当前位置。Leader 因为在本地拥有所有状态,所以只需在本地检索下一个数据并将其发送回 Follower 即可。
A byproduct of replication being performed by stateful fully integrated nodes is that cluster membership is relatively static. Yes you can perform cluster operations to add and remove members, but these are very infrequent operations with limits. The membership of a Raft cluster and the replicas that form a Kafka topic can be considered fixed in terms of the normal operation of the protocol.
有状态的完全集成节点执行复制时,集群成员相对静态。虽然用户可以执行集群操作来添加和移除成员,但这些操作不常发生且有限制。当协议正常运行时,可以认为 Raft 集群的成员和构成 Kafka 主题的副本没有发生变化。
BookKeeper is different. It has the consensus and storage separated. The storage nodes are simple and basically store and retrieve what they are told to. They understand almost nothing of the replication protocol itself. The replication protocol is external to the storage nodes and lives in the BookKeeper client. It is the client that performs the replication to the storage nodes.
BookKeeper 则不同,它将共识和存储分离开来。存储节点很简单,基本上仅存储和检索它们被告知的内容。这些节点本身与复制协议几乎毫无关联——复制协议位于存储节点的外部,封装在 BookKeeper 客户端中,由客户端将数据复制到存储节点。
Fig 2. The client performs replication
图 2. 客户端执行复制
BookKeeper was designed to act as the distributed log storage sub-system of another data system, like a messaging system or database, for example Apache Pulsar. Pulsar brokers use BookKeeper to store topics and cursors and each broker uses the BookKeeper client to do the reading and writing to those BookKeeper nodes.
BookKeeper 旨在充当其他数据系统的分布式日志存储子系统,这个其他数据系统可以是消息系统或数据库。例如 Apache Pulsar 使用 BookKeeper 来存储主题和游标,每个 Broker 通过 BookKeeper 客户端从 BookKeeper 节点读取和写入数据。
The client is external and stateless which has a number of cascading effects that inform the design of the rest of the protocol. For example, because the client doesn’t have the full state locally, it needs to treat failure differently.
客户端在存储节点外部且无状态,这会对协议其余部分的设计产生一些级联影响。例如,因为客户端在本地没有完整的状态,所以对待失败的处理方式就会不一样。
With Raft, if one node becomes unavailable for an hour we don’t have a big problem. When the node comes back, the stateful leader will simply resume replication to the follower from where it left off. The BookKeeper client doesn’t have that luxury, if it wants to continue to make progress, it can’t be storing the last X hours of data in memory, it must do something differently.
一个 Raft 节点宕机一个小时,也不会产生太大问题。当节点重新上线时,有状态的 Leader 很容易就能从该 Follower 离开的位置恢复复制数据。BookKeeper 客户端则没有这种功能。如果想恢复复制数据,它不可能将最后 X 小时的数据都存储在内存中,所以必须另寻他法。
Because the replication and coordination logic lives externally of the storage nodes (in the client), the client is free to change the membership of a ledger when failure occurs. This dynamic membership is a fundamental feature differentiator and is one of BookKeeper’s most compelling features.
因为复制和协调逻辑存在于存储节点的外部(即客户端中),所以客户端可以在发生故障时自由更改 Ledger 成员。这种动态成员机制是一个根本上区别于其他协议的功能,也是 BookKeeper 最引人注目的功能之一。
A data system like Pulsar having a separate storage layer has its downsides, like an extra network hop before any data hits disk and having to operate a separate cluster of bookies. If BookKeeper didn’t offer some truly valuable features, then it would become more of a liability than an asset. Luckily for us, BookKeeper has many wonderful features that make it worth it.
像 Pulsar 这样具有单独存储层的数据系统也有自身的缺陷,例如任何数据在到达磁盘之前需要额外的网络请求,并且必须运维单独的 Bookie 集群。BookKeeper 需要提供一些真正有价值的功能才能为 Pulsar 加码。幸运的是,BookKeeper 具有许多值得使用它的出色功能。
Now that we’ve set the scene, we’ll dig further in to explore how an integrated, fixed membership protocol like Raft compares to an external consensus, dynamic membership protocol like BookKeeper.
现在我们已经为大家搭建了认知,在后文中将进一步深入探讨如何将像 Raft 这样集成的、固定成员的协议与像 BookKeeper 这样外部共识、动态成员的协议进行比较。
Each of our three protocols all have the concept of a commit index, though they have different names. A commit index is an offset in the log where all entries at that point and before will survive a certain agreed number of node failures.
本文提到的三个协议每个都有提交索引(Commit Index)的概念,不过名称各不相同。提交索引是日志中的一个偏移量,在该偏移量及其之前的所有 Entry 都能在一定数量的节点故障中幸存下来。
In each case, an entry must reach a certain replication factor to be considered committed:
对每个协议来说,Entry 都必须达到特定的复制因子才能被视为已提交:
acks=all
以及 Broker 配置 min-insync-replicas=[majority]
支持多数仲裁确认机制。默认情况下,它只需要 Leader 持久化 Entry 即可确认该 Entry。NOTE: Because each protocol is different I will refer to the quorum that is required for an entry to be considered “committed” as the Commit Quorum. This is my own invented term for this post.
注意:由于每个协议都不同,我将 Entry 被认定为“已提交”所需的仲裁数称为提交仲裁数(Commit Quorum)。这是我自己为这篇文章发明的术语。
Raft calls this point in the log the commit index, Kafka calls it the High Watermark and BookKeeper calls it the Last Add Confirmed (LAC). Each protocol relies on this commit index to deliver its consistency guarantees.
Raft 称日志中的这一点为提交索引(Commit Index),Kafka 称其为高水位(High Watermark),BookKeeper 称其为最后添加确认(Last Add Confirmed,缩写 LAC)。每个协议都依赖于这个提交索引来提供一致性保证。
In Raft and Kafka this commit index is transmitted between the leader and followers and so each node will have its own current knowledge of what the commit index is. The leader always knows the fully up to date value of the commit index whereas the followers may have a stale value, but that is ok.
在 Raft 和 Kafka 中,这个提交索引会在 Leader 和 Follower 之间传输,因此每个节点自己都保存了一份当前提交索引的值。Leader 总是知道提交索引的最新值,而 Follower 的值可能滞后,但这没关系。
Fig 3. All nodes have their own view, sometimes stale, of the current commit index.
图 3. 所有节点都有自己的当前(有时数据滞后)提交索引的信息
With Kafka, the leader includes the High Watermark in its fetch responses to followers.
在 Kafka 中,Leader 在其对 Follower 的 fetch 响应中包含高水位的信息。
With BookKeeper, the LAC is included with every entry that is sent to the storage nodes. The storage nodes themselves have little use for it, but it allows clients to retrieve this vital information at a later point. So the client that is writing a ledger knows the current LAC and the storage nodes may have a slightly stale view of the LAC, but this is also fine and the protocol handles that. More on this later.
在 BookKeeper 中,LAC 包含在发送到各存储节点的每个 Entry 中。存储节点本身几乎不会用到 LAC,但客户端之后可以检索此重要信息。因此,正在写入 Ledger 的客户端知道当前的 LAC,而存储节点的 LAC 信息可能有滞后,但这也没问题,复制协议会处理这种情况。稍后会详细介绍。
Fig 4. The client knows the current LAC and the bookies have a usually slightly stale view of it.
图 4. 客户端可检索当前最新的 LAC,而 Bookie 的 LAC 信息往往会滞后
Reads that go past the commit index would be dirty reads where there are no guarantees that you’d be able to read the same entry again. Entries beyond the commit index could be lost or could be replaced with a different entry. For that reason each of the protocols don’t allow readers to read past this point.
超过提交索引的读取是脏读,无法保证能够再次读取到相同的 Entry。提交索引之后的 Entry 可能会丢失或被不同的 Entry 替换。出于这个原因,各个协议都不允许读取超过提交索引的数据。
With a Raft based system, you’ll specify your replication factor and that will translate into a Raft cluster of that many Raft members. With Kafka, that translates into a topic with that many replicas.
对于基于 Raft 的系统,需要指定复制因子,这决定了 Raft 集群中有多少个成员。Kafka 也需要指定复制因子,这决定了主题有多少个副本。
Raft members and Kafka replicas are fixed in terms of the steady state replication. One cost of this fixed membership is the tension between replication factor, availability and latency.In an ideal world we’d want each entry to be fully replicated before being acknowledged. But followers can go down or be slow. Having a cluster become unavailable for writes because a single node becomes unavailable is not acceptable to most people with good reason. So the compromise is to reduce safety a little in order to gain availability and lower latency. We allow a minority of members to be unavailable and still offer good data safety and continued availability.
稳定状态下执行数据复制时,Raft 成员和 Kafka 副本固定不变。成员固定所带来的一个后果是复制因子、可用性和延迟之间的冲突。理想情况下,我们希望每个 Entry 在确认前都被完全复制。但是 Follower 可能宕机或者变慢,而大多数人都无法接受仅仅因为单个节点不可用就导致整个集群不可写。为此采取的折中办法是,略微降低安全性以获得较好的可用性和低延迟。在允许少数成员不可用的情况下仍然提供良好的数据安全性和持续的可用性。
That is why Raft and Kafka really need a commit quorum that is lower than the replication factor.
这就是为什么 Raft 和 Kafka 需要一个低于复制因子的提交仲裁(Commit Quorum)。
This reduction in safety can be mitigated by simply increasing the replication factor. So if you want guarantees that committed entries will survive the loss of 2 nodes, then you’d need a replication factor of 5. You pay more for storage and network and also latency takes a small hit, but you only need the fastest 2 of your 4 followers to confirm an entry in order to acknowledge the entry to the client. So even with 2 slow nodes, you have acceptable latency and you reach your minimum rep factor that you are comfortable with.
安全性的降低通过增加复制因子即可缓解。如果想保证已提交的 Entry 在丢失 2 个节点后仍然存活,那么需要指定复制因子为 5。这会增加存储和网络开销,并稍微影响延迟,不过只需要 4 个 Follower 中最快的那 2 个确认 Entry 即可返回确认给客户端。因此即便有 2 个慢速节点,延迟也是可接受的,同时复制因子也达到最小。
An invariant is something that must be true at all times. You can look at the state of a system at any time and verify that its state conforms to the invariant. For example, an invariant may state that no committed entries are lost.
首先介绍一下不变量(Invariant)和活性属性(Liveness Property)的概念。不变量是指在任何时候都必须为 true 的事情。通过随时查看系统状态可以验证其状态是不是不变量。例如,一个不变量可以是任何已提交 Entry 都不会丢失。
Liveness properties tell us what must happen at some point, for example, eventually a leader must be elected given that a majority of nodes are eventually functional and can see each other.
活性属性则告诉我们在某个时刻一定会发生什么。例如,假定多数节点最终都能正常工作并对彼此可见,那么最终一定能选出 Leader。
Our integrated log replication protocols have, among others, the invariants:
本文讨论的集成日志复制协议至少具有以下不变量:
One liveness property is that given all nodes are functional and have visibility of each other then eventually, any given committed entry will become fully replicated (as long as the prefix of the log is also fully replicated). In other words, the log tail will eventually reach the desired replication factor.
集成日志复制协议的一个活性属性是,假定所有节点都正常运行并对彼此可见,那么最终任何给定的已提交 Entry 都将被完全复制(前提是在此之前的日志也被完全复制)。换言之,日志尾部最终会达到所需的复制因子。
Fig 5. The three safety zones of a Raft or Kafka log
图 5. Raft 和 Kafka 日志的三种安全区域
We can think of a replicated Raft log as being split into 3 zones of safety. At the head, beyond the committed index we’re in the danger zone, these entries have no guarantees and may be lost. Then the committed prefix of the log can be split into the head that reaches the majority quorum but not fully replicated yet and the tail that is fully replicated.
我们可以认为 Raft 日志能分成 3 种安全区域。其一是“未提交区域(Uncommitted)”,指处于日志头部、在已提交索引之外的危险区域,这里的 Entry 没任何保证,可能会丢失。剩下两个区域),分别是已达到多数仲裁但尚未完全复制的“已提交头部(Committed Head)”,以及已完全复制的“已提交尾部(Committed Tail)”。
1
|
|
The rule is that for any given offset in the log, the prefix from that point must have reached the same or higher replication factor and the suffix after that point must have reached the same or lower replication factor.
对于任意给定的日志偏移量,该点之前的必定达到相同或者更高的复制因子,该点之后的必定达到相同或更低的复制因子。
What does all this mean for the administrator?
那么这一切对管理员意味着什么呢?
When everything is going well, we’d expect a small uncommitted zone, a small committed head and a very large committed tail. But things don’t always go well and the committed head/tail can be of arbitrary length — the tail could be 0 length meaning no there are no fully replicated entries. This could happen because a follower is too slow (and past data retention) or it could mean a follower just died catastrophically and started up empty.
当一切运行顺利时,预计未提交区域很小,已提交头部很小,而已提交尾部则非常大。然而事情并不总是顺利,已提交头部/尾部可能是任意长度——尾部长度可能为 0,意味着没有完全复制的条目。这可能是因为 Follower 太慢(并超过了数据保留期),或者 Follower 刚刚灾难性地宕机并重启。
The point is that the replication factor is not a guarantee but a desired goal. The only guarantee is the commit quorum. So the commit quorum is the minimum guaranteed replication factor. As an administrator, you need to plan your procedures and planning around that value, not just the replication factor. Hence why some people run Raft and Kafka with rep factors of 5.
这里的关键点在于复制因子并不是一个保证,而是一个期望的目标。唯一能保证的是提交仲裁。所以说提交仲裁是最小能保证的复制因子。作为管理员,需要围绕该值而不仅是复制因子来规划流程和计划。这就是为什么有些人使用复制因子 5 来运行 Raft 和 Kafka。
Systems that use integrated replication protocols make recovery from total disk failure “relatively” simple. Any empty follower can be refilled from the current leader in exactly the same way as a follower that is mostly caught up. Replication saves the day.
使用集成复制协议的系统可以“相对”简单地从整个磁盘故障中恢复。空的 Follower 可以通过当前 Leader 重新填入数据,这个过程和大多数追赶的 Follower 完全相同。数据复制保证了能从故障中成功恢复。
All these properties make reasoning about the state of a Raft/Kafka log relatively simple:
上述这些特性使得对 Raft/Kafka 日志状态的推理变得相对简单:
Now let’s take the same look at BookKeeper.
现在让我们同样来看看 BookKeeper。
BookKeeper has similar configurations for the desired replication factor and for the commit quorum.
BookKeeper 也有类似的配置来表示期望的复制因子以及提交仲裁。
NOTE: I will assume that the Ensemble Size is equal to our Write Quorum as striping lowers read performance and makes it not worthwhile in practice. 注意:本文假设 Ensemble Size 和 Write Quorum 相等,因为条带化会降低读取性能,在实践中不值得采用。
Write Quorum (WQ) is our replication factor and Ack Quorum (AQ) is our commit quorum. Most people simply set Ack Quorum to be the majority quorum, so with a Write Quorum of 3, the Ack Quorum is set to 2. It would be reasonable to expect that using the quorum values of WQ=3 and AQ=2 would translate to the same behaviour as Raft or Kafka.
Write Quorum(WQ)是 BookKeeper 的复制因子,Ack Quorum(AQ)则是 BookKeeper 的提交仲裁。大多数人简单地将 Ack Quorum 设置为多数仲裁,如果 Write Quorum 为 3,则 Ack Quorum 设为 2。因此可以合理地预期在 BookKeeper 内设置 WQ = 3 且 AQ = 2 的话,其行为与 Raft 或 Kafka 相同。
The answer is WQ and AQ do not map onto their equivalents in Raft or Kafka and to understand why we need to look more closely at the protocol with its external consensus and dynamic membership.
然而实际上是 WQ 和 AQ 在 Raft 或 Kafka 中并没有对等概念。想要理解其原因,我们需要更仔细地研究外部共识和动态成员协议。
The replication and consensus logic lives in the client. The client is stateless, it cannot keep data in memory for any arbitrary length of time until a bookie becomes available. So it stays nimble and simply selects a new bookie to replace the one that it cannot write to and continues on its way. This dynamic membership change is called an ensemble change.
BookKeeper 的复制和共识逻辑封装在客户端内。而客户端是无状态的,在 Bookie 可用之前,它无法将数据保存在内存中(无论保存多久都不行)。因此它灵活简单地选择一个新 Bookie 来替代无法写入数据的 Bookie,然后继续工作。这种动态的成员变化称为 Ensemble Change。
Fig 6. The client performs an ensemble change after a write failure to bookie3.
图 6. 客户端写入 bookie3 失败后,执行一次 Ensemble Change
This ensemble change is basically an operation to update the ledger metadata in ZooKeeper as well as resending all uncommitted entries to the new bookie.
这种 Ensemble Change 基本上就是对 ZooKeeper 中 Ledger 的元数据进行一次更新操作,并将所有未提交的 Entry 重新发送到新 Bookie 上。
The result of these ensemble changes is that a ledger can be considered a series of mini-logs (we’ll call them fragments) that constitute a larger log. Each fragment has a range of contiguous entries where each entry shares the same bookies (it’s ensemble). Each time a write to a bookie fails, the client does an ensemble change and carries on, creating a ledger that is formed from 1 or more fragments.
Ensemble Change 的结果是,Ledger 可以被认为是由一系列 mini-log(我们称之为 Fragment,即片段)构成的更大的日志。每个片段都有一系列连续的 Entry,其中每个 Entry 都共享相同的 Bookie(即 Ensemble)。每当客户端写入 Bookie 失败时,都会进行一次 Ensemble Change,并继续写入。这就创建出了由一个或多个片段组成的 Ledger。
Fig 7. A ledger made of 4 fragments.
图 7. 一个由 4 个片段组成的 Ledger。
If we were to look at each individual fragment, we’d see a similar pattern to a Raft log or Kafka topic partition. The current fragment can be split into the same three zones: committed tail, committed head and uncommitted zone.
如果细究每个片段,就会看到类似 Raft 日志和 Kafka 主题分区的模式。当前片段也可以被分为与其相同的三个区域:已提交尾部、已提交头部和未提交区域。
Fig 8. The three safety zones of an active fragment.
图 8. 活跃片段的三个安全区域
When an ensemble change occurs, the current fragment terminates at the end of the committed head (those entries that have reached Ack Quorum). The new fragment starts at the beginning of the uncommitted zone.
当发生 Ensemble Change 时,当前片段终止于已提交头部的末尾(即那些已经达到 Ack Quorum 的 Entry)。新片段则开始于未提交区域的开头。
Fig 9. An ensemble change moves uncommitted entries to the next fragment.
图 9. Ensemble Change 将未提交 Entry 移动到下一个片段
This leaves non-active fragments with entries that can remain at the Ack Quorum. Unlike Raft or Kafka, the core BookKeeper replication protocol will not eventually replicate those AQ entries in order to reach WQ — they will remain at Ack Quorum. Those entries can only be brought up to WQ via the use of a separate recovery process but that process is not part of the core protocol (and by default runs daily if enabled).This means that a ledger could look like this:
这样非活跃片段就会包含保留在 Ack Quorum 中的 Entry。与 Raft 或 Kafka 不同,BookKeeper 核心复制协议不会最终复制这些 AQ Entry 以达到 WQ——它们将保留在 Ack Quorum 中。这些 Entry 只能通过单独的恢复过程进入 WQ,而这个恢复过程并不是核心协议的一部分(如果启用恢复过程,则默认情况下每天运行)。Ledger 可能如下所示:
Fig 10. Ensemble changes only move uncommitted entries into the next fragment, leaving committed entries in their original fragment.
图 10. Ensemble Change 只将未提交 Entry 移动到下一个片段,而将已提交 Entry 保留在原始片段
This means that not only the head of a ledger may see entries at AQ, there can be multiple sections at this lower replication factor.
不仅 Ledger 头部有 AQ Entry,Ledger 其他部分也会有这种低于复制因子的 Entry。
Fig 11. Ensemble changes leave AQ replicated blocks mid-ledger.
图 11. Ensemble Change 将 AQ 复制块留在 Ledger 中间
The fact that sections in the middle of a ledger can remain at AQ is a surprise to many. Most people probably expect a Raft/Kafka-like pattern where only the head sees this.Is it important to note that Raft and Kafka logs can have arbitrarily long committed heads where entries have only reached the commit quorum but not replication factor. So whether you are an administrator of Kafka or BookKeeper, the fact is that the commit quorum is what counts.
与 Raft/Kafka 只有头部有 AQ Entry 的模式不同,BookKeeper Ledger 中间的部分可能包含 AQ Entry。需要注意的是 Raft 和 Kafka 日志可能有任意长度的已提交头部,其中的 Entry 仅达到提交仲裁但未达到复制因子。所以无论是对 Kafka 还是 BookKeeper 管理员来说,提交仲裁才是最重要的。
The fact that BookKeeper uses an external replicator (the client) makes a big difference to our choice of commit quorum. Essentially the Ack Quorum isn’t really like it’s equivalents in Raft and Kafka.
BookKeeper 使用外部复制机(即客户端),因此选择提交仲裁时有很大不同。本质上,BookKeeper Ack Quorum 与 Raft 和 Kafka 中的 Ack Quorum 并不真的相似。
As discussed earlier, because Raft and Kafka have fixed membership they really need a commit quorum that is lower than the replication factor or else suffer big availability and latency issues. The commit quorum is the compromise between safety and availability/latency.
如前所述,由于 Raft 和 Kafka 是固定成员的,所以它们确实需要一个低于复制因子的提交仲裁,否则会有严重的可用性和延迟问题。提交仲裁是在安全性和可用性/延迟之间的一个折中办法。
A BookKeeper ledger is different though, it does not have fixed membership. If one bookie becomes unavailable, we swap it out for another and continue. This makes the Ack Quorum not equal to the Raft majority quorum or Kafka’s configured quorum.
然而 BookKeeper Ledger 则不同,它并没有固定成员。如果一个 Bookie 不可用,我们会将其换成另外一个并继续。这使得 Ack Quorum 并不等同于 Raft 的多数仲裁或者 Kafka 中配置的仲裁。
With BookKeeper we can set the commit quorum to be equal to the replication factor, i.e WQ=AQ. If we set WQ=3, AQ=3 and one bookie is down, we select a new bookie and carry on. Notice that when WQ=AQ we don’t have the three zones of committed head/tail and uncommitted. It’s either fully replicated and committed or not.
对于 BookKeeper,我们可以将提交仲裁设置为等于复制因子,即 WQ = AQ。如果我们设置 WQ = 3、AQ = 3,一个 Bookie 宕机后可以选择一个新 Bookie 继续。请注意当 WQ = AQ 时,没有已提交头部、已提交尾部及未提交等三个区域,当前状态下的 Entry 或是完全复制并提交,或是完全未提交。
Fig 12. With WQ=AQ, either entries are fully replicated or not committed. Ensemble changes leave the original fragment in a fully replicated state.
图 12. 当 WQ = AQ 时,Entry 要么完全复制,要么完全未提交。Ensemble Change 后原始片段处于完全复制状态
This also means we don’t have sections in the middle of a ledger at a lower rep factor anymore. The entire tail reaches WQ.
这也意味着 Ledger 中间不存在低于复制因子的部分,整个日志尾部都达到了 WQ。
In terms of data safety this is great. BookKeeper doesn’t need a majority quorum to offer high availability, we can tell BooKeeper to only acknowledge fully replicated entries.
这样很大地保证了数据安全。BookKeeper 不需要靠多数仲裁即可保证高可用,我们可以让 BookKeeper 只确认那些完全复制的 Entry。
There are of course some limits and impacts that need to be considered before you switch your AQ from a majority quorum to your replication factor.
当然,在将 AQ 从多数仲裁切换到复制因子之前,需要考虑一些限制和影响。
Firstly, using WQ=AQ without loss of availability only applies when you have enough bookies. If you only have 3 bookies and use WQ=3, then you have a fixed membership like Raft. If you have 4 bookies then as soon as one bookie is down, you’re down to 3 again and fixed membership. So you would want to have many more than 3, opting for more smaller bookies than fewer large ones. If you have 5 bookies or less you may still want the wriggle room that AQ<WQ gives you.
首先,使用 WQ = AQ 同时又不损失可用性仅适用于 Bookie 足够多的场景。如果只有 3 个Bookie 并且设置 WQ = 3,那么就跟 Raft 一样是固定成员。如果有 4 个 Bookie,那么一旦一个 Bookie 宕机,就会再次降到 3 并成为固定成员。所以 Bookie 数量要远大于 3,建议选择更多的小 Bookie,而不是更少的大 Bookie。如果 Bookie 数量小于或等于 5 个,那么最好还是设置 AQ < WQ 以提供回旋余地。
Availability % does take a small hit when using WQ=AQ as availability now also depends on operations to ZooKeeper succeeding. As soon as a write to a bookie fails, we must be able to complete an ensemble change in order to be able to resume and get entries acknowledged.However, I consider that we’re already in that boat anyway. Ledgers are small, bounded logs unlike Raft and Kafka’s theoretical infinite logs. Ledgers act as log segments and so they are getting created and closed constantly, and this requires successful metadata operations, so you cannot go for long without metadata changes in any case.
其次,设置 WQ = AQ 时,可用性会受到少许影响,因为可用性还取决于随后对 ZooKeeper 的操作。一旦写入 Bookie 失败,我们必须能够完成一次 Ensemble Change,以便可以恢复并确认 Entry。不同于 Raft 和 Kafka 理论上无上限的日志,Ledger 是小而有界的日志。Ledger 由日志片段组成,会不断地被创建和关闭(需要成功地操作元数据),所以如果不更改元数据,则无法长久运行。
Write latency will have more variance as ensemble changes will cause more write latency. Ensemble changes are normally extremely fast but if ZooKeeper is under heavy load then it could be possible for slow ensemble changes to cause write latency spikes. So if having constant low latency is very important then you’ll likely want to stick with AQ being a majority quorum.
最后,因为 Ensemble Change 会增加写入延迟,所以写入延迟的变化会更大。通常情况下 Ensemble Change 会非常快,但如果 ZooKeeper 负载很大,那么 Ensemble Change 则可能变慢,从而导致写入延迟出现毛刺。因此,如果对延迟要求非常高的话,那么就需要将 AQ 设为多数仲裁。
Why can’t we have Raft clusters of 2 members? Because a single node going down makes the cluster unable to make progress. We still get redundancy but we get worse availability than a single node. Likewise with Kafka, we can either offer a rep factor 1 or of 3 but not 2. To guarantee a rep factor of 2 you need to set min-insync-replicas=2. So if one replica goes down, we have the same issue as Raft.
为什么 Raft 集群的成员不能是 2 个?因为在这种情况下,单个节点宕机会导致整个集群无法工作。虽然有冗余,但却比单节点的可用性更差。与 Kafka 类似,我们可以设置复制因子为 1 或者 3,但不能是 2。为了保证复制因子为 2,需要设置 min-insync-replicas = 2。因此当一个副本宕机,就会遇到和 Raft 一样的问题。
But with BookKeeper, we can use a rep factor 2 without an issue. We simply set WQ=2 and AQ=2. We get redundancy and also don’t lose availability if a single node goes down. That’s pretty neat.
然而在 BookKeeper 内可以将复制因子设为 2。简单地设置 WQ = 2 且 AQ = 2,既能获得冗余又不会在单个节点宕机的情况下失去可用性。
In this first post we’ve focused on BookKeeper’s external consensus and dynamic ledger membership and how that contrasts to a more traditional fully integrated protocol like Raft and Apache Kafka with fixed membership.
这是本系列的第一篇文章,重点介绍了 BookKeeper 的外部共识和动态 Ledger 成员,并将其与 Raft 和 Apache Kafka 这种固定成员的传统完全集成协议进行对比。
We’ve seen that BookKeeper’s dynamic membership allows it to side step the usual compromise between safety and availability/latency. Where conservative configurations with Raft might choose a rep factor of 5 to ensure it can survive the loss of 2 nodes, with BookKeeper we can achieve similar results with only a replication factor of 3. We can even choose WQ=4, AQ=3 to allow us to reduce the extra latency from slow ensemble changes. You have a bit more freedom than you think when setting your Write Quorum and Ack Quorum.
BookKeeper 的动态成员机制让它不需要降低安全性和可用性/延迟任一性能。如果保守地配置 Raft 的话,可能会设置复制因子为 5,来保证 2 个节点宕机时仍能存活;对于 BookKeeper 来说,设置复制因子为 3 就能达到类似结果。甚至可以选择 WQ = 4、AQ = 3 来减少慢速 Ensemble Change 带来的额外延迟。在设置 Write Quorum 和 Ack Quorum 时,BookKeeper 的用户自由度非常大。
We also saw that when AQ < WQ you may have blocks in the middle of your ledger that only reach AQ replication, which can surprise people. In a later post we’ll look at potential tweaks to the protocol that could change this behaviour and why it might not be worth it or even safe.
当 AQ < WQ 时,Ledger 中间可能包含只达到 AQ 复制的 Entry,这让人们感到惊讶。在后续文章中我们将看到可以通过调整协议来改变这种行为,以及为什么它可能不值得甚至不安全。
This is by no means the end of the ways that BookKeeper differs from integrated protocols like Raft and Kafka. There are many more things to consider when trying to understand the BookKeeper replication protocol in detail.
BookKeeper 与 Raft 和 Kafka 等集成协议的区别绝不仅仅是这些。要详细理解 BookKeeper 复制协议的话需要考虑更多。
]]>Finally, as with everything, it’s all about trade-offs. Integrated protocols make different trade-offs to BookKeeper and neither is “the best” and this post or even this series is not an attempt to do a This Versus That comparison. The comparison is there as a vehicle for learning. 最后,本文乃至本系列文章的目的都不是试图进行两两比较来评判系统的优劣。其他集成协议与 BookKeeper 只不过是有不同的权衡与侧重,不能单纯地评估二者优劣。本文中的对比仅为以更易懂的方式普及概念。
本文译自 StreamNatvie 官方发布的《2022 Pulsar 性能测试报告》
As we move into 2022, the Apache PulsarTM versus Apache KafkaⓇ debate continues. Organizations often make comparisons based on features, capabilities, size of the community, and a number of other metrics of varying importance. This report focuses purely on comparing the technical performance based on benchmark tests.
进入 2022 年,人们对 Apache PulsarTM 与 Apache KafkaⓇ 的争论仍在持续。大家通常会对比二者的特性、能力、社区规模以及其他一系列重要性各异的指标。本报告则侧重于基于基准测试对比二者的技术性能。
The last widely published Pulsar versus Kafka benchmark was performed in 2020, and a lot has happened since then. In 2021, Pulsar ranked as a Top 5 Apache Software Foundation project and surpassed Apache Kafka in monthly active contributors as shown in the chart below. Pulsar also averaged more monthly active contributors than Kafka for most of the past 18 months.
上一次广泛发布的 Pulsar 与 Kafka 基准测试是在 2020 年进行的,而此后发生了很多事情。2021 年,Pulsar 被评为 [Apache 软件基金会 Top 5 活跃提交项目],并在月度活跃贡献者数目上力压 Apache Kafka,如下图所示。在过去 18 个月中,大部分时间 Pulsar 的平均月度活跃贡献者都超越了 Kafka。
Figure 1: Pulsar vs. Kafka Monthly Active Contributors 图 1:Pulsar 与 Kafka 的月度活跃贡献者对比
These contributions led to major performance improvements for Pulsar. To measure the impact of the improvements, the engineering team at StreamNative, led by Matteo Merli, one of the original creators of Apache Pulsar, Apache Pulsar PMC Chairperson, performed a benchmark study using the Linux Foundation Open Messaging benchmark.
这些贡献为 Pulsar 带来了重大的性能改进。为了衡量这些改进的影响,由 Apache Pulsar 创始人之一、Apache Pulsar PMC 主席 Matteo Merli 领导的 StreamNative 工程师团队使用 Linux Foundation Open Messaging 基准进行了基准测试研究。
The team measured Pulsar performance in terms of throughput and latency, and then performed the same tests on Kafka. We’ve included the testing framework and details below and encourage anyone who is interested in validating the tests to do so.
该团队在吞吐量和延迟方面对 Pulsar 的性能进行了测试,同时对 Kafka 进行了相同的测试。下文中包含了测试框架及相关详细信息,欢迎有兴趣的伙伴验证这些测试。
Let’s take a look at three key findings before jumping into the full results.
在介绍完整的测试结果之前,让我们先看看三个主要发现。
2.5x 比 Kafka 更高的最大吞吐量 |
Pulsar 能够达到 Kafka 2.5 倍的最大吞吐量。这对于导入并处理大量数据的场景来说是一个巨大优势,例如日志分析、网络安全和传感器数据收集。更高的吞吐量意味着只需要更少的硬件,从而降低运营成本。 |
---|---|
100x 比 Kafka 更低的个位数的发布延迟 |
Pulsar 提供稳定的个位数的发布延迟,这比 Kafka 的 P99.99 (ms) 延迟低 100 倍。低发布延迟是很重要的,它使得系统能够快速将消息发布到消息总线。一旦消息发布成功,数据即能安全地进行各种“处理”。 |
1.5x 比 Kafka 更快的历史读取速率 |
Pulsar 的历史读取速率比 Kafka 快 1.5 倍,因此使用 Pulsar 作为消息系统的应用可以在意外中断后只需一半的时间即可追上数据。对于数据库迁移/复制这类将数据传输到记录系统的场景来说,读取吞吐量至关重要。 |
Below we provide details on how the benchmark was performed and its results.
下面介绍本次基准测试是如何执行的,以及测试结果相关的详细信息。
Using the Linux Foundation Open Messaging benchmark [1], we ran the latest versions of Apache Pulsar (2.9.1) and Apache Kafka (3.0.0). To ensure an objective baseline comparison, each test in this Benchmark Report compares Kafka to Pulsar in two scenarios: Pulsar with Journaling and Pulsar without Journaling.
我们使用 Linux Foundation Open Messaging 基准 [1],运行(测试期间)最新版本的 Apache Pulsar (2.9.1) 以及 Apache Kafka (3.0.0)。为了确保客观的基线对比,本基准报告中的每个测试都从如下两个设定对比 Kafka 与 Pulsar:启用 Journaling 情况下的 Pulsar,以及禁用 Journaling 情况下的 Pulsar。
Pulsar’s default configuration includes Journaling, which offers a higher durability guarantee than Kafka’s default configuration. Pulsar without Journaling provides the same durability guarantees as the default Kafka configuration, which results in an apples-to-apples comparison.
Pulsar 默认配置是启用 Journaling 的,这提供了比 Kafka 默认配置更高的持久性保证。而禁用 Journaling 的 Pulsar 则提供了与 Kafka 默认配置相同的持久性保证,这使得对比更公平。
For this benchmark, we selected a handful of tests to represent common patterns in the messaging and streaming domains and to test the limits of each system:
本次基准测试我们选择了能代表消息领域和流处理领域常见模式的几种测试场景,并测试每个系统的极限:
This test measures the maximum data throughput the system can deliver when consumers are keeping up with the incoming traffic.
本测试测量当消费者跟上传入流量的情况下系统可提供的最大数据吞吐量。
We ran this test in two scenarios to test the upper boundary performance and to test the cost profile for each system:
我们在如下两个前提设定下执行了该测试,用以测试各个系统的上限性能以及成本概况:
Topic with a single partition. This scenario tests the upper boundary performance for a total-order use case or, in the worst case, where partition keys’ data is skewed. At some scale, the design of a system that relies upon single ordering or handling large amounts of skewed data will need to be reconsidered. Pulsar has the ability to handle situations where total ordering is required at higher scale or large amounts of skew arise. 单分区主题。本设定用于测试全局有序场景的上限性能,或在最坏情况下当分区键数据倾斜时的上限性能。如果系统依赖全局有序或者处理大量倾斜数据,那么在某种程度上其设计需要重新考虑。Pulsar 有能力处理要求全局以更高规模排序、或者存在大量数据倾斜的场景。
Topic with 100 partitions. With more partitions to stress available resources, this test illustrates how well a system scales horizontally (by adding more machines) and its cost effectiveness. For example, by modeling the hardware cost per 1GB/s of traffic, it is easy to derive the cost profile for each system. 100 个分区的主题。更多的分区意味着需要更多的可用资源,本测试说明了系统水平扩展的能力 (通过增加更多机器) 及其成本效益。例如,通过对每 1GB/s 流量的硬件成本进行建模,很容易能得出每个系统的成本概况。
For this test, we set a fixed rate for the incoming traffic and measured the publish latency profile. Publish latency begins at the moment when a producer tries to publish a message and ends at the moment when it receives confirmation from the brokers that the message is stored and replicated.
在本测试中,我们设置了固定速率的传入流量并测量发布延迟。发布延迟是指从生产者尝试发布一条消息开始,到它接收到 Broker 确认该消息已被存储并复制为止的时间段。
In many real-world applications, it is required to guarantee a certain latency SLA (service-level agreement). In particular, this is true in cases where the message is published as the result of some user interaction, or when the user is waiting for the confirmation.
在许多现实生产环境的应用中,都要求保证一定的延迟 SLA (服务级别协议)。特别是在当消息发布是由某些用户交互触发的,或者需要用户等待确认的情况下。
One of the primary purposes of a messaging bus is to act as a “buffer” between different applications or systems. When the consumers are not available, or when there are not enough of them, the system accumulates the data.
消息总线的一个主要目的是充当不同应用或系统之间的”缓冲区“。当消费者不可用,或消费者数量不足时,消息系统能够积压数据。
In these situations, the system must be able to let the consumers drain the backlog of accumulated data and catch up with the newly produced data as fast as possible.
在这些情况下,消息系统必须能够让消费者尽快耗尽积压数据,并尽快追赶上新生产的数据。
While this catch-up is happening, it is important that there is no impact on the performance of existing producers (in terms of throughput and latency) on the same topic or in other topics that are present in the cluster.
当发生这种数据追赶时,重要的是不影响同一主题或同一集群其他主题的现有生产者的性能(吞吐量及延迟)。
In all the tests, producers and consumers are always running from a dedicated pool of nodes, and all messages contain a 1KB payload. Additionally, in each test, both Pulsar and Kafka are configured to provide two guaranteed copies of each message.
在所有测试中,生产者和消费者总是在专用的节点池上运行,所有消息都包含 1KB 的负载。此外,在每个测试中,Pulsar 和 Kafka 都配置为为每条消息提供两份副本。
Note: Pulsar also supports message queuing, complex routing, individual and negative acknowledgments, delayed message delivery, and dead-letter-queues (features not available in Kafka). This benchmark does not evaluate these features.
注意:相比 Kafka,Pulsar 还额外支持消息队列、复杂路由、单条确认、否定确认、延迟消息传递和死信队列等 Kafka 中不可用的功能。本次基准测试并不评估这些功能。
The benchmark uses the Linux Foundation Open Messaging Benchmark suite [1]. You can find all deployments, configurations, and workloads in the Open Messaging Benchmark Github repo [2].
本基准测试使用 Linux Foundation Open Messaging 基准测试套件[1]。您可以在 Open Messaging Benchmark Github 代码库 [2] 中找到所有的部署、配置和负载。
The testbed for the OpenMessaging Benchmark is set up as follows:
OpenMessaging 基准测试平台配置如下:
We tested two configurations for Pulsar:
我们测试了 Pulsar 的两种配置:
We configured Apache Pulsar 2.9.1 to run with the 3/3/2 persistence policy, which writes entries to 3 storage nodes and waits for 2 confirmations. We are deploying 1 broker and 1 bookie for each of the 3 VMs we are using.
我们以 3/3/2 持久化策略运行 Apache Pulsar 2.9.1,将 Entry 写入 3 个存储节点,并等待 2 个节点的确认。我们在 3 个虚拟机上均部署了 1 个 Broker 以及 1 个 Bookie。
We used Apache Kafka 3.0.0 and the configuration recommended by Confluent in its fork of the OpenMessaging benchmark.
我们使用 Apache Kafka 3.0.0 并使用 Confluent 在其 OpenMessaging 基准分支中推荐的配置。
Details on the Kafka configurations include:
Kafka 的详细配置信息如下:
Note: For both Kafka and Pulsar, the clients were configured to use ZGC to get lower GC pause time.
注意:对于 Kafka 和 Pulsar,客户端都配置使用 ZGC 来获得更低的 GC 停顿时间。
This test measures the maximum “sustainable throughput” reachable on a topic. Eg: The max throughput that is able to push from producers through consumers, without accumulating any backlog.
本测试测量在一个主题上可达到的最大“可持续吞吐量”。亦即在不积压 Backlog 的情况下,能够从生产者推送到消费者的最大吞吐量。
This first test uses a topic with a single partition to establish the boundary for ingesting data in a totally ordered way. This is common in all the use case scenarios where a single history of all the events in a precise order is required, such as “change data capture” or event sourcing.
Driver files: pulsar.yaml, kafka-throughput.yaml
Workload file: max-rate-1-topic-1-partition-4p-1c-1kb.yaml
第一个测试用例使用单分区主题,测量以完全有序方式消费数据的吞吐量上限。这个设定在那些要求按精确顺序记录所有事件的单一历史记录的场景中很常见,例如“变更数据捕获”(CDC)或事件溯源等场景。
驱动文件:pulsar.yaml, kafka-throughput.yaml
负载文件:max-rate-1-topic-1-partition-4p-1c-1kb.yaml
禁用 Journaling 情况下的 Apache Pulsar | 启用 Journaling 情况下的 Apache Pulsar (默认) | Apache Kafka | |
---|---|---|---|
吞吐量 (MB/s) | 700 | 580 | 280 |
Figure 2: Single partition max write throughput (MB/s): Higher is better.
图 2:单分区的最大写入吞吐量 (MB/s):数值越高,代表性能越好
The difference in throughput between Pulsar and Kafka reflects how efficiently each system is able to “pipeline” data across the different components from producers to brokers, and then the data replication protocol of each system.
Pulsar 与 Kafka 之间的吞吐量差异反映了这两个系统如何有效地将数据跨不同组件从生产者 “传输” 到 Broker,也反映了这两个系统数据复制协议的不同。
Pulsar achieves a throughput of 700 MB/s and 580 MB/s, respectively, on the single partitions, compared to Kafka’s 280 MB/s. This is possible because the Pulsar client library combines messages into batches when sending them to the brokers. The brokers then pipeline data to the storage nodes.
Pulsar 在单分区上的吞吐量分别达到了 700 MB/s 和 580 MB/s,而 Kafka 在单分区上的吞吐量为 280 MB/s。这是因为 Pulsar 客户端库在发送消息到 Broker 之前会将多条消息合并成批量消息。然后 Broker 再将数据传输到存储节点。
In Kafka, two factors impose a bottleneck on the maximum achievable throughput: (1) the producer default limit of 5 maximum outstanding batches; and (2) the producer buffer size (batch.size=1048576) recommended by Confluent for high throughput.
在 Kafka 中,有两个因素妨碍了最大可达吞吐量:(1) 生产者默认限制为 5 个最大未完成批次;(2) Confluent 为实现高吞吐量而推荐的生产者缓冲区大小为 (batch.size=1048576) 。
Note: Increasing the batch.size setting has negative effects on the latency. This is not the case for Pulsar producers, where the batching latency is controlled by the batchingMaxDelay()
setting, in addition to the batch max size.
注意:在 Kafka 中增加 batch.size 参数会对延迟产生负面影响。而 Pulsar 生产者则不然,其批处理延迟除了受批次最大大小影响,还受 batchingMaxDelay()
控制。
With the increase in single topic throughput, Pulsar provides developers and architects more options in how they build their system. Teams can worry less about finding optimal partition keys and focus instead on mapping their data into streams.
随着单主题吞吐量的增加,Pulsar 为开发人员和架构师提供了更多的构建系统的选择。团队不必操心寻找最佳分区键,而可以专注于将数据映射到流中。
Most use cases that involve a significant amount of real-time data use partitioning to avoid the bottleneck of a single node. Partitioning is a way for messaging systems to divide a single topic into smaller chunks that can be assigned to different brokers.
多数涉及大量实时数据的场景都会使用分区来避免单个节点的瓶颈。分区是消息系统将单个主题拆分为较小块的一种方式,这些块可以分配给不同的 Broker。
Given that we tested on a 3-nodes cluster, we used 100 partitions to maximize the throughput of the system across the nodes. There is no advantage to using a higher number of partitions on this cluster because the partitions are handled independently and spread uniformly across the available brokers.
鉴于我们在一个 3 节点集群上做测试,我们使用 100 个分区来最大化系统跨节点的吞吐量。在这个集群上使用更多的分区并没有任何好处,因为分区是独立处理的,并在可用 Broker 中均匀分布。
驱动文件:pulsar.yaml, kafka-throughput.yaml
负载文件:1-topic-100-partitions-1kb-4p-4c-2000k.yaml
禁用 Journaling 情况下的 Apache Pulsar | 启用 Journaling 情况下的 Apache Pulsar (默认) | Apache Kafka | |
---|---|---|---|
吞吐量 (MB/s) | 1600 | 800 | 1087 |
Figure 3: 100 partitions max write throughput (MB/s): Higher is better.
图 3: 100 分区的最大写入吞吐量 (MB/s): 数值越高,代表性能越好
Pulsar without Journaling achieves a throughput of 1600 (MB/s), Kafka achieves a throughput of 1087 (MB/s) and Pulsar with Journaling (Default) achieves a throughput of 800 (MB/s). At equivalent durability guarantees Pulsar is able to outperform Kafka in terms of maximum write throughput. The difference in performance stems from how Kafka implements access to the disk. Kafka stores data for each partition in different directories and files, resulting in more files open for writing and scattering the IO operations across the disk. This increases the stress and contention on the OS page caching system that Kafka relies on.
禁用 Journaling 情况下 Pulsar 取得了 1600(MB/s) 的吞吐量,Kafka 取得了 1087 (MB/s) 的吞吐量,而默认启用 Journaling 情况下 Pulsar 的吞吐量是 800 (MB/s) 。在同等持久性保证的情况下,Pulsar 在最大写入吞吐量方面能够胜过 Kafka。这个性能上的差异源于 Kafka 实现对磁盘的访问的方式。Kafka 将每个分区的数据存储在不同的目录和文件中,导致需要打开更多的文件用于写入,并将 IO 操作分散到各个磁盘。这增加了 Kafka 所依赖的操作系统页缓存的压力和争用。
When reading a file, the OS tries to cache blocks of data in the available system RAM. When the data is not available in the OS cache, the thread is blocked while the data is read from the disk and pulled in the cache.
读取文件时,操作系统尝试将数据块缓存到可用的系统 RAM 中。当数据在操作系统缓存中不可用时,则会从磁盘中读取数据并拉入缓存,此时线程会被阻塞。
The cost of pulling the blocked data into the cache is a significant delay (~100s of milliseconds) in serving write/read requests for other topics. This delay is observed in the benchmark results in the form of the publish latency experienced by the producers.
将阻塞数据拉入缓存的代价是在为其他主题处理写入/读取请求时会产生显著的延迟 (约 100 毫秒)。在基准测试结果中,这种延迟表现为生产者的发布延迟。
In the case of the default Pulsar deployment (with a journal for strong durability), the throughput is lower because 1 disk (out of 2 available in the VMs) is dedicated to the journal. Therefore we are capping the available IO bandwidth. In a production environment, this cap could be mitigated by having more disks to increase the IOPS/node capacity, but for this benchmark we used the same VM resources for each of the system configurations.
在 Pulsar 的默认部署下(启用 Journal 实现强持久性),吞吐量低于 Kafka,因为虚拟机中两块可用磁盘中的一个会专用于 Journal。因此可用 IO 带宽被限制了。在生产环境中,可以通过增加磁盘来增加每个节点的 IOPS 性能,从而缓解这个限制。但在本基准测试中,我们则为每个系统使用相同的虚拟机资源配置。
The difference in throughput can impact the cost of the solution. With parity of guarantees, this test shows that Pulsar would require 32% less hardware compared to Kafka for the same amount of traffic.
吞吐量的差异会影响解决方案的成本。我们的测试表明,在同等配置、同样的流量的情况下,Pulsar 比 Kafka 减少 32% 的硬件需求。
The purpose of this test is to measure the latency perceived by the producers at a steady state, with a fixed publish rate.
本测试的目的是测量生产者在稳定状态下以固定速率发布时所感知到的延迟。
Messaging systems are often used in applications where data must efficiently and reliably be moved from a producing application to be durably stored in the messaging system. In high volume scenarios, even momentary increases in latency can result in memory resources being exhausted. In other situations, a human user may be “in-the-loop” and waiting on an operation which publishes a message - for example, a web page needs the confirmation of the action before proceeding - and latency spikes can degrade the user experience. In these use cases, it is important to have a latency performance profile that is consistently within a given SLA (service-level agreement).
有些应用要求数据必须能从生产应用程序有效且可靠地移动到消息系统以便持久化存储起来,这也是消息系统的常见使用场景。在大容量前提下,即便只是延迟的瞬时增加也会导致内存资源耗尽。在其他情况下,桌面用户可能处于“循环中”,并等待发送消息的操作执行结束,例如一个网页需要等待确认才能继续下一步操作,而延迟的增加会损害用户体验。在这些场景中,将延迟性能稳定地保持在给定的 SLA(服务级别协议)之内是非常重要的。
It is also important to consider that a high latency in the long tail (eg: 99.9 percentile and above) will still have an outsized impact over an SLA that can be offered by an application. In practical terms, a higher 99.9% latency in the producer will often result in a significantly higher 99% latency for the application request.
同样重要的是要考虑到长尾高延迟(例如 99.9 百分位及以上的延迟)也会对应用可提供的 SLA 产生巨大影响。在实践中,更高的生产者 99.9% 延迟通常会导致应用程序请求的延迟明显高于 99.9% 。
Because the messaging bus sits at the bottom of the stack, it needs to provide a low and consistent latency profile so that applications can provide their own latency SLAs.This test is conducted by publishing and consuming at a fixed rate of 500 MB/s and comparing it to the publish latency seen by producers.
由于消息总线是底层技术栈组件,所以它需要提供稳定的低延迟,这样应用程序才好提供自己的延迟 SLA。本测试以 500 MB/s 的固定速率发布和消费消息,然后比较生产者观测到的发布延迟。
驱动文件:pulsar.yaml, kafka-latency.yaml
负载文件:1-topic-100-partitions-1kb-4p-4c-500k.yaml
禁用 Journaling 情况下的 Apache Pulsar | 启用 Journaling 情况下的 Apache Pulsar (默认) | Apache Kafka | |
---|---|---|---|
P50 (ms) | 0.77 | 2.64 | 1.75 |
P75 (ms) | 0.85 | 2.86 | 2.09 |
P95 (ms) | 1.36 | 4.62 | 2.86 |
P99 (ms) | 1.58 | 7.89 | 3.46 |
P99.9 (ms) | 1.68 | 12.24 | 54.56 |
P99.99 (ms) | 1.96 | 18.82 | 207.83 |
Max | 13.52 | 79.40 | 405.48 |
Figure 4: 500K rate publish latency percentiles (ms): Lower is better.
图 4:500K 速率的发布延迟百分位 (毫秒):数值越低,代表性能越好
In this test, Pulsar is able to maintain a low publish latency while sustaining a high per-node utilization. Pulsar without Journaling is able to sustain 1.58 milliseconds latency at the 99 percentile and Pulsar with Journaling is able to sustain 7.89 milliseconds.
在本测试中,Pulsar 能够在保持较高节点利用率的同时保持较低的发布延迟。禁用 Journaling 情况下 Pulsar 能够维持 1.58 毫秒的 99 百分位延迟,而启用 Journaling 情况下 Pulsar 能够维持在 7.89 毫秒。
Kafka maintains a low publish latency up to the 99 percentile, where it is able to sustain 3.46 milliseconds in latency. But at 99.9%, Kafka’s latency spikes to 54.56 ms.
Kafka 在 99 百分位之内可以保持低发布延迟,最高延迟为 3.46 毫秒。然而当 99.9% 时,Kafka 的延迟飙升至 54.56 毫秒。
Publishing at a fixed rate, below the max burst throughput, at 99.9% and above, Pulsar has lower latency than Kafka for both Pulsar with Journaling (default) and the Pulsar without Journaling.
在以固定速率发布的情况下,在最大突发吞吐量以下,在 99.9% 及以上时,Pulsar 的延迟都比 Kafka 更低,无论是否启用 Journaling。
The reasons for lower latency with Pulsar are:
Pulsar 能做到更低延迟的原因在于:
By contrast, Kafka replication protocol is set to wait for all three of the brokers that are in the in-replica-set. Because of that, unless a broker crashes or is falling behind the leader for more than 30 seconds, each entry in Kafka needs to wait for all three brokers to have the entry.
相比之下,Kafka 复制协议要等待同步副本集合中的所有三个 Broker 返回。因此,除非 Broker 崩溃或者落后于 Leader 30 秒以上,否则 Kafka 中每个 Entry 都需要等待写入所有三个 Broker。
In the consumer catch-up test, we build a backlog of data and then start the consumers. While the consumers catch-up, the writers continue publishing data at the same rate.
在消费者 catch-up 测试中,我们先构造数据积压,再启动消费者。在消费者 catch-up 的同时,生产者继续以同样的速率发布数据。
This is a common, real-life scenario for a messaging/streaming system. Below are a few common use cases:
这是消息/流系统的一个常见的真实设定。以下是一些常见的场景:
With this test, we can measure the following:
本测试可以测量如下内容:
The size of the backlog is 512 GBs. It is larger than the RAM available in the nodes in order to simulate the case where the entire data does not fit in cache and the storage systems are forced to read from disk.
Backlog 的大小为 512 GB,这比节点中的可用 RAM 要大,以便模拟整个数据不适合缓存并迫使存储系统从磁盘读取的情况。
驱动文件:pulsar.yaml, kafka-latency.yaml
负载文件:1-topic-100-partitions-1kb-4p-4c-200k-backlog.yaml
禁用 Journaling 情况下的 Apache Pulsar | 启用 Journaling 情况下的 Apache Pulsar (默认) | Apache Kafka | |
---|---|---|---|
最大读取吞吐量 (GB/s) | 3.2GB/s | 3.1GB/s | 2GB/s |
Figure 5a: Catch-up read throughput (msgs/s): Higher is better.
图 5a:追赶读的吞吐量 (msgs/s):数值越高,代表性能越好。
禁用 Journaling 情况下的 Apache Pulsar | 启用 Journaling 情况下的 Apache Pulsar (默认) | Apache Kafka | |
---|---|---|---|
追赶时间 (s) | 230 | 260 | 460 |
Figure 5b: Catch-up read chase time (seconds): Shorter is better.
图 5b:追赶读取时间 (秒):数值越短,代表性能越好。
禁用 Journaling 情况下的 Apache Pulsar | 启用 Journaling 情况下的 Apache Pulsar (默认) | Apache Kafka | |
---|---|---|---|
P99 发布延迟 (毫秒) | 最高可达 2.5 | 最高可达 21 | 最高可达 380 |
Figure 5c: Impact publish latency during catchup read (ms): Lower is better.
图 5c:追赶读期间对发布延迟的影响 (毫秒):数值越低,代表越好。
The test shows that Pulsar consumers are able to drain the backlog of data ~2.5x faster than Kafka consumers, without impacting the performance of the connected producers.
测试表明 Pulsar 消费者能够比 Kafka 消费者快约 2.5 倍耗尽 Backlog,而不会影响相关生产者的性能。
With Kafka, the test showed that while the consumers are catching up, the producers are heavily impacted, with 99% latencies up to ~700 milliseconds and consequent throughput reductions.
而对于 Kafka,测试表明当消费者赶上时,生产者受到了严重影响,其 99% 延迟高达约 700 毫秒,吞吐量也因此降低。
The increase in latency is caused by the contention on the OS page cache used by Kafka. When the size of the backlog of data exceeds the RAM available in the Kafka broker, the OS will start to evict pages from the cache. This causes page cache misses that stop the Kafka threads. When there are enough producers and consumers in a broker, it becomes easy to end up in a “cache-thrashing” scenario, where time is spent paging data in from the disk and evicting it from the cache soon after.
这种延迟的增加是由于 Kafka 所使用的操作系统页缓存的争用引起的。当 Backlog 大小超过 Kafka Broker 中的可用 RAM 时,操作系统会从缓存中逐出页。这会导致页缓存未命中,从而停止 Kafka 线程。当 Broker 中有足够多的生产者和消费者时,就会很容易陷入“缓存抖动”场景,时间就会花在从磁盘中缓存数据而很快又将其从缓存中逐出。
In contrast, Pulsar with BookKeeper adopts a more sophisticated approach to write and read operations. Pulsar does not rely on the OS page cache because BookKeeper has its own set of write and read caches, for which the eviction and pre-fetching are specifically designed for streaming storage use cases.
相比之下,Pulsar 底层的 BookKeeper 采用更巧妙的方式来执行写入和读取操作。Pulsar 并不依赖操作系统页缓存,因为 BookKeeper 有自己的一组写入和读取缓存,其逐出和预读都是专门为流式存储场景特别设计的。
This test demonstrates the degradation that consumers can cause in a Kafka cluster. This impacts the performance of the Kafka cluster and can lead to reliability problems.
测试表明在 Kafka 集群中消费者可能导致降级。这会影响 Kafka 集群的性能,并可能导致可靠性问题。
The benchmark demonstrates Apache Pulsar’s ability to provide high performance across a broad range of use cases. In particular, Pulsar provides better and more predictable performance, even for the use cases that are generally associated with Kafka, such as large volume streaming data over partitioned topics. Key highlights on the Pulsar versus Kafka performance comparison include:
本次基准测试展示了在广泛的测试场景中 Apache Pulsar 均有能力提供高性能。特别是,即使面对通常与 Kafka 相关的场景,例如分区主题上的大量流数据,Pulsar 也能提供更高和更可预测的性能。Pulsar 与 Kafka 性能对比的主要亮点包括:
Pulsar provides 99pct write latency <1.6ms without journal, and <8ms with journal for fixed 500MB/s write throughput. The latency profile does not degrade at the higher quantiles, while Kafka latency quickly spikes up to 100s of milliseconds. 在固定 500MB/s 的写入吞吐量情况下,Pulsar 在禁用 Journal 情况下提供 <1.6ms 的 99 百分位写入延迟,在启用 Journal 情况下则为 <8ms。在高百分位下,Pulsar 的延迟也仅轻微增加没有降级,而 Kafka 的延迟则快速飙升至 100 毫秒。
Pulsar can prove up to 3.2 GB/s historical data read throughput, 60% more than Kafka which can only achieve 2.0 GB/s. Pulsar 可以达到高达 3.2 GB/s 的历史数据读取吞吐量,比只能达到 2.0 GB/s 的 Kafka 高出 60%。
During historical data reading, Pulsar’s I/O isolation provides a low and consistent publish latency, 2 orders of magnitude lower than Kafka. This ensures that the real-time data stream will not be affected when reading historical data. 在历史数据读取过程中,Pulsar 的 I/O 隔离特性提供了稳定的低发布延迟,比 Kafka 低 2 个数量级。这保证了在读取历史数据时实时数据流不受影响。
While Pulsar is often adopted for streaming use cases, it also provides a superset of features and is widely adopted for message queuing use cases and for use cases that require unified messaging and streaming capabilities. This benchmark did not cover the message queuing capabilities of Pulsar, but you can learn more in the Pulsar Launches 2.8.0, Unified Messaging and Streaming blog.
虽然 Pulsar 常被用于流处理场景,但它同时还提供了许多其他特性并广泛用于消息队列场景,以及需要统一的消息和流处理能力的场景。本基准测试并未涵盖 Pulsar 的消息队列功能,但您可以在Pulsar 2.8.0 新增特性概览:独占 Producer、事务等、Apache Pulsar 里程碑简史:打造统一消息流平台与生态中了解更多。
Beyond the development of Pulsar’s capabilities, the Pulsar ecosystem continues to expand. Protocol handlers allow for Pulsar brokers to natively communicate via other protocols, such as Kafka and RabbitMQ, enabling teams to easily integrate existing applications with Pulsar. Integrations with Apache Pinot, Delta Lake, Apache Spark, and Apache Flink have allowed teams to make Pulsar the ideal choice to help teams use one technology across both the data and application tiers.
除了对功能的开发,Pulsar 的生态系统也在不断壮大。协议处理器允许 Pulsar Broker 与 Kafka 和 RabbitMQ 等其他协议天然地进行通信,使得团队能够轻松地将现有应用与 Pulsar 集成。与 Apache Pinot、Delta Lake、Apache Spark 和 Apache Flink 的集成使得 Pulsar 成为团队跨数据层和应用层的一站式技术的理想选择。
For more on Pulsar, check out the resources below.
有关 Pulsar 的更多信息,请查看以下资源。
[1] Linux Foundation Open Messaging 基准测试套件: http://openmessaging.cloud/docs/benchmarks/
[2] Open Messaging Benchmark Github 代码库: https://github.com/openmessaging/benchmark
[3] 更准确地了解 Pulsar 性能:https://streamnative.io/blog/tech/2020-11-09-benchmark-pulsar-kafka-performance/
]]>本文翻译自《Offset Implementation in Kafka-on-Pulsar》
- 译文发表于 Apache Pulsar 公众号:https://mp.weixin.qq.com/s/JXLquQkJFAzu8uw_lJaIcg
Protocol handlers were introduced in Pulsar 2.5.0 (released in January 2020) to extend Pulsar’s capabilities to other messaging domains. By default, Pulsar brokers only support Pulsar protocol. With protocol handlers, Pulsar brokers can support other messaging protocols, including Kafka, AMQP, and MQTT. This allows Pulsar to interact with applications built on other messaging technologies, expanding the Pulsar ecosystem.
协议处理器 是 2020 年一月份发布的 Pulsar 2.5.0 所引入的新功能,目的是将 Pulsar 的能力扩展到其他消息领域。默认情况下 Pulsar Broker 仅支持 Pulsar 协议。而通过协议处理器,Pulsar Broker 就可以支持其他消息协议,包括 Kafka、AMQP 以及 MQTT(现已新增 RocketMQ)。这使得 Pulsar 可以与基于其他消息技术的应用进行交互,从而扩展 Pulsar 生态系统。
Kafka-on-Pulsar (KoP) is a protocol handler that brings native Kafka protocol into Pulsar. It enables developers to publish data into or fetch data from Pulsar using existing Kafka applications without code change. KoP significantly lowers the barrier to Pulsar adoption for Kafka users, making it one of the most popular protocol handlers.
Kafka-on-Pulsar (KoP) 就是一种协议处理协议,它将原生 Kafka 协议引入了 Pulsar,使得开发人员能够使用现有的 Kafka 应用将数据发布到 Pulsar 或从 Pulsar 读取数据,而无需更改代码。KoP 极大降低了 Kafka 用户使用 Pulsar 的壁垒,这让 KoP 成为最受欢迎的协议处理器之一。
KoP works by parsing Kafka protocol and accessing BookKeeper storage directly via streaming storage abstraction provided by Pulsar. While Kafka and Pulsar share many common concepts, such as topic and partition, there is no corresponding concept of Kafka’s offset in Pulsar. Early versions of KoP tackled this problem with a simple conversion method, which did not allow continuous offset and was prone to problems.
KoP 解析 Kafka 协议,并通过 Pulsar 提供的流式存储抽象接口直接访问 BookKeeper。虽然 Kafka 和 Pulsar 有许多通用的概念,例如主题和分区,但在 Pulsar 中没有对应 Kafka 偏移量的概念。KoP 的早期版本通过一种简单的转换来应对这个问题,但这种转换不支持连续偏移量,同时也容易出现问题。
To solve this pain point, broker entry metadata was introduced in KoP 2.8.0 to enable continuous offset. With this update, KoP is available and production-ready. It is important to note that with this update backward compatibility is broken. In this blog, we dive into how KoP implemented offset before and after 2.8.0. and explain the rationale behind the breaking change.
为了解决这个痛点,KoP 2.8.0 引入了 Broker Entry Metadata,以实现连续偏移量。这个更新使得 KoP 可用并且生产就绪。需要特别注意的是,这个更新破坏了向后兼容性。本文将深入探讨 KoP 2.8.0 之前和之后分别是如何实现偏移量的,并解释该突破性变化背后的基本原理。
Note on Version Compatibility
Since Pulsar 2.6.2, KoP version has been updated with Pulsar version accordingly. The version of KoP x.y.z.m conforms to Pulsar x.y.z, while m is the patch version number. For instance, the latest KoP release 2.8.1.22 is compatible with Pulsar 2.8.1. In this blog, 2.8.0 refers to both Pulsar 2.8.0 and KoP 2.8.0.
版本兼容性说明
Pulsar 2.6.2 版本之后,KoP 版本即随着相应的 Pulsar 版本而更新。KoP x.y.z.m 版本对应 Pulsar x.y.z 版本,其中 m 是补丁版本号。例如,最新的 KoP 2.8.1.22 版本与 Pulsar 2.8.1 版本兼容。本文中 2.8.0 同时指代 Pulsar 2.8.0 和 KoP 2.8.0。
In Kafka, offset is a 64-bit integer that represents the position of a message in a specific partition. Kafka consumers can commit an offset to a partition. If the offset is committed successfully, after the consumer restarts, it can continue consuming from the committed offset.
在 Kafka 中,偏移量是一个 64 位整数,表示消息在特定分区中的位置。Kafka 消费者可以向分区提交偏移量。如果偏移量提交成功,那么消费者重启后就能够从已提交的偏移量位置继续消费。
Kafka stores messages in each broker’s file system: Kafka 将消息存储在每个 broker 的文件系统中:
Since each message records the message size in the header, for a given offset, Kafka can easily find its segment file and position.
由于每条消息的头部都记录了消息大小,所以对于给定偏移量,Kafka 可以很容易地找到其分片文件以及位置。
Unlike Kafka, which stores messages in each broker’s file system, Pulsar uses BookKeeper as its storage system. In BookKeeper:
Kafka 将消息存储到每个 Broker 上的文件系统,而 Pulsar 则不同,它使用 BookKeeper 作为其存储系统。在 BookKeeper 中:
A bookie can find any entry via a 64-bit ledger ID and a 64-bit entry ID. Pulsar can store a message or a batch (one or more messages) in an entry. Therefore, Pulsar finds a message via its message ID that consists of a ledger ID, an entry ID, and a batch index (-1 if it’s not batched). In addition, the message ID also contains the partition number.
Bookie 可以通过 64 位 Ledger ID 和 64 位 Entry ID 找到任何 Entry。Pulsar 可以在一个 Entry 中存储单条消息或一批消息。因此,Pulsar 的消息 ID 由 Ledger ID、Entry ID、 批索引(如果不是批量消息则为 -1)以及分区编号组成,Pulsar 可通过这种消息 ID 找到一条消息。
Just like a Kafka consumer can commit an offset to record the consumer position, a Pulsar consumer can acknowledge a message ID to record the consumer position.
就像 Kafka 消费者可以提交偏移量来记录消费位置一样,Pulsar 消费者可以确认消息 ID 来记录消费位置。
KoP needs the following Kafka requests to deal with a Kafka offset:
KoP 需要如下 Kafka 请求来处理 Kafka 偏移量:
We must support computing a specific message offset or locating a message by a given offset.
我们必须支持计算特定消息的偏移量,或通过给定的偏移量定位消息。
As explained earlier, Kafka locates a message via a partition number and an offset, while Pulsar locates a message via a message ID. Before Pulsar 2.8.0, KoP simply performed conversions between Kafka offsets and Pulsar message IDs. A 64-bit offset is mapped into a 20-bit ledger ID, a 32-bit entry id, and a 12-bit batch index. Here is a simple Java implementation.
如前文所述,Kafka 通过 分区编号和偏移量 来定位消息,而 Pulsar 通过 消息 ID 来定位消息。在 Pulsar 2.8.0 之前,KoP 简单地在 Kafka 偏移量和 Pulsar 消息 ID 之间进行一个转换。将 64 位的偏移量映射为 20 位 Ledger ID、32 位 Entry ID 以及 12 位批索引。如下是一个简单的 Java 实现。
1 2 3 4 5 6 7 8 9 10 11 |
|
In this blog, we use (ledger id, entry id, batch index)
to represent a message ID. For example, assuming a message’s message ID is (10, 0, 0)
, the converted offset will be 175921860444160. This works in some cases because the offset is monotonically increasing. But it’s problematic when a ledger rollover happens or the application wants to manage offsets manually. The section below goes into details about the problems of this simple conversion implementation.
在本文中,我们使用 (ledger id, entry id, batch index)
来表示一个消息 ID。例如,假设一个消息的 ID 是 (10, 0, 0)
,则转换后的偏移量为 175921860444160。在一些情况下这样的数值能正常工作,因为偏移量是单调递增的。然而当发生 Ledger 翻转,或应用程序想要手动管理偏移量时,就会出现问题。下面详细介绍这种简单转换方法存在的问题。
The converted offset is not continuous, which causes many serious problems.
转换后的偏移量不连续,这会导致许多严重问题。
For example, let’s assume the current message’s ID is (10, 5, 100)
. The next message’s ID could be (11, 0, 0)
if a ledger rollover happens. In this case, the offsets of these two messages are 175921860464740 and 193514046488576. The delta value between the two is 17,592,186,023,836.
例如,假设当前消息 ID 是 (10, 5, 100)
。如果发生 Ledger 翻转,则下一条消息的 ID 可能是 (11, 0, 0)
。在这种情况下,两条消息的偏移量分别为 175921860464740 和 193514046488576,两者差了 17,592,186,023,836。
KoP leverages Kafka’s MemoryRecordBuilder
to merge multiple messages into a batch. The MemoryRecordBuilder
must ensure the batch size is less than the maximum value of a 32-bit integer (4,294,967,296). In our example, the delta value of the two continuous offsets is much greater than 4,294,967,296. This will result in an exception that says Maximum offset delta exceeded
.
KoP 利用 Kafka 的 MemoryRecordBuilder
将多条消息合并为一个批量消息。 MemoryRecordBuilder
必须确保批量大小小于 32 位整数的最大值 (4,294,967,296)。在上文示例中,两个连续偏移量的差值远大于 4,294,967,296。这将导致抛出 Maximum offset delta exceeded
异常。
To avoid the exception, before KoP 2.8.0, we must configure maxReadEntriesNum
(this config limits the maximum number of entries read by the BookKeeper client) to 1. Naturally, reading only one entry for each FETCH request worsens the performance significantly.
为了避免该异常,在使用 KoP 2.8.0 之前版本时,我们必须配置 maxReadEntriesNum
为 1 (此配置限制 BookKeeper 客户端读取的最大 Entry 条数)。如此一来,每个 FETCH 请求只读取一个 Entry,会显著降低性能。
However, even with the workaround of maxReadEntriesNum=1
, this conversion implementation doesn’t work in some scenarios. For example, the Kafka integration with Spark relies on the continuance of Kafka offsets. After it consumes a message with an offset of N, it will seek the next offset (N+1). However, the offset N+1 might not be able to convert to a valid message ID.
然而,即使使用 maxReadEntriesNum=1
这种变通方法,这种转换实现在某些场景下也不能正常工作。例如,Kafka 与 Spark 的集成依赖于 Kafka 偏移量的连续性。当消费偏移量为 N 的消息后,Spark 会寻找下一个偏移量 (N + 1)。但是偏移量 N + 1 可能无法转换为有效的消息 ID。
There are other problems caused by the conversion method. And before 2.8.0, there is no good way to implement the continuous offset.
转换方法还存在其他问题。而在 2.8.0 之前版本,没有好办法实现连续偏移量。
The solution to implement continuous offset is to record the offset into the metadata of a message. However, an offset is determined at the broker side before publishing messages to bookies, while the metadata of a message is constructed at the client side. To solve this problem, we need to do some extra jobs at the broker side:
实现连续偏移量的解决方案是将偏移量记录到消息的元数据中。然而,偏移量是由 Broker 端在将消息发布到 Bookie 之前决定的,而消息的元数据则是在客户端构建的。为了解决这个问题,我们需要在 Broker 端做一些额外的工作:
Deserialize the metadata 反序列化元数据
Set the “offset” property of metadata 设置元数据的“偏移量”属性
Serialize the metadata again, including re-computing the checksum value 再次序列化元数据,包括重新计算校验和值
This results in a significant increase in CPU overhead on the broker side.
这会导致 Broker 端的 CPU 开销显著增加。
PIP 70 introduced lightweight broker entry metadata. It’s a metadata of a BookKeeper entry and should only be visible inside the broker.
PIP 70 引入了轻量级 Broker Entry 元数据。它是 BookKeeper Entry 的元数据,并且只在 Broker 内部可见。 The default message flow is:默认的消息流如下图所示:
If you configured brokerEntryMetadataInterceptors
, which represents a list of broker entry metadata interceptors, then the message flow would be:
如果配置了 brokerEntryMetadataInterceptors
,即配置一组 Broker Entry 元数据拦截器,那么消息流将会是:
We can see the broker entry metadata is stored in bookies, but is not visible to a Pulsar consumer.
可以看到 Broker Entry 元数据存储在 Bookie 上,但对 Pulsar 消费者不可见。
From 2.9.0, a Pulsar consumer can be configured to read the broker entry metadata.
2.9.0 版本之后,可以将 Pulsar 消费者配置为可以读取 Broker Entry 元数据。
Each broker entry metadata interceptor adds the specific metadata (called “broker entry metadata”) before the message metadata. Since the broker entry metadata is independent of the message metadata, the broker does not need to deserialize the message metadata. In addition, the BookKeeper client supports sending a Netty CompositeByteBuf
that is a list of ByteBuf
without any copy operation. From the perspective of a BookKeeper client, only some extra bytes are sent into the socket buffer. Therefore, the extra overhead is low and acceptable.
每个 Broker Entry 元数据拦截器都在消息元数据前面加上特定的元数据(称之为 “Broker Entry 元数据”)。由于 Broker Entry 元数据和消息元数据是独立的,所以 Broker 无需反序列化消息元数据。此外,BookKeeper 客户端支持发送包含多个 ByteBuf
的 Netty CompositeByteBuf
,而无需任何复制操作。从 BookKeeper 客户端角度看,只是将一些额外字节发送到套接字缓冲区。因此,额外的开销会很低且可接受。
We need to configure the AppendIndexMetadataInterceptor
(we say index metadata interceptor) to support the Kafka offset.
我们需要配置 AppendIndexMetadataInterceptor
(即 索引元数据拦截器) 来支持 Kafka 偏移量。
1
|
|
In Pulsar brokers, there is a component named “managed ledger” that manages all ledgers of a partition. The index metadata interceptor maintains an index that starts from 0. The “index” term is used instead of “offset”.
Pulsar Broker 中有个名为 “Managed Ledger” 的组件,它管理分区中的所有 Ledger。索引元数据拦截器维护了一个从 0 开始的索引。Pulsar 使用术语“索引”而不是“偏移量”。
Each time before an entry is written to bookies, the following two things happen:
每次将 Entry 写入 Bookie 之前,都会发生如下两件事:
After that, each entry records the first message’s index, which is equivalent to the “base offset” concept in Kafka.
之后,每个 Entry 记录第一条消息的索引,相当于 Kafka 中的“基础偏移量”概念。
Now, we must make sure even if the partition’s owner broker was down, the index metadata interceptor would recover the index from somewhere.
现在,我们需要保证即使分区的 owner Broker 宕机,索引元数据拦截器也能从某个地方恢复索引。
There are some cases where the managed ledger needs to store its metadata (usually in ZooKeeper). For example, when a ledger is rolled over, the managed ledger must archive all ledger IDs in a z-node. Here we don’t look deeper into the metadata format. We only need to know there is a property map in the managed ledger’s metadata.
在某些场景下,Managed Ledger 需要将其元数据存储起来(通常存储到 ZooKeeper)。例如,当一个 Ledger 发生翻转,Managed Ledger 需要将所有 Ledger ID 归档到一个 z-node。这里我们不深入研究元数据的格式,只需要知道在 Managed Ledger 元数据中有一个属性映射。
Before metadata is stored in ZooKeeper (or another metadata store):
在将元数据存储到 ZooKeeper (或其他元数据存储) 之前:
Each time a managed ledger is initialized, it will restore the metadata from the metadata store. At that time, we can set the index metadata intercerptor’s index to the value associated with the “index” key.
每次初始 Managed Ledger 时,都会从元数据存储中恢复元数据。那时,我们可以将索引元数据拦截器中的索引设置为“index”键关联值。
Let’s look back to the How does KoP deal with a Kafka offset section and review how we deal with the offset in following Kafka requests.
让我们回顾一下 KoP 如何处理 Kafka 偏移量 一节,看看在如下 Kafka 请求中如何处理偏移量。
When KoP handles PRODUCE requests, it leverages the managed ledger to write messages to bookies. The API has a callback that can access the entry’s data.
当 KoP 处理 PRODUCE 请求时,它利用 Managed Ledger 将消息写入 Bookie。相关 API 有一个回调可以访问 Entry 数据。
1 2 |
|
We only need to parse the broker entry metadata from entryData
and then retrieve the index. The index is just the base offset returned to the Kafka producer.
我们只需要从 entryData
中解析出 Broker Entry 元数据,然后检索索引即可。该索引就是返回给 Kafka 生产者的基础偏移量。
The task is to find the position (ledger id and entry id) for a given offset. KoP implements a callback that reads the index from the entry and compares it with the given offset. It then passes the callback to a class named OpFindNewest
, which uses binary search to find an entry.
The binary search could take some time. But it only happens on the initial search unless the Kafka consumer disconnects. After the position is found, a non-durable cursor will be created to record the position. The cursor will move to a newer position as the fetch offset increases.
FETCH 是通过给定偏移量找到消息位置 (Ledger ID 和 Entry ID)。KoP 实现了一个回调,从 Entry 中读取索引并与给定的偏移量进行比较。然后将回调传给 OpFindNewest
类,该类使用二分查找算法来查找 Entry。
二分查找可能要花一些时间。但它仅发生在初始搜索中,除非 Kafka 消费者断开连接。当找到位置后,会创建一个非持久化的游标来记录该位置。随着 fetch 偏移量的增加,游标会移动到更新的位置。
KoP 2.8.0 implements continuous offset with a tradeoff – the backward compatibility is broken. The offset stored by KoP versions before 2.8.0 cannot be recognized by KoP 2.8.0 or higher.
KoP 2.8.0 实现的连续偏移量是有折衷的 —— 向后兼容性被破坏。KoP 2.8.0 之前版本存储的偏移量无法被 KoP 2.8.0 或更高版本识别。
If you have not tried KoP, please upgrade your Pulsar to 2.8.0 or higher and then use the corresponding KoP. As of this writing, the latest KoP release for Pulsar 2.8.1 is 2.8.1.22.
如果在此之前你还没有使用过 KoP,需将 Pulsar 升级到 2.8.0 或更高版本后使用相应版本的 KoP。
If you have already tried KoP before 2.8.0, you need to know that there’s a breaking change from version less than 2.8.0 to version 2.8.0 or higher. You must delete the __consumer_offsets
topic and all existing topics previously used by KoP.
如果你在此之前已经使用过 2.8.0 之前版本的 KoP,则需要知道从低于 2.8.0 版本到 2.8.0 或更高版本有突破性变化。使用新版本前,你必须删除 __consumer_offsets
主题以及 KoP 之前使用过的所有主题。
There is a latest feature in KoP that can skip these old messages by enabling a config. It would be included in 2.8.1.23 or later. Note that the old messages still won’t be accessible. It just saves the work of deleting old topics.
KoP 中有一个最新的功能,可以通过启用配置来跳过这些旧消息。这个功能将包含在 2.8.1.23 或更高版本。注意:旧消息仍将无法访问,这个功能只是节省了删除旧主题的工作量。
In this blog, we first explained the concept of Kafka offset and the similar concept of message ID in Pulsar. Then we talked about how KoP implemented Kafka offset before 2.8.0 and the related problems.
本文首先解释了 Kafka 偏移量的概念,以及 Pulsar 类似的消息 ID 概念。然后讲了 KoP 在 2.8.0 版本之前是如何实现 Kafka 偏移量的及其带来的相关问题。
To solve these problems, the broker entry metadata was introduced from Pulsar 2.8.0. Based on this feature, index metadata is implemented via a corresponding interceptor. After that, KoP can leverage the index metadata interceptor to implement the continuous offset.
为了解决这些问题,Pulsar 2.8.0 引入了 Broker Entry 元数据。基于此特性,通过相应的拦截器实现了索引元数据。之后,KoP 可以利用索引元数据拦截器来实现连续偏移量。
Finally, since it’s a breaking change, we talked about the upgrade from KoP version less than 2.8.0 to 2.8.0 or higher. It’s highly recommended to try KoP 2.8.0 or higher directly.
最后,由于这是一个突破性的变化,我们谈了从 2.8.0 之前版本到 2.8.0 或更高版本的升级。强烈建议直接尝试 KoP 2.8.0 或更高版本。
]]>You can implement a circuit break based on the official manual, and you may want to verify if it works well or not after . Here’re some tips
Before the testing, it’s a good idea to learn the Circuit Breaker Configurations, which are critical to a successful test. See some important configurations below:
failureRateThreshold
: When the failure rate is equal or greater than the threshold the CircuitBreaker transitions to open and starts short-circuiting calls.minimumNumberOfCalls
: it means the minimum number of calls which are required (per sliding window period) before the CircuitBreaker can calculate the error rate. For example, if minimumNumberOfCalls is 10, then at least 10 calls must be recorded, before the failure rate can be calculated. If only 9 calls have been recorded the CircuitBreaker will not transition to open even if all 9 calls have failed.slidingWindowSize
/ ringBufferSizeInClosedState (legacy)
: the size of the sliding window which is used to record the outcome of calls when the CircuitBreaker is closed.recordExceptions
: A list of exceptions that are recorded as a failure and thus increase the failure rate. If you specify a list of exceptions, all other exceptions count as a success.The thresholds in production enviroment are usually configured as a large value and not convenient for testing. For example,
minimumNumberOfCalls
might be 100, so you have to make 100 requests to trigger the error rate calculation.slidingWindowSize / ringBufferSizeInClosedState
might be 100, so you have to make another 100 requests to complete the error rate calculation.failureRateThreshold
might be 90, so you have to ensure 90% requests return exception so that the circuit breaker can be triggered to transit from CLOSED to OPEN.In short, reducing those thresholds in testing environment is recommended. For example, here’s how I set them:
minimumNumberOfCalls
= 1.slidingWindowSize
= 5.failureRateThreshold
= 10. (another option is mock a higher exception)Next you need to mock exception when calling the remote service so that the circuit break can recognize the call as an error, then trigger the state transition after the error rate increased and reached the threshold.
The first thing to notice is, not all exceptions will be counted in the error rate. So it is a good idea to figure out what kind of exception you need to mock first. If you’ve set recordExceptions
on CircuitBreakerConfig, those exceptions are what you need to mock!
For example, below snippet means only ApiServerException will be counted in the error rate.
1 2 3 4 5 |
|
So we need to mock ApiServerException during the remote call.
1 2 3 4 5 6 7 |
|
Note: this example mocks the exception on api client side, you can also mock on the server side.
Now you can request the target API, if the circuit breaker works well, you can find the state transit from CLOSED to OPEN once the error rate reachs the theshold. You may find a log like:
1
|
|
Note: assumed you’ve attached onStateTransition event.
circuitBreaker.getEventPublisher().onStateTransition(event -> log.warn("{}, {}", circuitBreaker.getName(), event.getStateTransition()));
In short, reducing those thresholds in testing environment is recommended, and mock exception when calling the remote service, then you can verify the state transition by monitoring the logs.
You may found changing the thresholds is a tedious job which need to change the code and deploy, I recommend store the thresholds in a Configuration Center, so that you can config a lower value for testing environment without touching the code while not affect production environment.
Another benefit of integrating it with Configuration Center is that you can easily adjust the thresholds to fit the real world at any time.
]]>2019年在大团队内部组织了一个 Study Group,主要目的是一起学习、一起分享、一起成长;上个季度我们的一项重点是完成了《Clean Code》这本书的拆解和分享,本文是对其中最后一章的一个总结。其实最后一章是对全书的总结,随意本文也可以近似看成是对全书的总结吧。
原始总结请参考 https://github.com/AlphaWang/alpha-book-clean-code/tree/master/17_smells_heuristics
注释只应该描述有关代码和设计的技术性信息。
反例:修改历史记录
过时、无关、不正确的注释就是废弃的注释。
问题:造成误导
注释应该谈及代码自身 没有(不能)提到的东西。
Comments should say things that the code cannot say for itself.
反例:
1
|
|
1 2 3 4 5 6 |
|
如果注释值得写,那就值得好好写。
看到注释掉的代码,就删掉它!
You should be able to check out the system with one simple command and then issue one other simple command to build it.
1 2 3 |
|
You should be able to run all the unit tests with just one command.
函数参数越少越好。
Readers expect arguments to be inputs, not outputs.
有布尔值参数,说明函数做了不止一件事。
永不被调用的方法应该丢弃。
Following The Principle of Least Surprise
, any function or class should implement the behaviors that another programmer could reasonably expect.
1
|
|
Reasonable expectations:
Don’t rely on your intuition. Look for every boundary condition and write a test for it.
Turning off certain compiler warnings may help you get the build to succeed, but at the risk of endless debugging sessions.
DRY: Don’t Repeat Yourself. 这是本书最重要的规则之一
1 2 3 4 5 |
|
BoundedStack
上。In general, base classes should know nothing about their derivatives.
Tips: find dead code by IntelliJ
Analyze
—> Run Inspection by Name…
—> Unused declaration
垂直距离要短,
If you do something a certain way, do all similar things in the same way.
HttpServletResponse response;
vs. HttpServletResponse httpResponse;
loadInterstellarVendorItem()
vs. getInterstellarVendorItem()
没有用到的变量;
从不调用的函数;
没有信息量的注释;
All these things are clutter and should be removed.
不互相依赖的东西就不该耦合。
反例:把普通的enum声明在特殊类中。
see Martin Fowler’s Refactoring.
The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes.
1 2 3 4 5 6 7 8 9 10 11 |
|
The calculateWeeklyPay method envies the scope of HourlyEmployee. It “wishes” that it could be inside HourlyEmployee.
但事无绝对,下面这个reportHours方法如果移到HourlyEmployee类中,就会违反SRP原则。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
什么是selector arguments:用于选择函数行为的参数。
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 |
|
这个函数看起来短小紧凑,但究竟是在做什么事情呢?
Question: PI常量应该放在Math类、Trigonometry类、还是Circle类?
The Principle of Least Surprise
. Code should be placed where a reader would naturally expect it to be.恰当的静态方法:
1 2 3 4 5 |
|
不恰当的静态方法:
1
|
|
原因:有理由希望这个函数是多态的。(OvertimeHourlyPayCalculator, StraightTimeHourlyPayCalculator)
可以作为Employee方法的非静态函数。
让程序可读的有力方法之一就是将计算过程打散,用有意义的变量名存储中间值。
1 2 3 4 5 6 |
|
反例:
1
|
|
正例:
1 2 |
|
Before you consider yourself to be done with a function, make sure you understand how it works. It is not good enough that it passes all the tests. You must know that the solution is correct.
Logical Dependency:
Physical Dependency:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
PAGE_SIZE应该是HourlyReportFormatter的职责; 此处HourlyReporter被假定知道PAGE_SIZE,所以是逻辑依赖。
HourlyReportFormatter
named getMaxPageSize()
.HourlyReporter
will then call that function rather than using the PAGE_SIZE
constant.反例: CartSectionHeaderAssembler
建议参考阿里巴巴Java开发手册:https://github.com/alibaba/p3c
反例:
正例:
问题:是不是所有数字都需要替换成常量?
1 2 3 |
|
float
表示货币;switch/cases with nicely named enumerations are inferior to base classes with abstract methods.
如果 if 或者 while 语句没有上下文,那就很难理解其判断逻辑了。
反例
1
|
|
正例
1
|
|
Negatives are just a bit harder to understand than positives.
反例
1
|
|
正例
1
|
|
SRP原则。
有时函数的执行次序很重要,这时就需要用某种机制(例如 bucket brigade)来确保其他程序员不能随意调整执行次序。
反例
1 2 3 4 5 6 7 8 9 10 11 |
|
正例
1 2 3 4 5 6 7 8 9 10 11 |
|
反例:
滥用内部类。本应是一个顶级类,却随意定义在另一个类内部作为内部类。
要把处理边界条件的代码集中到一处,而不是散落在代码中。
反例:
1 2 3 4 |
|
正例:
1 2 3 4 5 |
|
反例:
1 2 3 4 5 6 7 |
|
这个方法混杂了至少两个抽象层级:
正例:
1 2 3 4 5 6 7 8 9 10 11 |
|
如果你有一个常量值表示默认值或者配置值,不要把它埋在底层的函数中。
正例:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Law of Demeter.不要让模块了解太多其写作者的信息。
反例:
1
|
|
正例:
1
|
|
如果使用了来自同一包的两个或多个类,用通配符导入:import package.*
。
仅仅是为了减少代码行?
不要在子类中访问父类的常量。因为不方便找到常量究竟定义在哪个类里。
通过IDEA可以方便地找到呀。。。
推荐用枚举。
Don’t pick names that communicate implementation; choose names the reflect the level of abstraction of the class or function you are working in.
命名不要暴露实现细节,只用描述功能抽象。
如果用装饰者模式,就以Decorator结尾。
反例
1 2 3 4 5 6 7 8 9 |
|
正例
1 2 3 |
|
如果变量作用范围很小,例如5行之内,那么取名为i
j
是没问题的;但是如果作为范围较大,则应该使用较长的名称。
不要在名称中包含类型或作用域信息。
反例
1 2 3 4 5 6 7 |
|
正例
1 2 3 |
|
一套测试应该测到所有可能失败的东西。
只要还有没被测试探测过的条件,或是还有没被验证过的计算,那么测试就是不足的。
IDE支持。
小测试易于编写,其文档上的价值高于编写成本。
有时因为需求不明,我们可以把测试表为@Ignore
。
边界判断错误的情形很常见。
80% 的软件缺陷常常生存在软件 20% 的空间里。
有时可以通过测试用例失败的模式来诊断问题所在。
Looking at the code that is or is not executed by the passing tests gives clues to why the failing tests fail.
A slow test is a test that won’t get run. When things get tight, it’s the slow tests that will be dropped from the suite. So do what you must to keep your tests fast.
]]>最近在研究分布式链路跟踪系统,Google Dapper 当然是必读的论文了,目前网上能搜到一些中文翻译版,然而读下来个人感觉略生硬;这里试着在前人的肩膀上重新翻译一遍这个论文,权当是个人的学习笔记,如果同时能给其他人带来好处那就更好了。
同时把译文放到了 github,如您发现翻译错误或者不通顺之处,恳请提交 github PR: https://github.com/AlphaWang/alpha-dapper-translation
现代互联网服务通常都是复杂的大规模分布式系统。这些系统由多个软件模块构成,这些软件模块可能由不同的团队开发、可能使用不同的编程语言实现、可能布在横跨多个数据中心的几千台服务器上。这种环境下就急需能帮助理解系统行为、能用于分析性能问题的工具。
本文将介绍 Dapper 这个在 Google 生产环境下的分布式系统跟踪服务的设计,并阐述它是如何满足在一个超大规模系统上达到低损耗(low overhead)、应用级透明(application-level transparency)、大范围部署(ubiquitous deployment)这三个需求的。Dapper 与其他一些跟踪系统的概念类似,尤其是 Magpie[3] 和X-Trace[12],但是我们进行了一些特定的设计,使得 Dapper 能成功应用在我们的环境上,例如我们使用了采样并将性能测量(instrumentation)限制在很小一部分公用库里。
本文的主要目的是汇报两年多以来我们构建、部署并应用 Dapper 的经历,这两年多里 Dapper 对开发和运维团队非常有用,取得了显著的成功。最初 Dapper 只是一个自包含(self-contained)的跟踪工具,后来演化成了一个监控平台并促生出许多不同的工具,有些工具甚至 Dapper 的设计者都未曾预期到。我们将介绍一些基于 Dapper 构造的分析工具,分享这些工具在 Google 内部使用的统计数据,展示一些使用场景的例子,并讨论我们学习到的经验教训。
Dapper 的目的是为了将复杂分布式系统的更多行为信息提供给 Google 开发者。这种分布式系统利用大规模的小服务器,通常对于互联网服务是一个非常经济的平台,所以很受关注。要理解在这种上下文中要的系统行为的话,就需要观察横跨不同程序和不同机器的关联行为。
下面基于一个 web 搜索的例子来说明这种系统需要应对哪些挑战。前端服务器将一个 web 查询分发给上百台搜索服务器,每个搜索服务器在自己的 index 中完成搜索。同时这个 web 查询可能还会被发送给多个其他子系统,进行广告处理、拼写检查、查找相关的图片/视频/新闻等。所有这些服务的结果会被有选择地合并成结果页面;我们把这种模型称之为全局搜索 (universal search)
。处理一次全局搜索查询,总计需要上千台机器,涉及多种服务。而且 web 搜索的用户对延时很敏感,而任何一个子系统的性能差了都可能导致延时。工程师如果只看总体耗时的话,他能知道出问题了,但是他猜不到是哪个系统出问题、为什么出问题。首先,工程师可能无法准确知道到底调用了哪些服务;每周我们都会添加新的服务,用于实现用户需求、提升性能或安全性。其次,工程师不可能对每个服务的内部都了如指掌;每个服务都是由不同的团队开发维护的。第三,服务和服务器可能被许多不同的客户端调用,所以性能问题有可能是其他应用造成的。举例来说,前端服务器可能要处理多个不同的请求类型,或者类似 Bigtable 这种存储系统在被多个应用共享时效率最高。
上面描述的场景就对 Dapper 提出了两条最基本的要求:大范围部署 (uniquitous deployment)、持续监控 (continuous monitoring)。即便只有很小一部分系统没有被监控到,跟踪系统的作用也会大打折扣,所以大范围部署非常重要。另外,应该始终开启监控,因为通常来说异常系统行为很难重现,甚至根本无法重现。这两条基本要求提出了三个具体的设计目标:
另外一个设计目标是生成跟踪数据后要很快可用于分析:最好是在一分钟内。尽管一个能处理几小时前数据的跟踪分析系统已经很有用了,但是能分析最新数据的话会让我们能对生产环境的异常情况作出快速反应。
我们通过把 Dapper 跟踪植入的核心代码限制在线程调用、控制流以及 RPC 等库代码中,实现了真正的应用透明这个最具挑战性的目标。使用自适应的采样(见4.4节),我们做到了可扩展性、降低性能损耗。最终的系统还包括收集跟踪数据的代码、可视化数据的工具、用于分析大规模跟踪数据的库和 API。尽管开发人员有时通过 Dapper 就足以找出性能问题的根源,但 Dapper 并不会替代所有其他的工具。我们发现 Dapper 的数据往往侧重于性能排查,所以其他工具也有自己的用处。
之前已有一些优秀的文章探讨了分布式系统跟踪工具的设计空间,其中 Pinpoint[9]、Magpie[3] 和 X-Trace[12] 与 Dapper 最为相关。这些系统倾向于在开发过程中的早期就写成研究报告,而此时还没有机会明确地评估重要的设计选型。Dapper 已经在生产环境中被大型系统应用好几年了,我们认为本文最适合的侧重点是讨论我们在 Dapper 开发过程中有哪些收获、我们的设计决策是如何制定的、它在哪些方面最有用。Dapper 作为一个开发性能分析工具的平台以及作为一个监控工具,其价值是我们可以在回顾评估中找到一些意想不到的产出。
虽然 Dapper 的许多高层理念和 Pinpoint、Magpie 等其他系统是共通的,但是我们的实现包含了一系列新的贡献。举个例子,我们发现要想降低消耗的话采样就必不可少,尤其是在高度优化后的对延迟非常敏感的 web 服务中。或许最令人惊讶的是,我们发现即便只使用 1/1000 的采样率,已经能为跟踪数据的通用用例提供足够多的信息了。
Dapper 的另一个重要特征是我们实现的应用透明程度非常高。我们将性能测量限制在足够底层,所以即便是像 Google web 搜索这样的大型分布式系统也能进行跟踪,而无需额外的注解。虽然由于我们的部署环境具有一定的同质性,所以更容易实现应用透明这个目标,但是我们的结果也论证了实现透明性的充分条件。
分布式服务的跟踪系统需要记录在一次请求后系统完成的所有工作的信息。举个例子,图-1展示了拥有 5 台服务器的服务:一个前端服务器 A,两个中间层 B 和 C,两个后端服务器 D 和 E。当用户发起请求到前端服务器 A 之后,会发送两个 RPC 调用到 B 和 C。B 马上会返回结果,但是 C 还需要继续调用后端服务器 D 和 E,然后返回结果给 A,A 再响应最初的请求。对这个请求来说,一个简单的分布式跟踪系统需要记录每台机器上的每次信息发送和接收的信息标识符和时间戳。
(图-1. 由用户请求X 发起的穿过一个简单服务系统的请求路径。字母标识的节点表示分布式系统中的处理过程)
为了能将信息聚合到一起以便人们能将所有记录信息关联到一个初始请求(如图1中的请求 X),我们提出了两种解决方案:黑盒监控模式
和 基于标注的监控模式
。黑盒模式[1, 15, 2] 假定除了上面描述的信息记录之外无需任何额外的信息,而使用统计回归技术来推断关联关系。基于标注的模式[3, 12, 9, 16] 则要求应用程序或中间件显式地将每个记录关联到一个全局 ID,从而将这些信息记录关联回初始请求。黑盒模式比基于标注的模式更加轻便,但是它依赖统计推断,所以需要更多的数据以便获取足够的准确性。很明显,基于标注的模式关键缺点是需要有代码侵入。在我们的环境中,由于所有应用系统都使用相同的线程模型、控制流和 RPC 系统,所以我们可以将性能测量限制在小规模的公用库中,以此实现对开发人员有效透明的监控系统。
我们倾向于认为 Dapper 的跟踪是一个嵌入式的 RPC 树。然而,我们的核心数据模型并不局限于特定的RPC 框架;我们也能跟踪例如 Gmail SMTP 会话、来自外界的 HTTP 请求、对 SQL 服务器的查询等行为。正式一点说,Dapper 跟踪模型使用了树
、span
和 标注
。
在 Dapper 跟踪树中,树节点是基本单元,我们称之为 span
。节点之间的连线表示 span 与其父span
之间的关系。虽然节点在整个跟踪树中的位置是独立的,但 span 也是一个简单的时间戳日志,其中编码了这个 span 的开始时间、结束时间、RPC 时间数据、以及0或多个应用程序相关的标注,我们将在 2.3 节讨论这些内容。
(图-2. Dapper 跟踪树中5个 span 的因果和实时关系)
图2 阐释了 span 是如何构造成更大的跟踪结构的。Dapper 为每个 span 记录了一个可读的span name
、span id
和 parent id
,这样就能重建出一次分布式跟踪过程中不同 span 之间的关系。没有parent id 的 span被称为 根span
。一次特定跟踪的所有相关 span 会共享同一个通用的trace id
(trace id在图中没有绘出)。所有这些 ID 可能是唯一的 64 位整数。在一个典型的 Dapper 跟踪中,我们希望每个 RPC 对应一个 span,每一个组件层对应跟踪树上的一个层级。
(图-3. span 的详细视图)
图3 给出了 Dapper 跟踪 span 中记录的事件的更详细视图。这个 span 标示图 2 中更长的那次 Helper.Call
RPC 调用。Dapper 的 RPC 库记录下了 span 的开始时间和结束时间、RPC 的计时信息。如果应用程序负责人选择用他们自己的标注来注释这次跟踪(例如图中的foo
),那么这些信息也会跟随 span 的其他信息一起记录下来。
要着重强调的是,一个 span 中的信息可能来自多个不同的主机;实际上,每个 RPC span 都包含 client和 server 端的标注,这使得二主机span (two host span)
是最常见的情况。由于 client 和 server 的时间戳来自不同的主机,所以我们需要注意时钟偏差。在我们的分析工具中,我们利用了如下事实:RPC client 发送请求总是会先于 server 接受到请求,对于 server 响应也是如此。这样一来,RPC server 端的 span 时间戳就有了下限和上限。
通过对部分通用库进行性能测量,Dapper 能够做到在对应用程序开发者零干扰的情况下进行分布式路径跟踪:
跟踪上下文(trace context)
存储到ThreadLocal 中。跟踪上下文是一个小而容易复制的容器,里面包含了 trace id 和 span id 等 span属性。Dapper 的跟踪数据是语言无关的,生产环境中的许多跟踪结合了 C++ 和 Java 进程中的数据。在 3.2 节我们将讨论我们在实践中达到了何种程度的应用程序透明。
上述性能测量点足够推导出复杂分布式系统的跟踪细节,这使得 Dapper 的核心功能也适用于那些不可修改的 Google 应用程序。然而,Dapper 也允许应用程序开发者添加额外的信息,以丰富 Dapper 的跟踪数据,从而帮助监控更高级别的系统行为,或者帮助调试问题。我们允许用户通过一个简单的 API 来定义带时间戳的标注,其核心代码如图4 所示。这些标注支持任意内容。为了保护 Dapper 用户不至于意外加入太多日志,每个跟踪 span 都可配置一个标注量的上限。应用程序级别的标注是不能替代结构化的 span 信息以及 RPC 信息的。
(图-4. Dapper 标注 API 在 C++ 和 Java 中的通用使用模式)
除了简单的文本标注,Dapper 也支持 key-value map 的标注,给开发者提供更强的跟踪能力,例如维护计数器、记录二进制消息、传输任意用户自定义的数据。这些 key-value 标注可用于在分布式跟踪上下文中定义应用程序相关的对等类(equivalence classes)。
Dapper 的一个关键设计目标是低损耗,因为如果一个新工具的价值还未证实,而对性能有影响的话,服务运维人员是不会愿意去部署这个工具的。而且,我们还想要允许开发人员使用标注 API,而无需担心额外的损耗。我们同时也发现 web 服务确实对性能测量的损耗很敏感。所以,除了把 Dapper 的基本性能测量损耗限制得尽可能小,我们还通过仅记录一部分跟踪信息,来进一步降低损耗。我们将在 4.4 节详细讨论这种跟踪采样模式。
(图-5. Dapper 收集管道概览)
Dapper 的跟踪记录和收集管道分为三个阶段(如图5)。首先,把 span 数据写入(1) 到本地日志文件。然后 Dapper 守护进程从所有生产主机中将他们拉取出来(2),最终写入(3) 到 Dapper 的 Bigtable 仓库中。Bigtable 中的行表示一次跟踪,列表示一个 span。Bigtable 对稀疏表格布局的支持正适合这种情况,因为每个跟踪都可能有任意多个 span。跟踪数据收集即将应用程序二进制数据传输到中央仓库,其延迟中位数小于 15 秒。98 分位延迟呈现双峰形;大约 75% 时间里,98 分位延迟小于 2 分钟,但是在另外 25% 时间里可能会涨到几小时。
Dapper 还提供了一个 API 来简化对仓库中跟踪数据的访问。Google 开发者利用这个API来构造通用的或者特定应用程序的分析工具。5.1 节将介绍这个 API 的使用。
Dapper 系统在请求树 带外(out-of-band)
进行日志跟踪与收集。这样做有两个原因:首先,带内收集模式(in-band collection scheme)通过 RPC 响应头回传跟踪数据,这会影响应用的网络动态。Google 的许多大型系统里,一次跟踪有几千个 span 的情况并不少见。而即便是在大型分布式跟踪的根节点附近,RPC 响应仍然是相当小的:通常小于 10K。在这种情况下,带内跟踪数据会影响应用数据,并且使后续的分析结果产生偏差。其次,带内收集模式假定所有 RPC 调用时完美嵌套的。而我们发现许多中间件系统会在其后端服务返回最终结果前,返回一个结果给其调用者。带内收集系统不能适用于这种非嵌套的分布式执行模式。
记录 RPC payload 信息会丰富 Dapper 的跟踪能力,因为分析工具可能能从 payload 数据中找到导致性能异常的模式。然而在某些情况下,payload 数据可能会包含一些信息,这些信息不应该暴露给非授权内部用户,包括正在调试性能的工程师。
由于安全和隐私是不可忽略的问题,所以 Dapper 存储了 RPC 方法名,但不会存储任何 payload 数据。相反,应用级别的标注则提供了一个方便的可选机制:应用开发人员可以选择将那些对以后分析有用的任何数据关联到一个 span 上。
Dapper 还提供了一些设计者没料到的安全性好处。例如 Dapper 通过跟踪公开的安全协议参数,用来监控应用是否满足认证或加密的安全策略。Dapper 还可以提供信息以确保系统是否执行了预期的基于策略的隔离,例如承载敏感数据的应用不与未授权的系统组件交互。这种方法可比代码审核强多了。
我们把 Dapper 作为生产环境跟踪系统超过两年了。本节我们将汇报 Dapper 系统的状态,着重讲解Dapper 如何很好地满足大范围部署、应用级透明等目标的。
Dapper 代码中最关键的部分也许就是对基础 RPC、线程、控制流库的性能测量了,包含创建 span、采样以及记录到本地磁盘。我们的代码不仅需要轻量,还需要稳定、健壮,因为它与海量应用连接,维护和 bug 修复是很困难的。我们的C++ 性能测量的核心代码少于 1000 行,而 Java 代码则少于 800 行。key-value 标注的代码实现额外有 500 行代码。
Dapper 的渗透率可以通过两方面来衡量:其一是可以产生 Dapper 跟踪的生产环境进程比率(即与 Dapper 性能测量运行时库连接的那些),其二是运行 Dapper 跟踪收集守护进程的生产环境机器比率。Dapper 守护进程是我们基本机器镜像的一部分,所以实际上它在 Google 的每台服务器上都有。很难确定 Dapper-ready 进程精确比率,因为那些不产生跟踪信息的进程是对 Dapper 不可见的。尽管如此,因为 Dapper 性能测量库几乎无处不在,我们估么着几乎每一个 Google 生产环境进程都支持跟踪。
在有些情况下 Dapper 不能正确地跟踪控制流程。这通常是由于使用了非标准的控制流程,或是由于Dapper 错误地将因果关系归到无关的事件上。Dapper 提供了一个简单的库作为一种变通方法,可以帮助开发者手动控制跟踪的传播。目前有 40 个 C++ 应用和 33 个 Java 应用需要手工的跟踪传播,这对总计几千个应用来说只是很小的一部分。还有很小一部分程序使用的是没有性能测量的通讯库(例如通过原生 TCP Socket 或者 SOAP RPC),所以是不支持 Dapper 跟踪的。但如果真的需要的话,这些应用也可以做到支持 Dapper。
为了生产环境的安全性,Dapper 跟踪是可以被关闭的。实际上在早期它默认是关闭的,直到我们对Dapper 的稳定性和低损耗有信心之后,我们才把它开启了。Dapper 团队偶尔会进行审计检查配置文件的变化,找到那些关闭了跟踪配置的服务。这种变化很少见,并且通常是因为担心监控的消耗。经过对实际消耗的进一步调查和衡量,发现其消耗已经很小了,所以现在这些改动都已经被回退回去了。
程序员们喜欢用应用程序特定的标注来作为一种分布式调试日志文件,或者通过应用程序的特定功能来对跟踪进行分类。例如所有 Bigtable 的请求都标注了访问的表名。目前 Dapper 中 70% 的 span 和 90% 的 trace 都至少有一个应用指定的标注。
我们有 41 个 Java 应用和 68 个 C++ 应用添加了自定义的标注以便更好地理解 span 内的行为。值得注意的是 Java 开发者在每个 span 上加的标注比 C++ 开发者更多,这也许是因为 Java 的负载更接近最终用户;这类应用经常处理更广的请求,所以控制路径也相对更复杂。
跟踪系统的成本是由于生成追踪和收集数据造成的系统性能下降,以及用来存储和分析跟踪数据的资源量。尽管你可以说一个有价值的跟踪系统即便造成一点性能损耗也是值得的,但是我们相信如果基线损耗达到可以忽略的程度,那么一定会对跟踪系统的最初推广大有裨益。
本节我们将展示 Dapper 性能测量操作的消耗、跟踪收集的消耗、以及 Dapper 对生产环境负载的影响。同时还会介绍 Dapper 的适应性采样机制是如何帮助我们平衡低损耗的需求与代表性跟踪的需求。
跟踪生成的损耗是 Dapper 性能影响中最重要的部分,因为收集和分析可以在紧急情况下关闭掉。Dapper 运行库生成跟踪的消耗最重要的原因是创建销毁 span 和标注、以及记录到本地磁盘以便后续的收集。非根 span 的创建和销毁平均需要 176 纳秒,而根 span 则需要 204 纳秒。这个差别是因为要对根 span 分配全局唯一 trace id 的时间。
如果一个 span 没有被采样的话,那么额外标注的成本则几乎可以忽略不计,只需 Dapper 运行时在ThreadLocal 中查找,平均花费 9 纳秒。而如果这个 span 被采样的话,对 span 进行字符串标注(见图4)则平均需要花费 40 纳秒。这些数据都是基于 2.2GHz x86 服务器测量得出的。
Dapper 运行时库最昂贵的操作就是写入本地磁盘了,不过这个损耗可以大大减少,因为每个磁盘都会合并对多个日志文件写入操作,并且相对于被跟踪的应用系统来说是异步执行的。尽管如此,日志写入对高吞吐量系统仍然可能有可见的性能影响,尤其是当所有请求都都被跟踪时。在 4.3 节我们对一次Web 搜索过程中生成跟踪的损耗进行了量化。
读出本地的跟踪数据也会对正在监控的系统产生影响。表1 展示了在高于实际负载的测试情况下,Dapper 守护进程 CPU 使用率的最坏情况。
Process Count (per host) | Data Rate (per process) | Daemon CPU Usage (single CPU core) |
---|---|---|
25 | 10K/sec | 0.125% |
10 | 200K/sec | 0.267% |
50 | 2K/sec | 0.130% |
(表-1. Dapper 守护进程在负载测试中 CPU 资源使用率)
Dapper 守护进程在跟踪收集过程中,对生产环境单核 CPU 的占用率从未超过 0.3%,并且内存占用也很小。同时我们把 Dapper 守护进程在内核 scheduler 中的优先级限制到尽可能最低,以防在高负载机器上出现 CPU 竞争。
Dapper 对网络资源的消耗也轻量,我们仓库中每个 span 平均只有 426 byte。Dapper 跟踪数据在Google 生产环境中占用的网络流量小于 0.01%。
高吞吐量的在线服务处理每个请求都会用到大量的机器,这种在线服务最有需求进行高效的跟踪;他们会生成大量的跟踪数据,同时也对性能影响是最敏感的。在表2 中我们用 web 搜索集群作为例子,通过调整采样率,来测量 Dapper 对平均延迟和吞吐量的性能影响。
Sampling frequency | Avg. Latency (% change) | Avg. Throughput (% change) |
---|---|---|
1/1 | 16.3% | -1.48% |
½ | 9.40% | -0.73% |
¼ | 6.38% | -0.30% |
1/8 | 4.12% | -0.23% |
1/16 | 2.12% | -0.08% |
1/1024 | -0.20% | -0.06% |
(表-2. Dapper 采样频率对 Web 搜索集群延迟及吞吐量的影响。延迟及吞吐量的实验误差分别为 2.5% 和 0.15%)
可以看到,虽然对吞吐量的影响不是很明显,但为了避免明显的延迟,跟踪采样还是非常有必要的。然而,当采样率小于 1/16 时的延迟和吞吐量损失是在实验误差范围内的。实践中,我们发现对于高容量服务来说,即便把采样率设为 1/1024 这样低,仍然能够得到足够多的跟踪数据。保持 Dapper 的性能损耗基线极其低是很重要的,因为这就能为应用程度提供一个宽松的环境来使用完整的标注 API,而无需担心性能损失。使用低采样率还有一个额外的好处,可以让主机磁盘中的数据在被垃圾回收之前能持久化更长的时间,这就让收集组件有了更多的灵活性。
Dapper 对于任何给定进程的损耗是与单位时间内进程跟踪的数目成正比的。Dapper 的第一个生产版本在 Google 的几乎所有进程上使用同一个采样率,每 1024 个候选中平均采样一个。这个简单的方案对高吞吐量在线服务是有效的,因为大多数我们感兴趣的事件仍然会经常出现并被捕捉到。
然而,低流量的服务在这种低采样率下就可能会错失重要的事件,而更高采样率带来的性能损耗是可接受的。针对这种系统的解决方案是覆盖默认采样率,而这就需要手工干预,我们不想在 Dapper 中出现这种手工干预。
我们正在部署一种适应性的采样机制,不使用统一的采样率,而使用单位时间内的期望采样率。这样,低流量负载会自动提高采样率,而高流量负载则会自动降低采样率,从而掌控损耗。实际采样率会和跟踪数据一起记录下来;这有利于在基于 Dapper 数据的分析工具中精准使用采样率。
Dapper 新用户往往觉得低采样率(高流量服务中通常会低于 0.01%)会干扰他们的分析。我们在Google 中应用的经验让我们相信,对于高吞吐量服务来说,激进采样并不会妨碍最重要的那些分析。如果一个重要的执行模式在这种系统中出现过一次,那么就会出现上千次。每秒请求几十次而不是上万次的那些低流量服务则可以承受跟踪每一个请求;这驱动着我们往适应性采样方向前进。
上述采样机制用来尽量减少与 Dapper 运行时库协作的应用程序中的性能损耗。Dapper 团队还需要控制写入中央仓库的数据量,为此我们引入了第二轮采样。
目前我们生产集群每天产生超过 1 TB 的采样跟踪数据。Dapper 用户希望跟踪数据从生产进程中记录下来后最少保留两周时间。逐渐增长的跟踪数据带来了好处,同时 Dapper 仓库的机器和磁盘存储成本也在增加,我们需要作出权衡。对请求的高采样率还会使得 Dapper 收集器接近 Dapper Bigtable 仓库的写入吞吐量极限。
为了维持物资资源的需求和 Bigtable 的累积写入吞吐量之间的灵活性,我们在收集系统自身上增加了额外的采样。一个特定 trace 中的所有 span 都共享同一个trace id,即便这些span可能横跨数千个不同的主机。对于在收集系统中的每个 span,我们将其 trace id 哈希成一个标量 z (0<=z<=1)。如果 z 小于我们的收集采样系数,我们就保留这个 span 并将它写入 Bigtable;否则就丢弃。在采样决策中通过依靠 trace id,我们要么采样整个 trace,要么抛弃整个 trace,而不会对 trace 中的某些span进行处理。我们发现这种额外配置参数让我们对收集管道的管理变得简单得多,因为可以很容易地调整全局写入率,仅仅修改配置文件中的一个参数即可。
如果整个跟踪和收集系统都是用同一个采样参数则会更简单,但是那样就无法灵活地快速调整所有部署环境中的运行时采样配置。我们选择的运行时采样率产生的数据会稍微高于我们能写入仓库的数据,而我们可以通过调整收集系统中的二级采样参数对写入速度进行限流。因为我们可以通过对二级采样配置一下就能增加或减少全局覆盖率和写入速率,所以 Dapper 管道的维护工作变得更简单了。
几年前当 Dapper 还是一个原型时,在开发者的耐心支持下才能把 Dapper 用起来。从那时起,我们逐渐建立了收集组件、编程接口、以及基于 web 的用户交互界面,帮助 Dapper 用户独立地解决自己的问题。本节将总结哪些方法有用,哪些没用,并提供这些通用的分析工具的基本使用信息。
Dapper Deport API 又称 DAPI,通过它可以直接访问 Dapper 区域仓库中的分布式跟踪数据。DAPI 和 Dapper 跟踪仓库是串行设计的,DAPI 意在为 Dapper 仓库中的原始数据提供一个干净而直观的接口。我们的用例推荐如下三种方式来访问跟踪数据:
通过trace id访问(Access by trace id):DAPI 可以根据全局唯一的 trace id 来加载任何一次跟踪。
批量访问(Bulk access):DAPI 可通过 MapReduce 来并行访问数亿条 Dapper 跟踪数据。用户重写一个虚拟函数,它的唯一参数接受一个 Dapper 跟踪信息,然后框架将会对用户指定时间窗口内的每一条跟踪信息调用一次该函数。
索引访问(Indexed access):Dapper 仓库支持一个唯一索引,可用于匹配我们通用的访问模式。该索引将通用请求的跟踪特性映射到特定的 Dapper 跟踪。因为 trace id 是伪随机创建的,所以这是快速访问某个特定服务或特定主机追踪信息的最佳方式。
所有这三种访问方式都将用户引导到特定的 Dapper 追踪记录。正如 2.1 节所述,Dapper 的跟踪信息是由 trace span 组成的树,所以 Trace
数据结构就是一个由不同 Span
结构组成的遍历树。Span 通常对应 RPC 调用,在这种情况下,RPC 的耗时信息是有的。通过 span 结构还可访问基于时间戳的引用标注信息。
选择合适的用户索引是DAPI 设计中最具挑战性的部分。索引要求的压缩存储只比实际数据本身小 26%,所以成本是巨大的。最初我们部署了两个索引:一个是主机索引,另一个是服务名索引。然而我们发现相对于存储成本来说,用户对主机索引的兴趣尚不足够。当用户对某台机器的跟踪感兴趣的时候,他们也会对特定的服务感兴趣,所以我们最终将这两个索引合并成一个组合索引,允许按服务名、主机、时间戳高效地进行查找。
Dapper 在 Google 的使用有三类:使用 DAPI 的持久在线 web 应用,可在命令行启动的维护良好的基于 DAPI 的工具,以及编写、运行、然后即被遗忘的一次性分析工具。我们知道的有3 个基于DAPI的持久性应用、8个基于DAPI的分析工具、约15~20个一次性分析工具。在这之后就很难统计这些工具了,因为开发者可以构建、运行、然后丢弃,而不需要让 Dapper 团队知道。
绝大多数情况下,人们通过基于 web 的用户交互接口来使用 Dapper。篇幅所限我们不能展示每一个特性,不过图6 列出了一个典型的用户工作流。
(图-6. 通用 Dapper 用户接口中的一个典型用户工作流)
对于想查询实时数据的用户,Dapper 用户界面支持直接与每台生产环境服务器上的守护进程通信。在这个模式下,不能像上图那样查看系统级别的图表,不过仍然很容易地基于耗时和网络特性选择一个跟踪。在这个模式下,可在几秒内实时地查到数据。
根据我们的日志,每个工作日大概有 200 个 Google 工程师使用 Dapper UI;每周大约有 750 到 1000个独立用户访问。忽略掉发布新功能的因素,这个数据每个月都是一致的。用户通常会发送出特定跟踪的链接,这会不可避免地在跟踪查询中产生很多一次性的、短期的流量。
Dapper 在 Google中被广泛使用,通过 Dapper 用户界面直接访问,或者通过编程 API 以及基于这些API 构建的程序访问。本节我们不打算罗列出每一种已知的 Dapper 的使用方式,而会尝试讲解 Dapper 使用的"基本向量",阐述何种应用是最成功的。
Google AdWords 系统建立在关键词定位准则和相关文字广告的大型数据库之上。当新的关键词被插入或修改时,必须对他们进行校验,以遵循服务策略条款(例如检查不恰当的语言);这个过程使用自动审查系统来做的话会更有效率。
当从头开始重新设计一个广告审查服务时,团队从第一个系统原型开始,直到最终的系统维护,都使用了 Dapper。他们的服务通过 Dapper 有了以下方面的提高:
性能(Performance):开发人员跟踪请求延迟目标的进度,精确找到可优化的机会。Dapper 还被用来找出关键路径中的不必要请求序列(这种不必要请求通常源于不是开发者自己开发的子系统),然后促使相关团队修复这些问题。
正确性(Correctness):广告审查服务是围绕大型数据库系统的。系统同时具有只读副本服务器(廉价访问),以及可读写的主服务器(昂贵访问)。他们通过 Dapper 找到了好些不必要地访问主服务器而不是访问副本服务器的查询。Dapper 现在可用于解释主服务器被直接访问的原因,确保重要系统的不变式。
理解性(Understanding):广告审查查询跨越多种类型的系统,包括 Bigtable(即前文提到的数据库)、多维索引服务、以及许多其他 C++ 和 Java 后端服务。Dapper 跟踪用来评估总查询成本,促进对业务重新设计,使得系统依赖的负载最小。
测试(Testing):新代码的发布会经过一个 Dapper 跟踪的 QA 过程,验证正确的系统行为和性能。这个过程中发现了很多问题,包括广告审查代码自身的问题,及其依赖包的问题。
广告审查团队广泛使用了 Dapper 标注 API。Guice[13] 开源的 AOP 框架用来在重要的软件组件上标注 @Traced
。跟踪信息进一步标注的信息有重要子程序的输入输出大小、状态消息、以及其他调试信息;否则这些信息会被发到日志文件中。
Dapper 在广告审查团队的应用有一些不足的地方。例如,他们想在交互时间内搜索所有的跟踪标注,然而必须运行自定义的 MapReduce 或者手工检查每个跟踪。另外,Google 内还有其他的系统对通用目的的调试日志进行收集并进行集中化,把这些系统中的海量数据和 Dapper 仓库进行整合是有价值的。
即便如此,总的来说广告审查团队估计通过 Dapper 跟踪平台的数据分析,他们的延迟数据已经优化了两个数量级。
Google 维护了一个从运行进程中不断收集并集中异常报告的服务。如果这些异常发生在被采样的Dapper 跟踪中,则异常报告中会包含相关的 trace id 和 span id。然后异常监控服务前端就会在特定异常报告里提供一个链接,指向相应分布式跟踪。广告审查团队利用这个特性,来了解异常监控服务发现的那些 bug 的更大范围的上下文。Dapper 平台通过导出基于简单唯一 ID 构建的接口,相对容易地集成到其他事件监控系统中。
由于移动部件的数量、代码库及部署的规模,调试一个像全文搜索(universal search)那样的服务是非常有挑战性的。这里我们描述在减轻全文搜索延迟分布的长尾效应上做的努力。Dapper 能够验证端到端延迟的假设,更具体地说,它能够验证全文搜索请求的关键路径。当系统不仅涉及多个子系统,还涉及多个开发团队时,即便我们最好最有经验的工程师也经常猜错端到端性能差的根本原因。在这种情况下,Dapper 可以提供必需的事实,可以回答许多重要的性能问题。
一个工程师在调试长尾延迟的过程中建立了一个小型库,可以根据 DAPI Trace
对象推断出层次性的关键路径。这些关键路径结构可用来诊断问题、为全文搜索可预期的性能改进调整优先级。Dapper 的这项工作引出了下列发现:
在任意指定时刻,Google 的典型计算集群是成千上万个逻辑"任务"组成;一系列进程执行通用函数。Google 维护着许多这种集群,当然我们发现一个计算集群中的任务往往依赖其他集群中的任务。由于任务间的依赖是动态改变的,所以不可能仅仅从配置信息中推断出所有的服务间依赖。尽管如此,公司内部的许多进程要求知道准确的服务依赖信息,以便找出瓶颈,计划服务的迁移。Google 的"服务依赖"项目通过使用跟踪标注以及 DAPI MapReduce 接口,自动探测服务间的依赖。
使用 Dapper 核心性能检测以及 Dapper 的跟踪标注,服务依赖项目能够推断出任务之间的依赖关系,还能推断出这些任务所依赖的程序组件。例如,所有 Bigtable 的操作被标记上受影响的表名。通过 Dapper 平台,服务依赖团队就可以自动推断出多种服务粒度的依赖关系。
Google 在网络结构上投入了大量的人力物力。毫无疑问,网络运维人员要关注单个硬件的监控信息、自定义工具和 dashboard,来查看全局网络使用情况的鸟瞰图。网络运维人员可以一览整个网络的健康状况,但是当出现问题时,他们却缺少工具找到网络负载问题在应用级别的罪魁祸首。
虽然 Dapper 并不是设计用来做链路级的监控,但我们发现它非常适合集群之间网络活动应用级别分析的任务。Google 利用 Dapper 平台得以建立不断更新的终端,来显示集群间网络流量中最活跃的那些应用级别端点。此外,通过 Dapper 我们可以找出引起昂贵网络请求的跟踪,而不是面对孤立的机器。在 Dapper API 之上建立 dashboard 花费的时间没超过两周。
Google 的许多存储系统都由多个独立的复杂层次的分布式基础设施组成。例如,Google App Engine[5] 就是建立在一个可扩展实体存储系统之上。这个实体存储系统基于底层的 BigTable 暴露出一些 RDBMS 功能。Bigtable 则同时使用 Chubby[7](一个分布式锁系统)及 GFS。此外,像 BigTable这类系统会作为共享服务来管理,以简化部署并更好地利用计算资源。
在这种分层系统中,并不总是很容易发现终端用户的资源消费模式。例如,给定 BigTable 单元对 GFS 的大量请求可能来自一个用户或者许多用户,而在 GFS 层面这两种不同的使用模式的区别是模糊的。而且,如果缺乏像 Dapper 这种工具的话,对这种共享服务的竞争同样是难以调试的。
5.2节展示的 Dapper 用户界面可以分组聚合共享服务横跨多个客户端的跟踪性能信息。这就使得共享服务的负责人可以容易地根据多个指标对其用户进行排名(例如根据inbound网络负载、outbound网络负载、或者服务请求的总时间)。
Dapper 对于某些救火任务是有用的。这里的"救火"指的是对处于危险中的分布式系统进行的操作。典型情况下,Dapper 用户在进行救火时需要访问新鲜数据,并且没有时间写新的 DAPI 代码,也没时间等待周期性的报告运行。
对于那些正在经历高延迟的服务,或者更糟的在正常负载下都会超时的服务,Dapper 用户界面通常能把这些延迟的瓶颈隔离出来。通过与 Dapper 守护进程直接通信,可以容易地收集特定高延迟跟踪的新鲜数据。在灾难性故障时,通常没必要分析统计数据来确定根本原因,而查看示例跟踪就足够了。
然而,6.5 节描述的那种共享存储服务则要求当用户活动突然激增时能快速聚合信息。对于事后检验,共享服务仍然可以利用 Dapper 的聚合数据,但是除非可以在十分钟之内完成对 Dapper 数据的批量分析,否则 Dapper 对共享存储服务的救火就不会那么有用了。
虽然我们在 Dapper 上的经验已经基本满足我们的预期,但是也有一些积极的方面是我们没有充分预料到的。我们对非计划中的用例数目感到高兴。除了在第6节描述的一些经验外,还包括资源核算系统,用来检查敏感服务是否遵从指定的通讯模式的工具,RPC 压缩策略的分析工具,等等。这些非计划中的用例一定程度上归功于我们通过一个简单的编程接口开放了跟踪数据存储,这就允许我们利用上这个大得多的社区的创造力。Dapper 对旧系统的支持也比预期更简单,只需要基于新版本的库重新编译即可,这个库提供通用线程、控制流和 RPC 框架。
Dapper 在 Google 内部的广泛使用还为我们提供了关于其局限性的宝贵反馈。下面我们将介绍一些我们已知的最重要的一些不足之处。
合并的影响(Coalescing effects):Dapper 模型隐式地设想不同子系统一次只会处理一个跟踪请求。在某些情况下,在对一组请求执行操作之前缓冲一些请求会更有效率(例如对磁盘写入进行合并)。在这些情况下,一个跟踪请求可以看做是一个大型工作单元(a traced request can be blamed for a deceptively large unit of work)。此外,如果多个跟踪请求被批量执行,那么只会有一个请求被 span使用,这是因为我们我们对每个跟踪只会有一个唯一 trace id(if multiple traced requests are batched together, only one of them will appear responsible for the span due to our reliance on a single unique trace id for each trace)。我们正在考虑解决方案以识别这种情况,并记录最少的信息来区别这些请求。
跟踪批处理系统(Tracing batch workloads):Dapper 的设计是针对在线服务系统,最初的目标是了解 Google 的用户请求引起的系统行为。然而,离线的数据密集型系统也可以从对性能的洞悉中获益,例如适合 MapReduce 模型的系统。在这种情况下,我们需要把 trace id 关联到一些其他的有意义的工作单元,例如输入数据的 key(或key范围),或是一个 MapReduce shard。
寻找根本原因(Finding a root cause):Dapper 可以有效地确定系统中的哪个部分正在经历速度变慢,但并不总是足够找出问题的根本原因。举个例子,一个请求变慢可能并不是因为他自己的行为,而是因为其他请求还排在他前面。程序可以利用应用级别的标注把队列大小和过载情况转播到跟踪系统。同时,如果这种情况很常见,那么在ProfileMe[11] 中提出的成对采样技术就很有用了。它对两个时间重叠的请求进行采样、并观察它们在系统中的相对延迟。
记录内核级别的信息(Logging kernel-level information):内核可见事件的详细信息有时对确定问题根本原因很有用。我们有一些工具能够跟踪或者描述内核的执行,但是要想将这些信息绑定到用户级别的跟踪上下文上,用通用或是不那么突兀的方式是很难的。我们正在研究一种可能的妥协方案,对用户层面上的一些内核级别活动参数做快照,将其关联到一个活动 span 上。
在分布式系统跟踪领域,有一套完整的体系,一些系统主要关注定位到故障位置,另一些系统关注性能优化。Dapper 曾被用于故障发现,但它在发现性能问题、提升对大型复杂系统行为的理解方面更有用。
Dapper 与黑盒监控系统有关,就像 Project5[1]、WAP5[15] 和 Sherlock[2],黑盒监控系统不依赖于运行时库的性能测量,能够实现更高度的应用级透明。黑盒的缺点是有些不精确,并在统计推断因果路径过程中可能损耗更大。
对分布式系统的监控来说,显式的基于标注的中间件或应用本身的性能测量或许是更受欢迎的方式。Pip[14] 和 Webmon[16] 更依赖于应用级的标注,而 X-Trace[12]、Pinpoint[9] 和 Magpie[3] 则侧重对库和中间件的修改。Dapper 更接近后者。Dapper 与 Pinpoint、X-Trace 以及最新版本的 Magpie 类似,使用全局 ID 将分布式系统不同部分的相关事件关联起来。同样和这些系统类似,Dapper 把性能测量隐藏在通用软件模块中,尝试避免标注应用程序。Magpie 放弃使用全局 ID,就不用处理正确传播全局 ID 带来的挑战,而是为每个应用写入事件模式(event schema)
并显式地描述事件之间的关系。我们不清楚 schema 在实践中实现透明性到底有多有效。X-Trace 的核心标注需求比 Dapper 更有雄心,不仅在节点边界收集跟踪,还在节点内部不同软件层级间收集跟踪。而我们对于性能测量低损耗的要求迫使我们不能采用这种模式,而是朝着把一个请求连接起来完整跟踪所能做到的最小代价而努力。Dapper 跟踪仍然能通过可选的应用标注来扩展。
本文介绍了 Google 生产环境下的分布式系统跟踪平台 Dapper,并汇报了我们开发和使用 Dapper 的经验。Dapper 部署在 Google 的几乎所有系统上,使得大型系统得以被跟踪而无需修改应用程序,同时没有明显的性能影响。通过 Dapper 主跟踪用户界面的受欢迎程度可以看出 Dapper 对开发团队和运维团队的实用性,本文通过一些使用场景的例子也阐明了 Dapper 的实用性,甚至有些使用场景 Dapper 的设计者都未曾预料到。
据我们所知,本文是第一篇汇报一个大型的生产环境下的分布式系统跟踪框架的论文。实际上我们主要的贡献源于这样一个事实:我们汇报回顾的系统已经被使用超过两年了。我们发现,决定结合最小化应用透明的跟踪功能以及对程序员提供简单的 API 来增强跟踪是非常值得的。
我们相信,Dapper 比之前基于标注的分布式跟踪系统达到了更高的应用级透明性,只需要很少的人工干预。虽然这也归功于我们计算部署的一定程度上的同质性,但仍然是一个重大的挑战。最重要的是,我们的设计提出了一些实现应用级透明的充分条件,我们希望能够对更异质的环境下的解决方案有所帮助。
最后,通过把 Dapper 跟踪仓库开放给内部开发者,促使了更多分析工具的产生,而仅仅由 Dapper 团队封闭地独自开发肯定产生不了这么多工具,这大大提高了设计和实现的成就。
We thank Mahesh Palekar, Cliff Biffle, Thomas Kotzmann, Kevin Gibbs, Yonatan Zunger, Michael Kleber, and Toby Smith for their experimental data and feedback about Dapper experiences. We also thank Silvius Rus for his assistance with load testing. Most importantly, though, we thank the outstanding team of engineers who have continued to develop and improve Dapper over the years; in order of appearance, Sharon Perl, Dick Sites, Rob von Behren, Tony DeWitt, Don Pazel, Ofer Zajicek, Anthony Zana, Hyang-Ah Kim, Joshua MacDonald, Dan Sturman, Glenn Willen, Alex Kehlenbeck, Brian McBarron, Michael Kleber, Chris Povirk, Bradley White, Toby Smith, Todd Derr, Michael De Rosa, and Athicha Muthitacharoen. They have all done a tremendous amount of work to make Dapper a day-to-day reality at Google.
[1] M. K. Aguilera, J. C. Mogul, J. L. Wiener, P. Reynolds, and A. Muthitacharoen. Performance Debugging for Dis- tributed Systems of Black Boxes. In Proceedings of the 19th ACM Symposium on Operating Systems Principles, December 2003.
[2] P. Bahl, R. Chandra, A. Greenberg, S. Kandula, D. A. Maltz, and M. Zhang. Towards Highly Reliable Enter- prise Network Services Via Inference of Multi-level De- pendencies. In Proceedings of SIGCOMM, 2007.
[3] P.Barham,R.Isaacs,R.Mortier,andD.Narayanan.Mag- pie: online modelling and performance-aware systems. In Proceedings of USENIX HotOS IX, 2003.
[4] L. A. Barroso, J. Dean, and U. Ho ̈lzle. Web Search for a Planet: The Google Cluster Architecture. IEEE Micro, 23(2):22–28, March/April 2003.
[5] T. O. G. Blog. Developers, start your engines. http://googleblog.blogspot.com/2008/04/developers- start-your-engines.html, 2007.
[6] T. O. G. Blog. Universal search: The best answer is still the best answer. http://googleblog.blogspot.com/2007/05/universal- search-best-answer-is-still.html, 2007.
[7] M. Burrows. The Chubby lock service for loosely- coupled distributed systems. In Proceedings of the 7th USENIX Symposium on Operating Systems Design and Implementation, pages 335 – 350, 2006.
[8] F. Chang, J. Dean, S. Ghemawat, W. C. Hsieh, D. A. Wal- lach, M. Burrows, T. Chandra, A. Fikes, and R. E. Gru- ber. Bigtable: A Distributed Storage System for Struc- tured Data. In Proceedings of the 7th USENIX Sympo- sium on Operating Systems Design and Implementation (OSDI’06), November 2006.
[9] M. Y. Chen, E. Kiciman, E. Fratkin, A. fox, and E. Brewer. Pinpoint: Problem Determination in Large, Dynamic Internet Services. In Proceedings of ACM In- ternational Conference on Dependable Systems and Net- works, 2002.
[10] J. Dean and S. Ghemawat. MapReduce: Simplified Data Processing on Large Clusters. In Proceedings of the 6th USENIX Symposium on Operating Systems Design and Implementation (OSDI’04), pages 137 – 150, December 2004.
[11] J. Dean, J. E. Hicks, C. A. Waldspurger, W. E. Weihl, and G. Chrysos. ProfileMe: Hardware Support for Instruction-Level Profiling on Out-of-Order Processors. In Proceedings of the IEEE/ACM International Sympo- sium on Microarchitecture, 1997.
[12] R. Fonseca, G. Porter, R. H. Katz, S. Shenker, and I. Sto- ica. X-Trace: A Pervasive Network Tracing Framework. In Proceedings of USENIX NSDI, 2007.
[13] B. Lee and K. Bourrillion. The Guice Project Home Page. http://code.google.com/p/google-guice/, 2007.
[14] P. Reynolds, C. Killian, J. L. Wiener, J. C. Mogul, M. A. Shah, and A. Vahdat. Pip: Detecting the Unexpected in Distributed Systems. In Proceedings of USENIX NSDI, 2006.
[15] P. Reynolds, J. L. Wiener, J. C. Mogul, M. K. Aguilera, and A. Vahdat. WAP5: Black Box Performance Debug- ging for Wide-Area Systems. In Proceedings of the 15th International World Wide Web Conference, 2006.
[16] P. K. G. T. Gschwind, K. Eshghi and K. Wurster. Web- Mon: A Performance Profiler for Web Transactions. In E-Commerce Workshop, 2002.
]]>可以看出,JVM主要由以下几部分组成: - 类加载器子系统 - 运行时数据区(内存空间) - 执行引擎 - 本地方法接口
运行时数据区又分为: 1. 程序计数器 2. Java栈 3. 本地方法栈 4. 方法区 5. 堆
其中 方法区 和 堆 是所有Java线程共享的,而Java栈、本地方法栈、PC寄存器则由每个线程私有。
程序计数器可以看做是当前线程所执行的字节码的行号指示器。 字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令。
线程私有: - 为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
OOM: - 不会出现OOM。
Java栈描述的是Java方法执行的内存模型。 Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。 Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。
线程私有: - 前面已经提到Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区。
OOM: - 如果线程请求的栈深度大于JVM所允许的深度,则抛出StackOverflowError。 - 如果VM可以动态扩展,但是扩展是无法申请到足够的内存,则抛出OutOfMemoryError。 - 可以通过减少-Xss,同时递归调用某个方法,模拟StackOverflowError
它分为三部分:局部变量区、操作数栈、帧数据区。
局部变量区是以字长为单位的数组,在这里,byte、short、char类型会被转换成int类型存储,除了long和 double类型占两个字长以外,其余类型都只占用一个字长。特别地,boolean类型在编译时会被转换成int或byte类型,boolean数组会被当做byte类型数组来处理。局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。 局部变量区包含了 方法参数 和 局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。
操作数栈也是以字长为单位的数组,但是正如其名,它只能进行入栈出栈的基本操作。在进行计算时,操作数被弹出栈,计算完毕后再入栈。
帧数据区的任务主要有: 记录指向类的常量池的指针,以便于解析。 帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操作数栈中。 记录异常表,发生异常时将控制权交由对应异常的catch子句,如果没有找到对应的catch子句,会恢复调用方法的栈帧并重新抛出异常。
局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依此分配栈帧内存,压入Java栈。
本地方法栈类似于Java栈,主要存储了本地方法调用的状态。 在Sun JDK中,本地方法栈和Java栈是同一个。
线程私有: - 同上
OOM: - 同上
存储 类型信息 和 类的静态变量。 在Sun JDK中,方法区对应了永久代(Permanent Generation),默认最小值为16MB,最大值为64MB。
方法区中对于每个类存储了以下数据: - 类及其父类的全限定名(java.lang.Object没有父类) - 类的类型(Class or Interface) - 访问修饰符(public, abstract, final) - 实现的接口的全限定名的列表 - 常量池 - 字段信息 - 方法信息 - 静态变量 - ClassLoader引用 - Class引用
可见类的所有信息都存储在方法区中。
线程共享: - 由于方法区是所有线程共享的,所以必须保证线程安全,举例来说,如果两个类同时要加载一个尚未被加载的类,那么一个类会请求它的ClassLoader去加载需要的类,另一个类只能等待而不会重复加载。
OOM: - 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。 ——OSGi这种频繁自定义ClassLoader的场景,需要虚拟机剧本类卸载功能,以保证永久代不会溢出。 - 通过限制永久代大小-XX:PermSize, -XX:MaxPermSize;同时大量添加常量池;或借助CGLib生成大量动态类,可以模拟OutOfMemoryError。 ——注:运行时添加常量池可用list.add(String.valueOf(i++).intern()) //Jdk7以下
堆用于存储 对象实例 以及 数组。 堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。
线程共享: - 堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。 - 此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。
OOM: - Java堆是垃圾收集器管理的主要区域。 - 如果堆中没有内存完成实例分配,并且堆也无法再扩展时,则抛出OutOfMemoryError。 - 可以通过减少-Xms, -Xmx;同时创建无数对象来模拟OutOfMemoryError。 - 同时-XX:+HeapDumpOnOutOfMemoryError,可以dump出当前的内存堆转储快照,以便分析。
在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为新生代、旧生代。分代方式大大改善了垃圾收集的效率。
大多数情况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时的对象复制(Minor GC的过程在此不详细讨论)。 JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配,我们知道在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。
在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。
]]>Please ref to the demo code: git clone https://github.com/AlphaWang/guava-demo.git
The Guava library has its history rooted in working with collections, starting out as google-collections
. The Google Collections Library has long since been abandoned, and all the functionality from the original library has been merged into Guava.
com.alphawang.guava.ch2.collection.Test1_Create
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 |
|
1 2 3 4 5 6 7 8 |
|
Immutable objects have many advantages, including:
com.alphawang.guava.ch2.collection.Test2_Transform com.alphawang.guava.ch2.collection.Test3_Filter
Transform List:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Transform Map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Filter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
com.alphawang.guava.ch2.collection.Test4_Convert
Maps.uniqueIndex
method uses Function to generate keys from the given values.Maps.asMap
method takes a set of objects to be used as keys, and Function is applied to each key object to generate the value for entry into a map instance.Maps.toMap
returns ImmutableMap.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 |
|
Please ref to the demo code: git clone https://github.com/AlphaWang/guava-demo.git
Many of Guava’s utilities are designed to fail fast in the presence of null rather than allow nulls to be used.
com.alphawang.guava.ch1.optional.Test1_FailFast
1 2 3 4 5 6 7 8 |
|
Additionally, Guava provides a number of facilities both to make using null
easier, when you must, and to help you avoid using null
.
see http://www.tutorialspoint.com/design_pattern/null_object_pattern.htm
If it’s an enum, add a constant to mean whatever you’re expecting null to mean here.
example 1, java.math.RoundingMode
has an UNNECESSARY
value to indicate “do no rounding, and throw an exception if rounding would be necessary.”
example 2: use Unit.NONE instead of null
.
1 2 3 4 5 6 7 8 9 |
|
For example, return a empty list instead of null.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
com.alphawang.guava.ch1.optional.Test2_Optional
Optional<T>
is a way of replacing a nullable T reference with a non-null value.
Besides the increase in readability that comes from giving null
a name, the biggest advantage of Optional is its idiot-proof-ness. It forces you to actively think about the absent case if you want your program to compile at all, since you have to actively unwrap the Optional and address that case.
there are two main use cases of Optional
.
1 2 3 4 |
|
client:
1 2 3 |
|
1
|
|
Enums#getIfPresent
can help to get an Optional
1 2 3 4 5 6 7 8 9 10 11 |
|
com.alphawang.guava.ch1.optional.Test3_Objects
MoreObjects.firstNonNull
is an alternative of Optional.or
, both can return a default value if the object is null.
1
|
|
com.alphawang.guava.ch1.optional.Test4_Preconditions
Of course we can fail fast in the presence of null, just like guava code. Guava provides Preconditions.checkArgument
and Preconditions.checkNotNull
to do this.
1 2 3 4 5 6 7 |
|
本文是《Solr In Action》读书笔记,包含第3章
Document
:A document is a collection of fields that map to particular field types defined in a schema.
Solr is a document storage and retrieval engine. Every piece of data submitted to Solr for processing is a document.
Inverted index
:Maps each word/term in the corpus to all of the documents in which it appears.
determin textually similar words, understand synonyms, remove unimportant words, score each result… Solr accomplishes all of this by using an index that maps content to documents instead of mapping documents to content as in a traditional database model.
通过Inverted Index,可以实现如下查询:
Required Terms
: new AND house; +new +house
Optional Terms
: new house; new OR house. (default)
Negated Terms
: new house NOT rental; new house -rental
Phrases
: “new home” OR “new house”
Grouped expressions
: New AND (house OR home)
为了支持Phrase查询:
Term Position
: recording of the relative position of terms within a document. can tell us where in the document each term appears.
存储在inverted index中,可用于支持
Phrase查询
。
Fuzzy matching
is defined as the ability to perform inexact matches on terms in the search index. For example,
Wildcard searching
search for any words that start with a particular prefix.
1 2 3 |
|
注意:
- the more characters you specify at the beginning of the term before the wildcard, the faster the query should run.
- 执行
leading wildcard
(*ing)会很耗时,可以通过ReversedWildcardFilterFactory
来解决,不过这样会增加index时间,并减慢整体查询速度。
Range searching
1 2 |
|
Fuzzy searching
/ Edit-distance searching
find spelling variations within one or two characters (handle spelling errors).
1 2 |
|
Solr provides the ability to handle character variations using edit-distance measurements based upon Damerau-Levenshtein distances, which account for more than 80% of all human misspellings.
An
edit distance
is defined as an insertion, a deletion, a substitution, or a transposition of characters.
Proximity searching
match two terms within some maximum distance of each other. e.g. search:
1
|
|
改进:
1
|
|
Solr内部用tem positions
来计算edit distance.
Solr calculaes a relevancy score
for each document and then sorting the search results from the high- est score to the lowest.
【Q】那么relevancy score是如何计算的?受哪些因素的影响呢?
【A】Similarity:
Similarity
class, which can be defined on a per-field basis in Solr’s schema.xml
.Similarity
is a Java class that defines how a relevancy score is calculated based upon the results of a query.实现原理:two-pass model
Boolean model
to filter out any documents that do not match the customer’s query.vector space model
for scoring and drawing the query as a vector, as well as an additional vector for each document.cosine
between the query vector and that document’s vector.【Q】显然,重点是如何算出合理的vector?
【A】算法如下:
tf
),idf
),t.getBoost
),norm
),coord
),queryNorm
)Term frequency (tf)
is a measure of how often a particular term appears in a matching document.并非线性关系;而用了平方根。见上图。
Inverse document frequency (idf)
, a measure of how “rare” a search term is, is calcu- lated by finding the document frequency (how many total documents the search term appears within), and calculating its inverse.Term frequency
and inverse document frequency
, when multiplied together in the relevancy calculation, provide a nice counterbalance :term frequency
elevates terms that appear multiple times within a document,inverse document frequency
penalizes those terms that appear commonly across many documents.
作用:减少common words对评分的影响。
If you have domain knowledge about your content—you know that certain fields or terms are more (or less) important than others—you can supply boosts at either indexing time or query time to ensure that the weights of those fields or terms are adjusted accordingly.
1 2 |
|
it’s possible to boost documents or fields within documents at index time
The default Solr relevancy formula calculates three kinds of normalization factors (norms):
field norms
,query norms
, and thecoord factor
.
field normalization factor (field norm)
is a combination of factors describing the importance of a particular field on a per-document basis.This byte packs a lot of information:
Query Norm
does not affect the overall relevancy ordering, as the same queryNorm is applied to all documents.
It merely serves as a normalization factor to attempt to make scores between queries comparable.
Its role is to measure how much of the query each document matches.
1
|
|
1
|
|
区别:
平衡:
Recall
across the entire result set;Precision
only within the first page (or few pages) of search results.Solr is able to scale to handle billions of documents and an infinite number of queries by adding servers.
denormalized document
is one in which all fields are self-contained within the document, even if the values in those fields are duplicated across many documents.
- 不同document的字段值会有重复。
- 与传统关系数据库不一样!
1 2 3 |
|
即:
partition
,shard
;将大量的数据分区到不同的core,然后通过aggregated search并行地同时在这些core上进行查询。
you can insert, delete, and update documents, but not sin- gle fields (easily).
whenever a new field is added to Solr or the contents of an existing field have changed, every single document in the Solr index must be reprocessed in its entirety before the data will be populated for the new field in all documents.
Solr is not optimized for processing quite long queries (thousands of terms) or returning quite large result sets to users.
本文是《Solr In Action》读书笔记,包含第1~2章
Apache Solr, is a specific NoSQL technology.
Solr is a scalable
, ready-to-deploy
enterprise search engine that’s optimized to search
large volumes
of text-centric
data and return results sorted by relevance
.
Scalable
— Solr scales by distributing work (indexing and query processing) to multiple servers in a cluster.Ready to deploy
— Solr is open source, is easy to install and configure, and provides a preconfigured example to help you get started.Optimized for search
— Solr is fast and can execute complex queries in subsecond speed, often only tens of milliseconds.Large volumes of documents
— Solr is designed to deal with indexes containing many millions of documents.Text-centric
— Solr is optimized for searching natural-language text, like emails, web pages, resumes, PDF documents, and social messages such as tweets or blogs.Results sorted by relevance
— Solr returns documents in ranked order based on how relevant each document is to the user’s query.Solr is:
Information retrieval engine inverted index; ranking documents by relevance
Flexible schema management xml-configuration
Java web application REST-like services
Multiple indexes in one server data partitioning
Extendable (plugins) Each subsystem (document management; query processing; text analysis) is composed of a modular “pipeline” that allows you to plug in new functionality
Scalable
Provides flexible cache-management features.
Query throughput: add replicas
of your index so that more servers can handle more requests.
The number of documents indexed: split the index into smaller chunks called shards
, then distribute the searches across the shards.
Fault-tolerant
Solr are optimized to handle data exhibiting four main characteristics:
User-experience features
Pagination and sorting
Faceting
: category search results into subgroups.Autosuggest
Spell checker
Hit highlighting
Geospatial search
Data-modeling features
Result grouping /field collapsing
. 不同于facet: allows you to return unique groups instead of individual documents in the resultsFlexible query support
Joins
: like sql subqueryDocument clustering
: identify groups of documents that are similarImport rich document formats
: pdf, wordImport relational databases
Multilingual support
New features in Solr4
Near real-time search
: be searchable within seconds of being added to the index.Atomic updates with optimistic concurrency
: Solr uses a special version field named version to enforce safe update semantics for documents.Real-time get
: ?Write durability using a transaction log
: can control when to commit documents to make them visible in search results without risking data loss if a server fails before you commit.Easy sharding and replication using ZooKeeper
: uses Apache ZooKeeper to distribute configurations and manage shard leaders and replicasschema.xml
to represent all of the possible fields and data types necessary to map documents into a Lucene index.copy field
and dynamic field
).Lucene provides a powerful library for indexing documents, executing queries, and ranking results. And, with schema.xml, you have a flexible way to define the index structure using an XML-configuration document instead of having to program to the Lucene API.
inverted index
led to the invention of MapReduce.Apache
Hadoop
provides an open source implementation ofMapReduce
, and it’s used by the ApacheNutch
open source project to build aLucene
inverted index for web-scale search usingSolr
.
Filter query
: restricts the result set to documents matching this filter but doesn’t affect scoring.start, rows
: 用于分页。
the underlying Lucene index isn’t optimized for returning many documents at once.When you fill out the query form, an HTTP GET request is created and sent to Solr.
对应的HTTP Get请求:
返回结果是按照score排序的:Ranked retrieval。 除非指定了sort参数。
Solr prov ides a customizable example search UI, called Solritas, to help you prototype your own awesome search application. http://localhost:8983/solr/collection1/browse
]]>String#substring()
在Java6和Java7中的实现是不一样的。这是因为Java6的实现可能导致内存问题,所以Java7中为了改善这个问题修改了实现方式。那么Java7中的实现就真的合理吗?
首先让我们来猜测一下,Java是如何实现substring功能的。由于String是不可变的,可能我们会猜测实现机制如下图:
然而,这个图并不完全正确,或者说并没有完全表示出Java堆中真正发生的事情。
Java中字符串是通过字符数组来支持实现的,在JDK6中,String类包含3个实例变量:
- char[] value
表示真实的字符数组;
- int offset
表示数组的偏移量;
- int count
表示String所包含的字符的个数。
当调用substring()
方法时,会创建一个新的字符串对象,但是这个字符串的值在java堆中仍然指向的是同一个数组,这两个字符串的不同之处只是他们的count和offset的值。
可以参考Java6中的源代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
这么实现有一个问题:如果你有一个非常长的字符串,但是你仅仅只需要这个字符串的一小部分,你需要的只是很小的部分,而这个子字符串却要包含整个字符数组。这可能导致内存溢出问题。
我们可以用一个办法来规避这个问题:为substring()
得到的子字符串重新创建一个对象。例如:
1
|
|
或者:
1
|
|
Java7中对上述问题做了修正,当调用substring()
方法时,在堆中真正的创建了一个新的数组,当原字符数组没有被引用后就被GC回收了。
我们看源码:
1 2 3 4 5 6 7 8 9 |
|
可以看到Java7通过Arrays.copyOfRange
重新创建了一个字符数组。
Java7虽然规避了substring可能出现的内存问题,但是新的实现真的好吗?
Java6的实现,当进行substring时,使用共享内容字符数组,速度会更快,不用重新申请内存。虽然有可能出现本文中的内存性能问题,但也是有方法可以解决的。
而Java7的实现,对任何String,即便不是Large String,都会重新申请内存,速度也会更慢,性能会更差。如果我们程序中处理的大部分都不是Large String的话,这种对性能的影响是不是得不偿失?
如果保持Java6的实现,在处理非Large String时,我们直接调用substring即可;而对Large String则用上文提到的规避方法来解决。
Java中有一个和String#substring
有着类似逻辑、功能、实现机制的方法:List#sublist
。Java6 处理Large List的sublist时,也会出现内存问题;而奇怪的时Java7并未对这个实现进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
所以我们在处理Large List时还是需要用规避方法:
1 2 3 |
|
为什么Java7不对List#sublist
做修改,以让它和String#substring
的实现机制继续保持一致呢?不得而知。
http://www.programcreek.com/2013/09/the-substring-method-in-jdk-6-and-jdk-7/
]]>switch String的语法示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
那么在底层是如何实现的呢?我们可以通过反编译看看编译器是如何处理上述代码的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
可以看到处理流程是:
hashCode()
,返回一个int;equals()
比较来进行安全检查。我们看到实际上底层进行switch的是哈希值,所以Java7中所谓的支持switch字符串只是一个语法糖,底层还是一样:switch中只能使用整型,比如byte
,short
,char
以及int
。
另外switch case中通过equals()
方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。
正式因为加上了这个检查,因此它的性能一定是不如使用枚举进行switch或者使用纯整数常量。
因为上述性能问题,建议尽量使用纯整数常量进行swtich,或者用enum进行switch。
如果无法避免用字符串进行switch的话,还要注意大小写敏感的问题,建议统一用全大写字符串。
http://javarevisited.blogspot.sg/2014/05/how-string-in-switch-works-in-java-7.html
]]>String
。为了使这些类型在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个Java系统级别提供的缓存。
其中8种基本类型的常量池都是系统协调的,而String
类型的常量池比较特殊。它的主要使用方法有两种:
String
对象会直接存储在常量池中。String
对象,可以使用String.intern()
方法。intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中Jdk中源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
这个方法是一个 native 的方法,但注释写的非常明了:
如果常量池中存在当前字符串,就会直接返回当前字符串;如果常量池中没有此字符串,会将此字符串放入常量池中后,再返回。
native方法的大体实现逻辑是:
JAVA 使用 jni 调用c++实现的StringTable#intern()
方法, StringTable#intern()
方法跟Java中的HashMap
的实现是差不多的, 只是不能自动扩容。默认大小是1009。
要注意的是,String的String Pool是一个固定大小的Hashtable
,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern()
时性能会大幅下降(因为要一个一个找)。
在 jdk6中StringTable
是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable
的长度可以通过一个参数指定:
1
|
|
相信很多 JAVA 程序员都做做类似 String s = new String("abc")
这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。
上述的语句中是创建了2个对象,第一个对象是"abc"字符串存储在常量池中,第二个对象在Heap中的 String 对象。
来看一段代码:
1 2 3 4 5 6 7 8 9 10 11 |
|
打印结果是
false false
false true
然后将s3.intern();
语句下调一行,放到String s4 = "11";
后面。将s.intern();
放到String s2 = "1";
后面:
1 2 3 4 5 6 7 8 9 10 11 |
|
打印结果为:
false false
false false
注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。
Java6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 Heap 区域是完全分开的。
使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern()
方法也是没有任何关系的。
在 Java6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space
错误的。
所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Heap 区域了。
在第一段代码中,先看 s3和s4字符串。
String s3 = new String("1") + new String("1");
,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")
我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
接下来s3.intern();
这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个 “11” 的对象,关键点是 jdk7 中常量池不在 Perm 区域了。所以常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
最后String s4 = "11";
这句代码中"11"是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4
是 true。
再看 s 和 s2 对象。
String s = new String("1");
第一句代码,生成了2个对象。常量池中的“1” 和 Heap 中的字符串对象。s.intern();
s 对象去常量池中寻找后发现 “1” 已经在常量池里了。 – 这一点与s3/s4不一样。
接下来String s2 = "1";
这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。– ?????
来看第二段代码:
第一段代码和第二段代码的改变就是 s3.intern();
的顺序是放在String s4 = "11";
后了。这样,首先执行String s4 = "11";
声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();
时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
第二段代码中的 s 和 s2 代码中,s.intern();
,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");
的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:
将String常量池 从 Perm 区移动到了 Java Heap区。
调用String#intern()
方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
如果用到了大量相同的String,那么可以使用String#intern()
,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
运行的参数是:-Xmx2g -Xms2g -Xmn1500M
其实通过观察程序中只是用到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例子有些极端,但确实能准确反应出 intern 使用后产生的巨大空间节省。
另外,使用了 intern 方法后时间上有了一些增长。这是因为程序中每次都是用了 new String
后,然后又进行 intern 操作的耗时时间,这一点如果在内存空间充足的情况下确实是无法避免的,但我们平时使用时,内存空间肯定不是无限大的,不使用 intern 占用空间导致 jvm 垃圾回收的时间是要远远大于这点时间的。
上文提到:
在 jdk6中StringTable
是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable
的长度可以通过一个参数指定:
1
|
|
所以不能无用intern,把太多字符串放到常量池中。
http://tech.meituan.com/in_depth_understanding_string_intern.html
]]>通过一个boolean简单类型,构造Boolean对象引用。
优点:无需每次被调用时都创建一个新对象。同时使得类可以严格控制在哪个时刻有哪些实例存在
1 2 3 4 5 6 7 8 9 10 11 12 |
|
静态工厂方法Boolean.valueOf(String)几乎总是比构造函数Boolean(String)更可取。构造函数每次被调用时都会创建一个新对象,而静态工厂方法则从来不要求这样做,实际上也不会这么做。
构造方法BigInteger(int, int, Random)
返回一个可能为素数的BigInteger,而用一个名为BigInteger.probablePrime()
的静态工厂方法就更好。(JDK1.4最终增加了这个方法。)
优点:方法名对客户端更友好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
JDK1.5引入的java.util.EnumSet
类没有public构造函数,只有静态工厂方法。根据底层枚举类型的大小,这些工厂方法可以返回两种实现:
RegularEnumSet
实例,用单个long
来支持;JumboEnumSet
实例,用long数组
来支持。优点:静态工厂方法能返回任意子类型的对象。可以根据参数的不同,而返回不同的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Java集合框架中有32个集合接口的便利实现,提供不可修改的集合、同步集合等等。几乎所有的实现都通过一个不可实例化类(java.util.Collections
)中的静态工厂方法导出,返回对象的类都是非public的。
优点:静态工厂方法能返回任意子类型的对象。可以返回一个对象而无需使相应的类public。用这种方式隐藏实现类能够产生一个非常紧凑的API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这种工具类设计出来并不是为了实例化它。然而,如果不显式地编写构造函数,编译器则会提供一个公共的无参数的默认构造方法。 所以将构造函数私有化:
1 2 3 4 |
|
当然,还可以在这个私有构造器内部加上 throw new AssertionError()
,可以确保该方法不会再类内部被意外调用。
这种习惯用法的副作用是类不能被子类化了。子类的所有构造函数必须首先隐式或显式地调用父类构造函数,而在这种用法下,子类就没有可访问的父类构造函数可调用了。
java.util.concurrent.TimeUnit
使用枚举来实现Singleton:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Map接口的keySet()方法返回Map对象的一个Set视图,包含该Map的所有key。 看起来好像每次调用keySet()都需要创建一个新的Set实例。而实际上,虽然返回的Set通常是可变的,但返回的对象在功能上是等同的:如果其中一个返回对象改变,其他对象也会改变,因为他们的底层都是同一个Map实例。虽然创建多个KeySet视图对象并没有害处,但也没有必要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
注意在构造keySet之前 对齐进行了null检查;只有当它是null时才会初始化。
缓存实体的生命周期不容易确定,随着时间推移,实体的价值越来越低。在这种情况下,缓存应该不定期地清理无用的实体。可以通过一个后台线程来清理(可能是Timer
或ScheduledThreadPoolExecutor
),也可以在给缓存添加新实体时进行清理。
LikedHashMap可利用其removeEldestEntry,删除较老的实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
——可以继承LinkedHashMap,覆盖其removeEldestEntry方法。
注:如果想要缓存中的对象只要不被引用,就自动清理;则可以用WeakHashMap
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
对应的构造函数如下。这样程序员能容易地在对象及其字符串表示之间来回转换
1 2 3 4 5 6 7 8 9 10 11 12 |
|
我们可以自由地共享不可变对象,还可以共享他们的内部信息。
【例】BigInteger类内部使用了一个符号数值表示法(sign-magnitude representation),符号用一个int表示,数值则用一个int数组表示。negate()
方法会创建一个数值相同但符号相反的新BigInteger
,该方法不需要拷贝数组,新创建的BigInteger只需要指向源对象中的数组即可。
1 2 3 4 5 6 7 8 |
|
不可变对象生来就是线程安全的,他们不需要同步。当多个线程并发访问不可变对象时,他们不会遭到破坏。这无疑是实现线程安全的最容易的方法。实际上,不会有线程能观察到其他线程对不可变对象的影响。所以不可变对象可以被自由地共享。
不可变类应当利用这种优势,鼓励客户端尽可能重用现有实例。一个简单的方法是为常用的值提供public static final的常量。
1 2 3 |
|
不可变类的一个缺点是,对于每个不同的值都需要一个单独的对象。那么在执行复杂的多步操作时,每一步都会创建一个新的对象。
通过提供companion class
可以解决这个问题。例如当需要对String
执行复杂操作时,建议使用StringBuilder
。
不可变类所有域必须是final的。实际上这些规则比较强硬,为了提供性能可以有所放松。实际上应该是没有方法能够对类的状态产生外部可见的改变(no method may produce an externally visible change in the object’s state)。
然而,一些不可变类拥有一个或多个nonfinal域,用于缓存昂贵计算的结果。这个技巧可以很好地工作,因为对象是不可变的,保证了相同的计算总是返回同样的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Q:这个算法框架并不是设计在父类中,而是在一个工具类中
A:是的,与教科书上模板方法的定义有差异;因为sort要适用于所有数组,所以提供了一个Arrays工具类。但仍然是模板方法模式
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Applet中的init()/start()/stop()/destroy()/paint()这些方法,都是hook。
完整类名:java.util.concurrent.Executors.RunnableAdapter<T>
我们知道FutureTask
接受一个Callable
参数,那如果我们现有的是Runnable
该怎么办呢?
FutureTask
本身提供了适配:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Executors.callable()返回Adapter对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
AbstractExecutorService.submit()也用到了这个Adapter:
1 2 3 4 5 6 7 8 9 |
|
未完待续。。。
]]>如果你调用参数化类的构造函数,那么你必须要指定类型参数,即便上下文中已明确了类型参数。这通常要求你连续两次提供类型参数:
1
|
|
而假设HashMap提供了如下静态工厂:
1 2 3 |
|
那么你就可以讲上文冗长的声明替换为如下这种简洁的形式:
1
|
|
补充1:com.google.common.collect.Lists
可以解决这个问题:
1 2 3 4 5 6 |
|
补充2:Java7做了优化,可以这样声明:
1
|
|
Boolean(String)
有点多余,因为已经有静态工厂方法:Boolean.valueOf(String)
,它比Boolean(String)更可取。
构造函数每次被调用时都会创建一个新对象,而静态工厂方法则从来不要求这样做,实际上也不会这么做。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
要避免用finalize来释放资源,而应该提供一个显式的终止方法。例如FileInputStream.close();
finalizer的作用之一是,可以充当“安全网”,以防对象所有者忘记调用显式的终止方法。虽然不能保证finalizer会被及时调用,但当客户端没有调用显式终止方法时,迟一点释放资源总比不释放好。不过如果finalizer发现有未被终止的资源,则必须打印一条警告,表明客户端代码有bug,需要修复。
唯一声称保证finalizer()会被执行的方法是System.runFinalizersOnExit
,以及Runtime.runFinalizersOnExit
。
但这两个方法都有致命缺陷并且都已弃用。
JDK有四个类(FileInputStream
、FileOutputStream
、Timer
、Connection
)使用了finalizer作为安全网,以防显式终止方法未被调用。不幸的是,这些finalizer都没有打印警告。当API发布后,这种警告一般就不能添加到API了,因为可能破坏已有的客户端代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
java.net.UR
L的equals方法依赖于对URL中主机的IP地址的比较,而将主机名转译成IP地址需要访问网络,随着时间推移,并不保证能返回相同的结果。
——违反一致性。这就会导致URL的equals方法违反约定,并且已经在实践中引起问题了。
不幸的是,由于兼容性需求,这一行为无法改变。除了少数例外情况,equals方法必须对驻留在内存中的对象进行确定性计算。
java.sql.Timestamp
扩展了java.util.Date
类并增加了nanoseconds
字段。其equals方法违反了对称性:如果Timestamp和Date被用于同一个集合中,或以其他什么方式混在一起使用,则会引起错误的行为。
无法在扩展可实例化类(即非抽象类)的时候,增加一个值组件,同时又保证equals约定。
Timestamp
有一个免责声明,提醒程序员不要混用Date和Timestamp。虽然只要不混用他们就不会有麻烦,但是谁都不能阻止你混用他们,而结果导致的错误将会很难调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
compareTo方法的等同性测试必须与equals方法的结果相同。如果遵守了这一条,则称compareTo方法所施加的顺序与equals一致;反之则称为与equals不一致。
当然与equals不一致的compareTo方法仍然是可以工作的。但是,如果一个有序集合包含了该类的元素,则这个集合可能就不能遵守相应集合接口(Collection、Set、Map)的通用约定。这是因为这些集合接口的通用约定是基于equals方法的,但是有序集合却使用了compareTo而非equals来执行等同性测试。
BigDecimal类的compareTo方法与equals不一致:
HashSet
实例,并添加两个元素new BigDecimal("1.0")
和new BigDecimal("1.00")
,则集合会包含两个元素,因为这两个实例通过equals检测并不相等;TreeSet
而非HashSet,则集合中会只包含一个元素,因为这两个实例通过compareTo检测是相等的。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Cloneable接口的目的是作为对象的一个mixin接口,表明对象允许克隆;但这个目的没有达到。
其主要缺点是,Cloneable缺少一个clone()
方法,而Object.clone()
是受保护的。
通常,实现接口是为了表明类可以为它的客户做些什么;而Cloneable却改变了超类中受保护方法的行为。
——区别java.rmi.Remote接口,其中也不具有任何方法,它是一个记号接口。
如果一个类可以被包外访问,那么就要提供访问方法,以便可以灵活地改变类的内部表示。如果public类暴露了其数据域,那就不能在将来改变内部表示了。
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 7 8 9 |
|
不可变对象可以自由共享,所以无需进行保护性拷贝。实际上你根本无需做任何拷贝,因为这些拷贝始终与源对象相等。因此,你不需要,也不应该为不可变类提供clone方法或者拷贝构造函数。
【反例】这一点在Java平台早期并没有被很好地理解,导致String类具有拷贝构造函数,应该尽量不去用这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
当BigInteger和BigDecimal编写出来时,对于“不可变类实际上必须final”并没有得到广泛的理解,所以这两个类的方法都可以被重写。不幸的是,为了保持向后兼容,这个问题一直没有得到修正。
如果你编写的类的安全性依赖于(来自不可信客户端的)BigInteger或BigDecimal的不可变性,那么就必须检查参数是真正的BigInteger/BigDecimal,还是不可信任的子类实例。如果是后者,你必须把它当成是可变的,并进行保护性拷贝:
1 2 3 4 |
|
应该永远让小的值对象不可变,例如PhoneNumber、Complex。
Java平台库中有许多这种类,例如java.util.Date
、java.awt.Point
,它们理论上应当是不可变的,但实际上却是可变的。
It is especially difficult to understand the behavior of a program that executes a break, continue, or return statement in a Try block only to have the statement’s behavior vetoed by a finally block.
Never exit a finally block with a return, break, continue, or throw, and never allow a checked exception to propagate out of a finally block.
1 2 3 4 5 6 7 |
|
这个程序返回false。无论try块是否正常执行完,finnaly都会被执行。
另外,finally中应该禁止抛出异常。否则finally中剩下的语句就不会执行,破坏逻辑。
The set of checked exceptions that a method can throw is the intersection of the sets of checked exceptions that it is declared to throw in all applicable types, not the union.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
两个int相乘,得到的还是int,这就可能溢出!
1 2 |
|
应该自动切换到更大的类型,以避免溢出; 或者直接抛出Exception,都比溢出要好。
如果遇到跨类型计算,jdk会把低类型自动提升为高类型,然后再计算。但这种转换有时会导致问题。例如:
1
|
|
因为是long + int,所以后面的int会自动提升为long,再计算。即被提升为0xffffffffcafebabeL。
1 2 3 |
|
所以得到cafebase,而不是想象中的1cafebabe。 我们得出教训:跨类型计算可能带来混淆,所以要坚决避免!上例可以改为如下:
1
|
|
Negative hex literals appear positive。十六进制的字面值,其最高位代表正负。
要注意三元运算符,它没有要求第二和第三个操作数类型一致。
1 2 3 |
|
理应强制要求他们类型一致。
+=
、-=
等运算符会自动类型转换,即将计算结果自动转换为左侧操作数的类型。这有时会导致意想不到的问题。
例如:
1 2 3 4 |
|
应该不要做自动类型转换,以编译报错提醒用户。(与第二句普通赋值语句保持一致)
+=
左侧不能为Object,例如:1 2 3 4 |
|
1 2 3 4 |
|
所以上例是一个无限循环。
用小写L容易与数字1混淆! 应该强制用大写L,小写L非法。
1 2 |
|
貌似Array应该默认重写toString方法。
为了解决上一个问题,你可能想静态导入Arrays.toString(),然后调用:
1
|
|
但是会编译报错,编译器去查找当前类的toString()方法,发现参数不匹配。。
String(byte[])的文档说明,它依赖于默认字符集:
Constructs a new String by decoding the specified byte array using the platform’s default charset. The length of the new String is a function of the charset, and hence may not be equal to the length of the byte array. The behavior of this constructor when the given bytes are not valid in the default charset is unspecified。
但是JRE的默认字符集依赖于操作系统和locale。所以,it was not such a good idea to provide a String(byte[]) constructor that depends on the default charset:
1
|
|
Thread
didn’t have a public run
method, it would be impossible for programmers to invoke it accidentally.Thread
class has a public run
method because it implements Runnable
, but it didn’t have to be that way.Thread
instance to encapsulate a Runnable
, giving rise to composition in place of interface inheritance.methods should have names that describe their primary functions. Given the behavior of Thread.interrupted
, it should have been named clearInterruptStatus
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
perhaps it makes sense to forbid shadowing of type parameters, in the same way that shadowing of local variables is forbidden.
未完待续。。。
]]>