集群的概念
广义的集群:多个机器,构成了分布式系统,都可以称作为集群
狭义的集群:redis提供的集群模式,这个集群模式下,主要是解决存储空间不足的问题(拓展存储空间)
哨兵模式提高系统的可用性,哨兵模式中,本质上还是redis主从节点存储数据,其中就要求一个主节点/从节点,存储整个数据的全集,引入多台机器,每台机器存储一部分数据。
设有1TB数据需要存储,
拿两台机器来存储,每个机器只需要存储512GB即可
拿四台机器来存储,每个机器只需要存储256GB即可
把数据分成多分,如何分?
三种主流分片方式
1.哈希求余
借鉴了哈希表的基本思想,借助hash函数,把一个key映射到整数,再针对数组的长度求余,就可以得到一个数组下标
三个分片,编号0,1,2
此时就可以针对要插入的数据的key(redis都是键值对结构的数据)计算hash值(比如,使用md5)再把hash值余上分片个数,就得到了一个下标,此时就可以把这个数据放到该下标对应的分片中了 hash(key)%N==0 此时这个key存储在0号分片中,后续查询key的时候,也是同样的算法,key是一样,hash函数是一样的,得到的分片是值就是一样的。
分片主要目的:提高存储能力,分片越多,能存储的数据越多,成本也更高。一旦服务器集群需要扩容,就需要更高的成本了。一般是少搞几个先,随着业务的逐渐成长,数据变多了,3个分片不够了,需要引入新的分片N就改变了。
当hash函数和key都不变的情况下,如果N改变了,整体的分片结果都会改变。
搬运量是巨大的,上述扩容,开销是极大的,往往不能再生产环境上操作,只能通过替换的方式来扩容。(原来三组不变,找其他机器构成这四个,然后生产环境导入到这些机器里面)这也会找成依赖的机器更多,成本更高。
一致性哈希算法:
为了降低上述的搬运开销,能够更高效扩容,key映射到分片的序号不是简单的求余,而是改成以下过程,插入三号分片,现在属于把原先哈希的交替的(要一直算余数,这个是连在一起的,连续出现,这样搬运成本就会小了)
方案三:哈希槽分区算法
redis真正采用的分区算法:
hash_slot=crc16(key)%16384 相当于是16*1024(用来计算hash值的算法)
会进一步把上述这些哈希槽,分配到不同的分片上。 虽然不是严格意义上的均匀,但是此时三个分片上的数据就是比较均匀的了,这里只是一种可能的分片方式,实际上分片十分灵活,每个分片持有槽位号,可以连续,也可以不连续
0号分片[0,5461]共5462个槽位
1号分片[5463,10923]共5462个槽位=
2号分片[10924,16383]共5460个槽位
此处,每个分片都会使用位图这样的数据结构,表示出当前有多少槽位号16384个bit位,用每一个0/1来区分自己这个分片当前是否持有该槽位号 2048->2kb
0号分片[0,4095]
1号[5462 9557]
2号[10924,15019]
3号[4096,5461]+[9558,10923]+[15019,16383]共4096个槽位
针对某个分片,上面的槽位号,不一定非得是连续区间
redis,当前某个分片包含哪些个槽位,都是可以手动配置的
问题一
Redis集群是最多有16384个分片吗
每个分片上就只有一个槽位,如果每个分片包含的槽位比较多,如果槽位个数相当,就可以认为包含的key数量相当
如果每个分片包含的槽位非常少,槽位个数不一定能直观的反映到key的数目
实际上Redis作者建议分片最多不超过1000,
问题二:为什么是16384个槽位
这个是因为使用位图这样的数据结构表示的,16384(16k,个数基本够用)个slots,需要的是2kb大小,如果给定的slots数量更多了,则需要消耗更多的空间,假如65536 那么他就是(8kb) 心跳包是周期性通信的(非常频繁,吃网络带宽)
基于redis搭建出一个redis集群
注意:docker compose必须要在对应的目录下,才能够操作
配置文件期间,注意要把docker(运行的redis和哨兵)停掉
在linux以.sh为后缀,成为shell脚本,使用命令操作,非常适合吧命令写到一个文件中,批量化执行,还可以加入,条件,循环,函数等机制.
需要创建11个redis节点,这些redis配置文件大同小异,此时可以用脚本来批量生产。(不用脚步就一个一个改),注意这里的ip地址,要去看根据端口号,101,102,103···109
for port in $(seq 1 9); \ do \ mkdir -p redis$(port}/ touch redis${port}/redis.conf 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 for port in $(seq 10 11); \ do \ mkdir -p redis$(port}/ touch redis${port}/redis.conf 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
/是续行符,把下一行的内容和当前行,合并成一行,shell默认情况是所有代码写到一行里面的,使用续行符号来进行换行
对于shell中,用for都是使用do/done开始和结束的,预期效果,得到11个目录,每个目录里都有一个配置文件,配置文件中,ip地址各不相同
cluster-enabled yes. 开启集群
cluster-config-file nodes.conf. 需要多个节点之间进行交互,保持联络
cluster-node-timeout 5000 5s之内没有心跳包,则说明该节点出现问题
cluster-announce-ip 172.30.0.10${port}:该redis节点,自己所在的主机的ip地址
cluster-announce-port 6379 port是redis节点自身绑定的端口(容器内的端口)不同的容器内部可以有相同的端口,后续进行端口映射,再把这些容器内的端口映射到容器外的不同端口即可
cluster-announce-bus-port 16379
业务端口:用来进行业务数据通信的 ->响应redis客户端请求的
管理端口:为了完成一些管理上的任务来进行通信的 ->如果某个分片中的redis主节点挂了,就要让从节点成为主接待您,就需要通过刚才管理端口来完成对应操作
一个服务器,可以绑定多个端口号
for port in $(seq 1 9); \ do \ mkdir -p redis${port}/ touch redis${port}/redis.conf 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 for port in $(seq 10 11); \ do \ mkdir -p redis${port}/ touch redis${port}/redis.conf 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 ~ ~ ~ ~
我们可以看到下面是上述脚本的执行结果
此处为了后续创建静态ip,此时要先手动创建出网络,同时给这个网段也分配ip
网络原理-IP协议-IP地址规则
IP地址=网络号+主机号
使用子网掩码的方式来区分主机号和网络号
使用ifconfig查看网段,每个人主机上已有的网段,具体不一定一样
/24的意思是子网掩码,左边24是1,右边8位是0
相当于255.255.0 172.30.0网络号 ip是内网ip,
什么是内网ip,10*,172.16-172.31.*,192.168.*
注意,配置静态ip网络号部分要和前面的网段保持一致,主机号部分,可以随便配置(1-255,保证不重复)
必须要保证都关闭之后,再去启动集群
version: '3.7' 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 163,10
列出每个参与构建集群的ip和端口,端口都是写容器内部的端口号
描述集群的每个节点,应该有2个从节点
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 2
可以看到,敲出来之后弹出一堆信息
先去生成每个redis节点的配置文件
使用docker创建出11个节点,并且启动容器
使用redis-cli执行创造集群命令
从101到109九个节点,现在是一个整体,使用客户端连接任意一个节点,本质上都是等价的
cluseter nodes查看当前集群的信息
使用集群来存储,前面使用的redis基础命令大部分适用的
我们发现这个会错误,发现这个是来自于103的分片,所以我们在启动redis-cli的时候,加上-c的选项,可以根据当前key实际算出来的槽位号,自动找到匹配的分片主机,进一步就可以完成操作。
此时还是计算k1,属于103,然后后面这块给他重定向,重新转发给103这个节点(当然这个109也是从节点,不可以处理写的请求,103是主节点,能处理写的请求)
集群故障处理
使用集群之后,之前的命令大部分能用,但是有些指令是操作多个key,此时在不同分片上,是无法这么操作
当然也有特殊的方式,hash tag
如果集群中,有节点挂了,如果挂的是从节点,没事,如果是主节点?,主节点才能处理写操作,从节点不可以写
101挂了之后,我们发现105当上主节点了
集群机制,也能处理故障转移,此处故障转移和哨兵这里不同
1.故障判定
识别出某个节点是否挂了
1.节点A给B发送ping包,B给A返回一个pong包,ping和pong除了message type属性之外,其他部分都是一样的,这里包含了集群的配置信息(该节点的id,该节点属于哪个分片,是主节点还是从节点,从属于谁,持有哪些slots的位图)
2.每个节点每秒钟,都会给一些随机节点发送ping,而不是全发一遍,这样设定是为了避免在节点很多的时候,心跳包也非常大多
3.当节点给B发ping包,B不能如期回应的时候,此时A尝试重置B的tcp连接,看是否能够连接成功,如果仍然连接是比啊,A会给B设置为PFAIL状态(相当于主管下线)
4.A判定B为PFAIL之后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态(每个节点都会维护自己的下线列表,由于视角不同,每个节点的下线列表也不一定相同)
5.此时A发现其他很多节点,也认为B为PFAIL,并且数目超过总集群个数的一半,那么A就会把B标记成FAIL(相当于客观下线),并且把这个消息同步给其他节点(其他节点收到后,也把B标记成FAIL)
2.故障迁移
如果B是从节点,那么不需要进行故障迁移
如果B是主节点,那么会由B的从节点(如C,D)触发故障迁移了,类似于哨兵,
具体流程如下:
1.从节点判定自己是否有参选资格,如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太大了),时间超过阈值,就是去竞选资格
2.具有资格的节点,如果C和D,就会先休眠一定时间,休眠时间=500ms基础时间+[0-500ms]随机时间+排名*1000ms ,offset的值越大,则排名越靠前(越小)->(offset越大,数据越接近主节点,排名越要靠前,休眠时间就更短)
3.比如c的休眠时间到了,C就会给其他所有集群中的节点,进行拉票操作,但是只有主节点才有投票资格
4.主节点就会把自己的票投给C(每个主节点只有1票),当C收到的票数超过主节点数目的一半,C就会晋升成主节点(C自己负责slaveof no one,并且让D执行slaveof C)
5.同时,C还会把自己成为主节点的消息,同步给其他集群的节点,大家也都会更新自己保存的集群结构信息
谁休眠时间短,大概率就是新的主节点了,
以下三种情况会出现集群宕机:
1.某个分片,所有的主节点和从节点全挂了-该分片无法提供数据服务了
2.某个分片主节点挂了,但是没有从节点
3.超过半数的master节点都挂了 ->此时master挂了,但是后面还有slave做补充,如果突然一系列的master挂了,说明集群遇到了非常严重的现象,此时就得赶紧停下来,检查检查是不是有啥问题。
如果集群中有个节点挂了,无论什么节点,程序员都要尽快处理好(最晚第二天,上班之前,处理好)
集群扩容:
101-109 9个主机构成三主,六从,以110和111页加入到集群中
把数据分片从3->4. 集群扩容操作,是风险较高,成本很大的操作。
1.新的主节点110加入到集群中
前面的表示被新增的节点是啥 后面是集群中的任意一个节点,表示要把新节点加入到哪个集群中
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
我们添加过后可以看到,110这个小可怜一个也没有跟他连接到,
2.重新分配slots
把之前三组master上面的slots拎出来一些,分配给新的master,重新切分他会让你手动,写重新切分多少个,此时会先打印当前集群每个机器的情况,并且要求用户输入要移动多少个redis
预期是1/4,16384 ->4096,会问你哪个节点接收
输入要从哪些节点移动redis
1)all,表示从其他每个持有slots的master都拿一点
2)手动指定,从某个或者某几个节点来移动slots(以done为结尾)
输入all之后,会给出搬运的计划(还没真正搬运)当程序员输入yes之后,搬运才真开始,不仅是slots重新划分,也会把slots上对应的数据,也搬运到新的主机上
比较重量的操作!,谨慎!
此时再去查看,发现110,持有三个区间,一人薅一点。
如果搬运slots/key的过程中,此时客户端能否访问我们redis集群呢?
搬运key,大部分的key不用搬运的,针对这些未搬运的key,此时可以正常访问的,针对这些正在搬运中的key,是有可能出现访问出错的情况。
假设客户端访问k1,集群通过分片算法,得到k1是第一个分片的数据,就会重定向到第一个分片的节点,就有可能在重定向过去之后,正好k1被搬走了,自然也无法访问
因此不可以随意扩容,一般都是晚上(很明显,更高可用,搞多机器,多集群,倒入数据)
3.把从节点添加到集群中
111添加进来,作为110的从节点
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-slave --cluster-master-id [xx的nodeId]
注意是在客户端的外面操作
小结:
1)集群是什么,解决了什么问题?
2)数据分片算法[重点/考点]
a)哈希求余
b)一致性哈希算法
c)哈希槽分区算法(redis)
3)搭建redis集群
4)集群容灾,故障转移
5)集群扩容
6)代码的连接集群
使用的库,得能支持集群模式