redis集群解析

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

  1. 输入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

Supongo que te gusta

Origin juejin.im/post/7076744695891623972
Recomendado
Clasificación