論程式的健壯性——就看Redis

宜信技術學院發表於2020-09-21

論程式的健壯性——就看Redis

“眾裡尋他千百度,驀然回首,那人卻在,燈火闌珊處”。多年的IT生涯,一直希望自己寫的程式能夠有很強的健壯性,也一直希望能找到一個高可用的標杆程式去借鑑學習,不畏懼記憶體溢位、磁碟滿了、斷網、斷電、機器重啟等等情況。但意想不到的是,這個標杆程式竟然就是從一開始就在使用的分散式快取——Redis。


Redis(Remote Dictionary Server ),即遠端字典服務,是 C 語言開發的一個開源的高效能鍵值對(key-value)的記憶體資料庫。由於它是基於記憶體的所以它要比基於磁碟讀寫的資料庫效率更快。因此Redis也就成了大家解決資料庫高併發訪問、分散式讀寫和分散式鎖等首選解決方案。

那麼既然它是基於記憶體的,如果記憶體滿了怎麼辦?程式會不會崩潰?既然它是基於記憶體的,如果伺服器當機了怎麼辦?資料是不是就丟失了?既然它是分散式的,這臺Redis伺服器斷網了怎麼辦?

今天我們就一起來看看Redis的設計者,一名來自義大利的小夥,是如何打造出一個超強健壯性和高可用性的程式,從而不懼怕這些情況。

一、 Redis的記憶體管理策略——記憶體永不溢位

Redis主要有兩種策略機制來保障儲存的key-value資料不會把記憶體塞滿,它們是:過期策略和淘汰策略。

1、 過期策略

用過Redis的人都知道,我們往Redis裡新增key-value的資料時,會有個選填引數——過期時間。如果設定了這個引數的值,Redis到過期時間後會自行把過期的資料給清除掉。“過期策略”指的就是Redis內部是如何實現將過期的key對應的快取資料清除的。

在Redis原始碼中有三個核心的物件結構:redisObject、redisDb和serverCron。

  • redisObject:Redis 內部使用redisObject 物件來抽象表示所有的 key-value。簡單地說,redisObject就是string、hash、list、set、zset的父類。為了便於操作,Redis採用redisObject結構來統一這五種不同的資料型別。

  • redisDb:Redis是一個鍵值對資料庫伺服器,這個資料庫就是用redisDb抽象表示的。redisDb結構中有很多dict字典儲存了資料庫中的所有鍵值對,這些字典就叫做鍵空間。如下圖所示其中有個“expires”的字典就儲存了設定過期時間的鍵值對。而Redis的過期策略也是圍繞它來進行的。

  • serverCron:Redis 將serverCron作為時間事件來執行,從而確保它每隔一段時間就會自動執行一次。因此redis中所有定時執行的事件任務都在serverCron中執行。

瞭解完Redis的三大核心結構後,我們們回到“過期策略”的具體實現上,其實Redis主要是靠兩種機制來處理過期的資料被清除:定期過期(主動清除)和惰性過期(被動清除)。

  • 惰性過期(被動清除):就是每次訪問的時候都去判斷一下該key是否過期,如果過期了就刪除掉。該策略就可以最大化地節省CPU資源,但是卻對記憶體非常不友好。因為不實時過期了,原本該過期刪除的就可能一直堆積在記憶體裡面!極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,佔用大量記憶體。

  • 定期過期(主動清除):每隔一定的時間,會掃描Redis資料庫的expires字典中一定數量的key,並清除其中已過期的 key。Redis預設配置會每100毫秒進行1次(redis.conf 中通過 hz 配置)過期掃描,掃描並不是遍歷過期字典中的所有鍵,而是採用瞭如下方法:

(1)從過期字典中隨機取出20個鍵;
(server.h,檔案下 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP配置20)

(2)刪除這20個鍵中過期的鍵;

(3)如果過期鍵的比例超過 25% ,重複步驟 1 和 2;

具體邏輯如下圖:

因為Redis中同時使用了惰性過期和定期過期兩種過期策略,所以在不同情況下使得 CPU 和記憶體資源達到最優的平衡效果的同時,保證過期的資料會被及時清除掉。

2、淘汰策略

在Redis可能沒有需要過期的資料的情況下,還是會把我們的記憶體都佔滿。比如每個key設定的過期時間都很長或不過期,一直新增就有可能把記憶體給塞滿。那麼Redis又是怎麼解決這個問題的呢?——那就是“淘汰策略”。

官網地址: https://redis.io/topics/lru-cache
Reids官網上面列出的淘汰策略一共有8種,但從實質演算法來看只有兩種實現演算法,分別是LRU和LFU。

LRU(Least Recently Used):翻譯過來是最久未使用,根據時間軸來走,淘汰那些距離上一次使用時間最久遠的資料。
LRU的簡單原理如下圖:

從上圖我們可以看出,在容器滿了的情況下,距離上次讀寫時間最久遠的E被淘汰掉了。那麼資料每次讀取或者插入都需要獲取一下當前系統時間,以及每次淘汰的時候都需要拿當前系統時間和各個資料的最後操作時間做對比,這麼幹勢必會增加CPU的負荷從而影響Redis的效能。Redis的設計者為了解決這一問題,做了一定的改善,整體的LRU思路如下:

(1)、Redis裡設定了一個全域性變數 server.lruclock 用來存放系統當前的時間戳。這個全域性變數通過serverCron 每100毫秒呼叫一次updateCachedTime()更新一次值。

(2)、每當redisObject資料被讀或寫的時候,將當前的 server.lruclock值賦值給 redisObject 的lru屬性,記錄這個資料最後的lru值。

(3)、觸發淘汰策略時,隨機從資料庫中選擇取樣值配置個數key, 淘汰其中熱度最低的key對應的快取資料。

注:熱度就是拿當前的全域性server.lruclock 值與各個資料的lru屬性做對比,相差最久遠的就是熱度最低的。

Redis中所有物件結構都有一個lru欄位, 且使用了unsigned的低24位,這個欄位就是用來記錄物件的熱度。

LFU(Least Frequently Used):翻譯成中文就是最不常用。是按著使用頻次來算的,淘汰那些使用頻次最低的資料。說白了就是“末尾淘汰制”!
剛才講過的LRU按照最久未使用雖然能達到淘汰資料釋放空間的目的,但是它有一個比較大的弊端,如下圖:

如圖所示A在10秒內被訪問了5次,而B在10秒內被訪問了3 次。因為 B 最後一次被訪問的時間比A要晚,在同等的情況下,A反而先被回收。那麼它就是不合理的。LFU就完美解決了LRU的這個弊端,具體原理如下:

上圖是末尾淘汰的原理示意圖,僅是按次數這個維度做的末尾淘汰,但如果Redis僅按使用次數,也會有一個問題,就是某個資料之前被訪問過很多次比如上萬次,但後續就一直不用了,它本身按使用頻次來講是應該被淘汰的。因此Redis在實現LFU時,用兩部分資料來標記這個資料:使用頻率和上次訪問時間。整體思路就是:有讀寫我就增加熱度,一段時間內沒有讀寫我就減少相應熱度。

不管是LRU還是LFU淘汰策略,Redis都是用lru這個欄位實現的具體邏輯,如果配置的淘汰策略是LFU時,lru的低8位代表的是頻率,高16位就是記錄上次訪問時間。整體的LRU思路如下:

(1)每當資料被寫或讀的時候都會呼叫LFULogIncr(counter)方法,增加lru低8位的訪問頻率數值;具體每次增加的數值在redis.conf中配置預設是10(# lfu-log-factor 10)

(2)還有另外一個配置lfu-decay-time 預設是1分鐘,來控制每隔多久沒人訪問則熱度會遞減相應數值。這樣就規避了一個超大訪問次數的資料很久都不被淘汰的漏洞。

小結:“過期策略” 保證過期的key對應的資料會被及時清除;“淘汰策略”保證記憶體滿的時候會自動釋放相應空間,因此Redis的記憶體可以自執行保證不會產生溢位異常。

二、 Redis的資料持久化策略——當機可立即恢復資料到記憶體

有了記憶體不會溢位保障後,我們再來看看Redis是如何保障伺服器當機或重啟,原來快取在記憶體中的資料是不會丟失的。也就是Redis的持久化機制。

Redis 的持久化策略有兩種:RDB(快照全量持久化)和AOF(增量日誌持久化)

1、 RDB

RDB 是 Redis 預設的持久化方案。RDB快照(Redis DataBase),當觸發一定條件的時候,會把當前記憶體中的資料寫入磁碟,生成一個快照檔案dump.rdb。Redis重啟會通過dump.rdb檔案恢復資料。那那個一定的條件是啥呢?到底什麼時候寫入rdb 檔案?

觸發Redis執行rdb的方式有兩類:自動觸發和手動觸發
“自動觸發”的情況有三種:達到配置檔案觸發規則時觸發、執行shutdown命令時觸發、執行flushall命令時觸發。

注:在redis.conf中有個 SNAPSHOTTING配置,其中定義了觸發把資料儲存到磁碟觸發頻率。

“手動觸發”的方式有兩種:執行save 或 bgsave命令。執行save命令在生成快照的時候會阻塞當前Redis伺服器,Redis不能處理其他命令。如果記憶體中的資料比較多,會造成Redis長時間的阻塞。生產環境不建議使用這個命令。

為了解決這個問題,Redis 提供了第二種方式bgsave命令進行資料備份,執行bgsave時,Redis會在後臺非同步進行快照操作,快照同時還可以響應客戶端請求。

具體操作是Redis程式執行fork(建立程式函式)操作建立子程式(copy-on-write),RDB持久化過程由子程式負責,完成後自動結束。它不會記錄 fork 之後後續的命令。阻塞只發生在fork階段,一般時間很短。手動觸發的場景一般僅用在遷移資料時才會用到。

我們知道了RDB的實現的原理邏輯,那麼我們就來分析下RDB到底有什麼優劣勢。

優勢:

(1)RDB是一個非常緊湊(compact型別)的檔案,它儲存了redis在某個時間點上的資料集。這種檔案非常適合用於進行備份和災難恢復。

(2)生成RDB檔案的時候,redis主程式會fork()一個子程式來處理所有儲存工作,主程式不需要進行任何磁碟IO操作。

(3)RDB在恢復大資料集時的速度比AOF的恢復速度要快。

劣勢:

RDB方式資料沒辦法做到實時持久化/秒級持久化。在一定間隔時間做一次備份,所以如果Redis意外down掉的話,就會丟失最後一次快照之後的所有修改

2、 AOF(Append Only File)

AOF採用日誌的形式來記錄每個寫操作的命令,並追加到檔案中。開啟後,執行更改 Redis資料的命令時,就會把命令寫入到AOF檔案中。Redis重啟時會根據日誌檔案的內容把寫指令從前到後執行一次以完成資料的恢復工作。

其實AOF也不一定是完全實時的備份操作命令,在redis.conf 我們可以配置選擇 AOF的執行方式,主要有三種:always、everysec和no

AOF是追加更改命令檔案,那麼大家想下一直追加追加,就是會導致檔案過大,那麼Redis是怎麼解決這個問題的呢?
Redis解決這個問題的方法是AOF下面有個機制叫做bgrewriteaof重寫機制,我們來看下它是個啥

注:AOF檔案重寫並不是對原檔案進行重新整理,而是直接讀取伺服器現有的鍵值對,然後用一條命令去代替之前記錄這個鍵值對的多條命令,生成一個新的檔案後去替換原來的AOF檔案。

我們知道了AOF的實現原理,我們來分析下它的優缺點。

優點:

能最大限度的保證資料安全,就算用預設的配置everysec,也最多隻會造成1s的資料丟失。

缺點:

資料量比RDB要大很多,所以效能沒有RDB好!

小結:因為有了持久化機制,因此Redis即使伺服器當機或重啟了,也可以最大限度的恢復資料到記憶體中,提供給client繼續使用。

三、Redis的哨兵模式——可戰到最後一兵一卒的高可用叢集

記憶體滿了不會掛,伺服器當機重啟也沒問題。足見Redis的程式健壯性已經足夠強大。但Redis的設計者,在面向高可用面前,仍繼續向前邁進了一步,那就是Redis的高可用叢集方案——哨兵模式。

所謂的“哨兵模式”就是有一群哨兵(Sentinel)在Redis伺服器前面幫我們監控這Redis叢集各個機器的執行情況,並且哨兵間相互通告通知,並指引我們使用那些健康的服務。

Sentinel工作原理:

1、 Sentinel 預設以每秒鐘1次的頻率向Redis所有服務節點傳送 PING 命令。如果在down-after-milliseconds 內都沒有收到有效回覆,Sentinel會將該伺服器標記為下線(主觀下線)。

2、 這個時候Sentinel節點會繼續詢問其他的Sentinel節點,確認這個節點是否下線, 如果多數 Sentinel節點都認為master下線,master才真正確認被下線(客觀下線),這個時候就需要重新選舉master。

Sentinel的作用:

1、監控:Sentinel 會不斷檢查主伺服器和從伺服器是否正常執行

2、故障處理:如果主伺服器發生故障,Sentinel可以啟動故障轉移過程。把某臺伺服器升級為主伺服器,併發出通知

3、配置管理:客戶端連線到 Sentinel,獲取當前的 Redis 主伺服器的地址。我們不是直接去獲取Redis主服務的地址,而是根據sentinel去自動獲取誰是主機,即使主機發生故障後我們也不用改程式碼的連線!

小結:有了“哨兵模式”只要叢集中有一個Redis伺服器還健康存活,哨兵就能把這個健康的Redis伺服器提供給我們(如上圖的1、2兩步),那麼我們客戶端的連結就不會出錯。因此,Redis叢集可以戰鬥至最後一兵一卒。

這就是Redis,一個“高可用、強健壯性”的標杆程式!

作者:宜信技術學院 譚文濤

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2722845/,如需轉載,請註明出處,否則將追究法律責任。

相關文章