更新时间: 2018-08-17 17:15:09       分类: 学习笔记


服务端架构之缓存篇(2):分布式缓存

概述

缓存,作为高可用系统架构中相当重要的一环,需要具有很强的稳定性和处理能力。

随着服务规模的日渐庞大,单一机器的KV缓存结构已经无法满足需求,因此需要引入集群和分布式的概念。

在分布式缓存的架构下,如何安排结构,如何寻找一个KEY真正映射的数据位置都是很值得思考的问题。

Hash环

最简单的“分布式”架构,是类似Memcached的一种伪分布式实现思路:将不同的KEY按照一定的HASH规则映射到不同的实例上。

最简单的情况下,我们可以使用key % N这个哈希公式,其中N为实例的个数,这样我们就将众多的KEY平均地映射到了每一台实例上。

这种思路足够简单,但缺陷也显而易见:如果N改变了,比如添加或是减少了某个实例,那么之前做的所有HASH就都失效了,也就意味着之前的缓存全部都失效了。

所以考虑到实际的应用场景,我们得使用一个比较特别的Hash函数,使得在减少/增加机器时,失效的缓存尽可能的少,一个可行的方案就是使用Hash环,又叫一致性Hash。

思路&流程

计算Hash环,需要进行两次Hash:

  1. 把所有的机器编号Hash到环上
  2. 把Key也Hash到这个环上,然后在环上进行匹配,看这个Key和哪台机器匹配

示例

假设我们要使用的Hash函数值空间为0 ~ 2^32-1,那么实际的hash值会是一个32位整数,这些数字首尾相接,形成一个环路。

第一步,根据一定的规则(如使用ip),计算每个实例的hash,这个hash就是对应实例机器在此环上的位置。

第二步,对Key计算Hash,算出Key在环上的位置。

第三步,从Key在环上的位置开始往前走,遇到的第一台实例,就是该Key所对应的机器。

具体可参考下图:

使用Hash环带来的好处是:增加/减少某个实例不会影响其他实例在环上的位置,只有该实例附近的数据会失效,其他实例上储存的数据不会受到影响。

问题:数据倾斜

使用Hash环要面对的一个问题是,当实例数量并不多的时候,很有可能出现几台机器在环上的位置非常贴近的情况,换句话说就是无法保证实例的均匀分布。

这会导致大部分的数据都集中在相对靠前的那台实例上,数据的储存不均匀最终会影响整个缓存服务的性能。

解决这个问题,可以考虑引入“虚拟机器”,简单的说就是将一台机器hash到环上的多个位置(这些位置称为虚拟机),形成“实体机 -> 虚拟机”的一对多映射。

虚拟机的数量可以很多,因此分布也就相对均匀,这样子就间接地保证了实体储存实例的均匀分布。

More: 服务发现

当服务实例增加和减少时,客户端需要更新对应的实例列表。

最笨的办法是手动实现,每当缓存实例更新时,就要重新配置客户端,重启客户端。

更好的办法就是使用ZooKeeper之类的服务发现方案:缓存实例变更时,就要修改ZK上的注册表。客户端监听ZK,当发现节点数变化时,也主动更新自己的配置。(需要实现一个中心节点)

本部分内容参考自此博客,特别鸣谢作者的分享

Redis Cluster

上一部分谈了Memcached实现分布式缓存的思路,简单而质朴。现在我们来看看主流KV缓存界的另一大巨头Redis是怎么做的。

对比:Memcached vs Redis

  1. 单机对比

    * Memcached不支持数据持久化,Redis则支持异步/同步的持久化

    * Memcached的数据结构相对单一,Redis则提供包括List,Hash,Set在内的多种数据结构

    * Memcached采用多线程模型,Redis则采用单线程

  2. 集群对比

    * Memcached使用Hash环来实现“伪分布式”,而Redis Cluster则采用真正的分布式架构:P2P模式,无中央节点。

    * Memcached的扩容是有损的,Redis Cluster则可以做到无损扩容

    * Memcached不支持主从复制,Redis Cluster支持。

主从复制

简单的说就是每个数据储存节点分为主从两台实例(当然,可能大于两台),他们之间实现数据的同步。

这样做的意义在于:

P2P架构&Hash Slot

不同于Memcached基于Hash环实现的伪分布式,RedisCluster提供了真正去中心化的分布式访问。

这意味着:

下面是具体的实现思路:

  1. 使用HashSlot

    类似于Memcached的Hash环,Redis Cluster采用HashSlot来实现Key值的均匀分布和实例的增删管理。

    首先默认分配了16384个Slot(这个大小正好可以使用2kb的空间保存),每个Slot相当于Memcached的Hash环上的一个节点。接入集群的所有实例将均匀地占有这些Slot,而最终当我们Set一个Key时,使用CRC16(Key) % 16384来计算出这个Key属于哪个Slot,并最终映射到对应的实例上去。

    那么当增删实例时,Slot和实例间的对应要如何进行对应的改动呢?

    举个例子,原本有3个节点A,B,C,那么一开始创建集群时Slot的覆盖情况是:

     节点A	0-5460
     节点B	5461-10922
     节点C	10923-16383
    

    现在假设要增加一个节点D,RedisCluster的做法是将之前每台机器上的一部分Slot移动到D上(注意这个过程也意味着要对节点D写入的KV储存),成功接入后Slot的覆盖情况将变为如下情况:

     节点A	1365-5460
     节点B	6827-10922
     节点C	12288-16383
     节点D	0-1364,5461-6826,10923-12287
    

    同理删除一个节点,就是将其原来占有的Slot以及对应的KV储存均匀地归还给其他节点。

  2. P2P节点寻找

    现在我们考虑如何实现去中心化的访问,也就是说无论访问集群中的哪个节点,你都能够拿到想要的数据。其实这有点类似于路由器的路由表,具体说来就是:

    * 每个节点都保存有完整的HashSlot - 节点映射表,也就是说,每个节点都知道自己拥有哪些Slot,以及某个确定的Slot究竟对应着哪个节点。

    * 无论向哪个节点发出寻找Key的请求,该节点都会通过CRC(Key) % 16384计算该Key究竟存在于哪个Slot,并将请求转发至该Slot所在的节点。

    总结一下就是两个要点:映射表内部转发

最后我们可以给出Redis Cluster的结构图,和之前Memcached的Hash环还是有着很明显的区别的:

本部分内容参考自此文章,特别鸣谢作者的分享


评论

还没有评论