Microservice微服务面试

分布式系统的缓存设计

2022-08-23  本文已影响0人  AC编程

一、缓存简介

1.1 什么是缓存

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

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

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

1.2 何时需要缓存

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

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

1.3 缓存的基本原理

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

1.4 缓存淘汰策略
1.4.1 缓存淘汰的类型

1、基于空间:设置缓存空间大小。
2、基于容量:设置缓存存储记录数。
3、基于时间

1.4.2 缓存淘汰算法

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

二、缓存的分类

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

2.1 客户端缓存
2.2 服务端缓存

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

2.3 CDN 缓存

CDN 将数据缓存到离用户物理距离最近的服务器,使得用户可以就近获取请求内容。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。国内网络异常复杂,跨运营商的网络访问会很慢。为了解决跨运营商或各地用户访问问题,可以在重要的城市,部署 CDN 应用。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

1
2.3.1 CDN 原理

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

2.3.1.1 未部署 CDN 应用前的网络路径

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

2.3.1.2 部署 CDN 应用后网络路径

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

2.3.2 CDN 特点
2.3.2.1 优点
2.3.2.2 缺点
2.4 反向代理缓存

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

2
2.4.1 反向代理缓存原理

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

这种方式通过降低向 WEB 服务器的请求数,从而降低了 WEB 服务器的负载。反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理。常用的缓存应用服务器有 Varnish,Ngnix,Squid。

2.4.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 进程内缓存对比

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

3

四、分布式缓存

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

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

4.1 Memcached

Memcached 是一个高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的 Hash 表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。简单的说就是:将数据缓存到内存中,然后从内存中读取,从而大大提高读取速度。

4.1.1 Memcached 特性
4.1.2 Memcached 工作原理
4.1.2.1 内存管理

Memcached 利用 slab allocation 机制来分配和管理内存,它按照预先规定的大小,将分配的内存分割成特定长度的内存块,再把尺寸相同的内存块分成组,数据在存放时,根据键值 大小去匹配 slab 大小,找就近的 slab 存放,所以存在空间浪费现象。这套内存管理效率很高,而且不会造成内存碎片,但是它最大的缺点就是会导致空间浪费。

4.1.2.2 缓存淘汰策略

Memcached 的缓存淘汰策略是 LRU + 到期失效策略。当你在 Memcached 内存储数据项时,你有可能会指定它在缓存的失效时间,默认为永久。当 Memcached 服务器用完分配的内时,失效的数据被首先替换,然后是最近未使用的数据。在 LRU 中,Memcached 使用的是一种 Lazy Expiration 策略:Memcached 不会监控存入的 key/vlue 对是否过期,而是在获取 key 值时查看记录的时间戳,检查 key/value 对空间是否过期,这样可减轻服务器的负载。

4.1.2.3 分区

Memcached 服务器之间彼此不通信,它的分布式能力是依赖客户端来实现。具体来说,就是在客户端实现一种算法,根据 key 来计算出数据应该向哪个服务器节点读/写。而这种选取集群节点的算法常见的有三种:哈希取余算法、一致性哈希算法、虚拟 Hash 槽算法

4.2 Redis

Redis 是一个开源(BSD 许可)的,基于内存的,多数据结构存储系统。可以用作数据库、缓存和消息中间件。Redis 还可以使用客户端分片来扩展写性能。内置了 复制(replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

4.2.1 Redis 特性
4.2.2 Redis 原理
4.2.2.1 缓存淘汰

Redis 有两种数据淘汰实现;

4.2.2.2 分区
4.2.2.3 主从复制
4.2.2.4 数据一致性
4.3 分布式缓存对比

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

五、多级缓存

5.1 整体缓存框架

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

4

请求过程:
1、浏览器向客户端发起请求,如果 CDN 有缓存则直接返回;
2、如果 CDN 无缓存,则访问反向代理服务器;
3、如果反向代理服务器有缓存则直接返回;
4、如果反向代理服务器无缓存或动态请求,则访问应用服务器;
5、应用服务器访问进程内缓存;如果有缓存,则返回代理服务器,并缓存数据(动态请求不缓存);
6、如果进程内缓存无数据,则读取分布式缓存;并返回应用服务器;应用服务器将数据缓存到本地缓存(部分);
7、如果分布式缓存无数据,则应用程序读取数据库数据,并放入分布式缓存;

5.2 使用进程内缓存

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

这种方案存在以下问题:

5.3 使用分布式缓存

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

5

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

5.4 使用多级缓存

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

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

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

5.4.1 多级缓存查询
6

多级缓存查询流程如下:
1、首先,查询 L1 缓存,如果缓存命中,直接返回结果;如果没有命中,执行下一步。
2、接下来,查询 L2 缓存,如果缓存命中,直接返回结果并回填 L1 缓存;如果没有命中,执行下一步。
3、最后,查询数据库,返回结果并依次回填 L2 缓存、L1 缓存。

5.4.2 多级缓存更新

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

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

7

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

六、缓存问题

6.1 缓存雪崩

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

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

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

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

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

6.2 缓存穿透

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

针对于一些恶意攻击,攻击带过来的大量 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 缓存更新

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

八、总结

8

转载自:解析分布式系统的缓存设计

上一篇下一篇

猜你喜欢

热点阅读