Redis集群 — 主从复制

Redis集群 — 主从复制

主从复制

一个主节点(master),一个或多个从节点(slave)。

从节点充当主节点的“备份”,专门做持久化,用来缓解主节点服务线上请求的同时还需要进行数据持久化的压力。

从节点可以用来做负载均衡,处理读请求,分担主节点读压力(写请求必须由主节点处理)。

CAP理论

CAP原理是分布式应用存储的理论基石,该理论提出三个特性。在分布式系统实践中,不可能全部满足这三个特性,P 是无法避免的,CA 只能满足一个。

  • C:Consistent 一致性
  • A:Availability 可用性
  • P:Partition tolerance 分区容忍性

分区容忍性

分布式系统的节点分布在不同的机器上进行网络隔离开的,这就必然会出现节点之间网络断开的风险,这种场景叫做 网络分区,这是 分布式系统无法避免 的。

一致性

当网络分区发生时,两个分布式节点之间无法互相通信,一个节点对数据进行的修改无法同步到另一个节点,所以数据的 一致性 无法满足。除非我们牺牲 可用性,即暂停分布式节点服务,使其不提供数据修改功能来满足一致性。

可用性

满足了一致性,可用性就无法满足。满足可用性,就得接受两个节点之间数据的暂时不一致性。

最终一致性

Redis 的主从同步是异步进行的,这就意味着 Redis 是不满足「一致性」的 。当客户端在 Redis 的主节点修改数据后,即使网络断开,主节点依旧可以正常对外提供数据修改服务,所以 Redis 满足「可用性」。

最终一致性:牺牲数据的暂时不一致性来满足可用性,后续慢慢同步数据,达到数据最终一致。

Redis 满足最终一致性,从节点会尽量和主节点保持一致,网络断开后,主从节点的数据出现大量不一致。当网络恢复后,从节点会使用多种策略同步主节点的数据保持最终的一致。

增量同步

增量同步类似于持久化机制的 AOF,同步的是指令流。主节点将对自己数据的修改操作指令记录到 内存的复制 buffer 中,再异步的将 buffer 中的指令流发送给从节点,从节点一边执行 buffer 中的指令,一边给主节点反馈同步到了哪里(偏移量)。

由于内存 buffer 是有限的,不可能将所有的指令都放在 buffer 里。redis 的内存 buffer 是一个 环形数组 结构,当数组满了,后面新添加的指令就会覆盖掉前面的。

当主从节点网络断开时,主节点中那些还没有被同步的 buffer 中的指令有可能被后续的指令覆盖掉了,从节点无法通过指令流完全同步,这时需要使用更复杂的机制 — 快照同步。

快照同步

快照同步是很耗时的操作,首先主节点对数据生成一个RDB快照,然后将快照通过网络发送给从节点,从节点接收完快照进行全量加载,加载前会清除所有数据,加载完毕通知主节点继续进行增量同步。

整个快照同步过程中,主节点在继续提供服务,主节点的复制 buffer 在不停添加,很有可能由于快照同步时间过长或者 buffer 容量太小导致 buffer 中的指令流被覆盖。指令流被覆盖会导致快照同步完成后无法进行增量备份,然后会再次发起快照同步,这样很有可能会陷入快照同步死循环

所以在生产环境中,需要合理配置主节点内存的复制 buffer 大小,避免造成快照同步死循环。

无盘复制

快照同步非常耗 IO 的,会对系统的负载造成很大影响,特别是当 Redis 在进行 AOF 的 fsync 操作时(强制将内存缓冲区的指令写入到磁盘),快照同步会导致 fsync 被推迟,这将严重影响主节点的服务效率。

在 Redis2.8.18 之后支持 无盘复制,即主节点不生成快照数据,而是一边遍历内存,一边将数据通过网络套接字发送给从节点。从节点将接收到的数据存到磁盘,再进行一次性加载。

wait 指令

Redis 的复制是异步进行的,wait 指令可以使异步变同步,保证节点之间的一致性(不严格)。

wait numreplicas timeout

  • numreplicas: 节点的数量
  • timeout: 等待节点数据同步的时间(ms)

执行 wait 指令,线程进入阻塞,直至 wait 之前的所有修改指令在指定的节点数之间被同步。

1
2
3
4
5
set key a 1
set key b 2
......

wait 2 0 # 陷入阻塞

假设出现网络分区,wait 的 timeout 设为 0,主从节点的数据无法得到同步,此时会永远潜入阻塞,Redis 服务器将会 丧失可用性

实战:搭建主从环境

1⃣️启动三个实例

note1: Redis Docker 默认是没有 redis.conf 的,如有必要,需要手动挂载 redis.conf。
note2: 默认情况下容器会加入到基于 brige 网络驱动的名为 bridge 的默认网络环境。如果主从节点不在同一个网络环境,将不能互联。

1
2
3
4
# 主节点
docker run -d -p 63791:6379 --name redis-master \
-v ~/Learning/redis-learning/redis-master-slave/data:/data \
redis
1
2
3
# 从节点1,从节点做持久化。
docker run -d -p 63792:6379 --name redis-slave1 \
redis redis-server --appendonly yes
1
2
# 从节点2
docker run -d -p 63793:6379 --name redis-slave2 redis

docker启动redis

三个节点预览

note: 刚创建之后,三个节点都是主节点,除非从节点在启动时指定了主节点。

2⃣️建立复制

建立主从复制有三种方式:

  1. 在配置文件中加入 slaveof{masterHost}{masterPort} 随 Redis 启动生效。
  2. 在 redis-server 启动命令后加入 --slaveof{masterHost}{masterPort} 生效。
  3. 直接使用命令 slaveof {masterHost} {masterPort} 生效

slaveof 的配置都是在从节点发起的,从节点连接上主节点后能异步获取到主节点的所有 keys,从节点只读不可写。使用 info replication 指令可以查看复制相关的状态。

主从复制后

3⃣️断开复制

slaveof no one 指令断开复制

断开复制后,从节点晋升为主节点,从节点的数据依然存在,只是无法继续获取旧的主节点数据变化。

4⃣️切换主节点

slaveof {newMasterHost} {newMasterPort}

slaveof 指令同样还可以切换新主节点,只需要指定新的主节点 ip 和 port 即可,切主流程如下:

  1. 断开与旧主节点的复制关系
  2. 与新主节点建立连接
  3. 删除当前从节点所有数据
  4. 异步复制新主节点的所有数据

note: 切主操作会删除当前节点的所有数据,所以需要注意不要执行了错误的或者不存在的主节点导致数据被误删。

切主操作

实战:模拟故障转移

  1. docker stop redis-master 使主节点断开连接。
  2. 在 redis-slave1 上执行 slaveof no one 使其升级为主节点,数据依然存在。
  3. 在其他节点执行 slaveof {redis-slave1的IP} 6379 加入到新的主节点。

踩坑:从节点连接失败 Error condition on socket for SYNC: Connection refused

以上的主从复制是在从节点执行 slaveof {host} {port} 生效的,Docker 运行时默认情况下没有配置文件的,一切都很正常。如果启动的配置项多起来了,每次启动都在 redis-server 后面加一大堆参数显然不便于维护,下面演示挂载 redis.conf 配置文件并指定从节点的 slaveof 配置启动:

1)修改配置文件

redis-master.conf

appendonly no                       # 主节点关闭aof持久化
logfile "/data/redis-master.log"    # 日志文件输出路径
bind 127.0.0.1                      # 监听的网络接口(默认配置,此处留坑。。。)

redis-slave.conf

slaveof 172.17.0.2 6379         # 指定复制的主节点地址
appendonly yes                  # 从节点开启aof持久化
logfile "/data/redis-slave.log" # 日志文件输出路径

2)指定配置文件启动

master:

docker run -d -p 63791:6379 --name redis-master \
-v ~/Learning/redis-learning/redis-master-slave/redis-master.conf:/usr/local/etc/redis/redis.conf \
redis redis-server /usr/local/etc/redis/redis.conf

slave1 & slave2

# ======= 启动 slave1
docker run -d -p 63792:6379 --name redis-slave1 \
-v ~/Learning/redis-learning/redis-master-slave/redis-slave1.conf:/usr/local/etc/redis/redis.conf \
redis redis-server /usr/local/etc/redis/redis.conf

# ======= 启动 slave2
docker run -d -p 63793:6379 --name redis-slave2 \
-v ~/Learning/redis-learning/redis-master-slave/redis-slave2.conf:/usr/local/etc/redis/redis.conf \
redis redis-server /usr/local/etc/redis/redis.conf

docker-compose 一键启动:docker-compose up -d

3)查看主从状态

# ============== slave1
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:172.17.0.3
master_port:6379
master_link_status:down # 未连接上主节点

从节点未连接上主节点,查看日志 /data/redis-slave.log:

1:S 23 Apr 2020 02:23:27.404 * Connecting to MASTER 172.17.0.2:6379
1:S 23 Apr 2020 02:23:27.405 * MASTER <-> REPLICA sync started
1:S 23 Apr 2020 02:23:27.405 # Error condition on socket for SYNC: Connection refused

这是由于主节点设置了绑定接口 bind 172.0.0.1 为本机,所以其他从节点无法连接上。

redis.conf - NETWORK

如果不指定 bind 属性,Redis 会监听服务器上所有可用的网络接口,这样做会将当前实例暴露给网络上的所有人。所以 redis 默认只监听本机 172.0.0.1,也就意味着 redis 只接收客户端来自同一台机器的连接。

我们修改一下 bind 配置,使其能够被其他两个节点连接到:

# 172.17.0.2 172.17.0.3 172.17.0.4 分别是 redis-master, redis-slave1, redis-slave2
bind 127.0.0.1 172.17.0.2 172.17.0.3 172.17.0.4

然后重新启动观察,以从节点1为例,查看复制情况

slave1连接成功

slave1成功复制

Comments