redis专题17 主从同步系列问题
  热度 °
在生产环境中需要用到redis
做数据持久化落地数据库时, 一般应搭建专属的redis
集群来避免单点故障及单点读写性能问题, 如不是重度redis
用户, 数据量压力不是特别大时, 也可以考虑采用redis
主从同步架构代替, 本文将试图对redis
主从同步原理, 步骤, 配置项, 实践等方面进行学习总结;
主从同步目的
一旦主节点宕机, 从节点作为主节点的备份可以随时顶上来;
扩展主节点的读能力, 分担主节点读压力;
主从同步原理
redis
支持主从复制, redis
的主从结构可以采用一主多从或者级联结构, redis
主从复制可以根据是否是全量分为全量同步和增量同步,1
2
3
4
5 / slave-1 / slave-2-1
Master - slave-2-- slave-2-2
\ slave-3 \ slave-2-3
\ ... \ ...
slave-n slave-2-n
全量同步
redis
全量复制一般发生在slave
初始化阶段, 这时slave
需要将Master上的所有数据都复制一份, 具体过程如下,
- 1.从服务器连接主服务器, 发送
sync
命令; - 2.主服务器接收到
sync
命名后, 开始执行bgsave
命令生成RDB文件并使用复制积压缓冲区记录此后执行的所有写命令; - 3.主服务器
bgsave
执行完后, 向所有从服务器发送快照文件, 并在发送期间继续记录被执行的写命令; - 4.从服务器收到快照文件后丢弃所有旧数据, 载入收到的快照;
- 5.主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 6.从服务器完成对快照的载入, 开始接收命令请求, 并执行来自主服务器缓冲区的写命令;
完成上面几个步骤后就完成了从服务器数据初始化的所有操作, 从服务器此时可以接收来自用户的读请求。
若多个从服务器同时发来
sync
指令, 主服务器也只会执行一次bgsave
, 然后把持久化好的RDB文件发给多个下游;
增量同步
在redis2.8之前, redis仅支持sync
全量同步操作, sync
命令是一个非常耗费资源的操作, 为了解决主从服务器断线重来带来的sync
重复复制问题, redis
从2.8版本开始, 使用psync
命令代替sync
命令来执行复制时的同步操作。psync
命令具有完整重同步(full resynchronization)
和增量重同步(partial resynchronization)
两种模式, 而增量同步策略大大降低了连接断开的恢复成本。增量同步过程如下,
- 1.
master
端为复制流维护一个内存缓冲区(in-memory backlog)
, 记录最近发送的复制流命令; - 2.同时, Master和
slave
之间都维护一个复制偏移量(replication offset)
和当前master
服务器ID(Masterrun id)
。 - 3.当网络断开,
slave
尝试重连时:- a. 如果
masterID
相同(即仍是断网前的master
服务器), 并且从断开时到当前时刻的历史命令依然在master
的内存缓冲区中存在, 则Master会将缺失的这段时间的所有命令发送给slave
执行, 然后复制工作就可以继续执行了; - b. 否则, 依然需要全量复制操作;
- a. 如果
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令, 从服务器接收并执行收到的写命令。
可见增量重同步功能由以下三个部分构成,
- 1.主服务器的复制偏移量
(replication offset)
和从服务器的复制偏移量; - 2.主服务器的复制积压缓冲区
(replication backlog)
; - 3.服务器的运行
ID(run ID)
。
1.复制偏移量
执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量,
- 主服务器每次向从服务器传播
N
个字节的数据时, 就将自己的复制偏移量的值加上N
; - 从服务器每次收到主服务器传播来的
N
个字节的数据时, 就将自己的复制偏移量的值加上N
;
主从偏移量相同, 则当前主从处于一致状态, 反之则主从状态不一致;
示例分析
1 | --(网络故障断线 )--> slaveA ( offset=10086 ) |
如上示例, 假设从服务器A
在断线之后就立即重新连接主服务器成功, 那么接下来, 从服务器将向主服务器发送psync
命令, 报告从服务器A当前的复制偏移量为10086
, 那么这时, 主服务器应该对从服务器执行完整重同步还是部分重同步呢?如果执行部分重同步的话, 主服务器又如何补偿从服务器A
在断线期间丢失的那部分数据呢?以上问题的答案都和复制积压缓冲区有关。
2.复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)
先进先出(FIFO)
队列, 默认大小为1MB
。
和普通先进先出队列随着元素的增加和减少而动态调整长度不同, 固定长度先进先出队列的长度是固定的, 当入队元素的数量大于队列长度时, 最先入队的元素会被弹出, 而新元素会被放入队列。
当主服务器向从服务器发送命令时, 它不仅会将写命令发送给所有从服务器, 还会将写命令入队到复制积压缓冲区里面, 示意图,
1 | --(向从服务器发送命令)--> slaveA |
因此, 主服务器的复制积压缓冲区里面会保存着一部分最近发送的写命令, 并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量, 就像下表所示的那样,
1 | |偏移量| 10086 | 10087 | 10088 | 10089 | |
当从服务器重新连上主服务器时, 从服务器会通过psync
命令将自己的复制偏移量offset发送给主服务器, 主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
如果
offset
偏移量之后的数据(也即是偏移量offset+1
开始的数据)仍然存在于复制积压缓冲区里面, 那么主服务器将对从服务器执行部分重同步操作;
相反, 如果
offset
偏移量之后的数据已经不存在于复制积压缓冲区, 那么主服务器将对从服务器执行完整重同步操作。
根据需要调整复制积压缓冲区的大小
redis
为复制积压缓冲区设置的默认大小为1MB
, 如果主服务器需要执行大量写命令, 又或者主从服务器断线后重连接所需的时间比较长, 那么这个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当, 那么psync
命令的复制重同步模式就不能正常发挥作用, 因此, 正确估算和设置复制积压缓冲区的大小非常重要。
复制积压缓冲区的最小大小可以根据公式second*write_size_per_second
来估算:
其中
second
为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算);
而
write_size_per_second
则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和);
例如, 如果主服务器平均每秒产生1MB
的写数据, 而从服务器断线之后平均要5秒
才能重新连接上主服务器, 那么复制积压缓冲区的大小就不能低于5MB
。
为了安全起见, 可以将复制积压缓冲区的大小设为2*second*write_size_per_second
, 这样可以保证绝大部分断线情况都能用部分重同步来处理。
可以根据实际需要, 修改配置文件中的repl-backlog-size
选项来修改复制积压缓冲区的大小;
3.服务器运行ID
除了复制偏移量和复制积压缓冲区之外, 实现部分重同步还需要用到服务器运行ID(run ID)
:
每个
redis
服务器, 不论主服务器还是从服务, 都会有自己的运行ID
;
运行
ID
在服务器启动时自动生成, 由40个随机的十六进制字符组成, 例如53b9b28df8042fdc9ab5e3fcbbbabff1d5dce2b3
;
当从服务器对主服务器进行初次复制时, 主服务器会将自己的运行ID
传送给从服务器, 而从服务器则会将这个运行ID
保存起来(注意哦, 是从服务器保存了主服务器的ID
)。
当从服务器断线并重新连上一个主服务器时, 从服务器将向当前连接的主服务器发送之前保存的运行ID
:
如果从服务器保存的运行
ID
和当前连接的主服务器的运行ID
相同, 那么说明从服务器断线之前复制的就是当前连接的这个主服务器, 主服务器可以继续尝试执行部分重同步操作;
相反地, 如果从服务器保存的运行
ID
和当前连接的主服务器的运行ID
并不相同, 那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器, 主服务器将对从服务器执行完整重同步操作。
psync命令psync
命令的调用方法有两种,
如果从服务器以前没有复制过任何主服务器, 或者之前执行过
SLAVEOF NO ONE
命令, 那么从服务器在开始一次新的复制时将向主服务器发送psync ? -1
命令, 主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步);
相反地, 如果从服务器已经复制过某个主服务器, 那么从服务器在开始一次新的复制时将向主服务器发送
psync <runid> <offset>
命令: 其中runid
是上一次复制的主服务器的运行ID
, 而offset
则是从服务器当前的复制偏移量, 接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作。
主从同步策略
redis
的复制是异步进行的, redis3.0
开始提供的wait
指令可以让异步复制变身同步复制, 确保系统的强一致性,1
2
3
4> set key value
OK
> wait 1 0
(integer) 1
wait 提供两个参数, 第一个参数是从库的数量N
, 第二个参数是时间t
, 以毫秒为单位。它表示等待wait
指令之前的所有写操作同步到N
个从库 (也就是确保N
个从库的同步没有滞后), 最多等待时间t
。如果时间t=0
, 表示无限等待直到N
个从库同步完成达成一致。
假设此时出现了网络分区,wait
指令第二个参数时间t=0
, 主从同步无法继续进行, wait
指令会永远阻塞, redis
服务器将丧失可用性。
主从刚刚连接的时候, 进行全量同步; 全同步结束后, 进行增量同步。当然, 如果有需要, slave在任何时候都可以发起全量同步。redis策略是, 无论如何, 首先会尝试进行增量同步, 如不成功, 要求从机进行全量同步。
主从同步过程
从服务器每次启动时, 会立即通过slaveof master-host master-port
向主服务器发起主从复制同步请求; SLAVEOF
命令是一个异步命令, 在完成master-host
属性和master-port
属性的设置工作之后, 从服务器将向发送SLAVEOF
命令的客户端返回OK
, 表示复制指令已经被接收, 而实际的复制工作将在OK
返回之后才真正开始执行。从服务器开始发起主从复制请求到开始复制主要经历了如下7个步骤,
步骤1: 设置主服务器的地址和端口, 通过slaveof
指令发起主从复制同步请求,1
2127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK
步骤2: 建立套接字连接
在SLAVEOF命令执行之后, 从服务器将根据命令所设置的IP地址和端口, 创建连向主服务器的套接字连接, 如果从服务器创建的套接字能成功连接(connect)到主服务器, 那么从服务器将为这个套接字关联一个专门用于处理复制工作的文件事件处理器, 这个处理器将负责执行后续的复制工作, 比如接收RDB文件, 以及接收主服务器传播来的写命令, 诸如此类。而主服务器在接受(accept)从服务器的套接字连接之后, 将为该套接字创建相应的客户端状态, 并将从服务器看作是一个连接到主服务器的客户端来对待, 这时从服务器将同时具有服务器(server)和客户端(client)两个身份: 从服务器可以向主服务器发送命令请求, 而主服务器则会向从服务器返回命令回复。
步骤3: 发送PING命令
从服务器成为主服务器的客户端之后, 做的第一件事就是向主服务器发送一个PING命令。
通过发送PING命令检查套接字的读写状态;
通过PING命令可以检查主服务器能否正常处理命令。
从服务器在发送PING命令之后可能遇到以下三种情况:
主服务器向从服务器返回了一个命令回复, 但从服务器却不能在规定的时限内读取命令回复的内容(timeout), 说明网络连接状态不佳, 从服务器将断开并重新创建连向主服务器的套接字;
如果主服务器返回一个错误, 那么表示主服务器暂时没有办法处理从服务器的命令请求, 从服务器也将断开并重新创建连向主服务器的套接字;
如果从服务器读取到”PONG”回复, 那么表示主从服务器之间的网络连接状态正常, 那就继续执行下面的复制步骤。
步骤4: 身份验证
从服务器在收到主服务器返回的”PONG”回复之后, 下一步要做的就是决定是否进行身份验证:
如果从服务器设置了masterauth选项, 那么进行身份验证。否则不进行身份认证;
在需要进行身份验证的情况下, 从服务器将向主服务器发送一条AUTH命令, 命令的参数为从服务器masterauth选项的值。
从服务器在身份验证阶段可能遇到的情况有以下几种:
主服务器没有设置requirepass选项, 从服务器没有设置masterauth,那么就继续后面的复制工作;
如果从服务器的通过AUTH命令发送的密码和主服务器requirepass选项所设置的密码相同, 那么也继续后面的工作, 否则返回错误invaild password;
如果主服务器设置了requireoass选项, 但从服务器没有设置masterauth选项, 那么服务器将返回NOAUTH错误。反过来如果主服务器没有设置requirepass选项, 但是从服务器却设置了materauth选项, 那么主服务器返回no password is set错误;
所有错误到只有一个结果: 中止目前的复制工作, 并从创建套接字开始重新执行复制, 直到身份验证通过, 或者从服务器放弃执行复制为止。
步骤5: 发送端口信息
身份验证步骤之后, 从服务器将执行命令REPLCONF listening-port <port-number>
, 向主服务器发送从服务器的监听端口号。
主服务器在接收到这个命令之后, 会将端口号记录在从服务器所对应的客户端状态的slave_listening_port
属性,
slave_listening_port
属性目前唯一的作用就是在主服务器执行INFO replication
命令时打印出从服务器的端口号。
步骤6: 同步
在这一步, 从服务器将向主服务器发送psync
命令, 执行同步操作, 并将自己的数据库更新至主服务器数据库当前所处的状态。
需要注意的是在执行同步操作前, 只有从服务器是主服务器的客户端。但是执行同步操作之后, 主服务器也会成为从服务器的客户端,
如果
psync
命令执行的是完整同步操作, 那么主服务器只有成为了从服务器的客户端才能将保存在缓冲区中的写命令发送给从服务器执行;
如果
psync
命令执行的是部分同步操作, 那么主服务器只有成为了从服务器的客户端才能将保存在复制积压缓冲区中的写命令发送给从服务器执行;
步骤7: 命令传播
当完成了同步之后, 主从服务器就会进入命令传播阶段, 这时主服务器只要一直将自己执行的写命令发送给从服务器, 而从服务器只要一直接收并执行主服务器发来的写命令, 就可以保证主从服务器一直保持一致了。
心跳检测
在命令传播阶段, 从服务器默认会以每秒一次的频率, 向主服务器发送命令: REPLCONF ACK <replication_offset>
其中replication_offset
是从服务器当前的复制偏移量。
发送REPLCONF ACK
命令对于主从服务器有三个作用:
检测主从服务器的网络连接状态;
辅助实现min-slaves选项;
检测命令丢失。
检测主从服务器的网络连接状态
如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK
命令, 那么主服务器就知道主从服务器之间的连接出现问题了。
通过向主服务器发送INFO replication
命令, 在列出的从服务器列表的lag
一栏中, 我们可以看到相应从服务器最后一次向主服务器发送REPLCONF ACK
命令距离现在过了多少秒;
1 | 10.1.195.19:8001> info replication |
在一般情况下, lag
的值应该在0秒或者1秒之间跳动, 如果超过1秒的话, 那么说明主从服务器之间的连接出现了故障。
辅助实现min-slaves配置选项
redis
的min-slaves-to-write
和min-slaves-max-lag
两个选项可以防止主服务器在不安全的情况下执行写命令。
举个例子, 如果我们向主服务器提供以下设置:1
2min-slaves-to-write 3
min-slaves-max-lag 10
那么在从服务器的数量少于3个, 或者三个从服务器的延迟(lag)
值都大于或等于10秒时, 主服务器将拒绝执行写命令, 这里的延迟值就是上面提到的INFO replication
命令的(lag)
值。
检测命令丢失
我们从命令: REPLCONF ACK <replication_offset>
就可以知道, 每发送一次这个命令从服务器都会向主服务器报告一次自己的复制偏移量。那此时尽管主服务器发送给从服务器的SET key value
丢失了。也无所谓, 主服务器马上就知道了。
同步实践
0.环境准备
通过构建如下主从级联架构来进一步实践redis
主从复制同步机制,1
master---> slave01 --> slave02
即master
为主服务器, slave01
为master
从服务器, 同时也是slave02
主服务器, 由于要构建多个redis container
环境, 为方便起见通过docker-compose
来实践,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29version: '3'
services:
master-redis:
image: redis
container_name: master
ports:
- "8001:6379"
volumes:
- ~/workbench/docker/docker-compose/redis/conf/redis.conf:/etc/redis/redis.conf
entrypoint: ["redis-server", "/etc/redis/redis.conf"]
slave-redis-01:
image: redis
container_name: slave01
ports:
- "8002:6379"
volumes:
- ~/workbench/docker/docker-compose/redis/conf/redis.conf:/etc/redis/redis.conf
entrypoint: ["redis-server", "/etc/redis/redis.conf"]
slave-redis-02:
image: redis
container_name: slave02
ports:
- "8003:6379"
volumes:
- ~/workbench/docker/docker-compose/redis/conf/redis.conf:/etc/redis/redis.conf
entrypoint: ["redis-server", "/etc/redis/redis.conf"]
将redis.conf
配置做如下修改,
- 将
bind 127.0.0.1
修改为bind 0.0.0.0
; - 将
daemonize yes
修改为daemonize no
, 让redis
运行在前台; - 将
protected-mode yes
修改为protected-mode no
, 在protected-mode yes
模式下需要设置密码才能远程访问, 否则redis
只接受本地访问;
1.发送slaveof
指令,请求主从同步
1 | ➜ redis docker-compose up -d |
上述命令, 分别启动了一台master
主机, 两台slave01
,slave02
从机, 并得知当前宿主机ip为10.1.195.19
登录从机发送slaveof
指令, 开始主从复制同步,
主机master
执行命令,1
2
3
4
5
6
7
8
9
10
11
12
13➜ docker exec -it master redis-cli -h 10.1.195.19 -p 8001
10.1.195.19:8001> flushdb
OK
10.1.195.19:8001> hmset info name mike city shanghai code 110
OK
10.1.195.19:8001> hgetall info
1) "name"
2) "mike"
3) "city"
4) "shanghai"
5) "code"
6) "110"
10.1.195.19:8001>
从机slave01
查询命令,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18➜ docker exec -it slave01 redis-cli -h 10.1.195.19 -p 8002
10.1.195.19:8002> slaveof no one
OK
10.1.195.19:8002> flushdb
OK
10.1.195.19:8002> slaveof 10.1.195.19 8001
OK
10.1.195.19:8002> scan 0
1) "0"
2) 1) "info"
10.1.195.19:8002> hgetall info
1) "name"
2) "mike"
3) "city"
4) "shanghai"
5) "code"
6) "110"
10.1.195.19:8002>
从机slave02
查询命令,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15➜ docker exec -it slave02 redis-cli -h 10.1.195.19 -p 8003
10.1.195.19:8003> slaveof no one
OK
10.1.195.19:8003> flushdb
OK
10.1.195.19:8003> slaveof 10.1.195.19 8002
OK
10.1.195.19:8003> hgetall info
1) "name"
2) "mike"
3) "city"
4) "shanghai"
5) "code"
6) "110"
10.1.195.19:8003>
docker exec -it ${docker-name} redis-cli -h ${localhost-ip} -p ${port}
命令登录到各个redis
实例服务;slaveof no one
命令将当前redis
实例的主从服务;- 从上述实例可见, 通过在
master
主机中执行hmset
命令, 相应的从机以及级联从机也都同步了主机的执行命令, 上述示例显示主从复制同步成功;
从机配置默认开启了只读模式slave-read-only yes
,所以对从机进行修改操作是不许可的, 也不建议这么做;1
2
310.1.195.19:8003> flushdb
(error) READONLY You can't write against a read only replicx
10.1.195.19:8003>
当对主机操作flushdb
时, 可见从机也执行了flushdb
命令1
2
3
4
5
6
7
8
9
10
11
12
1310.1.195.19:8001> flushdb
OK
10.1.195.19:8001>
10.1.195.19:8002> scan 0
1) "0"
2) (empty list or set)
10.1.195.19:8002>
10.1.195.19:8003> scan 0
1) "0"
2) (empty list or set)
10.1.195.19:8003>
当关闭master
服务后, 对从机服务无影响, 所以从机服务的启动顺序及服务提供 与master
主机服务是否已启动及启动顺序无关;
当然上面docker-compose
配置也可以直接配置slaveof
参数, 就不需要手动去发起命令, 可参看 docker-compose-redis-replication
总结
redis
的主从同步是异步进行的, 这意味着主从同步不会影响主逻辑, 也不会降低redis
的处理性能。- 主从架构中, 可以考虑关闭主服务器的数据持久化功能, 只让从服务器进行持久化, 这样可以提高主服务器的处理性能。
- 在主从架构中, 从服务器通常被设置为只读模式, 这样可以避免从服务器的数据被误修改。但是从服务器仍然可以接受
config
等指令, 所以还是不应该将从服务器直接暴露到不安全的网络环境中。如果必须如此, 那可以考虑给重要指令进行重命名, 来避免命令被外人误执行。 - 主从同步分为
全量同步
和增量同步
, 文中对全量同步
和增量同步
原理,实现过程进行了阐述分析; - 进一步阐述了主从不同执行策略, 即从机初始连接主机时, 先进行增量同步, 若增量同步失败, 则进行全量同步, 同时可以利用
wait
指令 将redis主从同步的异步行为转变为同步行为; - 分析了主从同步过程, 主从同步开始至复制, 大致进来7个过程, 1.发起
slaveof
指令;2.建立套接字连接;3.发送ping指令测试连接状态;4.身份认证;5.发送端口信息;6.同步初始化阶段,从机载入主机RDB,准备接受主机命令;7.增量同步; - 通过docker-compose 构建
主-从-从
架, 深入实践分析redis
主从复制同步过程; - 主从复制是
redis
分布式的基础,redis
的高可用离开了主从复制将无从进行; - 不过复制功能也不是必须的, 如果你将
redis
只用来做缓存, 跟memcache
一样来对待, 也就无需要从库做备份, 挂掉了重新启动一下就行。但是只要你使用了redis
的持久化功能, 就必须认真对待主从复制, 它是系统数据安全的基础保障;
作者署名:朴实的一线攻城狮
本文标题:redis专题17 主从同步系列问题
本文出处:http://researchlab.github.io/2018/10/15/redis-17-replication/
版权声明:本文由Lee Hong创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处, 否则保留追究法律责任的权利。