Tair rdb(redis存儲引擎)實現介紹


淘寶那岩曾經在淘寶核心系統團隊博客上介紹過Tair ldb的實現,本文將嘗試着介紹rdb(redis存儲引擎)的實現。

Tair是淘寶開源的分布式KV緩存系統,內部將功能模塊化,抽離出底層存儲細節,可以接入不同的存儲引擎。redis是一個開源的、高效的key-value存儲,提供了strings、hashs、lists、sets、sorted sets等多種高級數據結構。redis作為Tair的存儲引擎接入,稱為rdb。

本文參考的源碼為Tair rdb實現最新的release版(這里),對照的redis實現為2.4.10版。

Tair首先是一個分布式的框架,有一系列策略滿足CAP(數據備份,遷移復制等)。另外,還有針對應用場景的功能特性(namespace,數據過期時間,原子計數等)。接入redis時做了比較多的修改,下邊逐一介紹。對redis內部實現感興趣的讀者,可以看這里(或這里)。

1.配置修改

為了達到配置統一管理的目的,tair rdb去掉了redis自帶的配置文件。它將redis所需的配置統一放在tair配置文件的TAIRRDB_SECTION下邊。當前保留的配置文件項有:

RDB_UNIT_NUM       一個命名空間下的單元划分,默認值是16,決定了並發粒度,下邊會有介紹;

RDB_AREA_GROUP_NUM     單機存儲引擎的命名空間范圍,默認值是512,這個值決定了單機上最多可以有多少個命名空間同時存在(命名空間類似於db name,使用中可以映射到項目,比如可以在area 0中保存項目名到命名空間id的映射,后續就可以在特定的項目內查找key-value);

RDB_MAXMEMORY   單機上rdb存儲引擎占用內存的上限值,達到上限后將依據maxmemory_policy的不同采取不同的行為;

RDB_MAXMEMORY_POLICY  達到內存上限后進行的操作,可以直接返回錯誤,也可以進行LRU,可以參考redis的設置;

RDB_MAXMEMORY_SAMPLES 與LRU實現相關的一個配置參數,可以參考redis的配置;

RDB_LIST(/HASH/ZSET/SET)_MAX_SIZE list、hash、sorted set、set數據結構value最大占用空間的大小,默認均是8k(感覺偏小了);

需要根據自身項目需求調整上述配置。

2.加鎖的邏輯

redis的主邏輯是一個單進程、單線程的程序,其並發是通過操作系統結合事件驅動框架(ae)實現的。但在rdb中,剝離了redis的網絡層,只保留了內部存儲實現,而上層是由多個線程調用的,因此就涉及到加鎖的邏輯。

我們先看一下tair的mdb存儲引擎(類memcache存儲引擎)中遇到的問題,下圖是一張調用邏輯圖:

所有的線程(無論是否同一個area,也不區分讀寫)都在爭用同一把鎖,存在嚴重的瓶頸(我們在生產環境中已經遇到了這個問題)。

rdb的調用邏輯與圖中所示基本是一致的,但在bottleneck部分做了優化。rdb利用了redis可以同時存在多個db的特性,一次性提供了area_group_num*unit_num個db,所有的操作都局限在db內,將鎖的粒度變成了以unit為單位,相當於將上圖中的單一的藍色框的部分分解成了大量的管道並行處理,同時,不同area之間實現互不干擾。以默認配置舉例,其加鎖的粒度由所有area爭用一把鎖,變成了一個area划分成16個unit,只有落在同一個unit中的查詢才會競爭一把鎖,大大的提高了並發度。

其加鎖的index通過下述的宏來查找,其中_hashcode為key值的hash:

76 #define get_redis_db(_area, _hashcode) ((_area) * context->get_unit_num() + (_hashcode) % context->get_unit_num())

3.定時任務的邏輯

redis的定時任務是由事件驅動框架實現的,其邏輯在redis.c的serverCron函數中實現,可以參考這篇博客

rdb的定時任務改成使用后台線程的方式執行,同redis默認配置一樣,每100ms執行一次。定時任務以unit為單位,執行定時任務前會獲取對應unit的鎖,其實現邏輯是redis定時任務的一個子集,去掉了和持久化相關的部分。

4.持久化的邏輯

Update: ForFunny(rdb作者)注   沒有使用2.4.x中的bio主要是原來是基於2.2做的rdb,改成bio的略顯麻煩 中間嘗試改過一次,需要涉及修改的東西比較多,所以先上2.2的方式。和redis不同的是這邊不會使用子進程來說這些事情,而是線程來做的,主要是防止cow可能的內存上升。

rdb提供了dump和load的接口,采用后台線程的方式,實現類似於redis的snapshot操作,但比較奇怪的是,最新的release版本中,這部分代碼都是被注釋掉的,猜測或許是這部分代碼的測試還不完善,也可能是因為rdb的定位就是提供豐富數據結構的cache服務,可靠性保證通過Tair中間件層提供的機制實現。由於沒有持久化的邏輯,相應的代碼中也去掉了redis精巧設計的bio。

1266 void* dump_db_thread(void* argv) {
1267     pthread_detach(pthread_self());
1268     dumpThreadInfo* info = (dumpThreadInfo *)argv;
1269     //rdbSave(info->server, info->filename, info->area);
1270     free_dump_thread_info(info);
1271     return NULL;
1272 }
1274 void* load_db_thread(void* argv) {
1275     pthread_detach(pthread_self());
1276     loadThreadInfo* info = (loadThreadInfo *)argv;
1277     //rdbLoad(info->server, info->filename, info->area);
1278     free_load_thread_info(info);
1279     return NULL;
1280 }

5.豐富的操作

與mdb相比,rdb從redis繼承了豐富的操作,包括list、hash、sorted set、set(與mdb相比,list不再那么ugly)。

但由於分布式的關系,不同的set很有可能落在不同的redis db實例中,因此不再支持集合的交(SINTER/ZINTERSTORE) 並(SUNION)操作。

6.對版本的支持

Update: ForFunny(rdb作者)注 關於version rdb不止有key有version的,db,value都是有的,現在主要要是hash結構的中的field的version來實現根據field刪數據的功能和清清namespace的作用。

memcache中有版本的概念,tair中同樣有版本的概念。我們看一下tair中引入版本的用途,其思路借鑒自Dynamo的vector clocks:

“Version支持

Tair中的每個數據都包含版本號,版本號在每次更新后都會遞增。這個特性有助於防止由於數據的並發更新導致的問題。

比如,系統有一個value為“a,b,c”,A和B同時get到這個value。A執行操作,在后面添加一個d,value為“a,b,c,d”。B執行操作添加一個e,value為”a,b,c,e”。如果不加控制,無論A和B誰先更新成功,它的更新都會被后到的更新覆蓋。

Tair無法解決這個問題,但是引入了version機制避免這樣的問題。還是拿剛才的例子,A和B取到數據,假設版本號為10,A先更新,更新成功后,value為”a,b,c,d”,與此同時,版本號會變為11。當B更新時,由於其基於的版本號是10,服務器會拒絕更新,從而避免A的更新被覆蓋。B可以選擇get新版本的value,然后在其基礎上修改,也可以選擇強行更新。”

redis自身沒有版本的概念,antirez曾就此進行過討論,rdb將版本引入,同key存在一塊,同時修改了存儲部分的代碼,加入了對版本的支持。

 

本文作者水平有限,而且更多的是從源碼的角度去反推設計,因此對於不正確和不完善的地方,還望大家指出,多多討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM