多级缓存系统
->缓存概述:
缓存是分布式系统中的重要组件;
主要解决:
高并发(大数据场景中的热点数据访问的性能问题)
->缓存的原理:
将数据缓存到写入/读取速度更快的存储(设备);
将数据缓存到离应用最近的位置;
将数据缓存到离用户最近的位置。
->缓存的分类:(从部署角度有以下几个方面的缓存应用)
CDN缓存;
反向代理缓存;
分布式Cache;
本地应用缓存。
->缓存媒介:
常用中间件:Nginx, Redis, Memcache等
缓存的内容:文件、数据、对象;
缓存的介质:CPU、内存(本地,分布式)、磁盘(本地,分布式)
->缓存设计:
缓存什么:(哪些数据需要缓存)
热点数据;
静态资源。
缓存的位置:
CDN、反向代理、分布式缓存服务器、本机(内存、硬盘)
如何进行缓存:
过期策略;
固定时间:比如指定缓存的时间是30分钟;
相对时间:比如最近10分钟内没有访问的数据;
同步机制;
实时写入;(推)
异步刷新。(推拉)
->CDN缓存:(主要解决将数据缓存到离用户最近的位置的问题,一般缓存静态资源文件:页面、脚本、图片、视频、文件等)
->CDN原理:
...
->反向代理缓存:(指在网站服务器机房部署代理服务器,实现负载均衡、数据缓存、安全控制等功能)
->缓存原理:(反向代理位于应用服务器机房,处理所有对WEB服务器的请求。)
若用户请求的页面在代理服务器上有缓冲的话,代理服务器直接将缓冲内容发送给用户;
若没有缓冲则先向WEB服务器发出请求,取回数据,本地缓冲后再发送给用户。
(通过降低向WEB服务器的请求数,从而降低了WEB服务器的负载)
反向代理一般缓存静态资源,动态资源转发到应用服务器处理。常用的缓存应用服务器有Nginx、Varnish、Squid
->代理缓存比较:
Varnish, Squid, Nginx
->分布式缓存:(CDN, 反向代理缓存,主要解决静态文件,或用户请求资源的缓存,数据源一般为静态文件或动态生成的文件,有缓存头标识)
分布式缓存,主要指缓存用户经常访问数据的缓存,数据源为数据库。一般起到热点数据访问和减轻数据库压力的作用。
常用的中间件有Memcache, Redis。
->Memcache:(高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的hash表,可以存储各种格式的数据,总之就是将数据调用到内存中,然后从内存中读取,从而大大提高读取速度。)
Memcache特性:
->使用物理内存作为缓存区,可独立运行在服务器上;
->使用key-value的方式来存储数据,即一种单索引的结构化数据组织形式,可使数据项查询的时间复杂度为O(1)
->协议简单:基于文本行的协议,直接通过telnet在memcached服务器上可进行存取数据操作
->基于Libevent高性能通信:
->内置的内存管理方式:所有数据都保存在内存中,存取比硬盘快,当内存满后,通过LRU自动删除不使用的缓存(但没有考虑数据的容灾问题,重启服务,所有数据会丢失)
->分布式:服务器不具有分布式功能,分布式部署取决于Memcache客户端;
->缓存策略:memcached的缓存策略是LRU(最近最少使用)到期失效策略;
Memcache集群:(虽然成为"分布式"缓存服务器,但服务器端并没有"分布式"功能。memcached的分布式,是由客户端程序实现的,当向memcached集群存入/取出key value时,memcached客户端程序根据一定的算法计算存入哪台服务器,然后再把key value值存到此服务器中)
分布式算法(Consistent Hashing):
根据余数来计算分布;
根据散列算法来计算分布。
->Redis:(基于内存的,多数据结构存储系统,可以用作数据库、缓存和消息中间件。支持多种类型的数据结构:....)
内置了复制(replication),LUA脚本, LRU驱动事件,事务和不同级别的磁盘持久化,并通过了Redis哨兵和自动分区提供高可用性。
Redis常用数据类型:
String:(常用命令:set, get, decr, incr, mget)
实现方式:String在Redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr, decr等操作时会转成数值类型进行计算,此时redisObject的encoding字段为int。
Hash:(常用命令:hget, hset, hgetall)
实现方式:Redis Hash对应的value, 内部实际就是一个HashMap
List:(常用命令:lpush, rpush, lpop, rpop, lrange)
实现方式:Redis list的实现为一个双向链表,可以支持反向查找和遍历,方便操作。
Set:(常用命令:sadd, spop, smembers, sunion)
实现方式:set的内部实现是一个value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
Sorted set: (常用命令:zadd, zrange, zrem, zcard)
实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且实现简单
Redis集群:
->通过keepalived实现的高可用方案:(切换流程)
当Master挂了后,VIP漂移到Slave;Slave上的keepalived通知redis执行:slaveof no one,开始提供业务/;
当Master起来后,VIP地址不变,Master的keepalived通知redis执行slaveof slave IP host,开始作为从同步数据;
依次类推。
->使用Twemproxy实现集群方案:
...
使用keepalived实现高可用主备方案,解决proxy单点问题。(优点)
对于客户端而言,redis集群是透明的,客户端简单,便于动态扩容;
Proxy为单点,处理一致性hash时,集群节点可用性检测不存在脑裂问题;
高性能,CPU密集型,而redis节点集群多CPU资源冗余,可部署在redis节点集群上,不需要额外设备。
->Memcache与Redis的比较:
数据结构:Memcache只支持key value存储方式,Redis支持更多的数据类型
多线程:Memcache支持多线程,Redis支持单线程;CPU利用方面,Memcache优于Redis;
持久化:Memcache不支持持久化,Redis支持持久化;
内存利用率:Memcache高,Redis低(采用压缩的情况下比Memcache高)
过期策略:Memcache过期后,不删除缓存,会导致下次取数据的问题,Redis有专门线程,清除缓存数据
->本地缓存:(指应用内部的缓存,标准的分布式系统,一般由多级缓存构成。)
->硬盘缓存:
->内存缓存
->缓存架构示例:
多级缓存
->缓存常见问题:
->数据一致性:
...
->缓存高可用:(一般通过分布式和复制实现)
分布式实现数据的海量缓存;(一致性Hash算法)
复制实现缓存数据节点的高可用。 (异步复制)
->缓存雪崩:(指当大量缓存失效时,导致大量的请求访问数据库,导致数据库服务器无法抗住请求或挂掉的情况)
合理规划缓存的失效时间;
合理评估数据库的负载压力;
对数据库进行过载保护或应用层限流;
多级缓存设计,缓存高可用。
->缓存穿透:
...
透明多级缓存解决方案(TMC)设计思路
->多级缓存解决方案需要解决的痛点:
热点探测:如何快速且准确的发现热点访问key;
数据一致性:前置在应用层的本地缓存,如何保障与分布式缓存系统的数据一致性;
效果验证:如何让应用层查看本地缓存命中率、热点key等数据,验证多级缓存效果;
透明接入:整体解决方案如何减少对应用系统的入侵,做到快速平滑接入。
->TMC本地缓存:
如何透明:
TMC对原生的jedis包的JedisPool和Jedis类做了改造,在JedisPool初始化过程中集成TMC"热点发现"+"本地缓存"功能Hermes-SDK包的初始化逻辑,使Jedis客户端与缓存服务端代理层交互时先与Hermes-SDK交互,从而完成"热点探测"+"本地缓存"功能的透明接入。
整体结构:
Jedis-Client: Java应用与缓存服务端交互的直接入口,接口定义与原生Jedis-Client无异。
Hermes-SDK: 自研"热点发现+本地缓存"功能的SDK封装,Jedis-Client通过与它交互来集成相应能力。
SDK接口:
获取key值:byte[] get(String key, Callable)
失效key值:invalid(String key)
热点模块:
热点维护:(key列表&过期时间)
本地缓存维护
通信模块:key访问上报、key失效上报、热点发现监听、热点失效监听
Hermes服务端集群:接收Hermes-SDK上报的缓存访问数据,进行热点探测,将热点key推送给Hermes-SDK做本地缓存。
?为什么不是在Hermes-SDK进行通信缓存数据
缓存集群:由代理层和存储层组成,为应用客户端提供统一的分布式缓存服务入口。
基础组件:etcd集群,Apollo配置中心,为TMC提供"集群推送"和"统一配置"能力;
基本流程:
key值获取
key值过期
热点发现
配置读取
稳定性:
数据上报异步化
通信模块线程隔离
缓存管控
一致性:(缓存一致性)
Hermes-SDK的热点模块仅缓存热点key数据,绝大多数非热点key数据由缓存集群存储
热点key变更导致value失效时,Hermes-SDK同步失效本地缓存,保证本地强一致
热点key变更导致value失效时,Hermes-SDK通过etcd集群广播事件,异步失效业务应用集群中其他节点的本地缓存,保证集群最终一致
->TMC热点发现:(整体流程分为以下四个步骤)
数据收集:(Hermes-SDK通过本地rsyslog将key访问事件以协议格式放入kafka,Hermes服务端集群的每个节点消费kafka消息,实时获取key访问事件)
访问事件协议格式如下:
appName: 集群节点所属业务应用
uniqueKey: 业务应用key访问事件的key
sendTime: 业务应用key访问事件的发生时间
weight: 业务应用key访问事件的访问权值
Hermes服务端集群节点将收集到的key访问事件存储在本地内存中,内存数据结构为Map<String, Map<String, LongAdder>>,对应业务含义映射为Map<appName, Map<uniqueKey, 热度>>
热度滑窗:
通过appName和uniqueKey分别对事件的热度进行映射,从而达到分级缓存的效果
时间滑窗:(Hermes服务端集群节点,对每个APP的每个key,维护一个时间轮:)
时间轮中共10个时间片,每个时间片记录当前key对应3秒时间周期的总访问次数;
时间轮10个时间片的记录累加即表示当前key从当前时间向前30秒时间窗口内的总访问次数。
映射任务:(Hermes服务端集群节点,对每个APP每3秒生成一个映射任务,交由节点内"缓存映射线程池"执行)
对当前APP,从Map<appName, Map<uniqueKey, 热度>>中取出appName对应的Map Map<uniqueKey, 热度>>;
遍历Map<uniqueKey, 热度>>中的key,对每个key取出其热度存入其时间轮对应的时间片中。
热度汇聚:(完成热度滑窗之后,映射任务继续对当前APP进行热度汇聚工作)
遍历APP的key,将每个key的时间轮热度进行汇总(即30秒时间窗口内总热度)得到探测时刻滑窗总热度;
将<key, 滑窗总热度>以排序集合的方式存入Redis存储服务中,即热度汇聚结果。
热点探测:
Redis设计简介
你会怎样设计一个缓存:(就用Map来实现一个缓存)
String value = map.get("somKey");
if (null == value) {
value = queryValueFromDB("someKey");
}
HashMap、TreeMap都是线程不安全的,就用HashTable、ConcurrentHashMap好了:不管用什么Map,其背后都是key-value的Hash表结构,目的就是为了实现O(1)复杂度的查找算法
->C/S架构:
->Redis客户端:
Redis的命令行、各种语言的Redis API
->Redis服务端:
Hash表所在的是Redis服务端(Server)
->Redis的Server是单线程服务器(不必考虑线程安全问题,简化开发,提高性能;减少线程切换损耗的时间)
->集群:
一台Redis内存有限(当很多台客户端使用同一台Redis服务器时,其内存毕竟是有限的,放不了那么多数据)
客户端变多了,Redis吞吐量变低
->通过集群来解决(一台Redis不够,就再多加几台):
客户端的请求会通过负载均衡算法(通常是一致性Hash),分散到各个Redis服务器上
扩大缓存容量
提升吞吐量
->主从复制:
数据可用性差:若其中一台Redis挂了,则上面的全部的缓存数据都会丢失,导致原来可以从缓存中获取的请求,都去访问数据库了,数据库陡增。
数据查询缓慢:监测发现,每天有一段时间,Redis 1的访问量非常高,而且大多数请求都是去查一个相同的缓存数据,导致Redis 1非常忙碌,吞吐量不足以支撑这个高的查询负载。
->解决可用性问题:给每一台Redis都加上一台Slave,通过Master-Slave模式,则实现了两个特性:
数据高可用:Master负责接收客户端的写入请求,将数据写到Master后,同步给Slave,实现数据备份。一旦Master挂了,可以将Slave提拔为Master。
提高查询效率:一旦Master发现自己忙不过来了,可以把一些查询请求,转发给Slave去处理,也就是Master负责读写或者只负责写,Slave负责读。
->Master/Slave chains架构
->Redis使用:
GET: GET var
SET: SET var *
INCR: 对操作的变量做+=1操作,且是原子性操作
Redis可以设置key的有效时间(通过EXPIRE和TTL命令来完成),如下:
SET resource:lock "Redis Demo"
EXPIRE resource:lock 20 //这个key将在120秒后被删除
TTL resource:lock //通过TTL命令来测试一个key还能存在多长时间,执行命令后会返回该key剩余的存在时间,-2表示这个key已经不存在了,-1表示
一致性hash简介
——>一致性Hash:(https://zhuanlan.zhihu.com/p/34985026)
为什么要使用一致性Hash?
一个常见的场景:当一条数据进入Redis缓存集群时,如果是以随机的方式进入的话,那取数据的时候就会有一个问题:要遍历Redis集群中的服务器才能找到取的数据。所以,这个时候就需要数据的存储具有一定的规律,这样在取数据的时候就能够按照某种规律快速取出数据。
其中Hash策略是规律性存储数据的一种方案;
使用Hash的问题:
使用Hash虽然提升了性能(不需要对整个Redis服务器进行遍历),但是考虑这样一个场景:当服务器数量变动的时候,所有缓存的位置都要发生改变。(即当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端数据库请求数据——>缓存雪崩)
一致性Hash算法:(很明显它就是用来解决上面所描述的问题的)
2的32次方个点所组成的圆环称为Hash环;
将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置;
接下来使用如下算法定位数据访问到相应服务器:
将数据key使用相同的函数Hash计算出哈希值;
确定此数据在环上的位置;
从此位置沿顺时针“行走”,第一台服务器就是其应该定位到的服务器
无论是增加机器数量还是减少机器数量,一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
Hash环的数据倾斜问题:
考虑这样一个场景:一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的数据大部分集中缓存在某一台服务器上)问题;
解决办法:一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
缓存架构设计方案
SDK | Manager | Server |
---|---|---|
本地缓存 | 路由策略 | 本地缓存 |
路由策略获取 | 心跳管理 | 心跳上报 |
API封装 | 指标管理 | 指标上报 |
指标上报 | 工单管理 | 限流控制 |
限流控制 |