引言
在负载均衡算法与应用详解中有提到,负载均衡有利于提升集群服务器的吞吐量、数据并行处理能力、减少用户响应时间,提升业务可靠性等。但是分布式缓存集群的伸缩性不能使用简单的负载均衡手段来实现。
缓存服务器集群和应用服务器集群负载均衡的不同点在于:
- 集群中所有应用服务器部署的应用都是相同的;
- 集群中不同缓存服务器中的缓存数据各不相同;
本文目录
- 缓存的定义;
- 缓存的算法;
- LRU算法(最近最久未使用算法 + LinkdList双向链表);
- 余数Hash路由算法( key的hashCode % 集群服务器数量);
- 一致性Hash算法(一致性Hash环 + 虚拟节点机制);
- 缓存的相关问题;
- 二八定律(热点数据分布规律)
- 缓存雪崩(缓存服务器宕机,导致数据库压力骤增);
- 缓存预热(新启动的缓存系统,缓存命中率低);
- 缓存穿透(高并发访问不存在的缓存数据,导致数据库压力骤增);
- 缓存的应用案例;
- Redis内存淘汰机制;
- Redis持久化机制;
缓存的定义
缓存是指将数据存储在相对较高访问速度的存储介质中,一方面缓存访问速度快,可以减少数据访问的时间,另一方面如果缓存的数据是经过计算处理得到的,那么被缓存的数据无需重复计算即可直接使用,因此缓存还起到减少计算时间的作用。
缓存的本质是一个内存Hash表;
网站数据访问通常遵循二八定律,即80%的访问落在20%的数据上,因此利用Hash表和内存的高速访问特性,将这20%的数据缓存起来,可很好的改善系统性能,提升数据读取速度,降低存储访问压力。
缓存算法
LRU算法
LRU全称是Least Recently Used,即最近最久未使用的意思;
LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
用LinkedList双向链表实现LRU,算法思路:
1. 初始化一个存放缓存数据的双向链表;
2. 判断请求是否直接击中缓存链表的表头,如果是则直接返回,一次请求完毕;如果不是,则执行后续操作;
3. 判断链表中是否已存在当前请求,如果存在,则移除对应的结点,如果不存在,则判定空间是否够用,以决定是否需要移除为尾结点;
4. 将当前请求添加至链表表头;
5. 下一次请求重复 2 - 4 步骤;
import java.util.LinkedList;
import java.util.Random;
class LRUTest {
private static CachedLinkedList cachedLinkedList = new CachedLinkedList();
public static void main(String[] args) {
int requestTimes = 10;
CachedLinkedList cachedLinkedList = new CachedLinkedList();
Random random = new Random();
for (int i =0; i<requestTimes; i++){
int num = random.nextInt(8);
System.out.println("Current request is : " + num );
cachedLinkedList.put(num);
// 下面是渣渣特效,不用计较
for (int j =0 ;j<cachedLinkedList.linkedList.size(); j++){
int k = j;
while (k-- > 0){
System.out.print("-");
}
System.out.print(cachedLinkedList.linkedList.get(j));
k = j;
while (k-- > 0){
System.out.print("-");
}
System.out.println();
}
System.out.println("----------------------");
}
}
static class CachedLinkedList{
// 默认缓存空间大小
private int CAPACITY = 4;
// 借助java.util.LinkedList维护一个双向链表
private LinkedList<Object> linkedList = new LinkedList<Object>();
public void put(Object request){
// 检测request是否直接击中链表头,如果是则跳过后续操作
if (linkedList.size() > 0 && linkedList.getFirst().equals(request)){
System.out.println("链表头缓存直接被击中");
return;
}
if (linkedList.contains(request)){
// 数据已存在,只需置顶,不需要检测缓存是否溢出
linkedList.remove(request);
System.out.println(">>> " + request + " 被置顶");
} else {
// 数据不存在,需要先判断缓存链表的空间是否够用
if (linkedList.size() == CAPACITY){
// 如果缓存链表的空间不够够用,需要溢出最近最少访问的数据,即链表尾部的数据
System.out.println(">>> " + linkedList.getLast() + " 被移出");
linkedList.removeLast();
}
}
// 最终,将刚访问的数据置顶
linkedList.addFirst(request);
}
}
}
// 输出结果
Current request is : 7
7
----------------------
Current request is : 6
6
-7-
----------------------
Current request is : 0
0
-6-
--7--
----------------------
Current request is : 7
>>> 7 被置顶
7
-0-
--6--
----------------------
Current request is : 3
3
-7-
--0--
---6---
----------------------
Current request is : 2
>>> 6 被移出
2
-3-
--7--
---0---
----------------------
Current request is : 2
链表头缓存直接被击中
2
-3-
--7--
---0---
----------------------
Current request is : 6
>>> 0 被移出
6
-2-
--3--
---7---
----------------------
Current request is : 5
>>> 7 被移出
5
-6-
--2--
---3---
----------------------
Current request is : 5
链表头缓存直接被击中
5
-6-
--2--
---3---
----------------------
余数Hash路由算法
将缓存数据KEY的Hash值对集群服务器数量值取余数,余数值即为服务器列表下标编号;
例如:某个key 的hashCode为490806430,对服务器数目3取余后,余数值为1,所以访问的缓存服务器为Node1;
由于HashCode具有随机性,因此使用余数Hash路由算法可保证缓存数据在整个Memcached服务器集群中比较均衡地分布。
但是,当分布式缓存集群需要扩容的时候,会导致大量的命中失效;
例如,3台服务器扩容至4台服务器,大约有75%(3/4)被缓存了的数据不能正确命中,随着服务器集群规模的增大,这个比例线性上升。当100台服务器的集群中加入一台新服务器,不能命中的概率是99%(N/(N+1))。
当大部分被缓存了的数据因为服务器扩容而不能正确读取时,这些数据访问的压力就落到了数据库的身上,这将大大超过数据库的负载能力,严重的可能会导致数据库宕机。
一致性Hash算法
- 一致性Hash算法即是用于解决服务器扩容导致的大部分缓存命中失准问题。
- 一致性Hash算法通过一个叫作一致性Hash环的数据结构实现KEY到缓存服务器的Hash映射;
一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT可以在P2P环境中真正得到应用。
但现在一致性hash算法在分布式系统中也得到了广泛应用,研究过memcached缓存数据库的人都知道,memcached服务器端本身不提供分布式cache的一致性,而是由客户端来提供,具体在计算一致性hash时采用如下步骤:
- 先构造一个长度为0~2^32的整数环(这个环被称作一致性Hash环);
- 根据节点名称(服务器的ip或主机名作为关键字)的Hash值(其分布范围同样为0~2^32)将缓存服务器节点放置在这个Hash环上;
- 然后根据需要缓存的数据的KEY值计算得到其Hash值(其分布范围也同样为0~2^32),然后在Hash环上顺时针查找距离这个KEY的Hash值最近的缓存服务器节点,完成KEY到服务器的Hash映射查找;
从上图的四台服务器(node 1-4)的基础上,再添加一台memcached服务器(node 5),如下图:
一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。
而如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。
综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
节点增减带来的另外一个问题:增减节点一方面只会影响部分数据的重新定位,但同时也导致服务器结点负载变得不均衡的问题。
例如:当集群只有node1, node2, node3三个节点时,概率上,各自承受的访问量为33%,而当在node3和node1之间再增添node4后:
- node 2 = node 3 = 33%;
- node4 = node1 = 16.5%;
即意味着,node2和node3换存的数据量和负载压力是node4和node1的两倍,如果4台机器的性能是一样的,那么这种结果不符合负载均衡的要求;
数据倾斜问题:解决节点增减导致负载不均衡的方案是:
为一致性哈希算法引入虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现;
最终的结果是:新加入一台缓存服务器,将会较为均匀地影响原来集群中已经存在的所有服务器;
例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点;
同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
》》》一致性Hash算法的分析实现
缓存相关问题
使用缓存对提高系统性能有很多好处,但是不合理使用缓存非但不能提高系统的性能,还会成为系统的累赘,甚至风险。实践中,缓存滥用的情景屡见不鲜——过分依赖低可用的缓存系统、不恰当地使用缓存的数据访问特性等。
二八定律(热点场景)
网站访问中的二八定律:80%的业务访问集中在20%的数据上;
对于存在二八访问规律的Web应用,即存在热点数据的应用,使用缓存才有意义,否则盲目使用缓存反而增加服务器的处理复杂性和性能压力;
缓存使用内存作为存储,内存资源宝贵而有限,不可能将所有数据都缓存起来,只能将最新访问的数据缓存起来,而将历史数据清理出缓存。如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被挤出缓存了。
缓存雪崩
缓存是为提高数据读取性能的,缓存数据丢失或者缓存不可用不会影响到应用程序的处理——它可以从数据库直接获取数据。但是随着业务的发展,缓存会承担大部分数据访问的压力,数据库已经习惯了有缓存的日子,所以当缓存服务崩溃时,数据库会因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用。这种情况被称作缓存雪崩,发生这种故障,甚至不能简单地重启缓存服务器和数据库服务器来恢复网站访问。
实践中,有的网站通过缓存热备等手段提高缓存可用性:当某台缓存服务器宕机时,将缓存访问切换到热备服务器上。但是这种设计显然有违缓存的初衷,缓存根本就不应该被当做一个可靠的数据源来使用。
通过分布式缓存服务器集群,将缓存数据分布到集群多台服务器上可在一定程度上改善缓存的可用性。当一台缓存服务器宕机的时候,只有部分缓存数据丢失,重新从数据库加载这部分数据不会对数据库产生很大影响。
缓存预热
缓存中存放的是热点数据,热点数据又是缓存系统利用LRU(最近最久未用算法)对不断访问的数据筛选淘汰出来的,这个过程需要花费较长的时间。新启动的缓存系统如果没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,那么最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫作缓存预热(warm up)。对于一些元数据如城市地名列表、类目信息,可以在启动时加载数据库中全部数据到缓存进行预热。
缓存穿透
如果因为不恰当的业务、或者恶意攻击持续高并发地请求某个不存在的数据,由于缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大压力,甚至崩溃。一个简单的对策是将不存在的数据也缓存起来(其value值为null)。
缓存的应用案例分析
Redis的内存淘汰机制
Redis的持久化机制
参考文献
- 《大型网站技术架构 核心原理与案例分析》
- 一致性哈希算法原理
- 一致性哈希算法原理分析及实现