我们讲到了当我们的系统面临持续增加的并发给我们的数据库磁盘IO带了了性能瓶颈,特此为我们的系统引入了缓存,并且学习了我们在开发中该怎么去正确的使用缓存的读写策略,同时结合案例给出一些建议防止数据不一致的情况,那我们的系统现在就是这样的架构了。
如上图所示,我们在服务层和数据库层之间增加一个缓存层,现在我们读取数据的时候,先从缓存里面读取,读不到的再去读数据库。
既然我们引入了缓存,那肯定是想更多的请求尽量落在缓存上,也就是说我们必须要关注缓存命中率,命中率越高就代表我们的后端存储就越不容易被拖垮成为性瓶颈,如果我们的缓存命中率下降一定要看是什么原因,因为对于高并发请求哪怕下降1% 都是灾难。
比如,现在的系统QPS是10000,每次请求会查询10次的缓存,现在命中率突然下降了1%,也就是我有 10000 * 10 * 1% =1000次的请求落到了我们后端数据库MySql上了。这就代表了MySQL数据库面临突然增加的1000的并发,这是很危险的,基本普通机器mysql也只能抗大概2000的并发。所以,缓存命中率是要我们关注的。
现在只是下降1%就对系统影响这么大,那要是我们的缓存节点挂了,不可用了,那岂不是又回到了原点,请求都会打到我们的数据库中的。所以,我们在使用缓存一定要搭建高可用缓存,避免上面的单点缓存架构。今天,我们就来学习该怎么做缓存的高可用方案即搭建分布式缓存的高可用方案。
依据经验来说,对于分布式缓存高可用方案目前一般采用应用端、中间代理层以及服务端这三大方案。
- 应用端方案,在应用端自己配置缓存节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。
- 代理层方案,在应用代码和缓存节点之间增加一个独立的代理层,应用端就直接喝代理层连接,代理层自己内置高可用策略,以提升缓存的可用性。
- 服务端方案,即为缓存服务自身提供的高可用,例如Redis Sentinel
接下来我们就来分别学习下这三种方案
应用端方案
在应用端也就是代码层面上,我们就需要自己管理缓存的读和写,也就是通过写代码方式来进行分布式缓存的写入和读取,主要是下面这两模块:
- 写缓存时,我们需要将数据分散到缓存的各个节点中,即要实现数据分片。
- 读缓存时,需考虑主从或者多副本粗略以及使用多组缓存进行容错。
下面我们来看看该怎么进行设计,其实这种设计思路不一定局限在缓存上,我们大部分的底层开发都能用上,希望大家好好掌握
缓存数据如何分片
我们知道单节点的缓存因受到各种原因如本身机器内存、网络带宽等,从而不能承受更高的并发,所以我们需要将数据进行分片存储,即将数据通过分片算法打散到各个缓存节点中。其实这块大家有没有注意到和我们前面的分库分表很类似,所以大部分架构思想都是相通的。
现在我们的数据就在各个缓存节点都有一部分,即使部分故障,也是不影响我们整个业务的。那这个时候,你可能在想,既然数据需要被均匀分散到各个节点,那我该怎么来写这个分片算法呢?别急,我们下面就来看怎么写这个分片算法。
数据分片算法
一般做数据分片算法的有两种,大家应该都清楚吧,前面分库分表就有用到的
- Hash分片算法
- 一致性Hash分片算法
Hash分片算法
Hash分片算法就是我们拿到缓存的key,然后对其做hash运算,最后将hash运算的结果对缓存总节点数取余,得到的数字则为具体的分片节点。比如,现在我们缓存节点一共有 3 个,当我们写入数据的时候,将key进行hash运算hash(key),然后将结果对3取余就行了,如下图所示:
这种分片算法优点就是开发简单且容易理解,缺点就是当我们的缓存总节点数改变的时候,就会导致数据不均匀,则会造成大量缓存失效不可用的情况。但是这种算法我们开发中也是会使用的,比如我们的业务对于缓存的命中率不是那么太在意的,就可以使用这种hash分片算法。
一致性Hash分片算法
上面简单的Hash分片算法对缓存命中率要求较高的业务会有一定影响,所以一致性Hash分片算法就出来了,它很好的解决了因缓存节点的增加或减少带来的缓存命中率下降的问题。那我们就来看看它是怎么做的。
- 首先维护一个2^32的hash环。
- 然后将各个缓存节点的IP或者机器名称计算hash值,是每个节点计算多个hash出来,也就是所谓的虚拟节点。为了数据能更加的均匀,且能避免节点雪崩的发生。
- 将计算出来的hash值即虚拟节点放到hash环上。
- 当我们要写入一个key的时候,就可以先对这个key做hash计算出hash值,,确定在hash环上的位置。
- 最后在环上按照顺时针的方式查找,遇到的第一个缓存节点就是它要存放的节点。
例如,下面key1 和 key2 就会进到 Node 1 里面,key3和key4 就会进到 Node 2 里面,key5 进到 Node3 中,key 6 则进到 Node 4 中去。
如上图所示,如果在 Node 1 和 Node 2 之间再加一个 Node 5,我们可以看到之前命中 Node 2 的 Key 3 现在就会命中到 Node 5,而其它的 Key 都没有变化;同样的道理,如果我们把 Node 3 从集群中移除,那么只会影响到 Key 5 。因此,在增加和删除节点时,只有少量的 Key 会跑到其它节点上,而大部分的 Key 命中的节点还是会保持不变,从而可以保证命中率不会大幅下降。
生产开发建议
我们在使用一致性哈希算法的时候,一定要设置缓存的过期时间,为什么这么说的呢?现在假设集群里面有两个节点分别为node1和node2,node1里面存放的(k,5),然后一客户端请求过来需要将5变成8,这个时候node1节点服务和客户端因网络问题断开连接了,那么这次的写入操作就会被路由到node2上了,等到node1网络好了恢复连接的话,客户端读取到node1 中k就为5,而其实这个k已经是8了,就造成了脏数据,所以我们需要设置过期时间。
Memcached 如何做主从机制
memcached不像redis本身支持主从复制机制,那我们该怎么保证memcached的高可用呢?其实和我们前面的数据库方案差不多的。
- 我们为每一组memcached的Master配置一组Slave。
- 数据更新的时候,我们就主从同步更新。
- 数据读取的时候,先从Slave中读取,读不到就穿透到Master中,再将数据写回到Slave中。
其主从复制优点就是当某一个 Slave 宕机时,还会有 Master 作为兜底,不会有大量请求穿透到数据库的情况发生,提升了缓存系统的高可用性。
中间代理层方案
上面的应用端方案基本能解决我们绝大部分问题了,现在主要是像有些公司技术语言比较多的话,这种就得每种语言都得开发一套,比如我们公司有Java PHP 还有.
net之类的,那么这个时候就需要中间代理层来最好不过了,不需要业务方进行考虑这些复杂情况,直接连接代理层就行了
代理层自己管理缓存节点高可用,通过某种协议,如redis协议,来和各种语言业务端连接。业界也有很多中间代理层方案,比如 Facebook 的Mcrouter,Twitter 的Twemproxy,豌豆荚的Codis。基本架构如下:
如上图所示,中间层代理方案即所有缓存读写的操作都直接通过代理层完成,代理层自己完成上面应用端所有的操作。
服务端方案
服务端方案主要是缓存服务自己管理的,对于我们开发人员不用自己写代码管理也不用引入中间层,就是需要相关运维配置支持,比如redis的sentinel模式就是用来解决redis部署时高可用问题,它可以在主节点挂了以后自动将从节点提升为主节点,保证整体集群的可用。所以服务端对于我们开发影响不是太大,redis的sentinel我们还得需要知道的,后面会专门进行讲解。
总结,今天我们讲到了在使用缓存的时候为了避免单节点所带来的各种问题,所以我们需要搭建高可用缓存架构,共讲到了三种方案,应用端、中间代理层以及服务端方案,大家可以根据公司的资源情况来选择合适的方案。