文章目录
前言
本文以手动操作redis cluster
集群为例, 讲解并证明redis cluster
的增容和缩容。
redis分三种模式:
- 主从
- 主从 + Sentinel(哨兵)
- cluster
主从模式不是分布式一致性里面的主从,需要手动指定Mater
(在slave
机器上通过slaveof
命令指定)。主负责读写,从负责读。
- 做到了读写分离。
- 主从数据完全一致。
- 主挂了就得手动切换从, 通常互联网网络环境, 得手动切换DNS。
针对主动模式发生宕机,需要手动切换主, Sentinel通过集群监控, 来修复这个问题。
cluster模式, redis 3.0
提供,非一致性hash
(https://www.jianshu.com/p/e968c081f563), 使用slots槽
。
基于slots槽的redis cluster增容
slots基本概念
笔者的理解, 类似于Java的
ConcurrentHash
(JDK1.8-ConcurrentHashMap的 rehash 扩容逻辑 ,
slots
类似于JavaConcurrentHash
里的Node
(仅仅是像,数据结构完全不一样), 迁移的时候逐个slot
迁移, 这样集群的可用性可得到保障。
Redis cluster
使用 slot
存储数据。默认16384
个。
存储的数据, 存储位置在:CRC16(key) mod 16384的值
上, 里面又会有细分数据结构。例如kv
、Hash
等
unsigned int keyHashSlot(char *key, int keylen) {
int s, e; /* start-end indexes of { and } */
for (s = 0; s < keylen; s++)
if (key[s] == '{') break;
if (s == keylen) return crc16(key,keylen) & 0x3FFF;
for (e = s+1; e < keylen; e++)
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF;
return crc16(key+s+1,e-s-1) & 0x3FFF;
}
slot 在redis结点上的位置并不固定
在redis集群中, 默认会分配连续的slot
空间给每个结点。但是这只是默认分配而已, 后续扩容的时候会做调整(例如下文,7台机器的集群, slot大致连续, 但是当slot迁移到新增服务器后, 每台的slot数字就不连续了):
127.0.0.1:7006> cluster slots
1) 1) (integer) 0
2) (integer) 865
3) 1) "127.0.0.1"
2) (integer) 7000
3) "ecee2fce5ddc618ad4e9d1738eb546653f4abd6f"
4) 1) "127.0.0.1"
2) (integer) 7001
3) "4dd154308c6af771857d82de5785d0fedc32a224"
2) 1) (integer) 867
2) (integer) 6666
3) 1) "127.0.0.1"
2) (integer) 7000
3) "ecee2fce5ddc618ad4e9d1738eb546653f4abd6f"
4) 1) "127.0.0.1"
2) (integer) 7001
3) "4dd154308c6af771857d82de5785d0fedc32a224"
3) 1) (integer) 6667
2) (integer) 12306
3) 1) "127.0.0.1"
2) (integer) 7002
3) "08da27b70687df11f480aed5d6e6dc58f0aa819e"
4) 1) "127.0.0.1"
2) (integer) 7003
3) "c04d2e2a661dba2624e0c84c237bd24aa418d1c3"
4) 1) (integer) 12307
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7004
3) "666868d5d51e060de165ec52d412926d1404af5a"
4) 1) "127.0.0.1"
2) (integer) 7005
3) "800552c4c96fd79dbf8bbf5ff37fb4174d4a166b"
5) 1) (integer) 866
2) (integer) 866
3) 1) "127.0.0.1"
2) (integer) 7006
3) "22e6f70ab45662aef226d83c6b4a7b2eabb28ad1"
4) 1) "127.0.0.1"
2) (integer) 7007
3) "62c5b7b83fcd86e7861a32c25d0d87c3998c5440"
slot迁移四步走
redis
扩容设计巧妙, 扩容迁移即为slot
迁移. 只是承担slot
的机器变多了,slot槽
本身结构并无变化
- 针对待迁移的
slot槽
, 原机器设定为导出- 针对待迁移的
slot槽
, 目标机器设定为导入- 执行数据迁移, 此时所有对该
迁移中的slot槽
请求不可用, 转为ask
状态- 周知集群其它结点,
slot槽
被迁移到了新的机器
四步走理解之后就很简单。 相较于一致性hash
算法, 影响面大大降低(单个slot槽
), 平滑程度增加(简单的槽迁移)。
在迁移过程中, 如果访问到正在被迁移中的slot槽
, redis
返回ASK
转向。
集群本身会维护slot槽
对应关系, 每个客户端的实现, 也大多会维护这个映射关系。
搭建redis cluster, 并手动操作增容
下载redis安装
下载在redis官网 redis.io
wget http://download.redis.io/releases/redis-5.0.4.tar.gz
安装make && make install
make -j && make install
它会要求你运行make test
, 可以尝试。
最好将其拷贝到你期望的目录中, 例如我默认喜欢/opt
mv * /opt/redis-5.0.4/
重点的两个文件redis-cli
和 redis-server
在 src
目录下
➜ conf ls /opt/redis-5.0.4/src/redis-cli
/opt/redis-5.0.4/src/redis-cli
➜ conf ls /opt/redis-5.0.4/src/redis-server
/opt/redis-5.0.4/src/redis-server
启动很简单, ./redis-server /opt/redis-5.0.4/redis.conf
(redis.conf默认目录在根目录下)
通过ps -ef | grep redis
可看到进程。
启动多个redis
纯手动操作, 新建一个redis的运行文件夹, 例如名叫
cluster-test
, 在它下面建立三个文件夹:
- conf
- data
- logs
➜ conf ls /opt/redis-5.0.4/cluster-test
conf data logs
进入conf
目录操作, 你需要很多的redis-conf, 例如
你期望启动8个redis进程组成cluster
,
偶数为master
,
基数为slave
,
先启动6台组成cluster
, 三主三从, 然后启动两台作为增容测试。
从端口7000开始:
vim redis-7000.conf
## 指定运行端口
port 7000
daemonize yes
dir "/opt/redis-5.0.4/cluster-test/data"
logfile "/opt/redis-5.0.4/cluster-test/logs/7000.log"
#dbfilename不能配置为路径
dbfilename "dump-7000.rdb"
cluster-enabled yes
cluster-config-file nodes-7000.conf
#是否需要每个节点都可用,集群才算可用,关闭
cluster-require-full-coverage no
然后通过如下命令生成8个配置文件;
sed "s/7000/7001/g" redis-7000.conf > redis-7001.conf
sed "s/7000/7002/g" redis-7000.conf > redis-7002.conf
sed "s/7000/7003/g" redis-7000.conf > redis-7003.conf
sed "s/7000/7004/g" redis-7000.conf > redis-7004.conf
sed "s/7000/7005/g" redis-7000.conf > redis-7005.conf
sed "s/7000/7006/g" redis-7000.conf > redis-7006.conf
sed "s/7000/7007/g" redis-7000.conf > redis-7007.conf
再分别通过每个命令启动服务
./redis-server ../cluster-test/conf/ redis-7000.conf
启动完八个redis之后则可以看到全部redis进程:
组装redis cluster
简单几个命令:
登陆某台机器:
➜ src ./redis-cli -p 7000
127.0.0.1:7000> cluster meet 127.0.0.1 7001
OK
依次对其他7002 7003 7004 7005机器执行该命令即可(暂时不要执行6、7两台, 他们作为扩容用)
此时查询集群状态:
127.0.0.1:7000> cluster info
cluster_state:fail(如果你启动过一次,并且分配了slot, data目录有数据,再次启动集群,此处将是ok)
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:1
cluster_size:0
cluster_current_epoch:0
cluster_my_epoch:0
cluster_stats_messages_sent:0
cluster_stats_messages_received:0
然后配置主从模式执行 cluster nodes
查询id:
127.0.0.1:7000> cluster nodes
62c5b7b83fcd86e7861a32c25d0d87c3998c5440 127.0.0.1:7007@17007 master - 0 1589044372000 7 connected
22e6f70ab45662aef226d83c6b4a7b2eabb28ad1 127.0.0.1:7006@17006 master - 0 1589044373669 5 connected
08da27b70687df11f480aed5d6e6dc58f0aa819e 127.0.0.1:7002@17002 master - 0 1589044374679 2 connected
c04d2e2a661dba2624e0c84c237bd24aa418d1c3 127.0.0.1:7003@17003 master - 0 1589044372664 3 connected
ecee2fce5ddc618ad4e9d1738eb546653f4abd6f 127.0.0.1:7000@17000 myself,master - 0 1589044369000 0 connected
800552c4c96fd79dbf8bbf5ff37fb4174d4a166b 127.0.0.1:7005@17005 master - 0 1589044373000 4 connected
666868d5d51e060de165ec52d412926d1404af5a 127.0.0.1:7004@17004 master - 0 1589044374000 6 connected
4dd154308c6af771857d82de5785d0fedc32a224 127.0.0.1:7001@17001 master - 0 1589044375690 1 connected
然后使用命令设定主从(表示将7001设置为7000的从, 下面的id对应7000的id):
./redis-cli -p 7001 cluster replicate ecee2fce5ddc618ad4e9d1738eb546653f4abd6f
如上操作的效果就是:
你需要给每个redis
进程分配slot
:
/opt/redis-5.0.4/src/redis-cli -p 7000 cluster addslots 0
因为要分配16384
个, 你可以使用脚本来分配vim addslots.sh
:
start=$1
end=$2
port=$3
for slot in `seq ${start} ${end}`
do
/opt/redis-5.0.4/src/redis-cli -p ${port} cluster addslots ${slot}
done
然后执行:
./addslots.sh 0 6666 7000
表示将0~6666
这么多号槽分配给端口为7000
这个进程。
如上, 你的redis cluster搭建完毕, 三主三从,
slot
也分配完毕, 可以接活了:
127.0.0.1:7000> set hello world
OK
127.0.0.1:7000> set hello1 world
(error) MOVED 11613 127.0.0.1:7002
127.0.0.1:7000> set hello2 world
(error) MOVED 7486 127.0.0.1:7002
127.0.0.1:7000> set hello3 world
OK
127.0.0.1:7000> get hello
"world"
127.0.0.1:7000> get hello1
(error) MOVED 11613 127.0.0.1:7002
127.0.0.1:7000>
127.0.0.1:7000> cluster keyslot hello
(integer) 866
127.0.0.1:7000> cluster keyslot hello1
(integer) 11613
扩容实操
目前有
7000~7007
八个redis
进程, 前六个三主三从。 准备加入7006
127.0.0.1:7006> cluster meet 127.0.0.1 7000
OK
然后开始迁移866这个slot
(就是hello
)。
1.在导入机器使用importing
将目标slot
设置为导入状态,id为原进程的id
127.0.0.1:7006> CLUSTER SETSLOT 866 importing ecee2fce5ddc618ad4e9d1738eb546653f4abd6f
OK
2.在导出机器使用migrating
将目标slot
设置为导出状态,id为被导入机器的id
127.0.0.1:7000> CLUSTER SETSLOT 866 migrating 22e6f70ab45662aef226d83c6b4a7b2eabb28ad1
OK
3.在导出机器执行migrate
数据迁移
127.0.0.1:7000> MIGRATE 127.0.0.1 7006 "" 0 1000 keys hello
OK
4.最后,在任意机器执行告知slot
迁移动作
127.0.0.1:7006> CLUSTER SETSLOT 866 node 22e6f70ab45662aef226d83c6b4a7b2eabb28ad1
OK
导出成功, 之后的每个slot
迁移都是这么做的了
缩容
懒得写, 类似增容的反方向。
Redis上的Key的一些概念
缓存雪崩
redis的key在设定的时候,设定的失效时间类似(比如服务刚刚启动, 满载流量请求), 一旦时间到期,大部分key值都发生过期。
导致请求被打到了后端DB, 后端DB扛不住瞬时的高流量,发生宕机。
类似case:
- 批量失效
- redis宕机
- 到redis网络不通
解决方案:
- 设置错开的redis key失效时间;
- 设置限流策略, 保障db安全;
- 设计降级策略, 确保redis以及db宕机时依旧能够提供部分服务, 等到DB和Redis恢复。
缓存穿透
通常出现在程序错误或者遭受共计的时刻。 大量不存在的key请求到redis,进而请求到db
因为db中不存在数据,redis也不会缓存, 导致redis缓存意义缺失;
解决方案:
- 当发现空key的时候,给一个空的默认值。 尽量保证请求在缓存层被拦截;
缓存击穿
热点key问题。 并发大, 热点key失效, 当导致大量请求打到DB, 然后再回写缓存。 此时会存在不必要的性能浪费。
解决方案:
- 热点key统一解决方案
热点key统一解决方案
热点key问题:
- key的请求大量, 导致请求都打到了key对应机器的网卡上, 阻塞其它服务的运行;
- key 一旦失效,可能导致击穿、雪崩
解决:
- 分散热点key, 针对key值,代码自动添加后缀,使其被路由到不同的slot中。 同时, 设定的失效时间也不固定, 甚至永不失效。
- 增加二级缓存,优先从本地例如guava cache里面查询。
排查:
- redis hotkey命令
redis-cli --hotkeys
; - client收集, 然后统计;
- 业务估算