分布式缓存的背景
了解设计分布式缓存的基础知识。
本章的主要目标是设计分布式缓存。为了实现这个目标,我们应该有丰富的背景知识,主要是通过不同的阅读去更深层次的了解设计技巧。本课将帮助我们建立这些背景知识。让我们看看下表中本课的目录结构
本课结构
部分 | 动机 |
---|---|
编写策略 | 数据写入缓存和数据库。数据写入发生的顺序对性能有影响。我们将讨论各种写入策略,以帮助确定哪种写入策略适合我们要设计的分布式缓存。 |
驱逐策略 | 由于缓存是建立在有限的存储空间 (RAM) 上的,因此理想情况下我们希望将最常访问的数据保留在缓存中。因此,我们将讨论不同的驱逐策略,以用最常访问的数据替换不常访问的数据。 |
缓存失效 | 某些缓存数据可能会过时。在本节中,我们将讨论不同的失效方法,以从缓存中删除陈旧或过时的条目。 |
存储机制 | 分布式存储有很多服务器。我们将讨论重要的设计注意事项,例如哪个缓存条目应存储在哪个服务器中以及使用哪种数据结构进行存储。 |
缓存客户端 | 缓存服务器存储缓存条目,但缓存客户端调用缓存服务器请求数据。我们将在本节中讨论缓存客户端库的详细信息。 |
编写策略
通常,缓存存储数据的副本(或部分),该副本持久存储在数据存储中。当我们将数据存储到数据存储时,会出现一些重要的问题:
相关信息
- 我们首先将数据存储在哪里?数据库还是缓存?
- 每种策略对一致性模型的影响是什么?
简短的回答是,这取决于应用要求。让我们看一下不同写入策略的细节,以更好地理解这个概念:
- 直写缓存:直写机制在缓存和数据库上分别写入。在两个存储上写入可以同时发生,也可以一个接一个地发生。这增加了写入延迟,但确保了数据库和缓存之间的强一致性。
- Write-back cache:在write-back cache(回写缓存)机制中,数据先写入缓存,再异步写入数据库。虽然缓存中有更新的数据,但在客户端从数据库中读取陈旧数据的场景下,不一致是不可避免的。但是,使用这种策略的系统会有很小的写入延迟。
- Write-around cache:在Write-around cache(绕写缓存)这种策略只涉及将数据写入数据库。稍后,当触发对数据的读取时,它会在缓存未命中后写入缓存。数据库会有更新的数据,但这样的策略不利于读取最近更新的数据。
# 测验
# 第一题
系统想要写入数据并立即将其读回。同时,我们希望缓存和数据库保持一致。哪种写入策略是最佳选择?
a) 直写缓存 b) 绕写高速缓存 c)回写缓存
# 第二题
关于性能,写入密集型系统应该避免什么?
a) 回写缓存 b) 直写缓存 c) 绕写缓存
# 第三题
过时或陈旧的数据输入是哪个编写策略中的典型问题?
a) 直写式 b) 回写 c) 绕写
#注释: 一般我们把计算机系统分为数据密集型系统和计算密集型系统,而偏向业务工程一般都属于数据密集型系统。对于这些系统,CPU往往不是系统瓶颈,
关键在于数据大、数据复杂度以及数据快速多变性。数据密集型系统也由各个模块组成,每个模块负责单一职责,一般的数据密集型系统包含以下模块:
数据库:用于存储数据,具有持久性,通常用作主存
高速缓存:内存读取,提高部分数据读取速度
索引:根据关键字搜索过滤
流式处理:持续发送消息到另一个进程,采用异步方式
批处理:定时处理大量数据
驱逐策略
高速缓存执行速度快的主要原因之一是因为它们很小。小缓存意味着存储容量有限。因此,我们需要一种逐出机制,将访问频率较低的数据从缓存中移除。
几种众所周知的策略用于从缓存中逐出数据。最著名的策略包括:
- 最近最少使用(LRU)
- 最近使用 (MRU)
- 最不常用 (LFU)
- 最常用 (MFU)
其他策略如先进先出 (FIFO) 也存在。这些算法中的每一个的选择取决于缓存被开发的系统。
数据温度
根据访问频率,数据可以分为三个温度区域:
- 热:这是高度访问的数据。
- 暖:这是访问频率较低的数据。
- 冷:这是很少访问的数据。
冷数据经常从缓存中被逐出,并被热数据或温数据取代。下图显示了数据温度的频谱:
缓存失效
除了逐出不常访问的数据外,缓存中的一些数据可能会随着时间的推移变得陈旧或过时。此类缓存条目无效,必须标记为删除。
这种情况我们会面临一个问题:我们如何识别过时的条目?
解决该问题需要存储与每个缓存条目对应的元数据。具体来说,维护一个生存时间 (TTL)值来处理过时的缓存项。
我们可以使用两种不同的方法来处理使用 TTL 的过期项目:
- 主动过期:此方法通过守护进程或线程主动检查缓存条目的 TTL。
- 被动过期:此方法在访问时检查缓存条目的 TTL。
每个过期的项目在发现后都会从缓存中删除。
提示
注释: TTL的主要作用是避免IP包在网络中的无限循环和收发,节省了网络资源,并能使IP包的发送者能收到告警消息。 TTL 是由发送主机设置的,以防止数据包不断在IP互联网络上永不终止地循环。转发IP数据包时,要求路由器至少将 TTL 减小 1。 虽然TTL从字面上翻译,是可以存活的时间,但实际上TTL是IP数据包在计算机网络中可以转发的最大跳数。TTL字段由IP数据包的发送者设置,在IP数据包从源到目的的整个转发路径上,每经过一个路由器,路由器都会修改这个TTL字段值,具体的做法是把该TTL的值减1,然后再将IP包转发出去。如果在IP包到达目的IP之前,TTL减少为0,路由器将会丢弃收到的TTL=0的IP包并向IP包的发送者发送 ICMP time exceeded消息。
存储机制
在缓存中存储数据并不像看起来那么简单,因为分布式缓存有多个缓存服务器。当我们使用多个缓存服务器时,需要回答以下设计问题:
- 我们应该将哪些数据存储在哪些缓存服务器中?
- 我们应该使用什么数据结构来存储数据?
以上两个问题是重要的设计问题,因为它们将决定我们分布式缓存的性能,这是我们最重要的需求。我们将使用以下技术来回答上述问题。
哈希函数
可以在两种不同的情况下使用散列:
- 识别分布式缓存中的缓存服务器以存储和检索数据。
- 在每个缓存服务器中找到缓存条目。
对于第一种情况,我们可以使用不同的哈希算法。然而,一致性哈希或其风格通常在分布式系统中表现良好,但是简单的哈希在崩溃或扩展的情况下并不是理想的。
在第二种情况下,我们可以使用典型的哈希函数来定位缓存条目以在缓存服务器内部进行读取或写入。然而,单独的散列函数只能定位缓存条目。它没有说明在缓存服务器中管理数据的任何内容。也就是说,它没有说明如何实施策略以从缓存服务器中逐出不常访问的数据。它也没有说明缓存服务器中使用什么数据结构来存储数据。这正是存储机制的第二个设计问题。接下来我们看一下数据结构。
提示
注释:Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
链表
我们将使用双向链表。主要原因是它的广泛使用和简单性。此外,在我们的例子中,在双向链表中添加和删除数据将是一个常量时间操作。这是因为我们要么从链表的尾部逐出一个特定的条目,要么将一个条目重新定位到双向链表的头部。因此,不需要迭代。
相关信息
布隆过滤器 是一个有趣的选择,用于快速查找缓存服务器中是否不存在缓存条目。我们可以使用布隆过滤器来确定某个缓存条目肯定不存在于缓存服务器中,但它存在的可能性是概率性的。布隆过滤器在大型缓存或数据库系统中非常有用。
缓存集群中的分片
为了避免 SPOF 和单个缓存实例的高负载,我们引入了分片。分片涉及在多个缓存服务器之间拆分缓存数据。可以通过以下两种方式进行。
注释:分片(sharding)是数据库分区的一种,它将大型数据库分成更小、更快、更容易管理的部分,这些部分叫做数据碎片。碎片这个词意思就是整体的一小部分。
专用缓存服务器
在专用缓存服务器方法中,我们将应用程序和 Web 服务器与缓存服务器分开。
使用专用缓存服务器的优点如下:
- 每个功能的硬件选择都非常灵活。
- 可以分别扩展 Web/应用程序服务器和缓存服务器。
除了上述优势之外,作为独立的缓存服务工作还可以让其他微服务从中受益——例如,缓存即服务。在这种情况下,缓存系统必须了解不同的应用程序,以免它们的数据发生冲突。
并置缓存
并置缓存将缓存和服务功能嵌入到同一主机中。
该策略的主要优点是减少了额外硬件的资本支出和运营支出。此外,随着一个服务的缩放,获得了另一个服务的自动缩放。但是,一台机器出现故障会导致两种服务同时丢失。
缓存客户端
我们讨论了散列函数应用于缓存服务器的选择。但是什么实体执行这些哈希计算?
缓存客户端是驻留在托管服务器中的一段代码,这些代码执行(散列)计算以在缓存服务器中存储和检索数据。此外,缓存客户端可以与其他系统组件(如监控和配置服务)协调。所有缓存客户端都以相同的方式编程,以便来自不同客户端的相同PUT和GET操作返回相同的结果。缓存客户端的一些特征如下:
- 每个缓存客户端都知道所有的缓存服务器。
- 所有客户端都可以使用众所周知的传输协议(如 TCP 或 UDP)与缓存服务器通信。
警告
问题
如果其中一个缓存服务器死了,缓存客户端对访问请求的行为是什么?
答案
由于缓存服务器中的数据将不再可用,缓存客户端会将访问请求标记为缓存未命中。
结论
在本课中,我们了解了分布式缓存是什么,并强调了它们在分布式系统中的重要性。我们还讨论了缓存的不同存储和驱逐机制。缓存对于任何分布式系统都至关重要,并且位于系统设计中的不同位置。了解如何将分布式缓存设计为大型系统的一部分非常重要。