按照 Redis 官方文档 - Replication 的说法:Redis replication 是一种 master-slave 模式的复制机制,这种机制使得 slave 节点可以成为与 master 节点完全相同的副本。
我们知道,单个 Redis 节点也是可以直接工作的。那为什么一个 Redis 节点(master)还需要一个或多个副本(slave)呢?或者说 replication 到底想要解决什么问题?官方文档如是说:
Replication can be used both for scalability, in order to have multiple slaves for read-only queries (for example, slow O(N) operations can be offloaded to slaves), or simply for improving data safety and high availability.
简而言之,replication 主要用于解决两个问题:
一个 master 用于写,多个 slave 用于分摊读的压力。
如果 master 挂掉了,可以提升(promote)一个 slave 为新的 master,进而实现故障转移(failover)。
思考:如果没有 replication,上述两个问题该如何应对?
开两个终端,分别启动一个 Redis 节点:
1 2 3 4 |
# Terminal 1 $ redis-4.0.8/src/redis-server -p 6379 # Terminal 2 $ redis-4.0.8/src/redis-server -p 6380 |
在 6379 节点上设置并获取 key1:
1 2 3 4 5 |
$ redis-4.0.8/src/redis-cli -p 6379 127.0.0.1:6379> SET key1 value1 OK 127.0.0.1:6379> GET key1 "value1" |
在 6380 节点上尝试获取 key1:
1 2 3 |
$ redis-4.0.8/src/redis-cli -p 6380 127.0.0.1:6380> GET key1 (nil) |
可以看出,两个 Redis 节点各自为政,二者的数据并没有同步。
下面我们让 6380 成为 6379 的 slave 节点:
1 2 |
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 OK |
然后再尝试获取 key1:
1 2 |
127.0.0.1:6380> GET key1 "value1" |
很显然,最初在 6379 节点(后续称为 master)设置的 key1 已经被同步到了 6380 节点(后续称为 slave)。
实验:尝试在 master 设置更多的 key 或删除 key,然后在 slave 上获取并观察结果。
上述过程中,在 slave 上执行 SLAVEOF 命令以后,可以看到 slave 的日志如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
31667:S 03 Jul 21:32:17.809 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer. 31667:S 03 Jul 21:32:17.809 * SLAVE OF 127.0.0.1:6379 enabled (user request from 'id=2 addr=127.0.0.1:58544 fd=8 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=slaveof') 31667:S 03 Jul 21:32:17.825 * Connecting to MASTER 127.0.0.1:6379 31667:S 03 Jul 21:32:17.826 * MASTER <-> SLAVE sync started 31667:S 03 Jul 21:32:17.826 * Non blocking connect for SYNC fired the event. 31667:S 03 Jul 21:32:17.826 * Master replied to PING, replication can continue... 31667:S 03 Jul 21:32:17.826 * Trying a partial resynchronization (request 823e1002c282b4c088a6f80d4251de04f920068d:1). 31667:S 03 Jul 21:32:17.827 * Full resync from master: 599456031709498747f866bc3f7f4382db99ed89:0 31667:S 03 Jul 21:32:17.827 * Discarding previously cached master state. 31667:S 03 Jul 21:32:17.926 * MASTER <-> SLAVE sync: receiving 193 bytes from master 31667:S 03 Jul 21:32:17.927 * MASTER <-> SLAVE sync: Flushing old data 31667:S 03 Jul 21:32:17.927 * MASTER <-> SLAVE sync: Loading DB in memory 31667:S 03 Jul 21:32:17.927 * MASTER <-> SLAVE sync: Finished with success |
对应 master 的日志如下:
1 2 3 4 5 6 7 |
31655:M 03 Jul 21:32:17.826 * Slave 127.0.0.1:6380 asks for synchronization 31655:M 03 Jul 21:32:17.826 * Partial resynchronization not accepted: Replication ID mismatch (Slave asked for '823e1002c282b4c088a6f80d4251de04f920068d', my replication IDs are '4014bea143e2ade5aa81012849b0775ab0377b85' and '0000000000000000000000000000000000000000') 31655:M 03 Jul 21:32:17.826 * Starting BGSAVE for SYNC with target: disk 31655:M 03 Jul 21:32:17.826 * Background saving started by pid 31669 31669:C 03 Jul 21:32:17.827 * DB saved on disk 31655:M 03 Jul 21:32:17.926 * Background saving terminated with success 31655:M 03 Jul 21:32:17.926 * Synchronization with slave 127.0.0.1:6380 succeeded |
分析上述输出日志,我们可以初步总结出 slave 和 master 的交互时序:
思考:在同一台机器上,如何模拟 master 和 slave 的网络断开与恢复?
master 日志:
1 2 3 4 |
33518:M 03 Jul 22:46:48.432 # Disconnecting timedout slave: 127.0.0.1:6380 33518:M 03 Jul 22:46:48.432 # Connection with slave 127.0.0.1:6380 lost. 33518:M 03 Jul 22:46:50.538 * Slave 127.0.0.1:6380 asks for synchronization 33518:M 03 Jul 22:46:50.538 * Partial resynchronization request from 127.0.0.1:6380 accepted. Sending 0 bytes of backlog starting from offset 1541. |
slave 日志:
1 2 3 4 5 6 7 8 9 |
33519:S 03 Jul 22:46:48.432 # Connection with master lost. 33519:S 03 Jul 22:46:48.432 * Caching the disconnected master state. 33519:S 03 Jul 22:46:50.536 * Connecting to MASTER 127.0.0.1:6379 33519:S 03 Jul 22:46:50.537 * MASTER <-> SLAVE sync started 33519:S 03 Jul 22:46:50.537 * Non blocking connect for SYNC fired the event. 33519:S 03 Jul 22:46:50.537 * Master replied to PING, replication can continue... 33519:S 03 Jul 22:46:50.537 * Trying a partial resynchronization (request 6b1b77bebea22557686922f99cfa3103ba0824ae:1541). 33519:S 03 Jul 22:46:50.538 * Successful partial resynchronization with master. 33519:S 03 Jul 22:46:50.538 * MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization. |
可以看出:
实验:redis.conf 中有两个参数 repl-timeout
(默认值为 60 秒)和 repl-backlog-ttl
(默认值为 3600 秒),尝试都设置为 10 秒,然后断开网络一直等到 25 秒后再恢复,再观察 master 和 slave 的日志会有什么不同?
通过 telnet 连接到 master:
1 2 3 4 |
$ telnet 127.0.0.1 6379 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. |
键入 PSYNC 命令,尝试与 master 进行同步:
1 2 3 4 5 6 7 8 9 |
$ telnet 127.0.0.1 6379 ... PSYNC ? -1 +FULLRESYNC 8cdd5be435af5bcda9bb332e319cae9b71f788d7 344 $194 REDIS0008? redis-ver4.0.8? redis-bits?@?ctime?6?@[used-mem???repl-stream-db??repl-id(8cdd5be435af5bcda9bb332e319cae9b71f788d7? repl-offset?X? aof-preamble???key1value1?'>?w?Z |
此时查看 master 的日志:
1 2 3 4 5 6 7 |
40535:M 07 Jul 17:04:51.009 * Slave 127.0.0.1: |
随后在 master 上设置 key2:
1 2 |
127.0.0.1:6379> SET key2 value2 OK |
然后观察 telnet 的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ telnet 127.0.0.1 6379 ... *1 $4 PING *2 $6 SELECT $1 0 *3 $3 SET $4 key2 $6 value2 *1 $4 PING |
可以看出:
SET key2 value2
),会被传播(propagate)到 salve 上,进而保证了 slave 与 master 的数据一致性。上面的三种情景,其实已经涵盖了 Redis replication 的两大核心操作:
下面我们对这两种操作,做进一步阐述。
「重同步」用于将 slave 的数据库状态更新至 master 当前所处的数据库状态。
SYNC 与 PSYNC
旧版本 Redis 中,「重同步」通过 SYNC 命令来实现。从 2.8 版本开始,Redis 改用 PSYNC 命令来代替 SYNC 命令。
SYNC 命令和 PSYNC 命令的区别:
命令 | 初次复制 | 断线后复制 |
---|---|---|
SYNC | 完整重同步 | 完整重同步 |
PSYNC | 完整重同步:PSYNC ? -1 |
部分重同步:PSYNC |
完整重同步
说明:
PSYNC ? -1
。PSYNC
,但是
不是 master 的 replication-id,或者 slave 给的
不在 master 的「复制积压缓冲区」backlog 里面。部分重同步
说明:
PSYNC
命令,向 master 发起「部分重同步」请求。
是 master 的 replication-id,并且 slave 给的
在 master 的「复制积压缓冲区」backlog 里面由上可以看出,「复制积压缓冲区」backlog 是「部分重同步」得以实现的关键所在。
复制积压缓冲区
「复制积压缓冲区」是 master 维护的一个固定长度(fixed-sized)的先进先出(FIFO)的内存队列。值得注意的是:
repl-backlog-size
决定,默认为 1MB。当队列长度超过 repl-backlog-size
时,最先入队的元素会被弹出,用于腾出空间给新入队的元素。repl-backlog-ttl
决定,默认为 3600 秒。如果 master 不再有与之相连接的 slave,并且该状态持续时间超过了 repl-backlog-ttl
,master 就会释放该队列,等到有需要(下次又有 slave 连接进来)的时候再创建。master 会将最近接收到的写命令(按 Redis 协议的格式)保存到「复制积压缓冲区」,其中每个字节都会对应记录一个偏移量 offset。
. | . | . | . | . | . | . | . | . | . | . | . | . | . |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
偏移量 | … | 10087 | 10088 | 10089 | 10090 | 10091 | 10092 | 10093 | 10094 | 10095 | 10096 | 10097 | … |
字节值 | … | ‘*’ | 3 | ‘\r’ | ‘\n’ | ‘$’ | 3 | ‘\r’ | ‘\n’ | ‘S’ | ‘E’ | ‘T’ | … |
与此同时,slave 会维护一个 offset 值,每次从 master 传播过来的命令,一旦成功执行就会更新该 offset。尝试「部分重同步」的时候,slave 都会带上自己的 offset,master 再判断 offset 偏移量之后的数据是否存在于自己的「复制积压缓冲区」中,以此来决定执行「部分重同步」还是「完整重同步」。
「命令传播」用于在 master 的数据库状态被修改时,将导致变更的命令传播给 slave,从而让 slave 的数据库状态与 master 保持一致。
说明:master 进行命令传播时,除了将写命令直接发送给所有 slave,还会将这些命令写入「复制积压缓冲区」,用于后续可能发生的「部分重同步」操作。