Redis 可載入模組系統
原文由 Salvatore Sanfilippo 于 發布,訂閱此部落格
這只是時間早晚的問題,但終究還是發生了。7 年前在 Redis 1.0 的發行說明中,我曾提到未來一個有趣的功能就是「可載入模組」。當時我對這個功能真的很感興趣,但這些年來,我對在 Redis 中加入可載入模組的想法越來越抱持懷疑態度。或許也是有充分理由的。
模組可以同時是系統最有趣的功能,也是最棘手的功能:版本之間的 API 不相容、低品質的模組導致系統當機、可擴充的系統缺乏明確主體性,都是可能出現的問題。所以,多年來我設法避免在 Redis 中加入模組,而 Lua 腳本則是延後加入模組的好工具。同時,多年使用腳本的經驗也證明,腳本是一種「組合」既有功能的方式,卻無法將系統的能力擴展到它原本並非為其設計的使用情境。
先前對模組的嘗試也顯示,將 Redis 與可載入模組結合時,主要的痛點之一在於模組與 Redis 核心的綁定方式。在許多方面,Redis 更像是一種程式語言,而非資料庫。要正確地擴充 Redis,模組需要能夠存取系統的內部 API。直接將 Redis 核心函式匯出給模組會帶來巨大的問題:模組會開始依賴 Redis 的內部細節。如果 Redis 核心持續演進,模組就得重寫。這要嘛會造成脆弱的模組生態系,要嘛會阻礙 Redis 核心的演進。Redis 的內部實作不可能停止演進,模組開發者也不可能不斷修改模組以跟上內部變化(這在過去某些熱門系統中就曾發生過,結果並不好)。
帶著這些教訓,我從卡塔尼亞出發飛往特拉維夫,準備在 Redis Labs 開會,討論未來幾個月的開發藍圖。我們討論的主題之一就是可載入模組。在飛行途中,我問自己是否有可能在真正將 Redis 核心與模組 API 解耦的同時,仍保有能直接操作 Redis 資料結構的低階存取能力。於是我立刻開始動手寫些東西。我想要的是未來極高程度的 API 相容性,讓今天寫的模組在 4 年後仍能用同樣的 API 運作,無論 Redis 核心如何改變。我也想要二進位相容性,讓 4 年前的模組甚至能在新版 Redis 中直接 *load* 並如預期運作,連重新編譯都不需要。
飛行結束抵達特拉維夫時,我已經在「modules」分支上有了可運作的雛形。我們一起討論了 API 該如何運作,最後大家一致認為,能夠直接操作 Redis 內部是一項基本功能。我們想達成的是,讓 Redis 開發者能夠建立出與原生 Redis 指令一樣強大、也一樣快速的指令。這光靠一個呼叫 Redis 指令的高階 API 是做不到的,那樣太慢也太受限。如果 Redis 模組系統只能做到 Lua 已經能做的事,那就沒有意義了。你必須能夠說:幫我取出這個 key 對應的值,它是什麼型別?對這個值執行這個低階操作。給我一個指向 sorted set 中這個位置的游標,移到下一個元素,以此類推。要打造一個作為這種低階存取中介層的 API 並不容易,但絕對是可行的。
回到家後,我立刻開始投入模組系統的開發。幾週後,我就有了一個功能已足以開發出有趣模組的原型,具備像是資料型別的低階存取、需要時可直接操作字串內部而不經封裝的字串 DMA、複寫 API、有趣的 sorted set 迭代器 API 等等。這看起來是個非常有前景的開始,不過這個專案有點算是「秘密」進行,因為一開始還不確定是否真的可行。而且我們也想避免在 API 極不穩定、隨時可能變動的階段,就讓大家一窩蜂開始開發模組。
這個過程的成果雖然尚未完成,但非常令人期待,所以今天我在 Redis Conference 2016 上宣布這項新功能,程式碼也剛推送到「unstable」分支。不過,讓我們先來看看 API 吧……
以下是一個模組能做什麼、以及如何運作的簡單範例。它實作了一個「list splice」操作,會將元素從一個 list 搬到另一個 list:
int HelloListSpliceAuto_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 4) return RedisModule_WrongArity(ctx);
RedisModule_AutoMemory(ctx);
RedisModuleKey *srckey = RedisModule_OpenKey(ctx,argv[1],
REDISMODULE_READ|REDISMODULE_WRITE);
RedisModuleKey *dstkey = RedisModule_OpenKey(ctx,argv[2],
REDISMODULE_READ|REDISMODULE_WRITE);
/* Src and dst key must be empty or lists. */
if ((RedisModule_KeyType(srckey) != REDISMODULE_KEYTYPE_LIST &&
RedisModule_KeyType(srckey) != REDISMODULE_KEYTYPE_EMPTY) ||
(RedisModule_KeyType(dstkey) != REDISMODULE_KEYTYPE_LIST &&
RedisModule_KeyType(dstkey) != REDISMODULE_KEYTYPE_EMPTY))
{
return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
}
long long count;
if ((RedisModule_StringToLongLong(argv[3],&count) != REDISMODULE_OK) ||
(count < 0))
{
return RedisModule_ReplyWithError(ctx,"ERR invalid count");
}
while(count-- > 0) {
RedisModuleString *ele;
ele = RedisModule_ListPop(srckey,REDISMODULE_LIST_TAIL);
if (ele == NULL) break;
RedisModule_ListPush(dstkey,REDISMODULE_LIST_HEAD,ele);
}
size_t len = RedisModule_ValueLength(srckey);
RedisModule_ReplyWithLongLong(ctx,len);
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx) {
if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"hello.list.splice.auto",
HelloListSpliceAuto_RedisCommand,
"write deny-oom",1,2,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
}我們花了很多心力來提供一個乾淨、且不易被誤用的 API。舉例來說,系統支援自動記憶體管理,讓指令運作的上下文會自動收集使用者未明確釋放的物件,以便在指令返回時視需要釋放。這讓撰寫模組變得簡單許多。
你可以在這裡找到 API 文件(還不完美,但已足以讓你熟悉):
https://github.com/antirez/redis/blob/unstable/src/modules/INTRO.md
API 參考文件則在這裡:
https://github.com/antirez/redis/blob/unstable/src/modules/API.md
許多簡單的指令範例在這裡:
https://github.com/antirez/redis/blob/unstable/src/modules/helloworld.c
API 目前還不完整也不穩定,將會隨著下一個 Redis 穩定版本(可能是 4.0)一起發布。不過它已經足以完成許多事情,我的同事們也已經做出了非常有趣的成果,從反向索引到驗證系統都有。在接下來的幾週裡,我們會補齊所有缺口,例如目前還沒有 Set 的低階 API,所以暫時還得使用 Call() 形式的 API。同樣地,迭代器目前也只為 sorted set 型別提供,以此類推。
但重點是,這個進程現在已經啟動,Redis 正在成為一個可擴充的系統。我認為這將賦予 Redis 使用者更多力量,讓他們在使用 Redis 來建模自身問題時,能夠比專案本身「跑得更快」,並且有一個重要的承諾:在 Redis 4.0 RC 推出之後,我們將在未來數年內絕不破壞 API,讓大家在模組上的心血不會白費。請注意,我們*可以改進* API,因為模組註冊時會要求指定某個 API 版本,所以我們有可能在維持向下相容的同時,發布新版本的 API。
很快就會有一個 Modules Directory,你可以用 redis-cli 將自己的模組註冊到一個使用 Redis 協定的伺服器上。很可惜來不及及時完成,但應該就只是幾週內的事了。
我對接下來將發生的事感到非常非常興奮!模組將會像客戶端那樣採用市集(Bazaar)模式,所以不會有所謂的「官方模組」。好的模組自然會被大家採用,而所有的模組都會列在 Redis 網站上,可能會依 GitHub 星星數之類的指標來排名。
我希望許多使用者能開始加入模組生態系,讓 Redis 能夠解決那些在核心中處理並不明智、卻非常適合用模組來解決的特定使用情境。
現在 API 還在可塑的階段,我需要你們的回饋。告訴我你們的想法吧!
隨機一篇部落格
留言
登入後參與討論