首页 > IT基础架构 > 服务器/存储 > 缓存 — PlanetScale
每次你使用计算机时,缓存都在发挥作用,确保你的体验流畅快速。计算机所做的每一件事——从在 CPU 上执行指令,到请求你的 X.com 信息流,再到加载这个网页——都严重依赖缓存。
你即将享受一次引导式的、交互式的缓存之旅:这是计算领域中最优雅、最强大、最普遍的创新。
我们用计算机所做的每一件事都需要数据。当你登录电子邮件时,你所有的邮件和联系人作为数据存储在服务器上。当你在 iPhone 上浏览照片库时,它们作为数据存储在你手机的存储和/或 iCloud 中。当你加载这篇文章时,它必须从 Web 服务器获取数据。
在设计用于存储和检索数据的系统时,我们面临着容量、速度、成本和持久性之间的权衡。
硬盘驱动器每 GB 的成本比固态硬盘更低,但后者延迟更低。随机存取存储器比上述两者都快,但更昂贵且易失。
在给定预算下,你要么获得大量较慢的数据存储,要么获得少量较快的存储。工程师通过将两者结合来解决这个问题:将大量较便宜的慢速存储与少量昂贵的快速存储配对。访问频率较高的数据可以放在快速存储中,而我们使用频率较低的其他数据则放在慢速存储中。
这就是缓存的核心原理。
如果你对 PlanetScale for Postgres 感到兴奋,试试 Postgres 模式:
我们可以在下面看到可视化效果。每个彩色方块代表我们需要存储和检索的一个唯一数据项。你可以点击“数据请求器”来发送额外的数据请求,或者点击“慢速 + 大容量”区域中的任何彩色方块来发起该颜色的请求。
每次我们请求一条数据时,请求首先被发送到缓存。如果数据在缓存中,我们认为这是一次缓存命中,并迅速将数据发送回去。
如果没有找到,我们称之为缓存未命中。此时,我们将请求传递给慢速存储,找到数据,将其发送回缓存,然后再发送给请求者。
“命中率”是我们获得缓存命中的时间百分比。换句话说:
hit_rate = (cache_hits / total_requests) x 100
我们希望保持命中率尽可能高。这样做意味着我们最小化了对较慢存储的请求数量。
在下面的设置中,我们有一个小缓存,许多项目要存储,而且我们对这些项目的请求没有可预测的模式。这导致命中率低。
您可以在下面的条形图中看到上述缓存的命中率以及它随时间的变化。
如果我们有一个几乎与数据大小相当的缓存,并且具有相同的随机请求模式,会怎么样?
在这种情况下,我们能够实现更高的命中率。
增加缓存的大小会增加我们数据存储系统的成本和复杂性。这一切都关乎权衡。
计算机中最普遍的缓存形式或许是随机存取存储器(RAM)。
计算机的 CPU 负责完成所有“工作”。有人将 CPU 称为计算机的大脑。大脑需要记忆才能运作,否则就没有多大用处。
计算机想要永久存储的任何信息都会保存在硬盘上。硬盘很棒,因为即使计算机关机或电池耗尽,它们也能记住东西。问题在于,硬盘很慢。
点击下方可视化图中的“CPU”,向存储发送数据请求。你也可以点击“Hard Drive”部分中的任意彩色方块,请求该特定元素。
为了解决这个问题,我们使用 RAM 作为 CPU 和硬盘之间的中间存储位置。RAM 比硬盘更昂贵,存储容量也更小。但关键在于,从 RAM 获取和写入数据的速度,比与硬盘交互所需的时间快一个数量级。因此,计算机会将硬盘中的一部分数据——即需要更频繁访问的数据——存储在 RAM 中。许多数据请求会发往 RAM,并可以快速执行。偶尔,当 CPU 需要 RAM 中尚不存在的内容时,它会向硬盘发起请求。
实际上,计算机上的缓存层不止 RAM 和磁盘。现代 CPU 为 RAM 设置了一个或多个缓存层。尽管 RAM 很快,但直接内置于 CPU 的缓存甚至更快,因此程序运行时,频繁使用的值和变量可以存储在那里,以提高性能。
大多数现代 CPU 都有多个这样的缓存层,称为 L1、L2 和 L3 缓存。L1 比 L2 快,L2 比 L3 快,但 L1 的容量小于 L2,L2 的容量小于 L3。
这通常是缓存中的权衡——更快的数据查找意味着更高的成本或更多的容量限制,因为数据在物理上需要离请求者更近。这一切都是权衡。要让计算机获得出色性能,就需要仔细平衡并调优缓存层,使其对工作负载达到最优。
缓存并不局限于本地计算机的语境。它在云端一直被用来为你喜爱的应用提供支持。让我们以万能应用 X.com 为例。
X.com 整个历史中的推文数量轻松达到数万亿。然而,𝕏 时间线几乎只充满过去约 48 小时的帖子。几天之后,除非有人重新发布,或有人向下滚动到很久以前的历史记录,否则这些帖子很少被查看。这会产生一种新近偏好。
让我们来看看 Karpathy 的这条爆款内容。
这条推文发布两年多以来,已获得超过 43,000 个点赞和 700 万次展示。虽然我们无法获取确切的趋势数据,但这条推文浏览量的大致趋势线可能是:
最近发布的推文、图片和视频的请求量远高于较旧的内容。因此,我们希望将缓存资源集中在近期帖子上,以便它们能被更快地访问。较旧的帖子则可以加载得慢一些。
这是许多网站的标准做法,并非只有 X.com 如此。这些网站将大部分内容(邮件、图片、文档、视频等)存储在"慢速"存储中(如 Amazon S3 或类似服务),但将近期内容缓存到更快的内存存储中(如 CloudFront、Redis 或 Memcached)。
请看下面的缓存可视化图。偏红色的方块代表近期数据("热"数据),而偏蓝色的方块代表访问频率较低的较旧数据("冷"数据)。当我们点击应用服务器发送请求时,可以看到红色数据被更频繁地访问。这导致缓存主要保留红色数据。
在缓存被近期数据填满后,点击数据库侧的一些较冷的数据。这些数据都需要从数据库加载,而不是直接从缓存中加载。
这就是为什么在某个人的时间线上查看较旧帖子通常比查看近期帖子加载更慢的原因!缓存很少被需要的数据并没有太大意义。打开某人的 𝕏 页面,开始往回滚动到很久以前。当你翻过几天或几周的历史记录后,加载速度会明显变慢。
另一个重要的考量因素是空间局部性(Spatial Locality)。
在某些数据存储系统中,当读取一块数据时,紧邻其"前面"或"后面"的数据也很可能会被读取。以相册应用为例。当用户从云端照片存储中点击一张照片时,他们接下来查看的很可能是按时间顺序紧接其后的那张照片。
在这些情况下,数据存储和缓存系统会利用这种用户行为。当一张照片被加载时,我们可以预测用户接下来想看哪些照片,并将它们预取到缓存中。在这种情况下,我们还会加载前后几张照片。
这被称为空间局部性(Spatial Locality),它是一种强大的优化技术。
在下面的示例中,你可以看到空间局部性是如何工作的。每个数据库单元格按顺序编号。当你点击一个单元格时,它会将该单元格及其两个相邻单元格加载到缓存中。
当存在可预测的数据访问模式时,这种对相关数据的预取能够提升性能,而许多应用(远不止相册)都具有这种模式。
行星尺度上的物理距离是缓存中另一个值得考虑的要素。无论你喜欢与否,我们生活在一个周长 25,000 英里、不断旋转的巨大岩石上,数据从 A 点传输到 B 点的速度受限于“物理规律”。
在某些情况下,我们将数据放在单台服务器上并承受其后果。例如,我们可能将所有数据放在美国东部。东海岸请求数据的计算机延迟会低于西海岸。东海岸用户会经历 10-20ms 的延迟,而西海岸用户会经历 50-100ms。世界另一端的请求者则会经历 250 毫秒以上的延迟。
工程师经常使用内容分发网络(CDN)来缓解这一问题。使用 CDN 时,数据仍然只有一个真实来源,但我们会在全球多个位置添加数据缓存。数据请求会被发送到地理上最近的缓存。如果该缓存没有所需数据,它才会向核心真实来源请求数据。
我们在下方提供了一个简单的可视化。使用箭头在地图上移动请求者,看看它如何影响数据请求被发送到的位置。
我们也可以选择在所有 CDN / 缓存节点中存储完整数据。在这种情况下,如果数据从不改变,我们就可以完全避免缓存未命中!唯一需要获取数据的时候是原始数据被修改时。
当缓存已满且需要存储新条目时,替换策略决定淘汰哪个条目来为新条目腾出空间。
我们可以使用多种算法来决定淘汰哪些值以及何时淘汰。在这里做出正确的选择对性能有显著影响。
先进先出(First In, First Out,FIFO)是最简单的缓存替换策略。它像队列一样工作。新条目被添加到开头(左侧)。当缓存队列已满时,最早添加的条目会被淘汰(右侧)。
在这个可视化中,数据请求从顶部进入。当一个项目被请求并且是缓存命中时,它会立即向上滑动,表示响应。当发生缓存未命中时,新数据从左侧添加。
虽然实现简单,但 FIFO 对于大多数缓存场景并不是最优的,因为它不考虑使用模式。我们有更智能的技术来处理这个问题。
最近最少使用(LRU)是一种流行的选择,也是许多缓存系统的行业标准。与 FIFO 不同,LRU 总是淘汰最近最少被请求的项目,这是最大化缓存命中的合理选择。这与现实世界数据访问模式中的时间局部性很好地吻合。
当发生缓存命中时,该项目的位置会更新到队列的开头。这种策略在许多现实场景中效果很好,因为最近访问过的项目很可能很快会再次被访问。
我们可以通过多种方式增强 LRU 算法,使其对时间敏感。具体如何实现取决于我们的用例,但通常涉及使用 LRU 并为缓存中的每个元素设置一个计时器。时间到了,我们就淘汰该元素!
我们可以想象这在以下情况下很有用:
对于社交网络,在 48 小时后自动从缓存中淘汰帖子。
对于天气应用,当时间进入新的一天时,自动从缓存中淘汰前几天的天气信息。
在电子邮件应用中,一周后从缓存中移除邮件,因为之后不太可能再次被阅读。
我们可以利用用户的查看模式来对元素设置自动过期。我们在下面进行了可视化。这个缓存使用 LRU,但每个元素还有一个淘汰计时器,超过时间后无论使用模式如何都会被移除。
在某些场景中还使用了其他很酷/小众的算法。一个很酷的是最近最不频繁使用(Least-Frequently Recently Used)。这涉及管理两个队列,一个用于高优先级项目,一个用于低优先级项目。高优先级队列使用 LRU 算法,当需要淘汰一个元素时,它会被移动到低优先级队列,然后该队列使用更激进的替换算法。
你还可以设置缓存,根据时间做定制化的事情,比简单的时间感知 LRU 更复杂。
缓存经常被放在数据库前面,作为缓存慢查询结果的一种机制。然而,像 Postgres 和 MySQL 这样的 DBMS 在其实现中也使用了缓存。
Postgres 实现了两层缓存策略。首先,它使用 shared_buffers,这是一个用于存储表信息的数据页的内部缓存。这会将频繁读取的行数据保留在内存中,而不太频繁访问的数据则留在磁盘上。其次,Postgres 严重依赖操作系统的文件系统页缓存,它在内核级别缓存磁盘页。这创建了一个双重缓冲系统,数据可以同时存在于 Postgres 的 shared_buffers 和操作系统页缓存中。许多部署将 shared_buffers 设置为可用 RAM 的约 25%,并让文件系统缓存处理大部分剩余的缓存工作。shared_buffers 的值可以在 postgres 配置文件中配置。
MySQL 通过 buffer pool 做了类似的事情。与 Postgres 一样,这是一个内部缓存,用于将最近使用的数据保留在 RAM 中。
可以说,这些比“常规”缓存更复杂,因为它们还必须能够以完整的 ACID 语义和数据库事务运行。随着数据的发展,这两个数据库都必须采取谨慎措施,确保这些页包含准确的信息和元数据。
我非常认真地说,这篇文章仅仅触及了缓存的皮毛。我们完全避开了缓存系统中处理写入和更新的主题。我们很少讨论用于缓存的具体技术,如 Redis、Memcached 等。我们没有涉及一致性问题、分片缓存以及许多其他有趣的细节。
我希望这能让你对缓存有一个很好的概述和认识,以及它在计算各个层面中的普遍性。你使用的几乎每一项数字技术都依赖于缓存,现在你对它有了更多一点了解。
如果你喜欢这篇文章,你可能会喜欢这些关于大型科技公司如何实现缓存的案例研究:
Twitter 的 PB 级缓存系统
Uber 如何处理每秒 4000 万次缓存请求
TikTok 的数据库处理 1 亿 QPS(并使用了缓存)
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:PlanetScale Blog
原文作者:PlanetScale Blog;如有侵权,请联系管理员删除。