【深入學習Redis】主從復制(下)
(續上文)
六、應用中的問題
1. 讀寫分離及其中的問題
在主從復制基礎上實現的讀寫分離,可以實現Redis的讀負載均衡:由主節點提供寫服務,由一個或多個從節點提供讀服務(多個從節點既可以提高數據冗余程度,也可以最大化讀負載能力);在讀負載較大的應用場景下,可以大大提高Redis服務器的並發量。下面介紹在使用Redis讀寫分離時,需要註意的問題。
1、延遲與不一致問題
前面已經講到,由於主從復制的命令傳播是異步的,延遲與數據的不一致不可避免。如果應用對數據不一致的接受程度程度較低,可能的優化措施包括:優化主從節點之間的網絡環境(如在同機房部署);監控主從節點延遲(通過offset)判斷,如果從節點延遲過大,通知應用不再通過該從節點讀取數據;使用集群同時擴展寫負載和讀負載等。
在命令傳播階段以外的其他情況下,從節點的數據不一致可能更加嚴重,例如連接在數據同步階段,或從節點失去與主節點的連接時等。從節點的slave-serve-stale-data參數便與此有關:它控制這種情況下從節點的表現;如果為yes(默認值),則從節點仍能夠響應客戶端的命令,如果為no,則從節點只能響應info、slaveof等少數命令。該參數的設置與應用對數據一致性的要求有關;如果對數據一致性要求很高,則應設置為no。
2、數據過期問題
在單機版Redis中,存在兩種刪除策略:
-
惰性刪除:服務器不會主動刪除數據,只有當客戶端查詢某個數據時,服務器判斷該數據是否過期,如果過期則刪除。
-
定期刪除:服務器執行定時任務刪除過期數據,但是考慮到內存和CPU的折中(刪除會釋放內存,但是頻繁的刪除操作對CPU不友好),該刪除的頻率和執行時間都受到了限制。
在主從復制場景下,為了主從節點的數據一致性,從節點不會主動刪除數據,而是由主節點控制從節點中過期數據的刪除。由於主節點的惰性刪除和定期刪除策略,都不能保證主節點及時對過期數據執行刪除操作,因此,當客戶端通過Redis從節點讀取數據時,很容易讀取到已經過期的數據。
Redis 3.2中,從節點在讀取數據時,增加了對數據是否過期的判斷:如果該數據已過期,則不返回給客戶端;將Redis升級到3.2可以解決數據過期問題。
3、故障切換問題
在沒有使用哨兵的讀寫分離場景下,應用針對讀和寫分別連接不同的Redis節點;當主節點或從節點出現問題而發生更改時,需要及時修改應用程序讀寫Redis數據的連接;連接的切換可以手動進行,或者自己寫監控程序進行切換,但前者響應慢、容易出錯,後者實現復雜,成本都不算低。
4、總結
在使用讀寫分離之前,可以考慮其他方法增加Redis的讀負載能力:如盡量優化主節點(減少慢查詢、減少持久化等其他情況帶來的阻塞等)提高負載能力;使用Redis集群同時提高讀負載能力和寫負載能力等。如果使用讀寫分離,可以使用哨兵,使主從節點的故障切換盡可能自動化,並減少對應用程序的侵入。
2. 復制超時問題
主從節點復制超時是導致復制中斷的最重要的原因之一,本小節單獨說明超時問題,下一小節說明其他會導致復制中斷的問題。
超時判斷意義
-
在復制連接建立過程中及之後,主從節點都有機制判斷連接是否超時,其意義在於:
-
如果主節點判斷連接超時,其會釋放相應從節點的連接,從而釋放各種資源,否則無效的從節點仍會占用主節點的各種資源(輸出緩沖區、帶寬、連接等);此外連接超時的判斷可以讓主節點更準確的知道當前有效從節點的個數,有助於保證數據安全(配合前面講到的min-slaves-to-write等參數)。
-
如果從節點判斷連接超時,則可以及時重新建立連接,避免與主節點數據長期的不一致。
判斷機制
主從復制超時判斷的核心,在於repl-timeout參數,該參數規定了超時時間的閾值(默認60s),對於主節點和從節點同時有效;主從節點觸發超時的條件分別如下:
主節點:每秒1次調用復制定時函數replicationCron(),在其中判斷當前時間距離上次收到各個從節點REPLCONF ACK的時間,是否超過了repl-timeout值,如果超過了則釋放相應從節點的連接。
從節點:從節點對超時的判斷同樣是在復制定時函數中判斷,基本邏輯是:
-
如果當前處於連接建立階段,且距離上次收到主節點的信息的時間已超過repl-timeout,則釋放與主節點的連接;
-
如果當前處於數據同步階段,且收到主節點的RDB文件的時間超時,則停止數據同步,釋放連接;
-
如果當前處於命令傳播階段,且距離上次收到主節點的PING命令或數據的時間已超過repl-timeout值,則釋放與主節點的連接。
主從節點判斷連接超時的相關源代碼如下:
/* Replication cron function, called 1 time per second. */
void replicationCron(void) {
static long long replication_cron_loops = 0;
/* Non blocking connection timeout? */
if (server.masterhost &&
(server.repl_state == REDIS_REPL_CONNECTING ||
slaveIsInHandshakeState()) &&
(time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
redisLog(REDIS_WARNING,"Timeout connecting to the MASTER...");
undoConnectWithMaster();
}
/* Bulk transfer I/O timeout? */
if (server.masterhost && server.repl_state == REDIS_REPL_TRANSFER &&
(time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the ‘repl-timeout‘ parameter in redis.conf to a larger value.");
replicationAbortSyncTransfer();
}
/* Timed out master when we are an already connected slave? */
if (server.masterhost && server.repl_state == REDIS_REPL_CONNECTED &&
(time(NULL)-server.master->lastinteraction) > server.repl_timeout)
{
redisLog(REDIS_WARNING,"MASTER timeout: no data nor PING received...");
freeClient(server.master);
}
//此處省略無關代碼……
/* Disconnect timedout slaves. */
if (listLength(server.slaves)) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
if (slave->replstate != REDIS_REPL_ONLINE) continue;
if (slave->flags & REDIS_PRE_PSYNC) continue;
if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
{
redisLog(REDIS_WARNING, "Disconnecting timedout slave: %s",
replicationGetSlaveName(slave));
freeClient(slave);
}
}
}
//此處省略無關代碼……
}
需要註意的坑
下面介紹與復制階段連接超時有關的一些實際問題:
1、數據同步階段:在主從節點進行全量復制bgsave時,主節點需要首先fork子進程將當前數據保存到RDB文件中,然後再將RDB文件通過網絡傳輸到從節點。如果RDB文件過大,主節點在fork子進程+保存RDB文件時耗時過多,可能會導致從節點長時間收不到數據而觸發超時;此時從節點會重連主節點,然後再次全量復制,再次超時,再次重連……這是個悲傷的循環。為了避免這種情況的發生,除了註意Redis單機數據量不要過大,另一方面就是適當增大repl-timeout值,具體的大小可以根據bgsave耗時來調整。
2、命令傳播階段:如前所述,在該階段主節點會向從節點發送PING命令,頻率由repl-ping-slave-period控制;該參數應明顯小於repl-timeout值(後者至少是前者的幾倍)。否則,如果兩個參數相等或接近,網絡抖動導致個別PING命令丟失,此時恰巧主節點也沒有向從節點發送數據,則從節點很容易判斷超時。
3、慢查詢導致的阻塞:如果主節點或從節點執行了一些慢查詢(如keys *或者對大數據的hgetall等),導致服務器阻塞;阻塞期間無法響應復制連接中對方節點的請求,可能導致復制超時。
3. 復制中斷問題
主從節點超時是復制中斷的原因之一,除此之外,還有其他情況可能導致復制中斷,其中最主要的是復制緩沖區溢出問題。
復制緩沖區溢出
前面曾提到過,在全量復制階段,主節點會將執行的寫命令放到復制緩沖區中,該緩沖區存放的數據包括了以下幾個時間段內主節點執行的寫命令:bgsave生成RDB文件、RDB文件由主節點發往從節點、從節點清空老數據並載入RDB文件中的數據。當主節點數據量較大,或者主從節點之間網絡延遲較大時,可能導致該緩沖區的大小超過了限制,此時主節點會斷開與從節點之間的連接;這種情況可能引起全量復制->復制緩沖區溢出導致連接中斷->重連->全量復制->復制緩沖區溢出導致連接中斷……的循環。
復制緩沖區的大小由client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds}配置,默認值為client-output-buffer-limit slave 256MB 64MB 60,其含義是:如果buffer大於256MB,或者連續60s大於64MB,則主節點會斷開與該從節點的連接。該參數是可以通過config set命令動態配置的(即不重啟Redis也可以生效)。
當復制緩沖區溢出時,主節點打印日誌如下所示:
需要註意的是,復制緩沖區是客戶端輸出緩沖區的一種,主節點會為每一個從節點分別分配復制緩沖區;而復制積壓緩沖區則是一個主節點只有一個,無論它有多少個從節點。
4. 各場景下復制的選擇及優化技巧
在介紹了Redis復制的種種細節之後,現在我們可以來總結一下,在下面常見的場景中,何時使用部分復制,以及需要註意哪些問題。
1、第一次建立復制
此時全量復制不可避免,但仍有幾點需要註意:如果主節點的數據量較大,應該盡量避開流量的高峰期,避免造成阻塞;如果有多個從節點需要建立對主節點的復制,可以考慮將幾個從節點錯開,避免主節點帶寬占用過大。此外,如果從節點過多,也可以調整主從復制的拓撲結構,由一主多從結構變為樹狀結構(中間的節點既是其主節點的從節點,也是其從節點的主節點);但使用樹狀結構應該謹慎:雖然主節點的直接從節點減少,降低了主節點的負擔,但是多層從節點的延遲增大,數據一致性變差;且結構復雜,維護相當困難。
2、主節點重啟
主節點重啟可以分為兩種情況來討論,一種是故障導致宕機,另一種則是有計劃的重啟。
主節點宕機
主節點宕機重啟後,runid會發生變化,因此不能進行部分復制,只能全量復制。
實際上在主節點宕機的情況下,應進行故障轉移處理,將其中的一個從節點升級為主節點,其他從節點從新的主節點進行復制;且故障轉移應盡量的自動化,後面文章將要介紹的哨兵便可以進行自動的故障轉移。
安全重啟:debug reload
在一些場景下,可能希望對主節點進行重啟,例如主節點內存碎片率過高,或者希望調整一些只能在啟動時調整的參數。如果使用普通的手段重啟主節點,會使得runid發生變化,可能導致不必要的全量復制。
為了解決這個問題,Redis提供了debug reload的重啟方式:重啟後,主節點的runid和offset都不受影響,避免了全量復制。
如下圖所示,debug reload重啟後runid和offset都未受影響:
但debug reload是一柄雙刃劍:它會清空當前內存中的數據,重新從RDB文件中加載,這個過程會導致主節點的阻塞,因此也需要謹慎。
3、從節點重啟
從節點宕機重啟後,其保存的主節點的runid會丟失,因此即使再次執行slaveof,也無法進行部分復制。
4、網絡中斷
如果主從節點之間出現網絡問題,造成短時間內網絡中斷,可以分為多種情況討論。
第一種情況:網絡問題時間極為短暫,只造成了短暫的丟包,主從節點都沒有判定超時(未觸發repl-timeout);此時只需要通過REPLCONF ACK來補充丟失的數據即可。
第二種情況:網絡問題時間很長,主從節點判斷超時(觸發了repl-timeout),且丟失的數據過多,超過了復制積壓緩沖區所能存儲的範圍;此時主從節點無法進行部分復制,只能進行全量復制。為了盡可能避免這種情況的發生,應該根據實際情況適當調整復制積壓緩沖區的大小;此外及時發現並修復網絡中斷,也可以減少全量復制。
第三種情況:介於前述兩種情況之間,主從節點判斷超時,且丟失的數據仍然都在復制積壓緩沖區中;此時主從節點可以進行部分復制。
5. 復制相關的配置
這一節總結一下與復制有關的配置,說明這些配置的作用、起作用的階段,以及配置方法等;通過了解這些配置,一方面加深對Redis復制的了解,另一方面掌握這些配置的方法,可以優化Redis的使用,少走坑。
配置大致可以分為主節點相關配置、從節點相關配置以及與主從節點都有關的配置,下面分別說明。
1、與主從節點都有關的配置
首先介紹最特殊的配置,它決定了該節點是主節點還是從節點:
-
slaveof <masterip> <masterport>:Redis啟動時起作用;作用是建立復制關系,開啟了該配置的Redis服務器在啟動後成為從節點。該註釋默認註釋掉,即Redis服務器默認都是主節點。
-
repl-timeout 60:與各個階段主從節點連接超時判斷有關,見前面的介紹。
2、主節點相關配置
-
repl-diskless-sync no:作用於全量復制階段,控制主節點是否使用diskless復制(無盤復制)。所謂diskless復制,是指在全量復制時,主節點不再先把數據寫入RDB文件,而是直接寫入slave的socket中,整個過程中不涉及硬盤;diskless復制在磁盤IO很慢而網速很快時更有優勢。需要註意的是,截至Redis3.0,diskless復制處於實驗階段,默認是關閉的。
-
repl-diskless-sync-delay 5:該配置作用於全量復制階段,當主節點使用diskless復制時,該配置決定主節點向從節點發送之前停頓的時間,單位是秒;只有當diskless復制打開時有效,默認5s。之所以設置停頓時間,是基於以下兩個考慮:(1)向slave的socket的傳輸一旦開始,新連接的slave只能等待當前數據傳輸結束,才能開始新的數據傳輸 (2)多個從節點有較大的概率在短時間內建立主從復制。
-
client-output-buffer-limit slave 256MB 64MB 60:與全量復制階段主節點的緩沖區大小有關,見前面的介紹。
-
repl-disable-tcp-nodelay no:與命令傳播階段的延遲有關,見前面的介紹。
-
masterauth <master-password>:與連接建立階段的身份驗證有關,見前面的介紹。
-
repl-ping-slave-period 10:與命令傳播階段主從節點的超時判斷有關,見前面的介紹。
-
repl-backlog-size 1mb:復制積壓緩沖區的大小,見前面的介紹。
-
repl-backlog-ttl 3600:當主節點沒有從節點時,復制積壓緩沖區保留的時間,這樣當斷開的從節點重新連進來時,可以進行全量復制;默認3600s。如果設置為0,則永遠不會釋放復制積壓緩沖區。
-
min-slaves-to-write 3與min-slaves-max-lag 10:規定了主節點的最小從節點數目,及對應的最大延遲,見前面的介紹。
3、從節點相關配置
-
slave-serve-stale-data yes:與從節點數據陳舊時是否響應客戶端命令有關,見前面的介紹。
-
slave-read-only yes:從節點是否只讀;默認是只讀的。由於從節點開啟寫操作容易導致主從節點的數據不一致,因此該配置盡量不要修改。
6. 單機內存大小限制
在 深入學習Redis(2):持久化 一文中,講到了fork操作對Redis單機內存大小的限制。實際上在Redis的使用中,限制單機內存大小的因素非常之多,下面總結一下在主從復制中,單機內存過大可能造成的影響:
-
切主:當主節點宕機時,一種常見的容災策略是將其中一個從節點提升為主節點,並將其他從節點掛載到新的主節點上,此時這些從節點只能進行全量復制;如果Redis單機內存達到10GB,一個從節點的同步時間在幾分鐘的級別;如果從節點較多,恢復的速度會更慢。如果系統的讀負載很高,而這段時間從節點無法提供服務,會對系統造成很大的壓力。
-
從庫擴容:如果訪問量突然增大,此時希望增加從節點分擔讀負載,如果數據量過大,從節點同步太慢,難以及時應對訪問量的暴增。
-
緩沖區溢出:(1)和(2)都是從節點可以正常同步的情形(雖然慢),但是如果數據量過大,導致全量復制階段主節點的復制緩沖區溢出,從而導致復制中斷,則主從節點的數據同步會全量復制->復制緩沖區溢出導致復制中斷->重連->全量復制->復制緩沖區溢出導致復制中斷……的循環。
-
超時:如果數據量過大,全量復制階段主節點fork+保存RDB文件耗時過大,從節點長時間接收不到數據觸發超時,主從節點的數據同步同樣可能陷入全量復制->超時導致復制中斷->重連->全量復制->超時導致復制中斷……的循環。
此外,主節點單機內存除了絕對量不能太大,其占用主機內存的比例也不應過大:最好只使用50%-65%的內存,留下30%-45%的內存用於執行bgsave命令和創建復制緩沖區等。
7. info Replication
在Redis客戶端通過info Replication可以查看與復制相關的狀態,對於了解主從節點的當前狀態,以及解決出現的問題都會有幫助。
主節點:
從節點:
對於從節點,上半部分展示的是其作為從節點的狀態,從connectd_slaves開始,展示的是其作為潛在的主節點的狀態。
info Replication中展示的大部分內容在文章中都已經講述,這裏不再詳述。
七、總結
下面回顧一下本文的主要內容:
-
主從復制的作用:宏觀的了解主從復制是為了解決什麽樣的問題,即數據冗余、故障恢復、讀負載均衡等。
-
主從復制的操作:即slaveof命令。
-
主從復制的原理:主從復制包括了連接建立階段、數據同步階段、命令傳播階段;其中數據同步階段,有全量復制和部分復制兩種數據同步方式;命令傳播階段,主從節點之間有PING和REPLCONF ACK命令互相進行心跳檢測。
-
應用中的問題:包括讀寫分離的問題(數據不一致問題、數據過期問題、故障切換問題等)、復制超時問題、復制中斷問題等,然後總結了主從復制相關的配置,其中repl-timeout、client-output-buffer-limit slave等對解決Redis主從復制中出現的問題可能會有幫助。
主從復制雖然解決或緩解了數據冗余、故障恢復、讀負載均衡等問題,但其缺陷仍很明顯:故障恢復無法自動化;寫操作無法負載均衡;存儲能力受到單機的限制;這些問題的解決,需要哨兵和集群的幫助,我將在後面的文章中介紹,歡迎關註。
參考文獻
-
《Redis開發與運維》
-
《Redis設計與實現》
-
《Redis實戰》
-
http://mdba.cn/2015/03/16/redis復制中斷問題-慢查詢/
-
https://redislabs.com/blog/top-redis-headaches-for-devops-replication-buffer/
-
http://mdba.cn/2015/03/17/redis主從復制(2)-replication-buffer與replication-backlog/
-
https://github.com/antirez/redis/issues/918
-
https://blog.csdn.net/qbw2010/article/details/50496982
-
https://mp.weixin.qq.com/s/fpupqLp-wjR8fQvYSQhVLg
【深入學習Redis】主從復制(下)