redis集群解析和水平扩展

1,920 阅读22分钟

redus集群方案比较

哨兵模式

image.png

在redis3.0之前, 要实现集群一般是通过哨兵sentinel工具来监控master节点的状态, 如果master节点出现异常, 则会出现主从切换, 将一台slave作为master, 哨兵的配置略为复杂, 并且性能和高可用等方面表现的一般, 特别是在主从切换的那十几秒访问是中断的, 而且哨兵模式只有一个主节点对外提供服务, 无法支持很高的并发, 并且单个主节点内存也不宜设置过大, 否则会持久化文件过大, 影响数据恢复或者主从同步的效率.

高可用集群模式

image.png

redis集群是一个由多个主从节点群组成的分布式服务器集群, 它具有复制/高可用和分片特性. redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能. 需要将每个节点设置为集群模式, 这种集群模式没有中心节点, 可水平扩展, redis集群的节点可以拓展到上万个, 但是官方建议不超过1000个. redis集群的性能和高可用性完爆之前版本的哨兵模式, 并且集群配置非常简单.

redis高可用集群搭建

这里搭建一个具有9个节点的集群, 也就是3主6从.

  1. 准备3台centos7.

image.png

  1. 分别在3台机器上创建3个文件夹, 7001, 8001, 9001, 代表本机的3个redis服务分别使用7001, 8001, 9001这3个端口.

image.png

  1. 在每个7001, 8001, 9001下面解压安装redis, 也就是说一共要安装9次redis, 具体安装方法见上篇博客<redis持久化和主从哨兵>中.

  2. 选择linux1机器, 进入7001的redis中: /opt/cluster-redis/7001/redis-5.0.14, 修改redis.conf文件, 具体修改如下几个参数:

# 保证任何IP都能访问该redis
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 指定端口7001
port 7001
daemonize yes
# 修改pid文件, 和端口号对应
pidfile /var/run/redis_7001.pid
# 开启aof
appendonly yes
# 开启集群
cluster-enabled yes
# 集群配置文件, 最好跟端口号一致, 这样又可以区分不同文件(如果不区分不同文件会导致集群部署不成功)也容易看
cluster-config-file nodes-7001.conf
# 修改文件存放位置, 依然和端口号对应(本来我的redis目录就和启动端口号对应的)
dir /opt/cluster-redis/7001/redis-5.0.14
cluster-node-timeout 10000
# 可以设置密码
requirepass darkness
# 主节点访问密码, 和上面一致即可
masterauth darkness
  1. 将该7001的配置文件复制给8001, 9001下的redis, 然后只要修改对应端口号分别为8001, 9001即可.

  2. 将7001, 8001, 9001的配置文件全部复制给linux2, linux3这两台机器对应的7001, 8001, 9001的redis中.

  3. 分别在三台机器使用命令启动这9个redis. 可以看到, 三台机器一共9个redis都已经启动成功, 端口号都为7001, 8001, 9001, 并且后面均有一个[cluster]标识.

image.png

  1. 截止目前, 9台redis已经创建成功, 但是这9台redis都是独立的9台, 它们之间并没有任何关联, 因此只需要一个命令就能让这9台独立的redis组合成一个3主6从的真正集群.
  • 关闭防火墙 // 关闭当前防火墙

systemctl stop firewalld

// 禁用开启自动启动防火墙

systemctl disable firewalld

  • 执行命令

bin/redis-cli -a darkness --cluster create --cluster-replicas 2 192.168.200.128:7001 192.168.200.129:7001 192.168.200.130:7001 192.168.200.128:8001 192.168.200.129:8001 192.168.200.130:8001 192.168.200.128:9001 192.168.200.129:9001 192.168.200.130:9001

得到如下反馈:

image.png

master开头的前面三行, 表示说将0-16383个slot平均分配给3个master.

adding开头的表示这是redis默认分配主从节点的策略, 告诉用户将会把哪几台机器作为从节点交给哪台master作为主节点

M:开头的和S:开头的分别代表主节点(master)和从节点(slave), slots代表主节点被分配的槽位, replicates代表这是个备份(从节点), 一连串的类似于uuid的东西就是redis给每个节点分配的唯一ID.

  • 输入yes继续

image.png

至此已经成功搭建出一个redis的3主6从集群

验证集群

  1. 连接任意客户端, 指明-c, -a指明密码. bin/redis-cli -c -a darkness -p 7001

  2. 输入cluster info查看集群信息

image.png

  1. 输入cluster nodes查看集群节点信息

节点信息很明确的展示了哪个是从节点, 哪个是主节点, 并且也明确展示了每个从节点对应的主节点的id, 每个主节点也明确展示了自己所对应的槽位. 每个节点后面有@17001和@18001等字样, 这个就是后面所说的gossip协议的端口, 取当前服务端口+10000.

image.png

  1. 输入set name darkness

redis会根据name这个key进行CRC16算法算出一个hash值, 并且使用这个值&16383, 这样得到一个余数就是槽位, 算出槽位是5798, 于是定位到了129这台机器上的master.

image.png

redis集群特性

redis cluster将所有数据划分为16384个slots(槽位), 每个节点负责其中一部分槽位. 槽位的信息存储于每个节点中.

redis集群中的主节点参与读取和写入, 而从节点并不会像redis主从那样还具有读功能. redis集群的从节点是没有读写功能的, 只用于数据备份.

当redis cluster的客户端来连接集群时, 它也会得到一份集群的槽位配置信息并将其缓存在客户端本地, 这样当客户端要查找某个key时, 可以直接定位到目标节点. 同时因为槽位的信息可能会存在客户端与服务器不一致的情况, 还需要纠正机制来实现客户端中的槽位信息的校验调整.

槽定位算法

cluster会默认对key值使用crc16算法进行hash得到一个整数值, 然后使用这个整数值对16384(2^14)进行取模得到具体槽位.

跳转重定位

当客户端向一个错误的节点发出了指令, 该节点会发现该指令的key所在的槽位并不归自己管理, 这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址, 告诉客户端去连接这个节点获取数据. 客户端收到指令后除了跳转到正确节点上去操作以外, 还会同步更新纠正本地的槽位映射表缓存, 后续所有的key将使用新的槽位映射表.

集群节点的通信方式

一个分布式的服务之间的通信, 归根结底还是得找到要访问的目的服务的ip, 端口之类, 简称元数据信息, 一般保存这样的元数据信息的方式有2种.

集中式

所有元数据信息放在一个第三方的数据库中, 该数据库具有增删查改的功能, 并且还有监听机制, 比如zookeeper. 这样的优点在于元数据的更新和读取的时效性分厂好, 一旦元数据出现变更, 就立即可以更新到集中式的存储中, 其它节点读取的时候立即就可以感知到. 不足之处在于所有的元数据信息的更新压力全部集中在一个地方, 可能导致元数据的存储压力. 不过还是有很多中间件会使用这种方式, 比如dubbo, kafka之类, 都会用zk来做元数据的存储或者是注册中心.

gossip协议

gossip, 翻译过来是闲话, 龙门阵, 因此gossip协议还有多种名字, 比如谣言协议, 传染病协议.

gossip过程由root节点发起, 每当一个root节点有状态需要更新到网络中的其它节点时, 它会随机选择周围几个节点散播消息, 收到消息的节点也会重复这个过程, 最终网络中所有的节点都收到了这个消息. 这个过程并不是一次性就完成的, 需要一定的散播时间, 由于虽然不保证某个时刻的信息都能同步成功, 但是可以保证最终所有节点都能同步成功, 因此是一个最终一致性的协议, 这也是它的名字的由来.

gossip协议包含多种消息, 包括ping, pong, meet, fail

meet: 某个节点发送meet给新加入的节点, 让新节点加入集群中, 然后新节点就会开始与其它节点进行通信.

ping: 每个节点都会频繁给其他节点发送ping, 其中包含自己的状态还有自己维护的集群元数据, 互相通过ping交换元数据(类似自己感知到的集群节点增加和移除, hash slot信息等)

pong: 对ping和meet消息的返回, 包含自己的状态和其它信息, 也可以用于信息广播和更新.

fail: 某个节点判断另一个节点fail之后, 就发送fail给其它节点, 通知其它节点指定的这个节点挂了.

gossip协议的优势在于元数据更新比较分散, 不是集中在一个地方, 更新请求会陆陆续续地打到所有节点上, 有一定的延时, 去中心化, 降低了压力.

gossip协议的缺点在于, 元数据更新有一定的延迟. 并且消息冗余, 可能之前某个节点已经收到过这个消息了, 但是又有另一个节点再次发给这个节点同样的消息.

redis中的gossip通信

每个节点有一个专门用于节点之间gossip通信的端口, 就是自己的服务端口号+10000. 比如端口8001, 那么gossip协议的端口就是18001. 每个节点每隔一段时间都会往邻接的几个节点发送ping消息, 同时其它几个节点收到ping消息之后返回pong消息.

网络抖动解决

真实的机房网络并不是一帆风顺的, 时而会发生小问题. 比如网络抖动了, 突然之间一部分连接不可访问, 但是过了一会儿又恢复了. 为了解决这个问题, 我们在开始就配置一个timeout, 就是cluster-node-timeout 10000这个属性, 这个属性表示只有当某个节点持续了10000毫秒没有响应的时候, 才会认定这个节点出现故障, 需要进行主从切换. 如果没有这个选项, 网络抖动会导致主从频繁切换(数据的重新复制).

redis集群选举原理分析

当slave发现自己的master变为fail状态时, 便尝试进行failover, 以期望成为新的master. 由于挂掉的master可能会有多个slave, 从而存在多个slave竞争成为master节点的过程. 过程如下:

  1. 某个master挂了, 它下面的所有slave节点都感知到了.

  2. 这些slave分别将各自记录的集群currentEpoch+1, 并广播FAILOVER_AUTH_REQUEST信息.

  3. 其它节点收到该信息, 只有master会响应, 判断请求的合法性, 并发送FAILOVER_AUTH_ACK, 每个master只会对每个epoch发一次ack: 如果挂掉的master下面有3个slave, 那么这3个slave均会发送信息给其余的活着的master, 虽然是并行发送, 但是对于每个接收消息的master来说, 总会有个先后顺序, master只会响应它接收到的第一个REQUEST, 通过epoch区分.

  4. 尝试failover的slave收集各自收到的master返回的FAILOVER_AUTH_ACK.

  5. 当某个slave收到超过半数的master的ack后, 就会变成新的master.

  6. slave广播pong消息通知其它集群节点.

上面的第三点说: 并行, 实际上还是有一些延迟的区分的. 从节点并不是在主节点一进入fail状态就立刻尝试选举, 而是有一个延迟计算公式去计算延迟, 这个延迟首先是为了确保fail状态在集群中传播(gossip协议), 假如说主节点一fail, slave就发送选举消息, 那么其它master由于没有收到fail的传播信息, 将会判定此次FAILOVER_AUTH_REQUEST不合法, 拒绝ACK.

  • 延迟计算公式

delay = 500ms + random(0~500ms) + slave_rank * 1000ms

  • 延迟的rank

slave_rank表示此slave已经从master复制数据的总量rank, rank越小代表已经复制的数据越新, 在这个公式下, 将会保证最新的slave将会先去发送选举, 再根据第三点所说的master只响应第一次的request, 那么理论上将会确保这个数据最新的slave成为新的master.

集群脑裂丢失数据问题

比如有一个多主2从的redis集群, 其中某个master位于linux1机器上, 它的2个从节点在linux2和linux3上, 而linux1, linux2, linux3这三台机器分别位于不同的三个分区. 假如说某个时刻, 这个分区出现问题, 导致linux1孤立了, 无法与linux2和linux3乃至于整个集群所有分区通信(比如重庆分区北京分区上海分区, 很可能重庆分区无法与另外两个分区通信, 但是重庆本地的请求依然能正常使用该服务), 但是linux1本身对外却可以正常提供服务, 那么由于linux1的两个从节点都认为自己的master挂了, 就发起选举, 成为了新的master, 那么就出现问题了. 因为linux1对外界还是仍然正常的, 所以它上面部署的原本的master依然对外提供着服务, 新来的master节点本身就是没问题的, 所以对于外界来说, 本来是3个master的节点, 现在变成了4个master, 打入的请求很可能有一部分打入了linux1上, 另一部分打入了新的master上. 当这个网络分区恢复以后, 老的master会将自己变为slave, 这样就导致丢失了一部分打入了老的master的数据.

上述问题的规避方法就是在redis上配置一个参数, 使每次写数据的时候, 最少同步给半数个slave节点, 这样加上本身的主节点就是超过半数, 这种方法虽然无法百分百保证数据不丢失, 但是至少还是有一定的效果的. 需要注意的是, 这个办法会影响集群的可用性, 比如slave要是挂了超过半数个了, 就导致写数据不可能成功.

# 写数据最少同步的slave数量, 可以模仿半数机制来配置
# 比如1主4从, 这个参数就配置2, 加上主节点, 就是超过半数了
min-replicas-to-write 1

集群是否完整才能对外提供服务

当redis.conf的配置cluster-require-full-coverage为no时, 表示当负责一个插槽的主库下线并且没有响应的从库进行故障恢复时, 集群仍然可用, 如果为yes则集群不可用.

redis集群为什么至少要3个master节点, 并且推荐为奇数?

因为新master的选举需要大于半数的集群master节点同意才能选举成功, 如果只有两个master节点, 当其中一个挂了, 是永远无法达到新master选举成功的条件要求的.

奇数个master节点可以在满足选举该条件的基础上节省一个节点, 比如3个master节点和4个master节点的集群相比, 大家如果都挂了一个master节点, 就都可以选举成功, 而如果都挂了2个master, 都不能选举成功, 因此奇数个master节点是从节省机器资源角度出发的.

redis集群对批量操作命令的支持

对于类似mst, mget这样的多个key的原生批量操作命令, redis集群只能支持命令中的所有key都落在同一个slot的情况, 比如下图所示.

image.png

set name darkness, 会计算name的hash, 最终判断落在5798这个slot上, set age 28, 计算age的hash, 判断落在741这个slot上. 同样的, get name和get age也是一样会去计算key的槽位. 而使用mget和mset的时候, 将会报错(error) CROSSSLOT Keys in request don't hash to the same slot, 批量操作的key不落于同一个槽位.

其实解决这个问题很简单, 只要让redis计算的时候, 计算落入同一个槽位就可以了, 比如: mset {user}:name darkness {user}:age 28. 如下图所示.

image.png

发现set {user}:name darkness, 最终计算的slot是5474, 而且使用set {user}:age 28的时候, 并没有redirect到其它master, 后面使用mset也是完全没有任何问题. 接下来测试set user 33, 发现user定位的也是5474, 说明确实redis是取了{}中的字段作为CRC16算法计算hash的参数的.

redis集群进行hash操作

集群中无法直接使用hset, 会报错:

image.png

同理, 需要把user使用{}括起来, 让它落入同一个槽位中.

image.png

redis集群水平扩展

前面我们搭建了一个3主6从的redis集群, 也大概介绍了一下集群的工作机制, 但是还有一点没有提到, 就是redis集群的水平扩展.

redis集群水平扩展的操作稍微有点麻烦, 但是实际上是比较灵活的, 因为水平扩展不仅可以进行数据迁移, 还可以撤销扩展的节点.

我们现在就为刚才的3主6从集群再加一个1主2从, 变为一个4主8从的redis集群.

构建第四组主从节点

在第四台机器上启动3个redis服务(集群模式启动)

第四组主从节点一样需要3个redis服务, 分别取7001, 8001, 9001, 安装和配置直接复制前面的随便一台机器就行, 然后启动服务, 查看服务是否启动成功:

image.png

完全没任何问题, 现在已经在第四台机器上启动了3个redis服务, 并且也都是集群模式, 只不过这3个redis还是游离的独立的redis, 和之前我们搭建的集群没半毛钱关系, 因此我们现在就需要通过命令, 将这3个节点加入集群, 并且指定7001是master, 8001和9001是slave.

使用--cluster add-node命令添加master

执行

bin/redis-cli -a darkness --cluster add-node 192.168.200.131:7001 192.168.200.128:7001

其中add-node后面跟着的那个ip:port是将要添加的node的ip和port, 最后面的那个ip:port是随便挑选一台master的ip和port, 之所以命令要这么写, 和上面说的gossip协议有关, 新加一个节点的时候, 会由一个master去发送meet命令给要加入的那个节点, 在本命令中, 就是由128:7001机器的redis去发送meet给131:7001机器的redis, 让131:7001的redis加入集群, 然后131:7001的机器再去使用gossip协议通知集群其它节点自己加入了集群.

命令执行完毕之后会有如下图的反馈.

image.png

这时候查看一下集群状态, 我们直接使用bin/redis-cli -c -a darkness -h 192.168.200.131 -p 7001连接新加入的这个master, 看看能否连成功, 如果连接成功, 使用cluster nodes去查看一下集群的nodes.

image.png

发现它自己目前的确是一个master, 但是分配的槽位没有, 因此这时候外界如果连接它而使用一些get/set命令, 将会直接发生重定位跳转, 没有任何数据会坐落到这台新加入的master中, 因此我们需要给这个master分配槽位.

使用--cluster reshard命令给新加入的master分配槽位

键入bin/redis-cli --cluster -a darkness reshard 192.168.200.128:7001命令, 为新加入的这个131机器上的master分配槽位. 你可能奇怪为什么输入的ip是128, 而不是131, 这是因为分配槽位随便哪个机器都可以, 只要它是master, 之所以区分输入是为了明确这个命令中, 输入的节点并不是目的节点, 而是指定由哪台master去进行这个重新分配槽位的动作.

image.png

回车执行之后, 系统将会询问需要分配多少个槽位给新加入的节点, 这时候随便输1-16384中任意一个数字就行, 这次我们输入3000. 然后系统又会询问要给哪个master重新分配槽位, 需要输入redis的id, 上面使用cluster nodes的时候, 就能看到每个master的id, 因此就输入ce084df99f18b46528dac14fbf12f99e1c1db18a. 回车之后又会询问是all还是done, 选择done就是自己指定从哪些源master分配槽位, 选择all就是从所有源master分配槽位, 输入all即可. 接下来系统还会给出一个重分方案, 然后再次询问是否确认重新分配槽位, 输入yes.

执行完毕上述的操作之后, 所有槽位已经分配完毕.

查看集群最新状态

通过cluster nodes命令可以看到, 新加入的这个131节点的master, 已经拥有3000个槽位了.

image.png

分配槽位也会迁移数据

在前面我们测试的时候, 发现name这个key, 计算的槽位是在5798位置, 刚好我们新加入的这个master, 分配的槽位包括5798这个slot, 所以我们可以在131这台机器上使用get name命令看看, 是否会发生重定位跳转.

image.png

发现确实没有发生重定位跳转, 说明在分配slot的时候, 顺带将该slot的数据也迁移了过来.

使用命令添加另外两个redis服务作为从节点

  1. 同样的, 首先是使用--cluster add-node命令将这两个redis节点添加到集群中去.
  • bin/redis-cli -a darkness --cluster add-node 192.168.200.131:8001 192.168.200.128:7001

  • bin/redis-cli -a darkness --cluster add-node 192.168.200.131:9001 192.168.200.128:7001

add完毕之后, cluster nodes看看状态.

image.png

发现新加进来的节点依然是master, 但是分配的槽位依然是空, 因此我们需要使用命令将其变为从节点.

  1. 分别进入8001和9001这两个客户端, 然后在其内部执行cluster replicate [master id]命令, 将自己添加为指定的这个[master id]的从节点, master id就是使用cluster nodes看到的131:7001这个节点的id.

image.png

执行完毕之后, 使用cluster nodes看到, 8001和9001已经成为了7001的slave.

撤销第四组主从节点

撤销9001

很简单, 直接使用--cluster del-node命令即可

命令后跟节点的ip:port, 以及节点的id

bin/redis-cli -a darkness --cluster del-node 192.168.200.131:9001 e4388b82b503c32124a4baeb37bc795531140fbc

image.png

使用cluster nodes查看9001已经不在集群内了, 并且9001的redis也被关闭服务

image.png

撤销master7001

由于7001是master, 它是被分配了槽位的, 因此在撤销7001之前, 还得将它所保管的槽位全部交给其它master管理, 所以需要涉及一次--cluster reshard.

由于reshard只能分配给一个目标master, 因此这个逆向操作只能选取一个master, 如果想要复原成最开始集群初始的那样, 只能人工进行多次操作才行.

与之前不同的是, 在提示是all还是done的时候, 需要输入131:7001这个master的id, 然后输入done来结束输入, 并开始执行.

bin/redis-cli -a darkness --cluster reshard 192.168.200.128:7001

然后输入目标master id, 输入128:7001的id即可.

接下来输入源master id, 输入131:7001的id, 然后输入done

image.png

在下一次的提示输入yes之后, 131:7001的slots已经全部交给128:7001了. 接下来就可以使用--cluster del-node去撤销这个节点了.

image.png

使用cluster-nodes可以看到, 131:7001已经被删除了, 其名下还有一个131:8001节点的master又变为了128:7001.

image.png