出處: redis主從復制常見的一些坑
讀寫分離的問題
1.數據復制的延遲
讀寫分離時,master會異步的將數據復制到slave,如果這是slave發生阻塞,則會延遲master數據的寫命令,造成數據不一致的情況
解決方法:可以對slave的偏移量值進行監控,如果發現某台slave的偏移量有問題,則將數據讀取操作切換到master,但本身這個監控開銷比較高,所以關於這個問題,大部分的情況是可以直接使用而不去考慮的。
2.讀到過期的數據
產生原因:
redis的從庫是無法主動的刪除已經過期的key的,所以如果做了讀寫分離,就很有可能在從庫讀到臟數據
例子重現:
主Redis
setex test 20 1 +OK get test $1 1 ttl test :18
從Redis
get test $1 1 ttl test :7
以上都沒問題,然而過幾秒再看從Redis
ttl test :-1 get test $1 1
test這個key已經過期了,然而還是可以獲取到test的值。
在使用Redis做鎖的時候,如果直接取讀從庫的值,這就有大問題了。
為什么從庫不刪除數據?
redis刪除過期數據有以下幾個策略:
1.惰性刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key,很明顯,這是被動的!
2.定期刪除:由於惰性刪除策略無法保證冷數據被及時刪掉,所以 redis 會定期主動淘汰一批已過期的key。(在第二節中會具體說明)
3.主動刪除:當前已用內存超過maxMemory限定時,觸發主動清理策略。主動設置的前提是設置了maxMemory的值
int expireIfNeeded(redisDb *db, robj *key) { time_t when = getExpire(db,key); if (when < 0) return 0; /* No expire for this key */ /* Don't expire anything while loading. It will be done later. */ if (server.loading) return 0; /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ if (server.masterhost != NULL) { return time(NULL) > when; } /* Return when this key has not expired */ if (time(NULL) <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); return dbDelete(db,key); }
解決方法:
1)通過一個程序循環便利所有的key,例如scan
2)通過ttl判斷
3)升級到reidis3.2
主從配置不一致
這個問題一般很少見,但如果有,就會發生很多詭異的問題
例如:
1. maxmemory配置不一致:這個會導致數據的丟失
原因:例如master配置4G,slave配置2G,這個時候主從復制可以成功,但,如果在進行某一次全量復制的時候,slave拿到master的RDB加載數據時發現自身的2G內存不夠用,這時就會觸發slave的maxmemory策略,將數據進行淘汰。更可怕的是,在高可用的集群環境下,如果我們將這台slave升級成master的時候,就會發現數據已經丟失了。
2. 數據結構優化參數不一致(例如hash-max-ziplist-entries):這個就會導致內存不一致
原因:例如在master上對這個參數進行了優化,而在slave沒有配置,就會造成主從節點內存不一致的詭異問題。
規避全量復制
首先,我們知道,redis復制有全量復制和部分復制兩種(這個我前面博客有寫到)而全量復制的開銷是很大的。那么我們來看看,如何盡量去規避全量復制。
1.第一次全量復制
當我們某一台slave第一次去掛到master上時,是不可避免要進行一次全量復制的,那么,我們如何去想辦法降低開銷呢?
方案1:小主節點,例如我們把redis分成2G一個節點,這樣一來,會加速RDB的生成和同步,同時還可以降低我們fork子進程的開銷(master會fork一個子進程來生成同步需要的RDB文件,而fork是要拷貝內存快的,如果主節點內存太大,fork的開銷就大)。
方案2:既然第一次不可以避免,那我們可以選在集群低峰的時間(凌晨)進行slave的掛載。
2.節點RunID不匹配
例如我們主節點重啟(RunID發生變化),對於slave來說,它會保存之前master節點的RunID,如果它發現了此時master的RunID發生變化,那它會認為這是master過來的數據可能是不安全的,就會采取一次全量復制
解決辦法:對於這類問題,我們只有是做一些故障轉移的手段,例如master發生故障宕掉,我們選舉一台slave晉升為master(哨兵或集群)
3.復制積壓緩沖區不足
我在全量復制與部分復制那篇文章提到過,master生成RDB同步到slave,slave加載RDB這段時間里,master的所有寫命令都會保存到一個復制緩沖隊列里(如果主從直接網絡抖動,進行部分復制也是走這個邏輯),待slave加載完RDB后,拿offset的值到這個隊列里判斷,如果在這個隊列中,則把這個隊列從offset到末尾全部同步過來,這個隊列的默認值為1M。而如果發現offset不在這個隊列,就會產生全量復制。
解決辦法:增大復制緩沖區的配置 rel_backlog_size 默認1M,我們可以設置大一些,從而來加大我們offset的命中率。這個值,我們可以假設,一般我們網絡故障時間一般是分鍾級別,那我們可以根據我們當前的QPS來算一下每分鍾可以寫入多少字節,再乘以我們可能發生故障的分鍾就可以得到我們這個理想的值。
規避復制風暴
什么是復制風暴?舉例:我們master重啟,其master下的所有slave檢測到RunID發生變化,導致所有從節點向主節點做全量復制。盡管redis對這個問題做了優化,即只生成一份RDB文件,但需要多次傳輸,仍然開銷很大。
1.單主節點復制風暴:主節點重啟,多從節點全量復制
解決:更換復制拓撲如下圖:
1.我們將原來master與slave中間加一個或多個slave,再在slave上加若干個slave,這樣可以分擔所有slave對master復制的壓力。(這種架構還是有問題:讀寫分離的時候,slave1也發生了故障,怎么去處理?)
2.如果只是實現高可用,而不做讀寫分離,那當master宕機,直接晉升一台slave即可。
2.單機器復制風暴:機器宕機后的大量全量復制,如下圖:
當machine-A這個機器宕機重啟,會導致該機器所有master下的所有slave同時產生復制。(災難)
解決:
1.主節點分散多機器(將master分散到不同機器上部署)
2.還有我們可以采用高可用手段(slave晉升master)就不會有類似問題了。
附錄一份文章關於數據庫主從數據不一致問題
數據庫主從不一致問題解決方案
問:常見的數據庫集群架構如何?
答: 一主多從,主從同步,讀寫分離。
如上圖:
(1)一個主庫提供寫服務
(2)多個從庫提供讀服務,可以增加從庫提升讀性能
(3)主從之間同步數據
畫外音:任何方案不要忘了本心,加從庫的本心,是提升讀性能。
問:為什么會出現不一致?
答:主從同步有時延,這個時延期間讀從庫,可能讀到不一致的數據。
如上圖:
(1)服務發起了一個寫請求
(2)服務又發起了一個讀請求,此時同步未完成,讀到一個不一致的臟數據
(3)數據庫主從同步最后才完成
畫外音:任何數據冗余,必將引發一致性問題。
問:如何避免這種主從延時導致的不一致?
答:常見的方法有這么幾種。
方案一:忽略
任何脫離業務的架構設計都是耍流氓,絕大部分業務,例如:百度搜索,淘寶訂單,QQ消息,58帖子都允許短時間不一致。
畫外音:如果業務能接受,最推崇此法。
如果業務能夠接受,別把系統架構搞得太復雜。
方案二:強制讀主
如上圖:
(1)使用一個高可用主庫提供數據庫服務
(2)讀和寫都落到主庫上
(3)采用緩存來提升系統讀性能
這是很常見的微服務架構,可以避免數據庫主從一致性問題。
方案三:選擇性讀主
強制讀主過於粗暴,畢竟只有少量寫請求,很短時間,可能讀取到臟數據。
有沒有可能實現,只有這一段時間,可能讀到從庫臟數據的讀請求讀主,平時讀從呢?
可以利用一個緩存記錄必須讀主的數據。
如上圖,當寫請求發生時:
(1)寫主庫
(2)將哪個庫,哪個表,哪個主鍵三個信息拼裝一個key設置到cache里,這條記錄的超時時間,設置為“主從同步時延”
畫外音:key的格式為“db:table:PK”,假設主從延時為1s,這個key的cache超時時間也為1s。
如上圖,當讀請求發生時:
這是要讀哪個庫,哪個表,哪個主鍵的數據呢,也將這三個信息拼裝一個key,到cache里去查詢,如果,
(1)cache里有這個key,說明1s內剛發生過寫請求,數據庫主從同步可能還沒有完成,此時就應該去主庫查詢
(2)cache里沒有這個key,說明最近沒有發生過寫請求,此時就可以去從庫查詢
以此,保證讀到的一定不是不一致的臟數據。
總結
數據庫主庫和從庫不一致,常見有這么幾種優化方案:
(1)業務可以接受,系統不優化
(2)強制讀主,高可用主庫,用緩存提高讀性能
(3)在cache里記錄哪些記錄發生過寫請求,來路由讀主還是讀從