分布式

[转]解析分布式系统的缓存设计

2022-06-09  本文已影响0人  风静花犹落

作者:vivo互联网服务器团队-Zhang Peng

一、缓存简介

1.1 什么是缓存

缓存就是数据交换的缓冲区。缓存的本质是一个内存 Hash。缓存是一种利用空间换时间的设计,其目标就是更快、更近:极大的提高。

缓存是用于存储数据的硬件或软件的组成部分,以使得后续更快访问相应的数据。缓存中的数据可能是提前计算好的结果、数据的副本等。典型的应用场景:有 cpu cache, 磁盘 cache 等。本文中提及到缓存主要是指互联网应用中所使用的缓存组件。

缓存命中率是缓存的重要度量指标,命中率越高越好。

缓存命中率 = 从缓存中读取次数 / 总读取次数

1.2 何时需要缓存

引入缓存,会增加系统的复杂度。所以,引入缓存前,需要先权衡是否值得,考量点如下:

在数据层引入缓存,有以下几个好处:

1.3 缓存的基本原理

根据业务场景,通常缓存有以下几种使用方式:

1.4 缓存淘汰策略

缓存淘汰的类型:

1)基于空间:设置缓存空间大小。

2)基于容量:设置缓存存储记录数。

3)基于时间

缓存淘汰算法:

1)FIFO:先进先出。在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。

2)LRU:最近最少使用算法。在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。但是这个依然有个问题,如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。

3)LFU:最近最少频率使用。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。

这三种缓存淘汰算法,实现复杂度一个比一个高,同样的命中率也是一个比一个好。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的 LRU。

二、缓存的分类

缓存从部署角度,可以分为客户端缓存和服务端缓存。

客户端缓存

服务端缓存

其中,CDN 缓存、反向代理缓存、数据库缓存一般由专职人员维护(运维、DBA)。后端开发一般聚焦于进程内缓存、分布式缓存。

2.1 HTTP 缓存

2.2 CDN 缓存

CDN 将数据缓存到离用户物理距离最近的服务器,使得用户可以就近获取请求内容。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。

国内网络异常复杂,跨运营商的网络访问会很慢。为了解决跨运营商或各地用户访问问题,可以在重要的城市,部署 CDN 应用。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

图片引用自:Why use a CDN

2.1.1 CDN 原理

CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。

1)未部署 CDN 应用前的网络路径:

在不考虑复杂网络的情况下,从请求到响应需要经过 3 个节点,6 个步骤完成一次用户访问操作。

2)部署 CDN 应用后网络路径:

在不考虑复杂网络的情况下,从请求到响应需要经过 2 个节点,2 个步骤完成一次用户访问操作。与不部署 CDN 服务相比,减少了 1 个节点,4 个步骤的访问。极大的提高了系统的响应速度。

2.1.2 CDN 特点

优点

缺点

解决方案:主要缓存静态资源,动态资源建立多级缓存或准实时同步;

1.解决方案(主要是在性能和数据一致性二者间寻找一个平衡)。

2.设置缓存失效时间(1 个小时,过期后同步数据)。

3.针对资源设置版本号。

2.2 反向代理缓存

反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

image-20220609114522992.png
2.2.1 反向代理缓存原理

反向代理位于应用服务器同一网络,处理所有对 WEB 服务器的请求。反向代理缓存的原理:

这种方式通过降低向 WEB 服务器的请求数,从而降低了 WEB 服务器的负载。

反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理。常用的缓存应用服务器有 Varnish,Ngnix,Squid。

2.2.2 反向代理缓存比较

常用的代理缓存有 Varnish,Squid,Ngnix,简单比较如下:

三、进程内缓存

进程内缓存是指应用内部的缓存,标准的分布式系统,一般有多级缓存构成。本地缓存是离应用最近的缓存,一般可以将数据缓存到硬盘或内存。

常见的本地缓存实现方案:HashMap、Guava Cache、Caffeine、Ehcache。

3.1 ConcurrentHashMap

最简单的进程内缓存可以通过 JDK 自带的 HashMap 或 ConcurrentHashMap 实现。

3.2 LRUHashMap

可以通过继承 LinkedHashMap 来实现一个简单的 LRUHashMap。重写 removeEldestEntry 方法,即可完成一个简单的最近最少使用算法。

缺点:

3.3 Guava Cache

解决了LRUHashMap 中的几个缺点。Guava Cache 采用了类似 ConcurrentHashMap 的思想,分段加锁,减少锁竞争。

Guava Cache 对于过期的 Entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。直接通过查询,判断其是否满足刷新条件,进行刷新。

3.4 Caffeine

Caffeine 实现了 W-TinyLFU(LFU + LRU 算法的变种),其命中率和读写吞吐量大大优于 Guava Cache。其实现原理较复杂,可以参考你应该知道的缓存进化史

3.5 Ehcache

EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。

优点

缺点

3.6 进程内缓存对比

常用进程内缓存技术对比:

image-20220609114606280.png

总结一下:如果不需要淘汰算法则选择 ConcurrentHashMap,如果需要淘汰算法和一些丰富的 API,推荐选择。

四、分布式缓存

分布式缓存解决了进程内缓存最大的问题:如果应用是分布式系统,节点之间无法共享彼此的进程内缓存。分布式缓存的应用场景:

不同分布式缓存的实现原理往往有比较大的差异。本文主要针对 Memcached 和 Redis 进行说明。

4.1 Memcached

Memcached 是一个高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的 Hash 表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。

简单的说就是:将数据缓存到内存中,然后从内存中读取,从而大大提高读取速度。

4.1.1 Memcached 特性
4.1.2 Memcached 工作原理

1)内存管理

Memcached 利用 slab allocation 机制来分配和管理内存,它按照预先规定的大小,将分配的内存分割成特定长度的内存块,再把尺寸相同的内存块分成组,数据在存放时,根据键值 大小去匹配 slab 大小,找就近的 slab 存放,所以存在空间浪费现象。

这套内存管理效率很高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。

2)缓存淘汰策略

Memcached 的缓存淘汰策略是 LRU + 到期失效策略。

当你在 Memcached 内存储数据项时,你有可能会指定它在缓存的失效时间,默认为永久。当 Memcached 服务器用完分配的内时,失效的数据被首先替换,然后是最近未使用的数据。

在 LRU 中,Memcached 使用的是一种 Lazy Expiration 策略:Memcached 不会监控存入的 key/vlue 对是否过期,而是在获取 key 值时查看记录的时间戳,检查 key/value 对空间是否过期,这样可减轻服务器的负载。

3)分区

Memcached 服务器之间彼此不通信,它的分布式能力是依赖客户端来实现。具体来说,就是在客户端实现一种算法,根据 key 来计算出数据应该向哪个服务器节点读/写。

而这种选取集群节点的算法常见的有三种:

4.2 Redis

Redis 是一个开源(BSD 许可)的,基于内存的,多数据结构存储系统。可以用作数据库、缓存和消息中间件。

Redis 还可以使用客户端分片来扩展写性能。内置了 复制(replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

4.2.1 Redis 特性

volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰;

volatile-ttl :从已设置过期时间的数据集中挑选将要过期的数据淘汰;

volatile-random:从已设置过期时间的数据集中任意选择数据淘汰;

allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰;

allkeys-random:从所有数据集中任意选择数据进行淘汰;

noeviction :禁止驱逐数据。

4.2.2 Redis 原理

1)缓存淘汰

Redis 有两种数据淘汰实现;

2)分区

3)主从复制

完整重同步(full resychronization) - 用于初次复制。执行步骤与 SYNC 命令基本一致。

部分重同步(partial resychronization) - 用于断线后重复制。如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只需接收并执行这些写命令,即可将主从服务器的数据库状态保持一致。

4)数据一致性

4.3 分布式缓存对比

不同的分布式缓存功能特性和实现原理方面有很大的差异,因此他们所适应的场景也有所不同。


这里选取三个比较出名的分布式缓存(MemCache,Redis,Tair)来作为比较:


2.png

总结:如果服务对延迟比较敏感,Map/Set 数据也比较多的话,比较适合 Redis。如果服务需要放入缓存量的数据很大,对延迟又不是特别敏感的话,那就可以选择 Memcached。

五、多级缓存

5.1 整体缓存框架

通常,一个大型软件系统的缓存采用多级缓存方案:


1.png

请求过程:

5.2 使用进程内缓存

如果应用服务是单点应用,那么进程内缓存当然是缓存的首选方案。对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于:

这种方案存在以下问题:

5.3 使用分布式缓存

如果应用服务是分布式系统,那么最简单的缓存方案就是直接使用分布式缓存。其应用场景如图所示:

image.png

Redis 用来存储热点数据,如果缓存不命中,则去查询数据库,并更新缓存。这种方案存在以下问题:

5.4 使用多级缓存

单纯使用进程内缓存和分布式缓存都存在各自的不足。如果需要更高的性能以及更好的可用性,我们可以将缓存设计为多级结构。将最热的数据使用进程内缓存存储在内存中,进一步提升访问速度。

这个设计思路在计算机系统中也存在,比如 CPU 使用 L1、L2、L3 多级缓存,用来减少对内存的直接访问,从而加快访问速度。一般来说,多级缓存架构使用二级缓存已可以满足大部分业务需求,过多的分级会增加系统的复杂度以及维护的成本。因此,多级缓存不是分级越多越好,需要根据实际情况进行权衡。

一个典型的二级缓存架构,可以使用进程内缓存(如:Caffeine/Google Guava/Ehcache/HashMap)作为一级缓存;使用分布式缓存(如:Redis/Memcached)作为二级缓存。

5.4.1 多级缓存查询
6.png

多级缓存查询流程如下:

5.4.2 多级缓存更新

对于 L1 缓存,如果有数据更新,只能删除并更新所在机器上的缓存,其他机器只能通过超时机制来刷新缓存。超时设定可以有两种策略:

对于 L2 缓存,如果有数据更新,其他机器立马可见。但是,也必须要设置超时时间,其时间应该比 L1 缓存的有效时间长。为了解决进程内缓存不一致的问题,设计可以进一步优化;


7.png

通过消息队列的发布、订阅机制,可以通知其他应用节点对进程内缓存进行更新。使用这种方案,即使消息队列服务挂了或不可靠,由于先执行了数据库更新,但进程内缓存过期,刷新缓存时,也能保证数据的最终一致性。

六、缓存问题

6.1 缓存雪崩

缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。

举例来说,对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。

解决缓存雪崩的主要手段如下:

上面的解决方案简单来说,就是多级缓存方案。系统收到一个查询请求,先查本地缓存,再查分布式缓存,最后查数据库,只要命中,立即返回。

解决缓存雪崩的辅助手段如下:

6.2 缓存穿透

缓存穿透是指:查询的数据在数据库中不存在,那么缓存中自然也不存在。所以,应用在缓存中查不到,则会去查询数据库。当这样的请求多了后,数据库的压力就会增大。

解决缓存穿透,一般有两种方法:

1)缓存空值

对于返回为 NULL 的依然缓存,对于抛出异常的返回不进行缓存。


4.png

采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。

2)过滤不可能存在的数据

5.png

制定一些规则过滤一些不可能存在的数据。可以使用布隆过滤器(针对二进制操作的数据结构,所以性能高),比如你的订单 ID 明显是在一个范围 1-1000,如果不是 1-1000 之内的数据那其实可以直接给过滤掉。

针对于一些恶意攻击,攻击带过来的大量 key 是不存在的,那么我们采用第一种方案就会缓存大量不存在 key 的数据。此时我们采用第一种方案就不合适了,我们完全可以先对使用第二种方案进行过滤掉这些 key。针对这种 key 异常多、请求重复率比较低的数据,我们就没有必要进行缓存,使用第二种方案直接过滤掉。而对于空数据的 key 有限的,重复率比较高的,我们则可以采用第一种方式进行缓存。

6.3 缓存击穿

缓存击穿是指,热点数据失效瞬间,大量请求直接访问数据库。例如,某些 key 是热点数据,访问非常频繁。如果某个 key 失效的瞬间,大量的请求过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。

为了避免这个问题,我们可以采取下面的两个手段:

6.4 小结

上面逐一介绍了缓存使用中常见的问题。这里,从发生时间段的角度整体归纳一下缓存问题解决方案。

分布式缓存 Memcached ,由于数据类型不如 Redis 丰富,并且不支持持久化、容灾。所以,一般会选择 Redis 做分布式缓存。

七、缓存策略

7.1 缓存预热

缓存预热是指系统启动后,直接查询热点数据并缓存。这样就可以避免用户请求的时候,先查询数据库,然后再更新缓存的问题。

解决方案:

7.2 如何缓存

7.2.1 不过期缓存

不要把写缓存操作放在事务中,尤其是写分布式缓存。因为网络抖动可能导致写缓存响应时间很慢,引起数据库事务阻塞。如果对缓存数据一致性要求不是那么高,数据量也不是很大,可以考虑定期全量同步缓存。

这种模式存在这样的情况:存在事务成功,但缓存写失败的可能。但这种情况相对于上面的问题,影响较小。

7.2.2 过期缓存

采用懒加载。对于热点数据,可以设置较短的缓存时间,并定期异步加载。

7.3 缓存更新

一般来说,系统如果不是严格要求缓存和数据库保持一致性的话,尽量不要将读请求和写请求串行化。串行化可以保证一定不会出现数据不一致的情况,但是它会导致系统的吞吐量大幅度下降。

一般来说缓存的更新有两种情况:

为什么是删除缓存,而不是更新缓存呢?

你可以想想当有多个并发的请求更新数据,你并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况。所以一般来说考虑删除缓存。

对于一个更新操作简单来说,就是先去各级缓存进行删除,然后更新数据库。这个操作有一个比较大的问题,在对缓存删除完之后,有一个读请求,这个时候由于缓存被删除所以直接会读库,读操作的数据是老的并且会被加载进入缓存当中,后续读请求全部访问的老数据。

对缓存的操作不论成功失败都不能阻塞我们对数据库的操作,那么很多时候删除缓存可以用异步的操作,但是先删除缓存不能很好的适用于这个场景。先删除缓存也有一个好处是,如果对数据库操作失败了,那么由于先删除的缓存,最多只是造成 Cache Miss。

1)先更新数据库,再删除缓存(注:更推荐使用这种策略)。

如果我们使用更新数据库,再删除缓存就能避免上面的问题。

但是同样的引入了新的问题:假设执行更新操作时,又接收到查询请求,此时就会返回缓存中的老数据。更麻烦的是,如果数据库更新操作执行失败,则缓存中可能永远是脏数据。

2)应该选择哪种更新策略

通过上面的内容,我们知道,两种更新策略都存在并发问题。

但是建议选择先更新数据库,再删除缓存,因为其并发问题出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且同时有一个并发写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

如果需要数据库和缓存保证强一致性,则可以通过 2PC 或 Paxos 协议来实现。但是 2PC 太慢,而 Paxos 太复杂,所以如果不是非常重要的数据,不建议使用强一致性方案。更详细的分析可以参考:分布式之数据库和缓存双写一致性方案解析

八、总结

最后,通过一张思维导图来总结一下本文所述的知识点,帮助大家对缓存有一个系统性的认识。

九、参考资料

1、《大型网站技术架构:核心原理与案例分析》

2、你应该知道的缓存进化史

3、如何优雅的设计和使用缓存?

4、理解分布式系统中的缓存架构(上)

5、缓存那些事

6、分布式之数据库和缓存双写一致性方案解析

上一篇 下一篇

猜你喜欢

热点阅读