你见过一个失败并在眨眼间重新启动的数据库吗?
PayPal 的 JunoDB 就是一个能够做到这一点的数据库。根据 PayPal 的说法,JunoDB 可以在 6 个 9 的可用性(99.9999%)下运行。这相当于每天只有 86.40 毫秒的停机时间。
作为参考,我们平均眨眼需要大约 100-150 毫秒。
虽然统计数据确实令人惊叹,但 JunoDB 的架构和设计也有很多值得学习的地方。
在这篇文章中,我们将涵盖以下主题:
- JunoDB 的架构分解
- JunoDB 如何实现可扩展性、可用性、性能和安全性
- JunoDB 的使用案例
在我们继续之前,以下是一些关于 JunoDB 的关键事实,可以帮助我们更好地理解它。
- JunoDB 是一个分布式键值存储。把键值存储想象成一个字典,你查找一个单词(“键”)来找到它的定义(“值”)
- JunoDB 利用 Go 实现的高并发架构,有效处理数十万个连接
- 在 PayPal,JunoDB 每天服务近 3500 亿次请求,用于每个核心后端服务,包括登录、风险管理和交易处理等关键功能
- PayPal 主要使用 JunoDB 作为缓存,以减少主真相数据库的负载。但也有我们将在后面讨论的其他使用案例
下图显示了 JunoDB 如何适应 PayPal 的整体方案。

为什么 PayPal 不自用 Redis?
关于创建像 JunoDB 这样的东西,一个常见的问题是: “为什么 PayPal 不能直接使用像 Redis 这样的现成产品?”
原因是 PayPal 想要数据库的多核支持,而 Redis 不是设计用来从多个 CPU 核心受益的。它本质上是单线程的,只使用一个核心。通常,如果需要,你需要启动几个 Redis 实例来在多个核心上扩展。
顺便说一下,JunoDB 最初是作为一个单线程 C++ 程序开始的,最初的目标是将其用作内存短 TTL 数据存储。
作为参考,TTL 代表生存时间。它指定数据应保留的最大持续时间或被视为有效的最长时间。
然而,JunoDB 的目标随着时间的推移而演变。
- 首先,PayPal 希望 JunoDB 作为支持长 TTL 的持久数据存储
- 其次,JunoDB 还希望通过默认的磁盘加密和传输中 TLS 提供改进的数据安全性
这些目标意味着 JunoDB 必须是 CPU 绑定而不是内存绑定。
作为参考,“内存绑定”和”CPU 绑定”指的是计算机程序中不同的性能方面。顾名思义,内存绑定程序的性能受可用内存量的限制。另一方面,CPU 绑定程序取决于 CPU 的处理能力。
例如,Redis 是内存绑定的。它主要将数据存储在 RAM 中,所有关于它的优化都是为了快速内存访问。Redis 性能的限制因素是内存而不是 CPU。
然而,像加密这样的要求是 CPU 密集型的,因为许多加密算法需要原始处理能力来执行复杂的数学计算。
因此,PayPal 决定用 Go 重写早期版本的 JunoDB,使其对多核友好并支持高并发。
JunoDB 架构
下图显示了 JunoDB 的高级架构。

让我们看看整体设计的主要组件。
1. 客户端库
客户端库是客户端应用程序的一部分,提供通过 JunoDB 代理存储和检索数据的 API。
它用多种编程语言实现,如 Java、C++、Python 和 Golang,以便在不同的应用程序栈中轻松使用。
对于开发人员来说,只需选择各自编程语言的库并将其包含在应用程序中,即可执行各种操作。
2. JunoDB 代理
JunoDB 使用基于代理的设计,代理连接到所有 JunoDB 存储服务器实例。
这种设计有一些重要优势:
- 确定哪个存储服务器应处理查询的复杂性保持在客户端库之外。由于 JunoDB 是分布式数据存储,数据分布在多个服务器上。代理负责将请求定向到正确的服务器
- 代理还知道 JunoDB 集群配置(如分片映射)存储在 ETCD 键值存储中
但是 JunoDB 代理会变成单点故障吗?
为了防止这种可能性,代理在负载均衡器下游的多个实例上运行。负载均衡器接收来自客户端应用程序的传入请求,并将请求路由到适当的代理实例。
3. 存储服务器
JunoDB 架构中的最后一个主要组件是存储服务器。
这些是接受来自代理的操作请求并将数据存储在内存或持久存储中的实例。
每个存储服务器负责一组分区或分片,以实现高效的数据分布。
在内部,JunoDB 使用 RocksDB 作为存储引擎。在数据库世界中,使用现成的存储引擎(如 RocksDB)很常见,以避免从头开始构建所有内容。作为参考,RocksDB 是一个嵌入式键值存储引擎,针对高读写吞吐量进行了优化。
可扩展性
几年前,PayPal 过渡到水平可扩展的基于微服务的架构,以支持活跃客户和支付率的快速增长。
虽然微服务解决了许多问题,但它们也有一些缺点。
一个重要缺点是由于扩展应用层,到键值存储的持久连接数增加。JunoDB 通过两种主要方式处理这个扩展要求。
如前所述,JunoDB 使用基于代理的架构。
如果到数据库的客户端连接达到限制,可以添加额外的代理来支持更多连接。
在这种情况下有一个可接受的延迟权衡。
第二种类型的扩展需求与数据大小的增长有关。
为了确保高效存储和数据获取,JunoDB 支持基于一致性哈希算法的分区。分区(或分片)使用分片映射分布到物理存储节点。
一致性哈希在这种情况下非常有用,因为当集群中的节点由于添加或删除而改变时,只有最少数量的分片需要重新分配给不同的存储节点。

PayPal 使用固定数量的分片(准确说是 1024 个分片),分片映射是预生成的并存储在 ETCD 存储中。
分片映射的任何更改都会触发自动数据重新分发过程,使其可以根据需要轻松扩展 JunoDB 集群。
下图更详细地显示了这个过程。

高可用性
高可用性对 PayPal 至关重要。你不能让全球支付平台宕机而不造成巨大的声誉损失。
然而,由于各种原因,中断确实会发生并将继续发生,如软件 bug、硬件故障、停电,甚至人为错误。故障可能导致数据丢失、响应时间慢或完全不可用。
为了缓解这些挑战,JunoDB 依赖复制和故障转移策略。
在集群中,JunoDB 存储节点在逻辑上组织成网格。每一列代表一个区域,每一行表示一个存储组。
数据被分成分片并分配给存储组。在存储组内,每个分片基于法定协议在不同区域之间同步复制。

基于法定协议的协议是在分布式数据库中就值达成共识的关键。你有两个法定人数:
- 读法定人数:当客户端想要读取数据时,它需要接收来自一定数量区域的响应(称为读法定人数)。这是为了确保它获得最新的数据
- 写法定人数:当客户端想要写入数据时,它必须接收来自一定数量区域的确认,以确保数据写入大多数区域
关于法定人数有两个重要规则。
- 读法定人数和写法定人数之和必须大于区域数。如果不是这种情况,客户端最终可能读取过时的数据。例如,如果有 5 个区域,读法定人数为 2,写法定人数为 3,客户端可以向 3 个区域写入数据,但另一个客户端可能从尚未收到更新数据的 2 个区域读取
- 写法定人数必须超过区域数的一半,以防止对同一键进行两个并发写操作。例如,如果有一个具有 5 个区域和写法定人数为 2 的 JunoDB 集群,客户端 A 可以向键 K 写入值 X,当 2 个区域确认请求时被认为是成功的。同样,客户端 B 也可以向同一个键 K 写入值 Y,当两个不同的区域确认请求时也被认为是成功的。最终,键 K 的数据处于不一致状态
在生产中,PayPal 有一个配置,有 5 个区域,读法定人数为 3,写法定人数为 3。
最后,JunoDB 中的故障转移过程是自动和瞬时的,不需要领导者重新选举或数据重新分配。代理可以通过丢失的连接或超时的读取请求知道节点故障。
跨数据中心复制是通过在不同数据中心的每个集群的代理之间异步复制数据来实现的。
这对于确保即使一个数据中心发生灾难性故障,系统也能继续运行非常重要。
性能
JunoDB 的一个关键目标是提供大规模的高性能。
这意味着在提供出色用户体验的同时保持个位数毫秒的响应时间。
PayPal 分享的以下基准测试图展示了 JunoDB 在持久连接和高吞吐量情况下的性能。
安全性
作为值得信赖的支付处理器,安全性对 PayPal 至关重要。
因此,JunoDB 被设计为保护传输中和静态的数据。
- 对于传输安全,在客户端和代理之间以及用于复制的不同数据中心的代理之间启用 TLS
- 在客户端或代理级别执行负载加密,以防止多次加密相同数据。理想的方法是在客户端加密数据,但如果没有完成,代理通过元数据标志找出并执行加密
- 存储服务器接收并存储在引擎中的所有数据也被加密,以保持静态安全
密钥管理模块用于管理 TLS、会话的证书和加密密钥的分配,以促进密钥轮换。
下图更详细地显示了 JunoDB 的安全设置。

使用案例
随着 PayPal 将 JunoDB 开源,你也可以在你的项目中使用它。
JunoDB 可以帮助各种使用案例。让我们看看一些重要的使用案例。
1. 缓存
你可以使用 JunoDB 作为临时缓存来存储不经常变化的数据。
由于 JunoDB 支持短寿命和长寿命的 TTL,你可以存储从几秒到几天的数据。例如,一个使用案例是在 JunoDB 中存储短寿命令牌,而不是从数据库获取它们。
你可以在 JunoDB 中缓存的其他项目包括用户偏好、帐户详细信息和 API 响应。
2. 幂等性
你也可以使用 JunoDB 实现幂等性。
当操作即使应用多次也产生相同结果时,该操作是幂等的。有了幂等性,重复操作是安全的,你不需要担心像重复付款这样的事情被应用。
PayPal 使用 JunoDB 确保他们不会由于重试而多次处理特定付款。JunoDB 的高可用性使其成为跟踪处理细节而不过载主数据库的理想数据存储。
3. 速率限制
假设你有某些资源由于某种原因不可用,或者它们的使用有访问限制。例如,这些资源可以是数据库连接、API 速率限制或用户身份验证尝试。
你可以使用 JunoDB 存储这些资源的计数器,并跟踪它们的使用是否超过阈值。
4. 跨数据中心复制
正如我们之前讨论的,JunoDB 提供快速的集群间复制。这可以帮助你处理更传统设置中的慢复制。
例如,在 PayPal 的案例中,他们在主动 - 主动模式下运行 Oracle,但复制通常不像他们希望的那样快以满足他们的要求。
这意味着如果一个数据中心写入的记录没有在第二个数据中心复制并且第一个数据中心宕机,就有可能出现不一致的读取。
JunoDB 可以帮助弥补延迟,你可以写入数据中心 A(Oracle 和 JunoDB),即使它宕机,你也可以从数据中心 B 的 JunoDB 实例一致地读取更新。
请参阅下图以更好地理解这个概念。

结论
JunoDB 是一个分布式键值存储,在各种 PayPal 应用中发挥着至关重要的作用。它提供高效的数据存储,以快速访问减少对昂贵数据库解决方案的负载。
在这样做的同时,它还满足关键要求,如可扩展性、高可用性、性能、一致性和安全性。
由于其优势,PayPal 已开始在多种使用案例和模式中使用 JunoDB。对我们来说,它提供了一个很好的机会来了解一个令人兴奋的新数据库系统。
参考
- Unlocking the Power of JunoDB: PayPal’s Key-Value Store Goes Open-Source
- JunoDB: PayPal Open Sources Key-Value Store
- Redis Benchmark: Pitfalls and misconceptions
- Memory Bound Function
- High Availability
本文为学习目的的个人翻译,译文仅供参考。
原文链接:How PayPal Serves 350 Billion Daily Requests with JunoDB。
版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。