地址:https://zhuanlan.zhihu.com/p/55303228

作者:林林

前言

在高并发的分布式的系统中,缓存是必不可少的一部分。没有缓存对系统的加速和阻挡大量的请求直接落到系统的底层,系统是很难撑住高并发的冲击,所以分布式系统中缓存的设计是很重要的一环。下面就来聊聊分布式系统中关于缓存的设计以及过程中遇到的一些问题。

缓存的收益与成本

使用缓存我们得到以下收益:

· 加速读写。因为缓存通常是全内存的,比如redis、Memcache。对内存的直接读写会比传统的存储层如MySQL,性能好很多。举个例子:同等配置单机Redis QPS可轻松上万,MySQL则只有几千。加速读写之后,响应时间加快,相比之下系统的用户体验能得到更好的提升。

· 降低后端的负载。缓存一些复杂计算或者耗时得出的结果可以降低后端系统对CPU、IO、线程这些资源的需求,让系统运行在一个相对资源健康的环境。

但随之以来也有一些成本:

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(1)

3. 无底洞问题带来的危害:

(1) 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着实例的增多,耗时会不断增大。

(2) 服务端网络连接次数变多,对实例的性能也有一定影响。

所以无底洞似乎是一个无解的问题。实际上我们只要了解无底洞产生原因在业务前期规划好就可以减轻甚至避免无底洞的产生。

1、首先如果你的业务查询没有,像mget这种批量操作,恭喜你,无底洞将远离你。

2、将集群以项目组做隔离,这样每组业务的redis/memcache集群就不会太大。

3、如果你公司的最大峰值流量远不及FB,可能也不需要担心这个问题。

那技术上有没有一些优先点?解决思路如下:

1. IO的优化思路:

(1) 命令本身的效率:例如sql优化,命令优化。

(2) 网络次数:减少通信次数。

(3) 降低接入成本:长连/连接池,NIO等。

(4) IO访问合并:O(n)到O(1)过程:批量接口(mget)。

(1)、(3)、(4)通常是由缓存系统的设计开发者来决定的,作为使用者我们可以从(2)减少通信次数上做优化

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(2)

以mget来说有四种方案:

(1).串行mget

将mget操作(n个key)拆分为逐次执行N次get操作, 很明显这种操作时间复杂度较高,它的操作时间=n次网络时间 n次命令时间,网络次数是n,很显然这种方案不是最优的,但是足够简单。

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(3)

(2). 串行IO

将mget操作(n个key),利用已知的hash函数算出key对应的节点,这样就可以得到一个这样的关系:Map<node, somekeys>,也就是每个节点对应的一些keys

它的操作时间=node次网络时间 n次命令时间,网络次数是node的个数,很明显这种方案比第一种要好很多,但是如果节点数足够多,还是有一定的性能问题。

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(4)

(3). 并行IO

此方案是将方案(2)中的最后一步,改为多线程执行,网络次数虽然还是nodes.size(),但网络时间变为o(1),但是这种方案会增加编程的复杂度。

它的操作时间=1次网络时间 n次命令时间

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(5)

(4). hash-tag实现。

由于hash函数会造成key随机分配到各个节点,那么有没有一种方法能够强制一些key到指定节点到指定的节点呢?

redis提供了这样的功能,叫做hash-tag。什么意思呢?假如我们现在使用的是redis-cluster(10个redis节点组成),我们现在有1000个k-v,那么按照hash函数(crc16)规则,这1000个key会被打散到10个节点上,那么时间复杂度还是上述(1)~(3)

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(6)

那么我们能不能像使用单机redis一样,一次IO将所有的key取出来呢?hash-tag提供了这样的功能,如果将上述的key改为如下,也就是用大括号括起来相同的内容,那么这些key就会到指定的一个节点上。

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(7)

它的操作时间=1次网络时间 n次命令时间

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(8)

3. 四种批量操作解决方案对比:

分布式缓存源码分析总结篇(深入理解分布式缓存设计)(9)

关于无底洞优化这块的内容,详细可参考并发编程网上面的一篇文章。

提一下,生产中串行IO和并行IO的方案,我都有用过,其实效果还好。毕竟结点都是有限,不是FB、BAT这种流量那么多。并行IO如果你是用java,并且JDK8或以上,只要开启labmda并行流就可以实现并行IO了,很方便的,编程起来并不复杂,超时定位的话,可以加多些日志。

雪崩优化

缓存雪崩:由于缓存层承载着大量请求,有效保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求到达存储层,存储层的调用量会暴增,造成存储层级联宕机的情况。预防和解决缓存雪崩问题可以从以下几方面入手。

(1)保证缓存层服务的高可用性,比如一主多从,Redis Sentine机制。

(2)依赖隔离组件为后端限流并降级,比如netflix的hystrix。关于限流、降级以及hystrix的技术设计可参考以下链接。

(3)项目资源隔离。避免某个项目的bug,影响了整个系统架构,有问题也局限在项目内部。

热点key重建优化

开发人员使用"缓存 过期时间"的策略来加速读写,又保证数据的定期更新,这种模式基本能满足绝大部分需求。但是如果有两个问题同时出现,可能会对应用造成致命的伤害。

1. 当前key是一个hot key,比如热点娱乐新闻,并发量非常大。

2. 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL, 多次IO,多个依赖等。

当缓存失效的瞬间,将会有大量线程来重建缓存,造成后端负载加大,甚至让应该崩溃。要解决这个问题有以下方案:

1、互斥锁

具体做法是只允许一个线程重建缓存,其它线程等待重建缓存的线程执行完,重新从缓存获取数据即可。这种方案的话,有个风险就是重建的时间太长或者并发量太大,将会大量的线程阻塞,同样会加大系统负载。优化方法:除了重建线程之外,其它线程拿旧值直接返回。比如Google 的 Guava Cache 的refreshAfterWrite采用的就是这种方案避免雪崩效应。

2、永不过期

这种就是缓存更新操作是独立的,可以通过跑定时任务来定期更新,或者变更数据时主动更新。

3、后端限流

以上两种方案都是建立在我们事先知道hot key的情况下,如果我们事先知道哪些是hot key的话,其实问题都不是很大。问题是我们不知道的情况!既然hot key的危害是因为有大量的重建请求落到了后端,如果后端自己做了限流呢,只有部分请求落到了后端, 其它的都打回去了。一个hot key 只要有一个重建请求处理成功了,后面的请求都是直接走缓存了,问题就解决了

所以高并发情况下,后端限流是必不可少。

,