• Redis集群(Cluster)


    1. 什么是集群

    • 广义的集群:只要是多台机器,构成一个分布式系统,就可以称为一个“集群”。像前面的主从结构,哨兵模式都是“广义的集群”
    • 狭义的集群:redis提供的集群模式,这个集群模式主要解决存储空间不足的问题

    例如整个数据全局是1TB,引入三组Master/Slave来存储,那么每一组Master/Slave存储数据全集的一部分,从而构成一个更大的整体,称为redis集群(Cluster)

    image.png
    在上图中,这三组机器存储的数据都不一样,每个slave都是对应master的备份(当master挂了,就会选举一个slave成为新的master)。每个红框部分称为一个分片,如果全量数据进一步增加,只要再增加更多,即可解决。

    2.数据分片算法

    2.1 哈希求余

    借鉴哈希表的基本思想,针对要插入的数据的key(redis都是键值对结构)计算hash值(比如使用MD5计算hash值)。再把这个hash值余上分片个数,就得到一个下标。此时就可以把这个数据放到该下标对应的分片中。即hash(key) % N

    md5是一个非常广泛使用的hash算法

    1. md5计算的结果是定长的,无论输入的原字符串多长,最终算出的结果都是固定长度
    2. md5计算的结果是分散的,两个原字符串,哪怕大部分都相同,只有一小部分不同,算出来的结果也会差别很大。因此使用md5作为hash函数,可以有效避免hash冲突
    3. md5计算的结果是不可逆的,给你原字符串,很容易算出md5值;给你md5值,很难还原出原始字符串。因此常使用md5加密

    优点

    简单高效,数据分配均匀

    缺陷

    随着业务增长,数据变多,现有分片不够使用。需要进行“扩容”,需要重新进行hash,计算新的下标。
    image.png
    上图中一共20个数据,只有3个数据不需要搬运,如果是20亿的数据,就需要搬运17亿!!!并且每个分片中不止有主节点还有从节点,需要进行主从同步,开销特别大。

    2.2 一致性哈希

    使用hash求余中,当前key属于哪个分片是交替的。像上图中,102属于0号分片,103属于1号分片,104属于2号分片,105又数据0号分片,交替出现,导致搬运成本非常高。
    而一致性哈希把交替出现,改进成连续出现。

    1. 把0->2^32-1的数据空间,映射到一个圆环上,数据按照顺时针方向增长

    image.png

    1. 假设当前存在三个分片,就把分片放到圆环的某个位置上

    image.png

    1. 假定有一个key,计算得到的hash值为H,那么这个key映射到哪个分片规则是,从H所在位置,顺时针往下找,找到第一个分片,就是该key所从属的分片

    image.png
    N个分片把整个圆环分成了N个管辖区间,key的hash值落在某个区间内,就归对应区间管理
    image.png

    扩容

    从3个分片扩容到4个分片时
    image.png
    此时,只需要将0号分片上的部分数据给搬运到3号分片上即可,1,2号分片管理的区间不变。

    • 优点:大大降低了扩容是数据搬运的规模,提高了扩容操作的效率
    • 缺点:数据分配不均匀,会数据倾斜

    2.3 哈希槽分区算法

    这是redis采用的分片算法,可以有效解决搬运成本高和数据分配不均的问题,redis cluster引入哈希槽(hash slots)算法。
    :::tips
    hash_slot = crc16(key) % 16384
    :::

    hash_slot 哈希槽
    其中crc16也是一种hash算法
    16384 = 16 * 1024 即16k

    相当于把整个哈希值,映射到16384个槽位上,即[0,16383].
    然后再把这些槽位比较均匀的分配给每个分片,每个分片的节点都需要记录自己持有的那些槽位。
    例如当前有三个分片,可能的分配方式:

    • 0号分片:[0,5461],共5462个槽位
    • 1号分片:[5462,10923],共5463个槽位
    • 2号分片:[10924,16383],共5460个槽位
      :::success
      每个分片会使用“位图”这样的数据结构表示出当前有多少槽位。16384个bit位,用每一位的0/1来区分自己这个分片当前是否持有该槽位号。16384%8=2048,即2kb
      :::
    扩容

    例如新增一个分片,就需要针对原有的槽位进行重新分配

    • 0号分片: 【0,4095】共4096个槽位
    • 1号分片:【5462,9557】共4096个槽位
    • 2号分片:【10924,15019】共4096个槽位
    • 3号分片:【4096,5461】+【9558,10923】+【15020,16383】共4096个槽位

    在上述过程中,只有被移动的槽位,对应的数据才需要被搬运。并且分片上的槽位号,不一定是连续的区间。

    哈希槽分区算法的实质:结合了哈希算法和一致性哈希的思想,

    问题1:redis集群最多有16384个分片吗?

    一共有16384个槽位,如果是16384个分片,意味着一个分片一个槽位,此时很难保证数据再各个分片的均衡性。key要先映射到槽位,再映射到分片中。如果每个分片的包含的槽位比较多,槽位个数相当,就可以认为包含的key的数量也是相当的。如果每个分片包含的槽位非常少,槽位个数不一定能直观反应到key的数目,因为有的槽位,有多个key,有的槽位,可能没有key.并且redis作者建议集群分片数不超过1000

    问题2:为什么是16384个槽位

    redis作者答案:https://github.com/antirez/redis/issues/2576
    节点之间通过心跳包通信,心跳包中包含该节点持有拿下slots,这个是使用位图表示,表示16384(16k)个slots,需要位图大小是2kb。如果给定的slots数更多,比如65536,需要8kb位图表示,8kb对于内存不算什么,但是在频繁的网络心跳包中,是一个不小的开销。
    另一方面,redis集群不建议超过1000个分片,所以16k对于最多1000个分片来说是足够用的,同时也会使对应的槽位配置位图体积不至于很大。

    3. 搭建redis集群

    基于docker,搭建一个集群,每个节点都是一个容器,拓扑结构如下:
    image.png

    注意:我们一共会创建11个redis节点,其中前9个用来演示集群搭建,后两个用来演示集群扩容

    3.1创建目录和配置

    1. 创建redis-cluster目录,内部创建两个文件docker-compose.ymlgenerate.sh

    image.png

    1. 关掉所有启动的redis容器,防止后续发生端口冲突

    image.png

    可以通过docker ps -a来查看是否全部关闭

    1. 执行shell脚本,generate.sh内容如下
      :::tips
      for port in KaTeX parse error: Undefined control sequence: \< at position 12: (seq 1 9); \̲<̲br />do \
      {port}/
      touch redis p o r t / r e d i s . c o n f < b r / > c a t < < E O F > r e d i s {port}/redis.conf
      cat << EOF > redis
      port/redis.conf<br/>cat<<EOF>redis
      {port}/redis.conf
      port 6379
      bind 0.0.0.0
      protected-mode no
      appendonly yes
      cluster-enabled yes
      cluster-config-file nodes.conf
      cluster-node-timeout 5000
      cluster-announce-ip 172.30.0.10${port}
      cluster-announce-port 6379
      cluster-announce-bus-port 16379
      EOF
      done
      # 注意 cluster-announce-ip 的值有变化.
      for port in KaTeX parse error: Undefined control sequence: \< at position 14: (seq 10 11); \̲<̲br />do \
      {port}/
      touch redis p o r t / r e d i s . c o n f < b r / > c a t < < E O F > r e d i s {port}/redis.conf
      cat << EOF > redis
      port/redis.conf<br/>cat<<EOF>redis
      {port}/redis.conf
      port 6379
      bind 0.0.0.0
      protected-mode no
      appendonly yes
      cluster-enabled yes
      cluster-config-file nodes.conf
      cluster-node-timeout 5000
      cluster-announce-ip 172.30.0.1${port}
      cluster-announce-port 6379
      cluster-announce-bus-port 16379
      EOF
      done
      :::
      执行命令
      :::tips
      bash generate.sh
      :::
      生成目录如下
      image.png
      其中每个redis.conf都不相同,以redis1为例:
      image.png

    区别在于每个配置中cluster-announce-ip是不同的,其他部分都相同,因为后续会给每个节点分配不同的ip地址

    配置说明:

    • cluster-enabled yes开启集群
    • cluster-config-file nodes.conf集群节点生成的配置
    • cluster-node-timeout 5000节点失联的时间
    • cluster-announce-ip 172.30.0.101节点自身的ip
    • cluster-announce-port 6379节点自身的业务端口
    • cluster-announce-bus-port 16379管理端口,用来给一些管理上的任务通信的。例如主节点挂了,需要让从节点成为主节点,就需要通过刚才管理端口来完成对应的操作

    3.2 编写docker-compose.yml

    version: '3.3'
    networks:
      mynet:
        ipam:
          config:
            - subnet: 172.30.0.0/24
    services:
      redis1:
        image: 'redis:5.0.9'
        container_name: redis1
        restart: always
        volumes:
          - ./redis1/:/etc/redis/
        ports:
          - 6371:6379
          - 16371:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.101
    
      redis2:
        image: 'redis:5.0.9'
        container_name: redis2
        restart: always
        volumes:
          - ./redis2/:/etc/redis/
        ports:
          - 6372:6379
          - 16372:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.102
    
      redis3:
        image: 'redis:5.0.9'
        container_name: redis3
        restart: always
        volumes:
          - ./redis3/:/etc/redis/
        ports:
          - 6373:6379
          - 16373:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.103
    
      redis4:
        image: 'redis:5.0.9'
        container_name: redis4
        restart: always
        volumes:
          - ./redis4/:/etc/redis/
        ports:
          - 6374:6379
          - 16374:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.104
    
      redis5:
        image: 'redis:5.0.9'
        container_name: redis5
        restart: always
        volumes:
        - ./redis5/:/etc/redis/
        ports:
          - 6375:6379
          - 16375:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.105
    
      redis6:
        image: 'redis:5.0.9'
        container_name: redis6
        restart: always
        volumes:
          - ./redis6/:/etc/redis/
        ports:
          - 6376:6379
          - 16376:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.106
    
      redis7:
        image: 'redis:5.0.9'
        container_name: redis7
        restart: always
        volumes:
          - ./redis7/:/etc/redis/
        ports:
          - 6377:6379
          - 16377:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.107
    
      redis8:
        image: 'redis:5.0.9'
        container_name: redis8
        restart: always
        volumes:
          - ./redis8/:/etc/redis/
        ports:
          - 6378:6379
          - 16378:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.108
    
      redis9:
        image: 'redis:5.0.9'
        container_name: redis9
        restart: always
        volumes:
          - ./redis9/:/etc/redis/
        ports:
          - 6379:6379
          - 16379:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.109
    
      redis10:
        image: 'redis:5.0.9'
        container_name: redis10
        restart: always
        volumes:
          - ./redis10/:/etc/redis/
        ports:
          - 6380:6379
          - 16380:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.110
    
      redis11:
        image: 'redis:5.0.9'
        container_name: redis11
        restart: always
        volumes:
          - ./redis11/:/etc/redis/
        ports:
          - 6381:6379
          - 16381:16379
        command:
          redis-server /etc/redis/redis.conf
        networks:
          mynet:
            ipv4_address: 172.30.0.111
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171

    image.png
    image.png

    3.3 启动容器

    启动之前检查redis服务是否全部关闭

    image.png

    启动容器

    :::tips
    docker-compose up -d
    :::

    验证是否启动

    :::tips
    docker ps -a
    :::
    image.png

    3.4 构建集群

    此处,把前9个主机构成集群,3主6从。后两个主机用来演示后续扩容

    :::tips
    redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2172.30.0.105:6379
    :::
    image.png
    image.png

    3.5 使用集群

    此时,使用客服端连上集群中的任何一个节点,都相当于连上整个集群。使用命令redis-cli -h 172.30.0.101 -p 6379 -c

    • 客服端后面要加上-c选项,因为通过“哈希槽分区算法”计算出对应的哈希槽,如果该哈希槽不在该分片,而是在其他分片就访问不到。加上-c就会自动把请求重定向到对应的节点

    image.png

    • 使用cluster nodes可以查看整个集群的情况

    image.png

    • 如果在从节点进行写操作,会重定向到master节点

    image.png

    4. 故障处理

    4.1 主节点宕机

    在上述的拓扑结构中,redis1,redis2,redis3是主节点,挑选一个停掉

    :::tips
    docker stop redis1
    :::
    image.png
    重新启动redis1
    :::tips
    docker start redis1
    :::
    image.png

    4.2 故障处理流程

    1. 故障判定,识别出某个节点是否挂了
    1. 节点A给节点B发送ping包,B就给A返回pong包,包含右集群的配置信息(该节点的id,该节点从属于哪个分片,是主节点还是从节点,属于哪个主节点,持有那些slots的位图)
    2. 每个节点,每秒钟都会随机给一些节点发送ping包,而不是全发一遍。这样的设定是为了避免如果节点很多,心跳包也会很多(例如9个节点,如果全发,就是9*8=72组心跳包,而且是按照N^2级别增长)
    3. 当节点A给节点B发送ping包,B不能如期回应的时候,此时A就会尝试重置和B的tcp连
      接,看是否能连接成功,如果仍然连接失败,A就会把B设为PFALL状态(主观下线)
    4. A判定B为PFALL之后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态(每个节点都会维护一个自己的”下线列表“,由于视角不同,每个节点的下线列表也不一定相同)
    5. 此时A发现很多其他节点也认为B为PFALL,并且数目超过集群个数的一半,那么A就会把B标记FALL(客观下线),并把消息同步给其他节点,其他节点收到后,也会把B标记为FALL
    2. 故障迁移
    1. 如果B是从节点,那么不需要进行故障迁移
    2. 如果B是主节点,那么就会从B的从节点中挑选一个(比如C和D)触发故障迁移
    3. 从节点会判定自己是否具有参选资格,如果从节点和主节点已经很久没有通信,即很久没有同步过数据,主从节点之间差异较大,时间超过阈值,就失去竞选资格
    4. 具有资格的节点,比如C和D,就会先休眠一段时间,休眠时间=500ms基础时间+[0,500ms]随机时间 + 排名 * 1000ms。offset值越大,排名越靠前(越小)
    5. 比如C的休眠时间到了,C就会给集群中其他节点,进行拉票操作,但是只有主节点才有投票资格
    6. 主节点就会把自己的票投给C(每个主节点只有一票),当C收到的票数超过主节点数目的一半,C就会晋升为主节点。(C自己负责执行slaveof no one,并且让D执行slaveof C)
    7. 同时,C还会将自己成为主节点的信息,同步给集群的其他节点,大家也会更新自己保存的集群结构信息。

    哨兵模式,是先投票竞选出一个leader,让leader负责找一个从节点升级为主节点。而集群模式里直接投票选出新的主节点

    4.3 出现集群宕机

    1. 某个分片,所有的主节点和从节点都挂了
    2. 某个分片,主节点挂了,但是没有从节点
    3. 超过半数的master节点都挂了

    5. 集群扩容

    现在搭建的集群的主机号101-109,9个主机,构成了3主6从结构的集群。现在将主机号为110和111也加入集群中,以110为master,111为slave,数据分片从3->4。

    5.1 添加主节点

    将新的主节点110加入到集群中

    :::tips
    redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
    :::

    • 172.30.0.110:6379:新增节点的地址
    • 172.30.0.101:6379:集群上任意一个节点的地址都可以,表示要把这个新节点添加到这个集群上

    image.png

    重新分配slots

    :::tips
    redis-cli --cluster reshard 172.30.0.101:6379
    :::

    • 172.30.0.101:6379:集群中任意节点的地址都可以,表示这个集群

    image.png

    此处是询问用户要移动多少的slots给新增的主节点,我们这里分成4片,即4096
    image.png

    image.png

    这是问你要把这些哈希槽分配给谁,输入他的id
    image.png

    image.png

    有两种分配方式让你选择

    1. all,表示从其他每个持有slots的master节点都拿一些过来
    2. 手动指定,从某一个/几个节点来移动slots,输入完他们的id,以done结尾

    image.png

    当输入yes才是真正开始搬运

    image.png

    问题:在搬运slots/key的过程中,此时客服端能否访问到redis集群

    搬运key,大部分key是不用搬运的,针对这些未搬运的key,是可以正常访问的.针对这些正在搬运的key,是有可能出现访问出错的情况.

    例如客服端访问key1,集群通过分片算法,得到key1是第一个分片的数据,就会重定向到第一个分片的节点,就可能在重定向过去之后,正好key1被搬走,自然就无法访问.

    5.2 从节点添加到集群中

    将主机号111添加到集群中,作为主机号110的从节点
    :::tips
    redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [172.30.1.110节点的nodeid]
    :::
    image.png
    image.png

  • 相关阅读:
    【毕业设计】深度学习水果识别系统 - python CNN
    AnnotationAwareAspectJAutoProxyCreator aop核心类
    接口测试到底怎么做,5分钟时间看完这篇文章彻底搞清楚
    系统集成项目管理工程师认证高频考点:编制项目范围管理计划
    计算机毕设之基于python+django+mysql的影片数据爬取与数据分析(包含源码+文档+部署教程)
    7年测试工程师经验,浅谈一下如何测试一个web网站?
    Linux之(10)shell基础语法(2)
    初识Dockerfile
    如何将数据库迁移到 Amazon Aurora
    有外媒称,Linux 发行版Ubuntu 23.10也将正式支持树莓派 5
  • 原文地址:https://blog.csdn.net/weixin_61427900/article/details/133139203