Cloudflare:用数学与Rust优化一致性哈希节省100TB内存
Saving another 100TB of RAM with math (and Rust)
这是一篇极具参考价值的工程复盘,展示了如何用数学思维解决大规模分布式系统中的资源瓶颈,对做底层架构和性能优化的同学很有启发。
Cloudflare operates at a scale so big that even after working here for years, it doesn’t seem real. We have thousands of servers all over the world with petabytes of RAM and millions of CPU cores, and all of it is pushed to the max. As vast as those resources feel, they are still finite, and when you need every service to run on every node, it doesn’t leave room for wasted space.
Cloudflare 的运营规模之大,以至于即使在这里工作多年,仍让人觉得不真实。我们在全球拥有数千台服务器,配备 PB 级内存和数百万个 CPU 核心,且所有资源都被压榨到极致。尽管这些资源看似庞大,但它们仍然是有限的,当你需要每个服务都在每个节点上运行时,就没有多余的空间可以浪费。
At this scale, small improvements are greatly magnified, so even 1%-at-a-time improvements are worth celebrating. And some tweaks add up to a lot more: in this post, we’ll look at how small changes to a single algorithm reduced the memory footprint of one of our Pingora-based services significantly. That allowed us to reclaim more than 100TB of RAM globally, on top of the 100TB of memory the DNS team was able to shed last month.
在这个规模下,微小的改进会被大幅放大,因此即使是每次提升 1% 的改进也值得庆祝。而某些调整累积起来的效果更为显著:在本文中,我们将探讨对单一算法进行的微小改动如何显著降低了我们基于 Pingora 的一项服务的内存占用。这使得我们能够在全球范围内回收超过 100TB 的 RAM,此外 DNS 团队上个月还成功释放了 100TB 的内存。
Waste not
物尽其用
Maintaining equitable resource sharing between teams is not easy, especially in large organizations. One of the ways Cloudflare ensures the balance is kept is through the tireless efforts of the wonderful Performance team.
在团队之间维持公平的资源共享并不容易,尤其是在大型组织中。Cloudflare 保持平衡的方式之一,是依靠出色的性能团队(Performance team)不知疲倦的努力。
This story starts with a ticket filed by Ivan who found: Excessive memory usage from pingora-ketama in Pingora Backend Router. The finding was that our internal load-balancing service, Pingora Backend Router (yes, PBR), was using significantly more memory than expected — specifically in structures associated with pingora-ketama, which is our open-source library for handling consistent hashing.
这个故事始于 Ivan 提交的一个工单,他发现:Pingora Backend Router 中的 pingora-ketama 存在内存使用过高的问题。调查结果显示,我们的内部负载均衡服务 Pingora Backend Router(简称 PBR)使用的内存远超预期——特别是在与 pingora-ketama 相关的结构中,这是我们要用于处理一致性哈希的开源库。
In order to talk about how we addressed this seeming overuse of memory, we need to talk about what consistent hashing even is, why we are using it in PBR, and how it became so memory hungry. Along the way, we’ll learn some Rust and even a little math.
为了讨论我们如何解决这种看似过度的内存使用问题,我们需要先了解什么是一致性哈希、为什么我们在 PBR 中使用它,以及它是如何变得如此消耗内存的。在此过程中,我们将学习一些 Rust 知识,甚至涉及一点数学。
Consistent hashing
一致性哈希
Consistent hashing is a widely used method for distributing tasks across multiple servers in a way that does not require large changes when servers are added or removed. Internally we use it to route cacheable requests to servers by URL. This allows us to keep only one copy of a file stored per data center and gives a stable way to find the location of each file. We have mentioned this system before, but let’s take the time to walk through how and why this algorithm is used and how it works.
一致性哈希是一种广泛使用的方法,用于将任务分布在多台服务器上,且在添加或移除服务器时不需要进行大规模更改。在内部,我们使用它根据 URL 将可缓存的请求路由到服务器。这使我们能够确保每个数据中心只存储一份文件副本,并为查找每个文件的位置提供了一种稳定的方式。我们之前曾提到过这个系统,但让我们花点时间 walkthrough 一下该算法的使用原因及其工作原理。
The key concept of consistent hashing is that while hash functions can accept any kind of input, their output is limited to a single unsigned integer (32, 64, or 128-bit integers depending on which hash function). This allows us to relate tasks and servers to each other in a consistent way. Most discussions of consistent hashing have you think of that output space as a continuous, circular ring that wraps around from its max value to zero. This depiction makes for some nice visualizations, but it can also make the simple concept of integer ranges seem more complicated than it needs to be. For our discussion, we’ll represent the 32-bit output of our hash function as a number line.
一致性哈希的关键概念在于,虽然哈希函数可以接受任何类型的输入,但其输出仅限于一个无符号整数(具体为 32、64 或 128 位整数,取决于所使用的哈希函数)。这使得我们能够以一致的方式将任务和服务器相互关联。在大多数关于一致性哈希的讨论中,你会被引导将该输出空间视为一个连续的环形结构,其最大值会回绕到零。这种描绘方式便于进行可视化展示,但也可能让简单的整数范围概念显得比实际需要更复杂。在我们的讨论中,我们将把哈希函数的 32 位输出表示为数轴上的一个数值。
Now, let’s say we have a set of servers, A, B, & C, and a set of tasks t-z. We can map each onto the number line based on the hash of their representative values, so something like IP addresses for servers and cache keys for tasks.
现在,假设我们有一组服务器 A、B 和 C,以及一组任务 t-z。我们可以根据它们代表值的哈希值将它们映射到数轴上,例如服务器的 IP 地址和任务的缓存键。
Assigning tasks to servers is now just a matter of finding the first server to the left of each task. We can represent this visually by coloring in the region of hashes that will be associated with each server. Notice that the range covered by server C wraps around to the beginning, hence the idea that hashes exist in a ring.
将任务分配给服务器现在只需找到每个任务左侧的第一个服务器即可。我们可以通过着色将与每个服务器关联的哈希区域来直观地表示这一点。请注意,服务器 C 覆盖的范围回绕到了开头,因此产生了哈希存在于环中的概念。
And that’s it. At a base level, consistent hashing is this simple — but it doesn’t take long to see that there is room for improvement. Notice that the range covered by server A in our example is significantly larger than that of either B or C. This is a problem because the fraction of the requests a server handles is going to be proportional to the size of its range on the number line. Ideally we would like to guarantee each server will have an equal size, but because hashes are essentially random numbers, we have to talk about the size of the regions in terms of statistics. 😨
就是这样。从基础层面来看,一致性哈希非常简单——但很快就能看出还有改进的空间。请注意,在我们示例中,服务器 A 覆盖的范围明显大于 B 或 C 的范围。这是一个问题,因为服务器处理的请求比例将与其在数轴上范围的大小成正比。理想情况下,我们希望保证每个服务器拥有相等大小的范围,但由于哈希本质上是随机数,我们必须用统计学的术语来讨论区域的大小。😨
Math and consequences
数学及其后果
First: don’t panic. I promise I'm not about to lie to you and that we will stay safely within the bounds of a day-one probability lesson. When we talk about statistical distributions, there are two big factors that help us quantify uncertainty in helpful ways: expected value and standard deviation. In (over-)simplified terms, expected value gives us a point where measurements based on a distribution will be centered, and standard deviation tells how close to that central point most measurements are likely to be.
首先:不要惊慌。我保证我不会欺骗你,我们将安全地停留在第一天概率课程的范围内。当我们谈论统计分布时,有两个主要因素有助于我们以有益的方式量化不确定性:期望值和标准差。用(过度)简化的话说,期望值为我们提供了一个基于分布的测量值的中心点,而标准差则告诉我们要接近该中心点的测量值大概有多近。
For consistent hashing, we can calculate these factors for the fractional size of the range associated with one of N servers. (Details on where this formula comes from later).
对于一致性哈希,我们可以计算与 N 台服务器中某一台相关联的范围的分数大小(fractional size)的这些因子。(该公式的来源细节稍后介绍)。
In terms of concrete numbers, let’s say we have 100 servers. The formulas above give:
具体到数字方面,假设我们有 100 台服务器。上述公式给出:
That tells us that we can expect that the range each server handles will be centered around 0.99% of the total and most of the lengths to fall within 1% of what's expected. This sounds good until we realize that that’s 0.99% of the total length. We need to scale the standard deviation by the expected value to see how big the error is as a fraction of the target size. This value is called the coefficient of variation.
这表明我们可以预期每台服务器处理的范围将集中在总量的 0.99% 左右,且大多数长度落在预期值的 1% 范围内。这听起来不错,直到我们意识到那是总长度的 0.99%。我们需要将标准差乘以期望值,以查看误差相对于目标大小的比例。这个值被称为变异系数(coefficient of variation)。
What if we add hashes?
如果我们增加哈希数量会怎样?
The simplicity of consistent hashing is a double-edged sword. It’s easy to understand and implement because everything is turned into easily-relatable hashes on the same numberline, but any improvements to the system will also need to be relatable to that numberline. That means the solution to any consistent hashing problem can only be more hashes. It’s less like a golden hammer (a tool with which all problems look like nails) and more like a golden nail in that it turns all tools into hammers.
一致性哈希的简单性是一把双刃剑。它易于理解和实现,因为所有内容都被转换为同一数轴上易于关联的哈希值,但系统的任何改进也需要与该数轴相关联。这意味着解决任何一致性哈希问题的方案只能是更多的哈希值。它不像一把万能锤子(所有问题看起来都像钉子),更像是一枚万能钉,因为它将所有工具都变成了锤子。
To solve the problem of imbalanced workloads, we can add multiple hashes to represent each server instead of just one. We’ll get to the math behind this momentarily, but it should make some intuitive sense that while each individual range has a large standard deviation, adding a bunch together should make their total size even out. If we take our three-server example from the above diagrams and add two more hashes at random for each server, we see that it helps even out each server’s workload.
为了解决负载不均衡的问题,我们可以为每台服务器添加多个哈希值来代表它,而不仅仅是一个。我们稍后会讨论其背后的数学原理,但从直觉上讲,虽然每个单独的范围具有较大的标准差,但将它们加在一起应该会使总规模趋于平衡。如果我们将上面图表中的三台服务器示例扩展,并为每台服务器随机添加两个额外的哈希值,我们会发现这有助于使每台服务器的负载更加均衡。
This is an admittedly contrived example. The random nature of the system means there’s no guarantee how much improvement you will get from adding 2 additional hashes per server, but it should make some intuitive sense that combining more of these hash segments together produces a more even distribution. Each segment in the sum has a chance of balancing another. Maybe one is too short; maybe one is too long. This is essentially what the law of large numbers tells us should happen… The obvious problem is it only works for large numbers. In NGINX, the baseline number of hashes per server is hardcoded to 160, and Pingora uses the same value as the default. I’ll spare you the math for now, but if we go back to our 100-server example, if we use 160 points per server instead of just one, the coefficient of variation (which we can think of like an error margin) drops from about 99% to about 8%, a significant improvement.
这是一个 admittedly(诚然)人为构造的例子。系统的随机性意味着无法保证每台服务器增加 2 个哈希值会带来多少改进,但直觉上可以认为,将这些哈希段组合得越多,分布就越均匀。总和中的每个段都有机会平衡另一个。也许某一段太短;也许某一段太长。这基本上就是大数定律告诉我们应该发生的情况……明显的问题是它仅适用于较大的数值。在 NGINX 中,每台服务器的基准哈希数量硬编码为 160,Pingora 也使用相同的默认值。我现在就不赘述数学计算了,但如果回到我们之前的 100 台服务器示例,如果我们将每台服务器的点数从 1 增加到 160,变异系数(我们可以将其视为误差范围)将从约 99% 降至约 8%,这是一个显著的改进。
What if we add more hashes?
如果我们增加更多的哈希值会怎样?
We saw above that increasing the number of hashes per server by a constant amount allows us to improve how evenly workloads are distributed per server, but what if we don’t want to distribute the work evenly? In Cloudflare’s case, we have some servers that have more storage space than others, so it would be better to have the number of requests allotted to a server be proportional to its disk space. One way to accomplish this is with the ketama algorithm. The naming is a little funny because the algorithm is named after the library where it was first implemented, and the library was named … well you can google it 😶🌫️.
如上所述,通过固定量增加每台服务器的哈希数量可以改善工作负载在每台服务器上的分布均匀性,但如果我们不想均匀分配工作呢?在 Cloudflare 的案例中,有些服务器的存储空间比其他服务器多,因此让分配给服务器的请求数量与其磁盘空间成比例会更好。实现这一点的一种方法是使用 ketama 算法。这个名字有点奇怪,因为该算法以其首次实现的库命名,而该库的名字……嗯,你可以去搜一下 😶🌫️。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力