1. 主從複製概述
#65 哥:有了RDB 和AOF 再也不怕宕機遺失資料了,但是Redis 實例宕機了怎麼實現高可用?
既然一台宕機了無法提供服務,那多台呢?是不是就可以解決了。 Redis 提供了主從模式,透過主從複製,將資料冗餘一份複製到其他 Redis 伺服器。
前者稱為主節點 (master),後者稱為從節點 (slave);資料的複製是單向的,只能由主節點到從節點。
預設情況下,每台 Redis 伺服器都是主節點;且一個主節點可以有多個從節點 (或沒有從節點),但一個從節點只能有一個主節點。
65 哥:主從之間的資料如何保證一致性呢?
為了確保副本資料的一致性,主從架構採用了讀寫分離的方式。
- 讀取操作:主、從庫都可以執行;
- 寫入操作:主庫先執行,之後將寫入操作同步到從庫;
#65 哥:為何要採用讀寫分離的方式?
我們可以假設主從庫都可以執行寫指令,假如對同一份資料分別修改了多次,每次修改發送到不同的主從實例上,就導致是實例的副本資料不一致了。
如果為了確保資料一致,Redis 需要加鎖,協調多個實例的修改,Redis 自然不會這麼做!
65 哥:主從複製還有其他作用麼?
故障復原:當主節點宕機,其他節點仍可提供服務;
- ## 負載平衡:Master 節點提供寫入服務,Slave 節點提供讀取服務,分擔壓力;
- 高可用基石:是哨兵和cluster 實施的基礎,是高可用的基石。
2. 建立主從複製
主從複製的開啟,完全是在從節點發起的,不需要我們在主節點做任何事。
65 哥:怎麼搭建主從複製架構呀? 可以透過 replicaof(Redis 5.0 之前使用 slaveof)指令形成主函式庫和從函式庫的關係。 在從節點開啟主從複製,有3 種方式:- 設定檔在從伺服器的設定檔中加入
replicaof
- #啟動指令redis-server 啟動指令後面加入
--replicaof
< ;masterport> - 客戶端指令啟動多個Redis 實例後,直接透過客戶端執行指令:
replicaof
< ;masterport> ,則該Redis 實例成為從節點。
replicaof 172.16.88.1 6379
3. 主從複製原理
主從庫模式一旦採用了讀寫分離,所有資料的寫入操作只會在主庫上進行,不用協調三個實例。
主庫有了最新的資料後,會同步給從庫,這樣,主從庫的資料就是一致的。
65 哥:主從函式庫同步是如何完成的呢?主庫資料是一次性傳給從庫,還是分批同步?正常運作中又怎麼同步呢?要是主從庫間的網路斷連了,重新連接後資料還能保持一致嗎?
65 哥你問題咋這麼多,同步分為三種情況:
第一次主從庫全量複製;
主從正常運作期間的同步;
主從庫間網路斷開重連同步。
主從庫第一次全量複製
65 哥:我好暈啊,先從主從庫間第一次同步說起吧。
主從庫第一次複製過程大體可以分為3 個階段:連接建立階段(即準備階段)、主庫同步資料到從庫階段、發送同步期間新寫入命令到從庫階段;
直接上圖,從整體上有一個全局觀的感知,後面具體介紹。
建立連接
#該階段的主要作用是在主從節點之間建立連接,為資料全量同步做好準備。 從庫會和主庫建立連接,從庫執行 replicaof 並發送 psync 命令並告訴主庫即將進行同步,主庫確認回復後,主從庫間就開始同步了。
65 哥:從庫怎麼知道主庫資訊並建立連結的呢?
在從節點的設定檔中的 replicaof 設定項中配置了主節點的 IP 和 port 後,從節點就知道自己要和那個主節點連線了。
從節點內部維護了兩個字段,masterhost 和 masterport,用於儲存主節點的 IP 和 port 資訊。
從庫執行 replicaof
並發送 psync
命令,表示要執行資料同步,主庫收到命令後根據參數啟動複製。指令包含了主庫的 runID 和 複製進度 offset 兩個參數。
- runID:每個Redis 實例啟動都會自動產生一個唯一識別ID,第一次主從複製,還不知道主庫runID,參數設定為「?」 。
- offset:第一次複製設定為 -1,表示第一次複製,記錄複製進度偏移量。
主庫收到psync 指令後,會用FULLRESYNC 回應指令帶上兩個參數:主函式庫runID 和主函式庫目前的複製進度offset,回傳給從函式庫 。從庫收到回應後,會記錄下這兩個參數。
FULLRESYNC 回應表示第一次複製採用的全量複製,也就是說,主庫會把目前所有的資料複製給從函式庫。
主庫同步資料給從庫
第二階段
master 執行bgsave
命令產生RDB 文件,並將檔案傳送給從函式庫,同時主函式庫為每一個slave 開闢一塊replication buffer 緩衝區記錄從產生RDB 檔案開始收到的所有寫指令。
從庫收到 RDB 檔案後儲存到磁碟,並清空目前資料庫的數據,再載入 RDB 檔案資料到記憶體中。
發送新寫入命令到從庫
第三階段
#從節點載入RDB 完成後,主節點將replication buffer 緩衝區的數據傳送到從節點,Slave 接收並執行,從節點同步到主節點相同的狀態。
65 哥:主庫將資料同步到從庫過程中,可以正常接受請求麼?
主庫不會被阻塞,Redis 身為一個唯快不破的男人,怎麼會動不動就阻塞呢。
在產生RDB 檔案之後的寫入操作並沒有記錄到剛剛的RDB 檔案中,為了確保主從庫資料的一致性,所以主庫會在記憶體中使用一個叫replication buffer 記錄RDB 檔案生成後的所有寫入操作。
65 哥:為啥從庫收到 RDB 檔案後要清空目前資料庫?
因為從庫在通過 replcaof
命令開始和主庫同步前可能保存了其他數據,防止主從數據之間的影響。
replication buffer 到底是什麼玩意?
一個在 master 端上建立的緩衝區,存放的資料是下面三個時間內所有的 master 資料寫入操作。
1)master 執行bgsave 產生RDB 的期間的寫入操作;
2)master 發送rdb 到slave 網路傳輸期間的寫入操作;
3)slave load rdb檔案把資料恢復到記憶體的期間的寫入操作。
Redis 和客户端通信也好,和从库通信也好,Redis 都分配一个内存 buffer 进行数据交互,客户端就是一个 client,从库也是一个 client,我们每个 client 连上 Redis 后,Redis 都会分配一个专有 client buffer,所有数据交互都是通过这个 buffer 进行的。
Master 先把数据写到这个 buffer 中,然后再通过网络发送出去,这样就完成了数据交互。
不管是主从在增量同步还是全量同步时,master 会为其分配一个 buffer ,只不过这个 buffer 专门用来传播写命令到从库,保证主从数据一致,我们通常把它叫做 replication buffer。
replication buffer 太小会引发的问题:
replication buffer 由 client-output-buffer-limit slave 设置,当这个值太小会导致主从复制连接断开。
1)当 master-slave 复制连接断开,master 会释放连接相关的数据。replication buffer 中的数据也就丢失了,此时主从之间重新开始复制过程。
2)还有个更严重的问题,主从复制连接断开,导致主从上出现重新执行 bgsave 和 rdb 重传操作无限循环。
当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;
这种情况可能引起全量复制 -> replication buffer 溢出导致连接中断 -> 重连 -> 全量复制 -> replication buffer 缓冲区溢出导致连接中断……的循环。
具体详情:[top redis headaches for devops – replication buffer]
因而推荐把 replication buffer 的 hard/soft limit 设置成 512M。
config set client-output-buffer-limit "slave 536870912 536870912 0"
65 哥:主从库复制为何不使用 AOF 呢?相比 RDB 来说,丢失的数据更少。
这个问题问的好,原因如下:
RDB 文件是二进制文件,网络传输 RDB 和写入磁盘的 IO 效率都要比 AOF 高。
从库进行数据恢复的时候,RDB 的恢复效率也要高于 AOF。
增量复制
65 哥:主从库间的网络断了咋办?断开后要重新全量复制么?
在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。
从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。
增量复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。
repl_backlog_buffer
断开重连增量复制的实现奥秘就是 repl_backlog_buffer
缓冲区,不管在什么时候 master 都会将写指令操作记录在 repl_backlog_buffer
中,因为内存有限, repl_backlog_buffer
是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。
master 使用 master_repl_offset
记录自己写到的位置偏移量,slave 则使用 slave_repl_offset
记录已经读取到的偏移量。
master 收到写操作,偏移量则会增加。从库持续执行同步的写指令后,在 repl_backlog_buffer
的已复制的偏移量 slave_repl_offset 也在不断增加。
正常情况下,这两个偏移量基本相等。在网络断连阶段,主库可能会收到新的写操作命令,所以 master_repl_offset
会大于 slave_repl_offset
。
当主从断开重连后,slave 会先发送 psync 命令给 master,同时将自己的 runID
,slave_repl_offset
发送给 master。
master 只需要把 master_repl_offset
与 slave_repl_offset
之间的命令同步给从库即可。
增量复制执行流程如下图:
65 哥:repl_backlog_buffer 太小的话从库还没读取到就被 Master 的新写操作覆盖了咋办?
我们要想办法避免这个情况,一旦被覆盖就会执行全量复制。我们可以调整 repl_backlog_size 这个参数用于控制缓冲区大小。计算公式:
repl_backlog_buffer = second * write_size_per_second
second:从服务器断开重连主服务器所需的平均时间;
write_size_per_second:master 平均每秒产生的命令数据量大小(写命令和数据大小总和);
例如,如果主服务器平均每秒产生 1 MB 的写数据,而从服务器断线之后平均要 5 秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于 5 MB。
为了安全起见,可以将复制积压缓冲区的大小设为2 * second * write_size_per_second
,这样可以保证绝大部分断线情况都能用部分重同步来处理。
基于长连接的命令传播
65 哥:完成全量同步后,正常运行过程如何同步呢?
当主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,使用长连接的目的就是避免频繁建立连接导致的开销。
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。
主->从:PING
每隔指定的时间,主节点会向从节点发送 PING 命令,这个 PING 命令的作用,主要是为了让从节点进行超时判断。
从->主:REPLCONF ACK
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:
REPLCONF ACK <replication_offset></replication_offset>
其中 replication_offset 是从服务器当前的复制偏移量。发送 REPLCONF ACK 命令对于主从服务器有三个作用:
检测主从服务器的网络连接状态。
辅助实现 min-slaves 选项。
检测命令丢失, 从节点发送了自身的 slave_replication_offset,主节点会用自己的 master_replication_offset 对比,如果从节点数据缺失,主节点会从
repl_backlog_buffer
缓冲区中找到并推送缺失的数据。注意,offset 和 repl_backlog_buffer 缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。
如何确定执行全量同步还是部分同步?
在 Redis 2.8 及以后,从节点可以发送 psync 命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。本文以 Redis 2.8 及之后的版本为例。
关键就是 psync
的执行:
-
从节点根据当前状态,发送
psync
命令给 master:- 如果从节点从未执行过
replicaof
,则从节点发送psync ? -1
,向主节点发送全量复制请求; - 如果从节点之前执行过
replicaof
则发送psync <runid> <offset></offset></runid>
, runID 是上次复制保存的主节点 runID,offset 是上次复制截至时从节点保存的复制偏移量。
- 如果从节点从未执行过
-
主节点根据接受到的
psync
命令和当前服务器状态,决定执行全量复制还是部分复制:- runID 与从节点发送的 runID 相同,且从节点发送的
slave_repl_offset
之后的数据在repl_backlog_buffer
缓冲区中都存在,则回复CONTINUE
,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可; - runID 与从节点发送的 runID 不同,或者从节点发送的 slave_repl_offset 之后的数据已不在主节点的
repl_backlog_buffer
缓冲区中 (在队列中被挤出了),则回复从节点FULLRESYNC <runid> <offset></offset></runid>
,表示要进行全量复制,其中 runID 表示主节点当前的 runID,offset 表示主节点当前的 offset,从节点保存这两个值,以备使用。
- runID 与从节点发送的 runID 相同,且从节点发送的
一个从库如果和主库断连时间过长,造成它在主库 repl_backlog_buffer
的 slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
总结下
每个从库会记录自己的 slave_repl_offset
,每个从库的复制进度也不一定相同。
在和主库重连进行恢复时,从库会通过 psync 命令把自己记录的 slave_repl_offset
发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。
replication buffer 和repl_backlog
replication buffer 對應到每個slave,透過
config set client-output-buffer-limit slave
設定.repl_backlog_buffer
是一個環形緩衝區,整個 master 行程只會存在一個,所有的 slave 公用。 repl_backlog 的大小透過repl-backlog-size 參數設置,預設大小是1M,其大小可以根據每秒產生的命令、(master 執行rdb bgsave) ( master 發送rdb 到slave) (slave load rdb 檔案)時間之和來估算積壓緩衝區的大小,repl-backlog-size 值不小於這兩者的乘積。
總的來說,replication buffer
是主從庫在進行全量複製時,主庫上用於和從庫連接的客戶端的buffer,而repl_backlog_buffer
是為了支援從函式庫增量複製,主函式庫上用來持續保存寫入作業的一塊專用buffer。
repl_backlog_buffer
是一塊專用 buffer,在 Redis 伺服器啟動後,開始一直接收寫入操作命令,這是所有從程式庫共享的。主庫和從庫會各自記錄自己的複製進度,所以,不同的從庫在進行恢復時,會把自己的複製進度(slave_repl_offset
)發給主庫,主庫就可以和它獨立同步。
如圖所示:
#4. 主從應用問題
#4.1 讀寫分離的問題
資料過期問題
#65 哥:主從複製的場景下,從節點會刪除過期資料麼?
這個問題問得好,為了主從節點的資料一致性,從節點不會主動刪除資料。我們知道 Redis 有兩種刪除策略:
惰性刪除:當客戶端查詢對應的資料時,Redis 判斷資料是否過期,過期則刪除。
定期刪除:Redis 透過定時任務刪除過期資料。
65 哥:那客戶端透過從節點讀取資料會不會讀取到過期資料?
Redis 3.2 開始,透過從節點讀取資料時,先判斷資料是否已過期。如果過期則不傳回客戶端,並且刪除資料。
4.2 單機記憶體大小限制
如果Redis 單機記憶體達到10GB,一個從節點的同步時間在幾分鐘的等級;如果從節點較多,恢復的速度會更慢。如果系統的讀取負載很高,而這段時間從節點無法提供服務,會對系統造成很大的壓力。
如果資料量過大,全量複製階段主節點fork 保存RDB 檔案耗時過大,從節點長時間接收不到資料觸發逾時,主從節點的資料同步同樣可能陷入全量複製->超時導致複製中斷->重連->全量複製->超時導致複製中斷…的循環。
此外,主節點單機記憶體除了絕對量不能太大,其佔用主機內存的比例也不應過大:最好只使用50% - 65% 的內存,留下30%-45%的記憶體用於執行bgsave 命令和建立複製緩衝區等。
總結
主從複製的作用:AOF 和RDB 二進位檔案保證了宕機快速恢復數據,盡可能的防止丟失數據。但宕機後依然無法提供服務,所以便演化出主從架構、讀寫分離。
主從複製原理:連線建立階段、資料同步階段、指令傳播階段;資料同步階段又分為全量複製與部分複製;指令傳播階段主從節點之間有PING 和REPLCONF ACK 指令互相進行心跳檢測。
主從複製雖然解決或緩解了資料冗餘、故障復原、讀取負載平衡等問題,但其缺陷仍很明顯:故障復原無法自動化;寫入操作無法負載平衡;儲存能力受到單機的限制;這些問題的解決,需要哨兵和叢集的幫助,我將在後面的文章中介紹,歡迎關注。
65 哥:碼哥你的圖畫的真好看,內容好,跟著你的文章我收穫了很多,我要收藏、點讚、在看和分享。讓更多優秀的開發者看到共同進步!
更多程式相關知識,請造訪:程式設計影片! !