Redis 原理及混合持久化
上一篇文章我們對redis的基本類型及相應(yīng)的應(yīng)用場景做了了解,那你知道redis 為什么這么快嗎?redis 宕機(jī)了怎么辦?redis 的持久化方式有哪些?如果你是云服務(wù)廠商,你怎么優(yōu)化你的云版的redis呢?
本文將對redis 的相關(guān)原理進(jìn)行分析。
線程 IO 模型
redis 是單線程程序的,除了 Redis 之外,Node.js 也是單線程,Nginx 也是單線程,但是它們都是服務(wù)器高性能的典范。
redis 單線程也這么快,也要歸功于非阻塞IO和 多路復(fù)用器。
非阻塞IO
能讀多少讀多少,取決于內(nèi)核為套接字分配的讀緩沖區(qū)內(nèi)部的數(shù)據(jù)字節(jié)數(shù)。
能寫多少寫多少,取決于內(nèi)核為套接字分配的寫緩存區(qū)空閑空間的字節(jié)數(shù)。
事件輪詢(多路復(fù)用)
非阻塞 IO 有個問題,那就是線程要讀數(shù)據(jù),結(jié)果讀了一部分就返回了,線程如何知道
何時才應(yīng)該繼續(xù)讀。也就是當(dāng)數(shù)據(jù)到來時,線程如何得到通知。寫也是一樣,如果緩沖區(qū)滿
了,寫不完,剩下的數(shù)據(jù)何時才應(yīng)該繼續(xù)寫,線程也應(yīng)該得到通知。
最簡單的事件輪詢API,如下圖所示。select函數(shù),它是操作系統(tǒng)提供的。在描述符特別多的情況下,性能特別差?,F(xiàn)代的操作系統(tǒng)多路復(fù)用改為:epoll(linux) kqueue(freeBsd 和 macosx)。
目前支持I/O多路復(fù)用的系統(tǒng)調(diào)用有 select,pselect,poll,epoll,I/O多路復(fù)用就是通過一種機(jī)制,一個進(jìn)程可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。但select,pselect,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫,異步I/O的實現(xiàn)會負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
select 和epoll的區(qū)別
select
首先是垮平臺的,select本質(zhì)上是通過設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來進(jìn)行下一步處理,
select最大的缺陷就是單個進(jìn)程所打開的FD是有一定限制的,它由FD_SETSIZE設(shè)置,默認(rèn)值是1024
對socket進(jìn)行掃描時是線性掃描,即采用輪詢的方法,效率較低。
epoll
是2.6 內(nèi)核提出的,是 select 和poll 的增強(qiáng)版,epoll使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
它的原理其實就是epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點在于邊緣觸發(fā),它只告訴進(jìn)程哪些fd剛剛變?yōu)榫途w態(tài),并且只會通知一次。
還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,
一旦該fd就緒,內(nèi)核就會采用類似callback的回調(diào)機(jī)制來激活該fd,epoll_wait便可以收到通知。
它的有點優(yōu)點:
1、沒有最大并發(fā)連接的限制,能打開的FD的上限遠(yuǎn)大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口)。
2、效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。
內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復(fù)制開銷。
單線程如何處理相應(yīng)請求?
通過 指令隊列,相應(yīng)隊列,和定時任務(wù)的設(shè)計來相應(yīng)請求。
指令隊列
客戶端指令通過隊列來排隊進(jìn)行順序處理,先到先服務(wù)
響應(yīng)隊列
等隊列里面有數(shù)據(jù)了再把客戶端fd 放進(jìn)去,避免浪費cpu
定時任務(wù)
服務(wù)器處理要響應(yīng) IO 事件外,還要處理其它事情。比如定時任務(wù)就是非常重要的一件事。如果線程阻塞在 select 系統(tǒng)調(diào)用上,定時任務(wù)將無法得到準(zhǔn)時調(diào)度。
那 Redis 是如何解決這個問題的呢?
Redis 的定時任務(wù)會記錄在一個稱為最小堆的數(shù)據(jù)結(jié)構(gòu)中。這個堆中,最快要執(zhí)行的任務(wù)排在堆的最上方。在每個循環(huán)周期,Redis 都會將最小堆里面已經(jīng)到點的任務(wù)立即進(jìn)行處理。處理完畢后,將最快要執(zhí)行的任務(wù)還需要的時間記錄下來,這個時間就是 select 系統(tǒng)調(diào)用的 timeout 參數(shù)。因為 Redis 知道未來 timeout 時間內(nèi),沒有其它定時任務(wù)需要處理,所以可以安心睡眠 timeout 的時間。
Nginx 和 Node 的事件處理原理和 Redis 也是類似的。
通信協(xié)議
Redis 的請求也相當(dāng)于一次rpc 的調(diào)用,rpc中,通信協(xié)議的設(shè)計是非常重要的。,Redis 的作者認(rèn)為數(shù)據(jù)庫系統(tǒng)的瓶頸一般不在于網(wǎng)絡(luò)流量,而是數(shù)據(jù)庫自身內(nèi)部邏輯處理上。所以即使 Redis 使用了浪費流量的文本協(xié)議,依然可以取得極高的訪問性能。Redis
將所有數(shù)據(jù)都放在內(nèi)存,用一個單線程對外提供服務(wù),單個節(jié)點在跑滿一個 CPU 核心的情況下可以達(dá)到了 10w/s 的超高 QPS。
RESP(Redis Serialization Protocol)
RESP 是 Redis 序列化協(xié)議的簡寫。它是一種直觀的文本協(xié)議,優(yōu)勢在于實現(xiàn)異常簡
單,解析性能極好。Redis 協(xié)議里有大量冗余的回車換行符,但是這不影響它成為互聯(lián)網(wǎng)技術(shù)領(lǐng)域非常受歡
迎的一個文本協(xié)議。有很多開源項目使用 RESP 作為它的通訊協(xié)議。在技術(shù)領(lǐng)域性能并不總
是一切,還有簡單性、易理解性和易實現(xiàn)性,這些都需要進(jìn)行適當(dāng)權(quán)衡。
關(guān)于持久化
一款優(yōu)秀的中間件都會考慮到持久化,Redis 的數(shù)據(jù)全部在內(nèi)存里,如果突然宕機(jī),數(shù)據(jù)就會全部丟失,因此必須有一種機(jī)制來保證 Redis 的數(shù)據(jù)不會因為故障而丟失,這種機(jī)制就是 Redis 的持久化機(jī)制
Redis 的持久化機(jī)制有兩種,第一種是RDB(快照)(Redis DataBase),第二種是 AOF(Append Only File)日志。
RDB原理:
redis 使用操作系統(tǒng)的多進(jìn)程(cow copy on write【寫時復(fù)制】)機(jī)制來實現(xiàn)快照的持久化;手動命令save,bgsave;配置文件設(shè)置.
fork 多進(jìn)程
調(diào)用glibc 的函數(shù) fork產(chǎn)生一個子進(jìn)程,快照持久化交個子進(jìn)程處理.子進(jìn)程剛剛產(chǎn)生時和父進(jìn)程共享代碼段和數(shù)據(jù)段,隨著父進(jìn)程修改操作持續(xù)進(jìn)行,more 共享頁面被分離出來,創(chuàng)建進(jìn)程變快;內(nèi)存增長,不會超過原來的2倍.另外一個 Redis 實例里冷數(shù)據(jù)占的比例往往是比較高的,所以很少會出現(xiàn)所有的頁面都會被分離,被分離的往往只有其中一部分頁面。每個頁面的大小只有 4K,一個 Redis 實例里面一般都會有成千上萬的頁面子進(jìn)程的數(shù)據(jù)沒有變化,可以安心的遍歷數(shù)據(jù),進(jìn)行序列化寫磁盤。
AOF原理
AOF 日志存儲的是 Redis 服務(wù)器的順序指令序列,AOF 日志只記錄對內(nèi)存進(jìn)行修改的指令記錄。隨著操作的進(jìn)行,指令文本會越來越大。需要對AOF 重寫,進(jìn)行日志瘦身,redis 提供bgrewriteaof 指令對AOF 進(jìn)行瘦身,原理是開辟一個子進(jìn)程對內(nèi)存進(jìn)行遍歷,序列化一個新的aof+這段時間的增量。Linux 的glibc 提供了 fsync(int fd) 強(qiáng)制從內(nèi)核緩存刷到磁盤,fsync 很慢,一般生產(chǎn)環(huán)境每隔一秒執(zhí)行一次,可配置。
redis 是先執(zhí)行指令再寫日志,leveldb\hbase 相反。
關(guān)于redis 運維的一些經(jīng)驗,不要在主節(jié)點進(jìn)行持久化,要在從節(jié)點進(jìn)行持久化,因為持久化是一個很耗時的IO操作。
關(guān)于混合持久化
為什么要進(jìn)行混合持久化?首先fork 動作需要copy 頁表,大內(nèi)存場景下阻塞server,百ms 或秒級 latency spike(延遲尖峰),比較適合每天定時的 全備場景。部分云廠商的方案比如:redis+rocksDB。從以下幾個方面來分析。
數(shù)據(jù)寫入
寫入Redis 內(nèi)存,并記錄增量,后臺線程異步應(yīng)用增量到RocksDB
數(shù)據(jù)可靠性
RocksDB 包含全量數(shù)據(jù),徹底避免Fork 調(diào)用,啟動時,從RocksDB 加載全量索引信息到內(nèi)存
數(shù)據(jù)讀取
數(shù)據(jù)在redis 內(nèi)存,直接讀取,數(shù)據(jù)不在Redis 內(nèi)存,整體換入Redis 再讀取,針對簡單命令,可以直接從Rocks DB 讀取。
關(guān)于RcoksDB,RcoksDB可嵌入的,持久型的key-value 存儲,
RocksDB項目起源于Facebook的一個實驗項目,該項目旨在開發(fā)一個與快速存儲器(尤其是閃存)存儲數(shù)據(jù)性能相當(dāng)?shù)臄?shù)據(jù)庫軟件,以應(yīng)對高負(fù)載服務(wù)。這是一個c++庫,可用于存儲鍵和值,可以是任意大小的字節(jié)流。它支持原子讀和寫。RocksDB具有高度靈活的配置功能,可以通過配置使其運行在各種各樣的生產(chǎn)環(huán)境,包括純內(nèi)存,Flash,硬盤或HDFS。它支持各種壓縮算法,并提供了便捷的生產(chǎn)環(huán)境維護(hù)和調(diào)試工具。
數(shù)據(jù)結(jié)構(gòu)編碼
每個key對應(yīng)一個RocksDB metakey,存儲key 的元數(shù)據(jù)。所有的metakey 相鄰存儲,啟動加載效率高,復(fù)雜key的每個元素在RocksDB 里面對應(yīng)一個Datakey,元素在RocksDB 相鄰存儲,訪問效率高。簡化String 類型,metakey 與Datakey 合并優(yōu)化IO。
數(shù)據(jù)交換模型(SWAP MODE)
內(nèi)存數(shù)據(jù)換出:后臺定期檢查是否超哥內(nèi)存使用閥值,根據(jù)訪問評率、大小等選擇key 預(yù)備淘汰。
磁盤數(shù)據(jù)換入:某個key 被頻繁讀取或有o(n) 的讀取操作,整個key 換入的RocksDB,加速訪問;key 寫入時,數(shù)據(jù)不再內(nèi)存,整體換入內(nèi)存;默認(rèn)后臺多線程異步換入。
主備同步&遷移
全量同步:使用RocksDB checkoutpoint 代替RDB,避免fork 系統(tǒng)調(diào)用,備接收到checkoutpoint 數(shù)據(jù)只需加載meta key
增量同步:從 checkoutpoint 對應(yīng)aof binlog 位點繼續(xù)增量同步,增量同步斷開,直接從斷開位置繼續(xù)同步,無需觸發(fā)全量同步。
一些其他的知識
管道
技術(shù)的本質(zhì):客戶端提供,pipline 包含多條指令;客戶端改變管道中的指令列表的讀寫順序,可以大幅節(jié)省IO 時間。自帶壓力測試工具 redis-beanchmark。普通 set 5w qps
小對象壓縮
如果redis 使用內(nèi)存不超過4G,使用32bit 編譯。