引入
单点复制的问题
单机故障
- 如果发生机器故障,例如磁盘损坏,主板损坏等,未能在短时间内修复好,客户端将无法连接redis。
当然如果仅仅是redis节点挂掉了,可以进行问题排查然后重启,姑且不考虑这段时间对外服务的可用性,那还是可以接受的。 - 而发生机器故障,基本是无济于事。除非把redis迁移到另一台机器上,并且还要考虑数据同步的问题。
容量瓶颈
- 假如一台机器是16G内存,redis使用了12G内存,而其他应用还需要使用内存,假设我们总共需要60G内存要如何去做呢,是否有必要购买64G内存的机器?
QPS瓶颈
- redis官方数据显示可以达到10w的QPS,如果业务需要100w的QPS怎么去做呢?
- 关于容量瓶颈和QPS瓶颈是redis分布式需要解决的问题,而机器故障就是高可用的问题了。
解决方法
在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器(主从模式),满足故障恢复和负载均衡的需求。
- 主(Master)和从(Slave)分别部署在不同的服务器上,当主节点服务器写入数据时,同时也会将数据同步到从节点服务器。
- 通常情况下,主节点负责写入数据,从节点负责读出数据
redis也是如此,它为我们提供了复制功能,实现了相同数据的多个redis副本。(复制功能是高可用redis的基础,哨兵和集群都是在复制的基础上实现高可用的)
但是这样会有如下问题,多个副本之间的数据如何保持一致呢?数据读写操作可以发送给所有实例呢?
实际上,Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式。
- 读操作:主库、从库都可以接收;
- 写操作:首先到主库执行,然后,主库将写操作同步给从库。
即:
- redis主机会一直将自己的数据复制给redis从机,从而实现主从同步
- 在这个过程中,只有master主机可以执行写命令,其他slave从机只能执行读命令
- 这种读写分离模式可以大大减轻redis主机的数据读取压力,从而提高了redis的效率,并同时提供了多个数据备份
- 主从模式是搭建Redis Cluster 集群最简单的一种方式
配置
- 参与复制的redis实例可以划分为主节点(master)和从节点(slave)
- 默认情况下,redis都是主节点。
127.0.0.1:6379> info replication
# Replication
role:master --默认情况下每个节点都是master节点
connected_slaves:0
master_replid:b4c7335839af0228e8f3318baf68c7a38c29c5f1
master_replid2:93e727752d0ea5557a89dfb2bf6d78dfe6c437c2
master_repl_offset:1050
second_repl_offset:1051
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:407
repl_backlog_histlen:644
- 每个从节点只能有一个主节点,而主节点可以同时有多个从节点
- 复制的数据流是单向的,只能由主节点复制到从节点
建立复制
配置复制的方式有如下三种
- 在配置文件中加入slaveof {masterHost} {masterPort} 随Redis启动生效
# 配置主节点的IP和端口号
slaveof 127.0.0.1 8000
# 从节点只做读的操作,保证主从数据的一致性
slave-read-only yes
- 在redis-server启动命令后加入–slaveof {masterHost} {masterPort}生效。
- 直接使用命令: slaveof {masterHost} {masterPort}生效。
综上所述, slaveof命令在使用时, 可以运行期动态配置, 也可以提前写到配置文件中。
- 例如本地启动两个端口为6379和6380的Redis节点, 在127.0.0.1:6380执行如下命令:
127.0.0.1:6380>slaveof 127.0.0.1 6379
- slaveof配置都是在从节点发起, 这时6379作为主节点, 6380作为从节点。 复制关系建立后执行如下命令测试:
127.0.0.1:6379>set hello redis
OK
127.0.0.1:6379>get hello
"redis"
127.0.0.1:6380>get hello
"redis"
从运行结果中看到复制已经工作了, 针对主节点6379的任何修改都可以同步到从节点6380中, 复制过程如图所示。
slaveof本身是异步命令,执行slave命令时,节点只保存主节点信息后返回,后继复制流程在节点内部异步执行。主从节点复制成功建立后,可以使用info replication查看复制相关状态
- 主节点6379复制状态信息:
[root@localhost 6379]# export PATH=$PATH:/usr/local/redis/bin/
[root@localhost 6379]# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=1260,lag=1
....
- 从节点6380复制状态信息:
[root@localhost ~]# export PATH=$PATH:/usr/local/redis/bin/
[root@localhost ~]# redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
...
断开复制
slaveof命令不但可以建立负责,还可以在从节点执行slaveof no one
来断开和主节点的复制关系。比如在6380节点上执行断开复制:
断开复制的主要流程:
- 断开与主节点复制关系
- 从节点晋升为主节点
从节点断开复制之后并不会抛弃原有数据,只是无法再获取从节点上的数据变化。
通过slaveof {newMasterIp} {newMasterPort}
还可以实现切主操作,也就是把当前从节点对主节点的复制切换到另一个主节点
从节点通过slave of切换新的主节点,切主操作流程如下:
- 断开与旧主节点复制关系
- 与新主节点建立复制关系
- 删除从节点当前所有数据
- 对新主节点进行复制操作
从节点slaveof no one之后再重新的主节点建立复制关系,从节点日志如下:
21609:S 08 May 2020 08:37:59.236 * Full resync from master: dbddc80fe41195ca35706a8a7bb396cde1bf1753:2548
21609:S 08 May 2020 08:37:59.237 * Discarding previously cached master state.
21609:S 08 May 2020 08:37:59.290 * MASTER <-> REPLICA sync: receiving 204 bytes from master
21609:S 08 May 2020 08:37:59.290 * MASTER <-> REPLICA sync: Flushing old data -- 删除从节点当前所有数据
运维提示:切主后从节点会清空之前所有的数据, 线上人工操作时小心slaveof在错误的节点上执行或者指向错误的主节点。
安全性
- 对于数据比较重要的节点,主节点会通过设置
requirepass参数进行密码验证
,这时所有的客户端访问必须使用auth命令进行校验
- 从节点与主节点的复制连接是通过一个特殊标识的客户端来完成的,因此需要配置从节点的masterauth参数与主节点密码保持一致。这样从节点才可以正确连接到主节点并发起复制流程
只读
- 默认情况下,从节点使用
slave-read-only=yes
配置为只读模式。 - 由于配置只能从主节点到从节点,对于从节点的任何修改,主节点都无法感知,修改从节点会造成主从数据不一致。建议线上不需修改从节点的只读模式
127.0.0.1:6379> set key hellow
(error) READONLY You can't write against a read only replica.
网络延迟
主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题。redis为我们提供了repl-disable-tcp-nodelay
参数用于控制是否关闭TCP_NODELAY
,默认关闭,说明如下:
- 当关闭时,主节点产生的命令数据无论大小都会及时的发送给从节点,这样主从之间的延迟会变小,但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景, 如同机架或同机房部署。
- 当开启时,主节点会合并较小的TCP数据报从而节省网络带宽。默认发送时间间隔取决于Linux内核,一般默认为40ms,这种配置节省了带宽但是增大了主从之间的延迟。适用于主从主从网络环境复杂或带宽紧张的场景, 如跨机房部署
部署主从节点时需要考虑网络延迟、 带宽使用率、 防灾级别等因素, 如要求低延迟时, 建议同机架或同机房部署并关闭repl-disable-tcp-nodelay。如果考虑高容灾性, 可以同城跨机房部署并开启repl-disable-tcp-nodelay。
实践
使用命令实现
方法1
- 使用命令在服务端搭建主从模式,其语法格式如下:
redis-server --port <slave-port> --slaveof <master-ip> <master-port>
- 执行以下命令:
#开启开启一个port为6300的从机,它依赖的主机port=6379
> redis-server --port 6300 --slaveof 127.0.0.1 6379
- 接下来开启客户端,并执行查询命令,如下所示:
> redis-cli -p 6300
127.0.0.1:6300> get name
"jack"
127.0.0.1:6300> get website
"www.biancheng.net"
#不能执行写命令
127.0.0.1:6300> set myname BangDe
(error) READONLY You can't write against a read only slave.
127.0.0.1:6300> keys *
1) "myset:__rand_int__"
2) "ID"
3) "title"
4) "course2"
从上述命令可以看出,port =6300 的主机,完全备份了主机的数据,它可以执行查询命令,但不能执行写入命令
从机服务端提示如下:
[18160] 20 Jan 17:40:34.101 # Server initialized #服务初始化
[18160] 20 Jan 17:40:34.108 * Ready to accept connections #准备连接
[18160] 20 Jan 17:40:34.108 * Connecting to MASTER 127.0.0.1:6379 #连接到主服务器
[18160] 20 Jan 17:40:34.109 * MASTER <-> REPLICA sync started #启动副本同步
[18160] 20 Jan 17:40:34.110 * Non blocking connect for SYNC fired the event.#自动触发SYNC命令,请求同步数据
[18160] 20 Jan 17:40:34.110 * Master replied to PING, replication can continue...
[18160] 20 Jan 17:40:34.112 * Partial resynchronization not possible (no cached master)
[18160] 20 Jan 17:40:34.431 * Full resync from master: 6eb220706f73107990c2b886dbc2c12a8d0d9d05:0
[18160] 20 Jan 17:40:34.857 * MASTER <-> REPLICA sync: receiving 6916 bytes from master #从主机接受了数据,并将其存在于磁盘
[18160] 20 Jan 17:40:34.874 * MASTER <-> REPLICA sync: Flushing old data #清空原有数据
[18160] 20 Jan 17:40:34.874 * MASTER <-> REPLICA sync: Loading DB in memory #将磁盘中数据载入内存
[18160] 20 Jan 17:40:34.879 * MASTER <-> REPLICA sync: Finished with success #同步数据完成
可以看出主从模式下,数据的同步是自动完成的,这个数据同步的过程,又称为全量复制
方法2
- 启动一个服务端,并指定端口号
#指定端口号为63001,不要关闭
redis-server --port 63001
- 打开一个客户端,连接服务器,如下所示:
# 连接port=63001的服务器
>redis-cli -p 63001
// 现在处于主机模式下,所以运行读写数据
127.0.0.1:63001> keys *
1) "name"
127.0.0.1:63001> get name
"aaaa"
127.0.0.1:63001> set name "bbbb"
OK
127.0.0.1:63001> get name
"bbbb"
127.0.0.1:63001>
# 将当前服务器设置为从服务器,从属于6379
127.0.0.1:63001> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:63001> keys *
1) "master_6379" # 现在它的数据和主服务器的数据一样了
#写入命令执行失败
127.0.0.1:63001> SET mywebsite www.biancheng.net
(error) READONLY You can't write against a read only replica.
#再次切换为主机模式,执行下面命令
127.0.0.1:63001> SLAVEOF no one
OK
127.0.0.1:63001> SLAVEOF no one
OK
127.0.0.1:63001> keys *
1) "master_6379" # 数据还是和之前主服务器的一样
127.0.0.1:63001> SET mywebsite www.biancheng.net # 但是现在可以写入命令了
OK
127.0.0.1:63001> keys *
1) "mywebsite"
2) "master_6379"
修改配置文件实现
每个 Redis 服务器都有一个与其对应的配置文件,通过修改该配置文件也可以实现主从模式。
- 新建 redis_6302.conf 文件,并添加以下配置信息:
slaveof 127.0.0.1 6379 #指定主机的ip与port
port 6302 #指定从机的端口
- 启动 Redis 服务器,执行以下命令:
$ redis-server redis_6302.conf
- 客户端连接服务器,并进行简单测试。执行以下命令:
$ redis-cli -p 6302
127.0.0.1:6300> HSET user:username biangcheng
#写入失败
(error) READONLY You can't write against a read only slave.
对比:
- 通过命令搭建主从模式,简单又快捷,所以不建议使用修改配置文件的方法。
主从拓扑结构
Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为三种:一主一从、一主多从、树状主从结构。
一主一从
- 一主一从是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持
- 当应用写命令并发量较高而且需要持久化时,可以只在从节点开启AOF,这样即保证了数据安全性同时也避免了持久化对主节点的性能干扰
- 但需要注意的是, 当主节点关闭持久化功能时,如果主节点脱机要避免自动重启操作。 因为主节点之前没有开启持久化功能自动重启后数据集为空, 这时从节点如果继续复制主节点会导致从节点数据也被清空的情况, 丧失了持久化的意义。 安全的做法是在从节点上执行slaveof no one断开与主节点的复制关系, 再重启主节点从而避免这一问题。
一主多从
- 一主多从结构(又称为星形拓扑结构) 使得应用端可以利用多个从节点实现读写分离(见图) 。 对于读占比较大的场景, 可以把读命令发送到从节点来分担主节点压力。
- 同时在日常开发中如果需要执行一些比较耗时的读命令, 如: keys、 sort等, 可以在其中一台从节点上执行, 防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。
- 对于写并发量较高的场景, 多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽, 同时也加重了主节点的负载影响服务稳定性。
树状主从结构
- 树状主从结构(又称为树状拓扑结构) 使得从节点不但可以复制主节点数据, 同时可以作为其他从节点的主节点继续向下层复制
- 通过引入复制中间层, 可以有效降低主节点负载和需要传送给从节点的数据量。
- 如图所示, 数据写入节点A后会同步到B和C节点, B节点再把数据同步到D和E节点, 数据实现了一层一层的向下复制。
- 当主节点需要挂载多个从节点时为了避免对主节点的性能干扰, 可以采用树状主从结构降低主节点压力。
主从模式不足
主从模式并不完美,它也存在许多不足之处,下面做了简单地总结:
- redis主从模式不具备自动容错和恢复功能,如果主节点宕机,redis集群将无法工作,此时需要人为干预,将从节点提升为主节点
- 如果主机宕机前有一部分数据未能及时同步到从机,及时切换主机后也会造成数据不一致的问题,从而降低了系统的可用性
- 因为只有一个主节点,所以其写入能力和存储能力都受到一定的限制
- 在进行数据全量同步时,如果同步的数据量较大可能会造成卡顿的现象
虽然主从模式存在上述不足,但他仍然是实现分布式集群的基础。Sentinel哨兵模式同样是依赖于主从模式实现