单机Redis存在的问题
Redis单机部署简单方便,只需下载编译,启动服务即可使用。
一个Redis服务就是一个Redis实例,对应于一个进程,Redis6.2版本支持多线程,但多线程只是用于网络和IO,执行命令依然是单线程。
一般情况下,使用一个Redis实例进行测试是足够的。
但在生产环境下,随着业务量增加,单机版Redis逐渐会暴露出以下问题:
- 存储能力局限于单机内存,扩容不便
- Redis实例宕机后无法持续提供服务,达不到高可用
- 所有读写操作单机承受,可能成为并发性能瓶颈
这时候,Redis主从复制的方案应运而生。
主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave), 数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点,且一个主节点可以有零个或多个从节点,但一个从节点只能有一个主节点。
一般主节点负责接收写请求,从节点负责接收读请求,从而实现读写分离。
主从一般部署在不同机器上,复制时存在网络延时问题,可以使用参数repl-disable-tcp-nodelay
配置TCP_NODELAY
,默认为no
:
- no:无论数据大小都会及时同步到从节点,占用带宽较大,适用于主从网络好的场景,如空闲局域网
- yes:主节点每隔指定时间合并数据一次性同步,为TCP包节省带宽,默认为40毫秒同步一次,适用于网络环境复杂或带宽紧张,如跨机房等
主从复制解决的痛点:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量
- 读写分离:主库写、从库读,读写分离不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础
开启主从复制的方式:
- 命令行配置:
# 客户端连接到想要成为从节点的服务
# 设置主机信息,想要取消所有从库设置:replicaof no one,注意旧版本的命令是slaveof
replicaof masterip masterport
# 设置从库为只读,这个是默认的
config set replica-read-only yes
# 查看主从信息
info replication
- 修改配置文件(需要重启服务):
replicaof masterip masterport
注意:
replicaof
是异步的,不会阻塞- 从服务器现有的数据会先被清空,然后才会同步主服务器的数据
应用模式
- 一主一从:最基础的主从复制模型,主节点负责处理写请求,从节点负责处理读请求,主节点使用RDB持久化模式,从节点使用AOF持久化模式
- 一主多从:一个主节点可以有多个从节点,但每个从节点只能有一个主节点。一主多从适用于写少读多的场景,多个从节点可以分担读请求负载,提升并发
- 树状主从:一主多从可以实现读请求的负载均衡,但当从节点数量多的时候,主节点的同步压力也是线性提升的,因此可以使用树状主从来分担主节点的同步压力
缺点
主从复制模式的优点明显,适用于写少读多的情况。
但缺点也比较明显,就是当读多时,主节点就会受到比较大的压力,且主节点宕机后,也可能造成灾难性后果。
针对这个问题,哨兵机制应运而生。
哨兵
哨兵(sentinel),用于对主从结构中的每一台服务器进行监控,当主节点出现故障后通过投票机制来挑选新的主节点,并且将所有的从节点连接到新的主节点上。
哨兵机制的使用,对服务高可用性提供了很大的便利。
作用:
- 监控:监控主从节点运行情况
- 通知:当监控节点出现故障,哨兵之间进行通讯
- 自动故障转移:当监控到主节点宕机后,断开与宕机主节点连接的所有从节点,然后在从节点中选取一个作为主节点,将其他的从节点连接到这个最新的主节点。最后通知客户端最新的服务器地址
哨兵也是一台redis服务器,只是不对外提供任何服务,redis的bin目录下的redis-sentinel其实就是redis-server。
哨兵节点最少三台且必须为单数。这个与其他分布式框架如zookeeper类似,如果是双数,在选举的时候就会出现平票的情况,所以必须是三台及以上的单数。
关于哨兵的配置,可以参考相关资料,这里不再阐述。
原理
哨兵之间会有通讯,哨兵和主从节点之间也有监控,基于这些信息同步和状态监控实现Redis的故障转移:
- 哨兵和哨兵之间以及哨兵和Redis主从节点之间每隔一秒发送ping监控它们的健康状态
- 哨兵向Redis主从节点每隔10秒发送一次info保存节点信息
- 哨兵向Redis主节点每隔2秒发送一次hello,直到哨兵报出sdown,代表主节点失联,然后通知其余哨兵尝试连接该主节点
Redis主节点下线:
- 主观下线(sdown):单独一个哨兵发现master故障了
- 客观下线(odown):半数哨兵都认为master节点故障就会触发故障转移
哨兵Leader选举规则如下:
- 一般情况下当哨兵发现主节点sdown之后 该哨兵节点会成为领导者负责处理主从节点的切换工作
- 哨兵A发现Redis主节点失联
- 哨兵A报出sdown,并通知其他哨兵,发送指令sentinel is-master-down-by-address-port给其余哨兵节点
- 其余哨兵接收到哨兵A的指令后尝试连接Redis主节点,发现主节点确实失联
- 哨兵返回信息给哨兵A,当超过半数的哨兵认为主节点下线后,状态会变成odown
- 最先发现主节点下线的哨兵A会成为哨兵领导者负责这次的主从节点的切换工作
- 哨兵的选举机制是以各哨兵节点接收到发送sentinel is-master-down-by-address-port指令的哨兵id 投票,票数最高的哨兵id会成为本次故障转移工作的哨兵Leader
故障转移:
- 当哨兵发现主节点下线之后经过上面的哨兵选举机制,选举出本次故障转移工作的哨兵节点完成本次主从节点切换的工作
- 哨兵Leader 根据一定规则从各个从节点中选择出一个节点升级为主节点
- 其余从节点修改对应的主节点为新的主节点
- 当原主节点恢复启动的时候,变为新的主节点的从节点
哨兵Leader选择新的主节点遵循下面几个规则:
- 健康度:从节点响应时间快
- 完整性:从节点消费主节点的offset偏移量尽可能的高 ()
- 稳定性:若仍有多个从节点,则根据从节点的创建时间选择最有资历的节点升级为主节点
注意,在哨兵模式下主从节点总是会变更,因此在客户端访问哨兵模式下的Redis时可以使用对应的哨兵接口连接。
哨兵机制在一定程度上解决了单机故障后不易恢复的可用性低的问题,但客户端的读写请求依然是单机承受,不能做到负载均衡。
集群可以解决上述问题。
集群
Redis集群(Redis Cluster)是从 Redis 3.0 开始引入的分布式存储方案。集群由多个节点(Node)组成,Redis 的数据分布在这些节点中。
集群中的节点分为主节点和从节点,只有主节点负责读写请求和集群信息的维护,从节点只进行主节点数据和状态信息的复制。
作用
- 数据分区:突破单机的存储限制,将数据分散到多个不同的节点存储
- 负载均衡:每个主节点都可以处理读写请求,提高了并发能力
- 高可用:集群有着和哨兵模式类似的故障转移能力,提升集群的稳定性
原理
先来说说数据分区。
衡量数据分区方法的标准有两个重要因素:
- 是否均匀分区
- 增减节点对数据分布的影响
由于哈希算法具有随机性,可以保证数据均匀分布,因此Redis集群采用哈希分区的方式对数据进行分区,哈希分区就是对数据的特征值进行哈希,然后根据哈希值决定数据放在哪里。
常用的哈希分区方式:
-
哈希取余
- 计算key的hash值,对节点数量做取余计算,根据结果将数据映射到对应节点
- 但当节点增减时,系统中所有数据都需要重新计算映射关系,将会引发大量数据迁移
-
一致性哈希
- 将hash值区间抽象为一个环形,节点均匀分布在该环形之上
- 然后根据数据的key计算hash值,在该hash值所在的圆环上的位置延顺时针行走找到的第一个节点的位置,该数据就放在该节点之上
- 相比于哈希取余,一致性哈希分区将增减节点的影响限制为相邻节点
- 但当节点数量较少的时候,增删节点对单个节点的影响较大,会造成数据分布不均,导致单个节点承受较大的负载
-
带虚拟节点的一致性哈希
- 在一致性哈希基础之上,引入虚拟节点的概念,虚拟节点被称为槽(slot)。Redis集群中,槽的数量为16384
- 槽介于数据和节点之间,将节点划分为一定数量的槽,每个槽包含哈希值一定范围内的数据。由原来的hash–>node 变为 hash–>slot–>node
- 当增删节点时,该节点所有拥有的槽会被重新分配给其他节点,可以避免在一致性哈希分区中由于某个节点的增删造成数据的严重分布不均。
- 使用的哈希函数:Hash()=CRC16[key]&16383,结果是0-16383,共16384个槽
- 对key进行CRC16计算得到16bit的值,只用了14位,所以最大支持16384个槽
通信机制
在上面的哨兵方案中,节点被分为数据节点和哨兵节点,哨兵节点也是redis服务,但只作为选举监控使用,只有数据节点会存储数据。
而在Redis集群中,所有节点都是数据节点,也都参与集群的状态维护。
在Redis集群中,数据节点提供两个TCP端口,在配置防火墙时需要同时开启下面两类端口:
- 普通端口:即客户端访问端口,如默认的6379
- 集群端口:普通端口号加10000,如6379的集群端口为16379,用于集群节点之间的通讯
集群的节点之间通讯采用Gossip协议,节点根据固定频率(每秒10次)定时任务进行判断,当集群状态发生变化,如增删节点、槽状态变更时,会通过节点间通讯同步集群状态,使集群收敛。
集群间发送的Gossip消息有下面五种消息类型:
- MEET:在节点握手阶段,对新加入的节点发送meet消息,请求新节点加入当前集群,新节点收到消息会回复PONG消息
- PING:节点之间互相发送ping消息,收到消息的会回复pong消息。ping消息内容包含本节点和其他节点的状态信息,以此达到状态同步
- PONG:pong消息包含自身的状态数据,在接收到ping或meet消息时会回复pong消息,也会主动向集群广播pong消息
- FAIL:当一个主节点判断另一个主节点进入fail状态时,会向集群广播这个消息,接收到的节点会保存该消息并对该fail节点做状态判断
- PUBLISH:当节点收到publish命令时,会先执行命令,然后向集群广播publish消息,接收到消息的节点也会执行publish命令
集群配置
Redis5之后就可以直接使用redis-cli建立集群了,本文也使用这种方式。
由于集群中节点至少3个,且每个主节点都要配置从节点,所以一个Redis集群一般至少6个Redis实例,并分别部署在6台服务器上。
这里为了简单说明,所有6个Redis实例都部署在同一台机器上,当部署环境不同时,大部分操作都是一样的。
集群环境
创建目录 redis_cluster
,使用该目录作为Redis集群的实例存放目录,以下称为主目录。
依次在该目录下创建6个目录,分别命名为:redis_7000,redis_7001,redis_7002,redis_7003,redis_7004,redis_7005。后面的数字表示待使用的端口号。
在主目录下载Redis最新稳定版代码,并编译出Redis实例。执行以下脚本即可:
#!/bin/bash
# pkg
sudo apt install -y wget
# get lastest stable version
wget http://download.redis.io/releases/redis-stable.tar.gz
# compile
tar zxf redis-stable.tar.gz
cd redis-stable
make -j4
编译好的实例位于源码目录的src文件夹下。
拷贝 redis-server redis.conf
到redis_7000
目录下,并进入该目录。
按以下方式修改配置文件:
# 通用设置
bind地址修改:注释掉bind行或指定绑定的地址
daemonize yes
port 7000
pidfile /var/run/redis_7000.pid
dir /home/redis_cluster/redis_7000
# 集群相关
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
ok,拷贝 redis-server redis.conf
到其余5个目录下,并分别修改 port 相关的配置为相应的端口号。
完成后,分别进入6个启动Redis实例,如:
cd 7000
../redis-server ./redis.conf
现在,可以看到6个实例已经启动完毕,通过日志可以看到每个节点都有了一个新ID,一切运行正常,
下面一步,创建集群:
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
其中,
create
指定创建一个新集群--cluster-replicas 1
为每个主节点创建一个从节点- 其余的参数为集群中的各个实例
创建过程中会打印相关信息,并由用户输入yes以继续。完成后,会打印以下信息:
[OK] All 16384 slots covered
over,下面可以愉快地使用Redis集群了。
使用集群注意事项
局限
由于Redis集群中数据分布在不同的节点上,因此有些功能会受限:
- db库:单机的Redis默认有16个db数据库,但在集群模式下只有一个db0
- 复制结构:单机的复制结构可以有树状结构,但在集群模式下只允许单层复制结构
- 事务/lua脚本:仅允许操作的key在同一个节点上才可以在集群下使用事务或lua脚本;(使用Hash Tag可以解决)
- key的批量操作:如mget,mset操作,只有当操作的key都在同一个节点上才可以执行;(使用Hash Tag可以解决)
- keys/flushall:只会在该节点之上进行操作,不会对集群的其他节点进行操作
- 号称能够支持1k个节点,但你最好不要这么做。当节点数量增加到10,就能够感受到集群的一些抖动
- 一定要避免产生热点,如果流量全部打到了某个节点,后果一般很严重
- 大key不要放redis,它会产生大量的慢查询,影响正常的查询
- 如果你不是作为存储,缓存一定要设置过期时间
- 大流量,不要开aof,开rdb即可
- redis cluster的操作,少用pipeline,少用multi-key,它们会产生大量不可预料的结果
Hash Tag:
- hash tag:当key包含{}的时候,不会对整个key做hash,只会对{}包含的部分做hash然后分配槽slot
- 可以让不同的key在同一个槽内,这样就可以解决key的批量操作和事务及lua脚本的限制了
- 但由于hash tag会将不同的key分配在相同的slot中,如果使用不当,会造成数据分布不均的情况
如:
参数优化
-
cluster_node_timeout:默认值为15s
- 影响ping消息接收节点的选择,值越大对延迟容忍度越高,选择的接收节点就越少,可以降低带宽,但会影响收敛速度。应该根据带宽情况和实际要求具体调整
- 影响故障转移的判定,值越大越不容易误判,但完成转移所消耗的时间就越长。应根据网络情况和实际要求具体调整
-
cluster-require-full-coverage
- 为了保证集群的完整性,只有当16384个槽slot全部分配完毕,集群才可以上线
- 若主节点发生故障且故障转移还未完成时,原主节点的槽不在任何节点中,集群会处于下线状态,影响客户端的使用
- no: 表示当槽没有完全分配时,集群仍然可以上线
- yes: 默认配置,只有槽完全分配,集群才可以上线
小结
本文阐述了Redis从单机、主从、哨兵到集群的变迁,着重解决了以下问题:
- 单机内存局限
- 单机故障数据丢失
- 单机故障恢复困难
- 负载均衡
- 高可用性
在实际使用中,应结合业务具体需求选择合适的部署方式。
注意,部署模式越复杂,相应的性能也会有一定的影响,要做好之间的平衡。
很多时候,简单的就是最好的,追求复杂的反而得不偿失。
参考资料
Redis cluster tutorial
Redis Cluster Specification
Redis6.0主从、哨兵、集群搭建和原理