Redis Loadable Modules System

Salvatore Sanfilippo

Redis 可載入模組系統

早晚會有這一天,終於還是發生了。7 年前在 Redis 1.0 的發行說明中,我曾提到未來值得關注的有趣功能之一就是「loadable modules(可載入模組)」。當時我對這項功能非常感興趣,但這些年來,我對於在 Redis 中加入 loadable modules 的想法卻愈來愈懷疑。或許這是有充分理由的。

模組可能是系統中最有趣的功能,同時也可能是問題最多的功能:版本之間的 API 不相容、低品質的模組導致系統當機、可延伸系統缺乏明確定位,都是可能出現的問題。因此,多年來我設法避免在 Redis 中加入模組,而 Lua 腳本則是延後加入模組的不錯工具。同時,多年使用腳本的經驗也顯示,腳本是一種「組合」既有功能的方式,卻無法將系統能力擴展到原本設計未涵蓋的使用情境。

先前嘗試導入模組的經驗也顯示,混合 Redis 與 loadable modules 時的主要痛點之一,在於模組與 Redis 核心的綁定方式。在許多方面,Redis 更像是一種程式語言,而非資料庫。要妥善擴充 Redis,模組必須能夠存取系統的內部 API。若直接將 Redis 核心函式匯出給模組,會造成巨大的問題:模組會開始依賴 Redis 的內部細節。一旦 Redis 核心演進,模組就必須重寫。這要不是造就一個脆弱的模組生態系,就是阻礙了 Redis 核心的演進。Redis 的內部實作不可能停止演進,模組開發者也不可能不斷修改模組以跟上內部變化(過去某些熱門系統就曾發生過這種情況,結果並不理想)。

帶著這些教訓,我從卡塔尼亞出發飛往特拉維夫,準備在 Redis Labs 開會討論未來幾個月的發展藍圖。我們聊的主題之一就是 loadable modules。在飛行途中,我自問是否有可能在真正將 Redis 核心與 modules API 解耦的同時,仍保有能夠直接操作 Redis 資料結構的低階存取能力。於是我立刻開始動手寫些東西。我想要的是未來極高程度的 API 相容性,讓今天撰寫的模組在 4 年後仍能以相同的 API 運作,無論 Redis 核心如何變化。我也想要二進位相容性,讓 4 年前的模組甚至能在新版 Redis 中直接*載入*並如預期運作,完全不需要重新編譯。

飛行結束抵達特拉維夫時,我已經在「modules」分支中做出可運作的東西。我們一起討論了 API 應如何運作,最後大家一致認為,能夠直接操作 Redis 內部是一項基本功能。我們想要達成的,是讓 Redis 開發者能夠建立出與 Redis 原生指令一樣強大、也一樣快速的指令。這無法單靠呼叫 Redis 指令的高階 API 來完成,那樣太慢也太受限。如果 Redis 模組系統只能做到 Lua 已經能做的事,那就沒有意義了。你必須能夠說:幫我取得這個鍵對應的值,它是什麼型別?對這個值執行這項低階操作。給我在這個位置的有序集合中的游標,移到下一個元素,依此類推。要打造一個作為此類低階存取中介層的 API 並不容易,但絕對可行。

我回到家後立刻開始著手開發模組系統。幾週後,我就有了一個原型,其功能已足以開發出有趣的模組,具備像是資料型別的低階存取、需要時可直接操作字串內部而無需包裝的字串 DMA、replication API、有趣的 sorted set iterator API 等低階功能。看起來是個非常有前景的開始,不過這個專案有點算是「祕密」進行,因為一開始還不清楚是否可行。此外,我們也想避免大家在 API 極不穩定且隨時可能變動時就一頭栽進來開發模組。

這個過程的成果雖然尚未完成,但非常令人期待,因此我今天在 Redis Conference 2016 上宣布這項新功能,程式碼也剛剛推送到「unstable」分支。不過,讓我們先來看看 API……

以下是一個模組能做什麼、以及如何運作的簡單範例。它實作了一個「list splice」操作,可將元素從一個串列搬移到另一個串列:

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 stars 或類似指標來排名。

我希望許多使用者能開始成為模組生態系的一份子,讓 Redis 得以解決那些在核心中處理並不明智、卻非常適合用模組來解決的特定使用情境。

現在,在 API 仍具可塑性之際,我需要你的回饋。告訴我你的想法!

原文由 Salvatore Sanfilippo 發布

本文章由 muse-spark-1.2-contributor 進行翻譯