Redis集群方案
哨兵的问题
虽然主从复制和哨兵模式完美的解决了Redis的单机问题,但是Redis仍然存在着以下两个问题:
- 所有的写操作都集中到主服务器上,主服务器CPU压力比较大
- 不管是主服务器还是从服务器,它们都同样保存了redis的所有数据,随着数据越来越多,可能会出现内存不够用的问题
Redis集群简介
redis从3.0开始支持集群功能。redis集群采用无中心节点方式实现,无需proxy代理,客户端直接与redis集群的每个节点连接,根据同样的hash算法计算出key对应的slot,然后直接在slot对应的redisj节点上执行命令。在redis看来,响应时间是最苛刻的条件,增加一层带来的开销是redis不能接受的。因此,redis实现了客户端对节点的直接访问,为了去中心化,节点之间通过gossip协议交换互相的状态,以及探测新加入的节点信息。redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。
Redis Cluster的具体实现细节是采用了Hash槽的概念,集群会预先分配16384个槽,并将这些槽分配给具体的服务节点,通过对Key进行CRC16(key)%16384运算得到对应的槽是哪一个,从而将读写操作转发到该槽所对应的服务节点。当有新的节点加入或者移除的时候,再来迁移这些槽以及其对应的数据。在这种设计之下,我们就可以很方便的进行动态扩容或缩容,个人也比较倾向于这种集群模式。
Redis 集群目标
- 高性能
- 线性扩容
- 高可用
Redis 集群结构特点
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
节点的fail是通过集群中超过半数的节点检测失效时才生效。
客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node、slot、value。
Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
Redis 集群提供的功能
数据自动分片
集群中每个节点都会负责一定数量的slot,每个key会映射到一个具体的slot,通过这种方式就可能找到key具体保存在哪个节点上了。
提供hash tags功能
通过hash tag功能可以将多个不同key映射到同一个slot上,这样就能够提供multi-key操作,hash tag的使用的方式是在key中包含“{}”,这样只有在“{…}”中字串被用于hash计算。
自动失效转移和手动失效转移
减少硬件成本和运维成本。
Redis 集群功能限制
Redis集群相对单机在功能上有一定限制。
key批量操作支持有限。如:MSET``MGET,目前只支持具有相同slot值的key执行批量操作。
key事务操作支持有限。支持多key在同一节点上的事务操作,不支持分布在多个节点的事务功能。
key作为数据分区的最小粒度,因此不能将一个大的键值对象映射到不同的节点。如:hash、list。
不支持多数据库空间。单机下Redis支持16个数据库,集群模式下只能使用一个数据库空间,即db 0。
复制结构只支持一层,不支持嵌套树状复制结构。
Redis集群方案
在redis集群中,key只能保存在按照某种规律计算得到的节点上,对该key的读取和更新也只能在该节点进行。比如redis集群一共有6个节点,现在我想执行 set name hello
,这个key为name,常见的某种规律有哈希取余"name".hashcode() % 6 + 1
得到节点的位置为4,所以就放在第四个的位置上,以后不管我是读取还是更新还是删除,我都到第四个节点上。如此一来,便完美解决了上述两个问题。
客户端分区方案
指在客户端计算key得到将要保存的节点,然后客户端再连接该节点端口,进行数据操作。这种方案比较简单,但是一旦节点数发生变化,将要更新新的计算算法(比如取余这个6改成10)到所有客户端上,会比较麻烦。
客户端分区方案 的代表为 Redis Sharding,Redis Sharding 是 Redis Cluster 出来之前,业界普遍使用的 Redis 多实例集群 方法。Java 的 Redis 客户端驱动库 Jedis,支持 Redis Sharding 功能,即 ShardedJedis 以及 结合缓存池 的 ShardedJedisPool。
优点
不使用 第三方中间件,分区逻辑 可控,配置 简单,节点之间无关联,容易 线性扩展,灵活性强。
缺点
客户端 无法 动态增删 服务节点,客户端需要自行维护 分发逻辑,客户端之间 无连接共享,会造成 连接浪费。
代理分区方案
指在客户端和服务器之间加了一层代理层,客户端的命令先到代理层,代理层进行计算,再分配到它对应的节点上;这种方法挺好的,节点数发生变化,只需要修改代理层的计算算法即可,但是需要多一层转发,需要一定的耗时。
代理分区 主流实现的有方案有 Twemproxy 和 Codis。
优点
简化 客户端 的分布式逻辑,客户端 透明接入,切换成本低,代理的 转发 和 存储 分离。
缺点
多了一层 代理层,加重了 架构部署复杂度 和 性能损耗。
查询路由方案
节点之间早就约定好哪些key是属于自己,哪些key是属于其它节点;客户端最开始随机把命令发给某个节点,节点计算并查看这个key是否属于自己的,如果是自己的就进行处理,并把结果发回去;如果是其它节点的,就会把那个节点的信息(ip + 地址)转发给客户端,让客户端重定向,这么一说感觉是有点像http协议中的3XX状态码。今天的主角Redis Cluster
就是基于查询路由方案。
优点
无中心节点,数据按照 槽 存储分布在多个 Redis 实例上,可以平滑的进行节点 扩容/缩容,支持 高可用 和 自动故障转移,运维成本低。
缺点
严重依赖 Redis-trib 工具,缺乏 监控管理,需要依赖 Smart Client (维护连接,缓存路由表,MultiOp 和 Pipeline 支持)。Failover 节点的 检测过慢,不如 中心节点 ZooKeeper 及时。Gossip 消息具有一定开销。无法根据统计区分 冷热数据。
数据分区
数据分布理论
分布式数据库 首先要解决把 整个数据集 按照 分区规则 映射到 多个节点 的问题,即把 数据集 划分到 多个节点 上,每个节点负责 整体数据 的一个 子集。
数据分布通常有 哈希分区 和 顺序分区 两种方式,对比如下:
分区方式 特点 相关产品 哈希分区 离散程度好,数据分布与业务无关,无法顺序访问 Redis Cluster,Cassandra,Dynamo 顺序分区 离散程度易倾斜,数据分布与业务相关,可以顺序访问 BigTable,HBase,Hypertable 由于 Redis Cluster 采用 哈希分区规则,这里重点讨论 哈希分区。常见的 哈希分区 规则有几种,下面分别介绍:
节点取余分区
使用特定的数据,如 Redis 的 键 或 用户 ID,再根据 节点数量 N 使用公式:hash(key)% N 计算出 哈希值,用来决定数据 映射 到哪一个节点上。
优点
这种方式的突出优点是 简单性,常用于 数据库 的 分库分表规则。一般采用 预分区 的方式,提前根据 数据量 规划好 分区数,比如划分为 512 或 1024 张表,保证可支撑未来一段时间的 数据容量,再根据 负载情况 将 表 迁移到其他 数据库 中。扩容时通常采用 翻倍扩容,避免 数据映射 全部被 打乱,导致 全量迁移 的情况。
缺点
当 节点数量 变化时,如 扩容 或 收缩 节点,数据节点 映射关系 需要重新计算,会导致数据的 重新迁移。
一致性哈希分区
一致性哈希 可以很好的解决 稳定性问题,可以将所有的 存储节点 排列在 收尾相接 的 Hash 环上,每个 key 在计算 Hash 后会 顺时针 找到 临接 的 存储节点 存放。而当有节点 加入 或 退出 时,仅影响该节点在 Hash 环上 顺时针相邻 的 后续节点。
优点
加入 和 删除 节点只影响 哈希环 中 顺时针方向 的 相邻的节点,对其他节点无影响。
缺点
加减节点 会造成 哈希环 中部分数据 无法命中。当使用 少量节点 时,节点变化 将大范围影响 哈希环 中 数据映射,不适合 少量数据节点 的分布式方案。普通 的 一致性哈希分区 在增减节点时需要 增加一倍 或 减去一半 节点才能保证 数据 和 负载的均衡。
注意
因为 一致性哈希分区 的这些缺点,一些分布式系统采用 虚拟槽 对 一致性哈希 进行改进,比如 Dynamo 系统。
虚拟槽分区
虚拟槽分区 巧妙地使用了 哈希空间,使用 分散度良好 的 哈希函数 把所有数据 映射 到一个 固定范围 的 整数集合 中,整数定义为 槽(slot)。这个范围一般 远远大于 节点数,比如 Redis Cluster 槽范围是 0 ~ 16383。槽 是集群内 数据管理 和 迁移 的 基本单位。采用 大范围槽 的主要目的是为了方便 数据拆分 和 集群扩展。每个节点会负责 一定数量的槽,如图所示:
当前集群有 5 个节点,每个节点平均大约负责 3276 个 槽。由于采用 高质量 的 哈希算法,每个槽所映射的数据通常比较 均匀,将数据平均划分到 5 个节点进行 数据分区。Redis Cluster 就是采用 虚拟槽分区。
节点1: 包含 0 到 3276 号哈希槽。
节点2:包含 3277 到 6553 号哈希槽。
节点3:包含 6554 到 9830 号哈希槽。
节点4:包含 9831 到 13107 号哈希槽。
节点5:包含 13108 到 16383 号哈希槽。
这种结构很容易 添加 或者 删除 节点。如果 增加 一个节点 6,就需要从节点 1 ~ 5 获得部分 槽 分配到节点 6 上。如果想 移除 节点 1,需要将节点 1 中的 槽 移到节点 2 ~ 5 上,然后将 没有任何槽 的节点 1 从集群中 移除 即可。
由于从一个节点将 哈希槽 移动到另一个节点并不会 停止服务,所以无论 添加删除或者 改变 某个节点的 哈希槽的数量 都不会造成 集群不可用 的状态.
Redis的数据分区
Redis Cluster 采用 虚拟槽分区,所有的 键 根据 哈希函数 映射到 0~16383 整数槽内,计算公式:slot = CRC16(key)& 16383。每个节点负责维护一部分槽以及槽所映射的 键值数据,如图所示:
Redis虚拟槽分区的特点
解耦 数据 和 节点 之间的关系,简化了节点 扩容 和 收缩 难度。
节点自身 维护槽的 映射关系,不需要 客户端 或者 代理服务 维护 槽分区元数据。
支持 节点、槽、键 之间的 映射查询,用于 数据路由、在线伸缩 等场景。
Redis集群的功能限制
Redis 集群相对 单机 在功能上存在一些限制,需要 开发人员 提前了解,在使用时做好规避。
key 批量操作 支持有限。
类似 mset、mget 操作,目前只支持对具有相同 slot 值的 key 执行 批量操作。对于 映射为不同 slot 值的 key 由于执行 mget、mget 等操作可能存在于多个节点上,因此不被支持。
key 事务操作 支持有限。
只支持 多 key 在 同一节点上 的 事务操作,当多个 key 分布在 不同 的节点上时 无法 使用事务功能。
key 作为 数据分区 的最小粒度
不能将一个 大的键值 对象如 hash、list 等映射到 不同的节点。
不支持 多数据库空间
单机 下的 Redis 可以支持 16 个数据库(db0 ~ db15),集群模式 下只能使用 一个 数据库空间,即 db0。
复制结构 只支持一层
从节点 只能复制 主节点,不支持 嵌套树状复制 结构。
Redis集群的大概流程
集群创建之初,我们可以自动或者手动给每个节点分配槽位。每个节点通过Gossip协议,会和其它节点交换槽信息,得到并且保存槽与节点的全局对应关系图。于是节点收到客户端发来的命令以后,对key进行CRC16(key) & 16383
的计算得到槽位,对比这个槽位是不是属于自己的,如果是自己的就进行处理,并把结果发回去;如果是其它节点的,就会把那个节点的信息(ip + 地址)转发给客户端,然后客户端再重新请求;当然客户端也不会那么傻,每次都是随机请求节点,客户端在启动的时候也会和服务器交换信息得到槽和节点的映射图,客户端请求的节点,也是客户端自己计算CRC16(key) & 16383
得到槽位,再对比关系图而得到的节点,如果节点发生变化了(即收到请求重定向),它也会更新这个关系图。
创建集群
准备节点
一个高可用的redis集群至少要有6个节点
1 | # redis-6379.conf |
6个节点启动成功后,我们可以在redis目录下看到生成的cluster-config-file文件,打开nodes-6379.conf
如下:
1 | 8ba45af25feef061507831ca1b3ddf71a7574631 :0 myself,master - 0 0 0 connected |
其中8ba45af25feef061507831ca1b3ddf71a7574631是6379redis的节点ID,这里我们只要知道它很重要就可以了。
节点握手
打开客户端进入6379,然后依次运行
cluster meet 139.199.168.61 6380
到cluster meet 139.199.168.61 6384
1 | 139.199.168.61:6379> cluster meet 139.199.168.61 6380 |
cluster meet
两个节点互相感知对方存在,发起节点发送发送Gossip协议中的meet消息给接收节点,接收节点收到meet消息后,保存发起节点的信息,然后通过返回pong消息把自己的信息也返回回去,之后两个节点会定期ping/pong进行节点通信。我们可以把它理解为把某个节点拉到一个集群里面,如果把其它节点也拉进来以后,集群里面的节点两两之间都会互相握手。等所有节点都拉到集群以后,我们可以执行cluster nodes
来查看集群中节点间的关系。
1 | 139.199.168.61:6379> cluster nodes |
分配槽
以上只是建立了一个集群,但是其实集群还不能工作,可以用
cluster info
来查看集群状态:
1 | 139.199.168.61:6379> cluster info |
可以看到此时集群的状态是fail,失败的,我们需要把这16383个槽分出去,集群才能正常工作,分配槽的命令如下:
1 | /usr/local/redis-3.2.6/src/redis-cli -h 139.199.168.61 -p 6379 cluster addslots {0..5461} |
注意:0和5641之间隔的是两个点,因为看书上写的是三个点,会报(error) ERR Invalid or out of range slot
的错误。
这样子就把所有的槽都分出去了,但是只用到了三个节点,剩下三个节点我们可以作为从节点,可以使用cluster replicate 主节点id
来把某个节点挂为某个节点的从节点。
1 | 139.199.168.61:6382> cluster replicate 8ba45af25feef061507831ca1b3ddf71a7574631 |
最后我们来看一下节点状态:
1 | 139.199.168.61:6379> cluster info |
再来查看一下节点关系:
1 | 139.199.168.61:6379> cluster nodes |
节点id,节点ip/端口,是否是主节点,节点的槽位分配一览无余。至此,一个完整的redis cluster集群创建成功。
故障转移原理
节点通信(Gossip协议)
Gossip协议
常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息。
meet消息
用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
ping消息
集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息,ping消息发送封装了自身节点和部分其他节点的状态数据。
pong消息
当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
fail消息
当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
通信过程
我们知道集群中的节点为了交换自身的槽位信息,节点与节点之间会不停的进行通信。通信采用Gossip协议,工作原理是节点彼此不停的通信交换信息,一段时间后所有的节点都会知道集群的完整信息,有点类似流言传播。节点ping其它节点的时候,也会把其它节点的信息带上,接收方会记录这些节点的信息,然后再向这些节点发送ping信息。
故障发现
当集群少量节点出行故障时,能通过自动化故障转移保证集群的高可用。那是怎么发现故障节点的呢?
包括两个环节:主观下线pfail (单个节点认为另一个节点下线,将它标记为pfail) 、客观下线 fail(节点彼此之间通过信息交换,大家达成共识了,都认为该节点下线,标记为fail)
这里所说的“大家达成共识了”,指的是主节点投票超过半数以上,就是说如果是3主3从集群,至少要有2个主节点认为该节点下线,从节点没资格参与投票。
如果是主节点,就要进行故障转移了
故障转移
主节点发生故障了,从节点收到fail广播消息,从节点会尝试发起选举
其他主节点接收到选举消息,会进行投票,超过半数以上通过才可以完成选举。
(详细过程请阅读redis开发与运维.pdf)
故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2 个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环 节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点 问题。