2 架构核心技术之分布式缓存

要自己努力去争取的。

学习自:架构师的 36 项修炼

1 开场白

缓存是架构设计中一个重要的手段,因为其技术比较简单,同时对性能提升的效果又很显著。使用缓存需要注意几个关键指标:缓存键集合大小、缓存空间的大小以及缓存的使用寿命。这三个指标决定了缓存的有效性、缓存的使用效率、缓存实现的效果。缓存的类型主要有代理缓存、反向代理缓存、 CDN 缓存和对象缓存几种。

不是所有的数据都适合使用缓存:

  • 数据频繁修改,这类数据使用缓存效果比较差。

  • 数据没有热点,这类数据缓存的命中率比较差。

  • 数据不一致,因为缓存的数据和数据库的数据是不同步的,可能存在数据不一致的情况,如果业务场景对数据一致性要求非常高,这个时候使用缓存也要注意。

  • 缓存雪崩,当缓存崩溃的时候,可能会导致整个系统的崩溃,这也是使用缓存中要注意的一个事项。

在架构中使用最多的是分布式缓存。分布式缓存最重要的几个技术点是:分布式对象缓存的架构、分布式对象缓存的访问模型,以及分布式缓存中一个重要的算法——一致性哈希算法

2 缓存的特点

缓存的主要特点:

  • 技术简单

  • 性能提升显著

  • 应用场景多

在计算机的整个体系结构中缓存几乎是无处不在的,比如,CPU 中就有缓存,在 CPU 固件里就有 Cache,当 CPU 进行计算的时候,它并不总是每次都去内存中读取数据,而是预加载一部分指令和数据到 Cache 里面,也就是 CPU 的缓存里面,CPU 核心计算取的数据其实大多数是 CPU 缓存中的数据。

                                           

操作系统的文件缓存。操作系统对磁盘进行操作的时候,它也会对数据进行缓存,以加快操作系统访问磁盘文件数据的速度。

数据库的查询缓存,数据库本身也会对一些数据表进行缓存。比如对索引的结构 B+ 树进行缓存,对一些热点的数据记录也要进行缓存,以加快应用程序的访问速度。

在外部应用系统中,比较常用的有 DNS 客户端缓存、HTTP 浏览器缓存、HTTP 代理和反向代理缓存、CDN 缓存,以及各种类型的对象缓存,对象缓存常用的比如 Redis、Memcached 等等。

3 缓存数据存储(Hash 表)

数据结构检索(查找)之入土攻略(二)

缓存是存储在内存中的,那么如何从内存中快速获取一个数据呢?

缓存使用的数据结构主要是哈希(Hash)表。哈希表最终的存储形式通常是一个顺序表,也就是一个数组结构。数组结构的特点是在内存中连续存储分配。那么,当我们要在哈希表中存储一个数据的时候,哈希表通常是以 key、value 这样的数据结构进行存储的。

哈希表真正的物理存储是一个数组,通过哈希表可以使整个数据存储或检索效率时间复杂都是 O(1)。

4 缓存的关键指标——命中率

缓存的主要特点是一次写入多次读出,通过这种手段减少对数据库的使用,尽快从缓存中读取数据,提高性能。所以缓存是否有效,主要就是看它一次写进去的缓存能不能够多次去读出来响应业务的请求,这个判断指标就叫作缓存的命中率。

缓存命中率怎么算呢?查询得到正确缓存结果除以总的查询次数,得到的比值就是缓存命中率,比如说十次查询九次都能够得到缓存的正确结果,命中率就是 90%。

影响缓存命中率的主要因素有三个,分别是:

  • 缓存键集合大小

  • 内存空间大小

  • 缓存的寿命

4.1 缓存键集合大小

缓存中的每个对象都是通过缓存键进行识别的。刚才 abc hello 这个例子里 abc 就是一个缓存的键,键是缓存中唯一的识别符,定位一个对象的唯一方式就是对缓存键进行精确的匹配。

比如说我们想缓存每个商品的在线商品信息,就需要使用商品 ID 作为缓存键。应用生成的唯一键越多,重用的机会越小。比如说根据 IP 地址缓存天气数据,可能需要 40 多亿个键。但是如果基于国家缓存天气数据,那么只需要几百个缓存键就够了,全世界也不过就几百个国家。

所以要尽可能减少缓存键的数量,键的数量越少,缓存的效率越高。设计缓存的时候要关注缓存键是如何进行设计的,它的整个的集合范围,限定在一个既能够高效使用,又可以减少它的数量,这个时候缓存的性能是最好的。

4.2 缓存内存空间大小

缓存通常是存储在内存中的,缓存对象可用的内存空间相对来说比较昂贵,而且受到严格限制。如果想缓存更多的对象,就需要先删除老的对象,再添加新的对象。而这些老的对象被删除掉,就会影响到缓存的命中率。所以物理上缓存的空间越大,缓存的对象越多,缓存的命中率也就越高。

4.3 缓存对象生存时间(缓存寿命)

 TTL:Time To Live

对象缓存的时间越长,被重用的可能性就越高。使缓存失效的方法有两种:一种是超时失效;一种是清除失效,也就是实时清除。

(1) 超时失效

写缓存的时候,每个缓存对象都设置一个超时时间,在超时之前访问缓存就会返回缓存的数据,而一旦超时,缓存就失效了,这时候再访问缓存,就会返回空。

(2) 实时清除

当有缓存对象更新的时候,直接通知缓存将已经被更新了的数据进行清除。清除了以后,应用程序下一次访问这个缓存对象键的时候,就不得不到数据库中去查找读取,这个时候就会得到最新的数据,因为更新总是更新在数据库里的。

还有一种,虽然时间上还没有失效但是新的对象要写入缓存,而内存空间不够了,这个时候就需要将一些老的缓存对象清理掉,为新的缓存对象腾出空间。

内存空间清除主要使用的算法是 LRU(Least Recently Used)算法,LRU 算法就是最近最久未用算法,也就是说清除那些最近最久没有被访问过的对象。这个算法使用链表结构实现的,所有的缓存对象都放在同一个链表上。当一个对象被访问的时候,就把这个对象移到整个链表的头部。当需要通过 LRU 算法清除那些最近最久未用对象的时候,只需要从队列的尾部进行查找,越是在队列尾部的,越是最近最久没有被访问过的,也就是优先清除的,腾出的内存空间让新对象加入进来。

5 缓存的主要类型

5.1 代理缓存

代理缓存是存在客户端一端的缓存,代理客户端访问互联网。它的主要作用是互联网访问代理,代理了所有的客户端 HTTP 请求,可以进行页面缓存。如果有一些其他的客户端已经访问过这个网页,那么当新的客户端连接的时候,就可以通过代理缓存中的数据直接返回,避免对数据中心的访问。

5.2 反向代理缓存

反向代理代理数据中心输出,存在于系统数据中心里,它是数据中心的统一入口,代理整个数据中心其他服务器的应用处理。

用户通过互联网连接到数据中心的时候,连接的通常是一个反向代理服务器,反向代理服务器根据用户的请求,在本地的反向代理缓存中查找是否有用户请求的数据,如果有就直接返回这个数据,如果没有再把这个请求向下继续转发,请求后面的应用服务器去处理生成数据。

反向代理缓存可以多层反向代理缓存的形式出现。因为我们的应用服务器也是经过分层的,在处理的前端通常是一个前端服务器,后面有 Web 服务器,之后有应用服务器,再后还有其他的各类服务器。在这样一个分层的服务器结构里,我们可以对每一层的服务器都进行反向代理缓存。

如下图所示,前端 Web 服务器和 Web 服务器分为两层,用户请求接入的时候,先接入前端 Web 服务器,其上可以加一层反向代理服务器来代理前端 Web 服务器的 HTTP 请求。如果用户请求的数据已经包含在这个反向代理服务器中,就可以直接返回;如果没有,就再把 HTTP 请求提交给前端 Web 服务器,前端 Web 服务器会把请求发给后面的 Web 服务器。在 Web 服务器和前端 Web 服务器之间还可以再加一层反向代理服务器。如果前端 Web 服务器的请求在这一层的反向代理服务器中存在,那么这一层反向代理服务器可以直接将数据返还;如果不存在,再将请求下发给 Web 服务器。

5.3 内容分发网络 CDN 缓存

所谓的 CDN 是指在用户请求的前端(尽量前的前端)为用户提供数据服务。CDN 并不存在于我们的数据中心,也不存在于用户的访问系统一端,它介于两者之间,作为网络服务商的缓存服务。用户进行互联网访问的时候,需要通过互联网网络服务商提供的网络链接才能够连接到数据中心,那么网络服务商就可以在自己提供的网络服务的机房里进行一次缓存操作,提供一次缓存服务。如下图所示。

客户端第一次访问 example.com 的时候,访问数据中心,数据中心返回 HTML 页面以后,客户端解析 HTML,HTML 里面还各种 js 文件、css 文件、图片等,这些静态资源访问的就是 CDN 服务器。CDN 服务器检查自己是否有需要的静态资源,如果有,就立即返回给客户端;如果没有,就自己访问数据中心,获得需要的静态资源后,缓存在 CDN 服务器上后,再返回客户端。

所以 CDN 缓存也叫作网络访问的“第一跳”,用户请求先到达的是互联网网络服务商的机房。在机房里面部署 CDN 服务器,提供缓存服务。如果 CDN 中存在用户请求的 Web 响应内容,那么就可以直接通过 CDN 进行返回;如果 CDN 中不存在,那么 CDN 会把这个请求通过后面的网络连接,把它发到系统的数据中心去。数据中心返回的结果依然是先通过 CDN 服务器,CDN 服务器就可以把数据缓存在自己的本地,供后面的用户请求操作响应。 

5.4 通读缓存

上面讲到的代理缓存、反向代理缓存、CDN 缓存,都是通读缓存。它代理了用户的请求,也就是说用户在访问数据的时候,总是要通过通读缓存。

当通读缓存中有需要访问的数据的时候,直接就把这个数据返回;如果没有,再由通读缓存向真正的数据提供者发出请求。其中重要的一点是客户端连接的是通读缓存,而不是生成响应的原始服务器,客户端并不知道真正的原始服务器在哪里,不会直接连接原始服务器,而是由通读缓存进行代理。

5.5 旁路缓存

和通读缓存相对应的叫作旁路缓存。前面提到的 key、value 这样的对象缓存就属于旁路缓存。旁路缓存和通读缓存不同。旁路缓存是客户端先访问旁路缓存中是否有自己想要的数据,如果旁路缓存中没有需要的数据,那么由客户端自己去访问真正的数据服务提供者,获取数据。客户端获得数据以后,会自己把这个数据写入到旁路缓存中,这样下一次或者其他客户端去读取旁路缓存的时候就可以获得想要的数据了。

在这里插入讲解一下各种介质数据访问的延迟,以便对数据的存储、缓存的特性以及数据的访问延迟有一个感性的认识。      

如上图所示,访问本地内存大概需要 100ns 的时间;使用 SSD 磁盘进行搜索,大概需要 10万ns 时间;数据包在同一个数据中心来回一次的时间,也就是在同一个路由环境里进行一次访问,大概需要 50万ns 时间,也就是 0.5ms;使用非 SSD 磁盘进行一次搜索,大概需要 1000万ns,也就是 10ms 的时间;按顺序从网络中读取 1MB 的数据也是需要 10ms 的时间;按顺序从传统的机械磁盘,即非 SSD 磁盘,读取 1MB 数据,大概需要 30ms 的时间;跨越大西洋进行一次网络数据传输,一个来回大概需要 150ms 的时间。其中,1s 等于 1000ms,等于 10亿ns。

6 合理使用缓存

6.1 注意频繁修改的数据

缓存数据是为一次写入多次读取准备的,但是如果写入的数据很快就被修改掉了,数据还没来得及读取就已经失效或者更新了,系统的负担就会很重,使用缓存也就没有太多的意义。一般说来,数据的读写比例至少在 2:1 以上,缓存才有意义。

6.2 注意没有热点的访问数据

写入的数据并不会被多次读取,也就是所谓的没有热点,这时候使用缓存也是没有意义的。

在淘宝中,那些热门的商品可能会被几百万几千万次的访问,那些冷门的商品可能一次访问都没有,热门商品数据就是有热点的,就需要缓存。在微博中也是,那些微博大V们的微博会被几百万几千万的粉丝访问,他们的微博数据也是有热点的,而那些没有几个粉丝的博主的微博,几乎不会被访问,这些数据是没有热点的。所以缓存存储的就是淘宝上那些热门的商品,微博上那些大V的微博,它们都是有热点的缓存,这些数据都能实现一次写入多次甚至非常多次的读取,这种缓存就有效果。但还是有一些业务场景数据是没有热点的,那么这一类业务场景数据就不需要使用缓存。

6.3 注意数据不一致和脏读

缓存中的数据有可能和主存储数据库中的数据不一致。这个问题主要是通过失效时间来解决的,也就是说这个业务能够容忍的失效时间之内,保持缓存中的数据和数据库中的数据不一致,比如说淘宝的商品数据,如果卖家在对商品的数据进行了编辑,这个时候可能买家是看不到这些被更新过的数据的,可能需要几分钟的时间,比如是 3 分钟,那么 3 分钟之内,卖家编辑的数据买家是看不到的,但这种延迟通常是可以接受的。

如果某些业务场景对更新非常敏感,必须要实时看到,这个时候就不能够使用失效时间进行缓存过期处理了,可能需要进行失效通知。当数据进行更新的时候,立即清除缓存中的数据,下次访问这个数据的时候,缓存必须要重新从主数据库中去加载,才能够得到最新的数据。

6.4 注意缓存雪崩

因为热点数据主要是从缓存中去读取的,而热点数据是数据访问压力最大的一类数据。这些数据都从缓存中读取,极大地降低了数据库的访问压力。

而数据库整个系统也是在有缓存的情况下进行设计的,数据库的处理能力是强依赖缓存的。如果缓存忽然崩溃了,那么所有的访问压力就都会传递到数据库上去。数据库不能够承受这样的访问压力,可能也会崩溃。数据库崩溃了以后,应用程序访问不到数据库,请求不断超时,负载压力不断升高,应用程序服务器也会崩溃,最后导致整个网站所有服务器崩溃。这就是缓存雪崩。这种情况下系统甚至无法启动,因为系统启动后,新的访问压力又过来,依然是那么大,还是会崩溃。

这时候重启缓存也是没有用的,因为重启的话缓存中是没有数据的。我们刚才也讲到,对象缓存是通过加载数据库中的数据并写入到缓存中才有数据的。重新启动的缓存没有数据,它就不能够承担提供数据读操作的能力。所以,对缓存有重点依赖的系统,需要特别关注缓存的可用性。缓存用的部分数据丢失可以到数据库中加载,但是如果全部的缓存数据都丢失了,可能导致整个系统都会崩溃,特别需要注意。

7 分布式缓存

分布式对象缓存:对象缓存以一个分布式集群的方式对外提供服务,多个应用系统使用同一个分布式对象缓存提供的缓存服务。这里的缓存服务器是由多台服务器组成的,这些服务器共同构成了一个集群对外提供服务。

如何找到正确的缓存服务器进行读写操作?

如果第一次写入数据的时候写入的是 A 服务器,但是数据进行缓存读操作的时候访问的是 B 服务器,就不能够正确地查找到数据,缓存也就没有了效果。

7.1 Memcached 服务器集群

当需要进行分布式缓存访问的时候,依然是以 Key、value 这样的数据结构进行访问。如上图所示的例子中就是 BEIJING 作为 Key,一个 DATA 数据作为它的 value。当需要进行分布式对象访问的时候,应用程序需要使用分布式对象缓存的客户端 SDK。比如说 Memcached 提供的一个客户端 API 程序进行访问,客户端 API 程序会使用自己的路由算法进行路由选择,选择其中的某一台服务器,找到这台服务器的 IP 地址和端口以后,通过通讯模块和相对应的服务器进行通信。

因为进行路由选择的时候,就是使用缓存对象的 key 进行计算。下一次使用相同的 key 使用相同路由算法进行计算的时候,算出来的服务器依然还是前面计算出来的这个服务器。所以通过这种方法可以访问到正确的服务器进行数据读写。服务器越多,提供的缓存空间就越大,实现的缓存效果也就越好。通过集群的方式,提供了更多的缓存空间。

路由算法又是如何进行服务器路由选择的?

使用余数哈希。比如说,我们这里缓存服务器集群中有 3 台服务器,key 的哈希值对 3 取模得到的余数一定在 0、1、2 三个数据之间,那么每一个数字都对应着一台服务器,根据这个数字查找对应的服务器 IP 地址就可以了。使用余数取模这种方式进行路由计算非常简单,但这种算法也有一个问题,就是当服务器进行扩容的时候,比如说我们当前的服务器集群有 3 台服务器,如果我们 3 台服务器不够用了,需要添加 1 台服务器,这个时候对 3 取模就会变成对 4 去取模,导致的后果就是以前对 3 取模的时候写入的数据,对 4 取模的时候可能就查找不到了。

上面也讲过缓存雪崩的情况,实际上如果使用取模算法进行服务器添加,因为除数的变化会导致和缓存雪崩一样的后果,也就是说前面写入缓存服务器集群中的缓存数据,添加了 1 台服务器后很多数据都找不到了,类似于雪崩,最后会导致整个服务器集群都崩溃。

我们添加服务器的主要目的是提高它的处理能力,但是不正确的操作可能会导致整个集群都失效。解决这个问题的主要手段是使用一致性哈希算法。

7.2 一致性哈希算法

一致性哈希和余数哈希不同,一致性哈希首先是构建一个一致性哈希环的结构。一致性哈希环的大小是 0~2 的 32 次方减 1,实际上就是我们计算机中无符号整型值的取值范围,这个取值范围的 0 和最后一个值 2 的 32 次方减 1 首尾相连,就构成了一个一致性哈希环,如下图所示。

对每个服务器的节点取模,求它的哈希值并把这个哈希值放到环上,所有的服务器都取哈希值放到环上,每一次进行服务器查找路由计算的时候,把 key 也取它的哈希值,取到哈希值以后把 key 放到环上,顺时针查找距离它最近的服务器的节点是哪一个,它的路由节点就是哪一个。通过这种方式也可以实现,key 不变的情况下找到的总是相同的服务器。这种一致性哈希算法除了可以实现像余数哈希一样的路由效果以外,对服务器的集群扩容效果也非常好。

在一致性哈希环上进行服务器扩容的时候,新增加一个节点不需要改动前面取模算法里的除数,导致最后的取值结果全部混乱,它只需要在哈希环里根据新的服务器节点的名称计算它的哈希值,把哈希值放到这个环上就可以了。放到环上后,它不会影响到原先节点的哈希值,也不会影响到原先服务器在哈希环上的分布,它只会影响到离它最近的服务器,比如上图中 NODE3 是新加入的服务器,那么它只会影响到 NODE1,原先访问 NODE1 的 key 会访问到 NODE3 上,也就是说对缓存的影响是比较小的,它只会影响到缓存里面的一小段。如果缓存中一小部分数据受到了影响,不能够正确的命中,那么可以去数据库中读取,而数据库的压力只要在它的负载能力之内,也不会崩溃,系统就可以正常运行。所以通过一致性哈希算法可以实现缓存服务器的顺利伸缩扩容。

但是一致性哈希算法有着致命的缺陷。我们知道哈希值其实是一个随机值,把一个随机值放到一个环上以后,可能是不均衡的,也就是说某两个服务器可能距离很近,而和其它的服务器距离很远,这个时候就会导致有些服务器的负载压力特别大,有些服务器的负载压力非常小。同时在进行扩容的时候,比如说加入一个节点 3,它影响的只是节点 1,而我们实际上希望加入一个服务器节点的时候,它能够分摊其它所有服务器的访问压力和数据冲突。

所以对这个算法需要进行一些改进,改进办法就是使用虚拟节点。也就是说我们这一个服务器节点放入到一致性哈希环上的时候,并不是把真实的服务器的哈希值放到环上,而是将一个服务器虚拟成若干个虚拟节点,把这些虚拟节点的 hash 值放到环上去。在实践中通常是把一个服务器节点虚拟成 200 个虚拟节点,然后把 200 个虚拟节点放到环上。key 依然是顺时针的查找距离它最近的虚拟节点,找到虚拟节点以后,根据映射关系找到真正的物理节点。

第一,可以解决我们刚才提到的负载不均衡的问题,因为有更多的虚拟节点在环上,所以它们之间的距离总体来说大致是相近的。第二,在加入一个新节点的时候,是加入多个虚拟节点的,比如 200 个虚拟节点,那么加入进来以后环上的每个节点都可能会受到影响,从而分摊原先每个服务器的一部分负载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值