論程序的健壯性——就看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,一個“高可用、強健壯性”的標杆程序!

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章