Redis Loadable Modules System

Salvatore Sanfilippo

Redis ローダブルモジュールシステム

時間の問題でしたが、ついにその時が来ました。7年前のRedis 1.0のリリースノートで、将来の興味深い機能の一つとして「ロード可能なモジュール(loadable modules)」に触れました。当時はこの機能にとても興味を持っていましたが、年を重ねるにつれ、Redisにロード可能なモジュールを追加するという考えにはどんどん懐疑的になっていきました。おそらく、それにはもっともな理由があったのだと思います。

モジュールは、システムにとって最も面白い機能であると同時に、最も問題の多い機能にもなり得ます。バージョン間のAPIの非互換性、品質の低いモジュールによるシステムクラッシュ、拡張可能であるがゆえにシステムのアイデンティティが失われることなど、起こり得る問題はさまざまです。そこで私は長年、なんとかRedisへのモジュール追加を避けてきました。その間、Luaスクリプトは追加を先延ばしするための良い手段でした。一方で、スクリプトを長年使ってきた経験から分かったのは、スクリプトは既存の機能を「組み合わせる」ための手段ではあっても、想定されていないユースケースに向けてシステム自体の能力を拡張する手段にはならないということです。

過去のモジュール化の試みからも、Redisとロード可能なモジュールを組み合わせる際の大きな課題の一つは、モジュールとRedisコアの結びつき方にあることが分かりました。多くの点でRedisはデータベースというよりプログラミング言語に近い存在です。Redisを適切に拡張するには、モジュールがシステムの内部APIにアクセスできる必要があります。しかし、Redisコアの関数をそのままモジュールに公開してしまうと、大きな問題が生じます。モジュールがRedisの内部実装の詳細に依存するようになるからです。Redisコアが進化すれば、モジュールは書き直しを迫られます。その結果、壊れやすいモジュールエコシステムが生まれるか、Redisコアの進化そのものが止まってしまいます。Redisの内部は進化を止めるわけにはいきませんし、モジュール開発者が内部実装の変化に追従し続けることもできません(過去に一部の有名なシステムでそうしたことが起き、芳しくない結果を招きました)。

そうした教訓を胸に、私はカターニアを発ち、テルアビブへ向かいました。今後数か月のロードマップについて話し合うため、Redis Labsでのミーティングに参加するためです。議題の一つがロード可能なモジュールでした。フライトの最中、私は考えました。RedisコアとモジュールAPIを真に分離しつつ、Redisのデータ構造を直接操作できる低レベルなアクセスは維持できないだろうか、と。そこで私はすぐにコーディングを始めました。目指したのは、将来にわたる徹底的なAPI互換性でした。今日書かれたモジュールが、Redisコアがどう変わっても4年後も同じAPIで動き続けるようにしたかったのです。さらにバイナリ互換性も求めました。4年前のモジュールが再コンパイルすら必要なく、そのまま新しいバージョンのRedisで読み込まれ、期待通りに動作するようにしたかったのです。

フライトが終わりテルアビブに到着したときには、すでに「modules」ブランチで動作するものができていました。私たちはAPIをどう設計すべきか話し合い、最終的に全員が、Redisの内部を直接操作できることが不可欠な機能だという点で合意しました。私たちが目指したのは、Redisの開発者がネイティブコマンドと同等の機能を持ち、同等の速度で動くコマンドを作れるようにすることでした。これはRedisコマンドを呼び出すだけの高レベルなAPIでは実現できません。遅すぎますし、制約も多すぎます。Luaでできることしかできないモジュールシステムを作っても意味がありません。必要だったのは、「このキーに紐づく値を取得して、型は何か教えてくれ。この値に対してこの低レベルな操作を実行してくれ。ソート済みセットのこの位置からカーソルを取得して、次の要素に進んでくれ」といったことが言える環境です。そうした低レベルアクセスのための中間層として機能するAPIを作るのは難しいことですが、確実に可能です。

帰国後、すぐにモジュールシステムの開発に取りかかりました。2週間ほどで、興味深いモジュールを開発できるだけの機能を備えたプロトタイプができあがりました。データ型への低レベルアクセス、必要に応じてラッパーを介さず文字列の内部を直接操作できる文字列のDMA、レプリケーションAPI、興味深いソート済みセットのイテレータ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を使う必要があります。同様に、イテレータも今のところソート済みセット型でのみ提供されています。

しかし重要なのは、このプロセスがすでに始まり、Redisが拡張可能なシステムになりつつあるということです。これはRedisユーザーにより大きな力を与えることになると思います。Redisを使って自身の課題をモデル化する際に、プロジェクト本体よりも「速く」進んでいく力です。そして大きな約束があります。Redis 4.0のRC版が出た後は、何年にもわたってAPIを壊すことは決してしないという約束です。モジュールのために費やした労力が無駄にならないようにします。なお、APIは*改善*していくことが可能です。モジュールの登録時に特定のAPIバージョンを指定する仕組みになっているため、後方互換性を保ちつつ、新しいバージョンのAPIをリリースしていくことができるのです。

まもなく、redis-cliを使ってRedisプロトコルで通信するサーバーにモジュールを登録できるModules Directoryが公開される予定です。残念ながら今回は間に合いませんでしたが、あと数週間の問題です。

これから何が起こるのか、とてもとても楽しみにしています!モジュールはクライアントと同様にバザールモデルで運営され、「公式モジュール」というものは存在しません。優れたものは必ず使われるようになりますし、すべてRedisのサイトに掲載され、おそらくGitHubのスター数などでランク付けされることになるでしょう。

多くのユーザーがモジュールエコシステムの一員となり、コアの中で解決するのは賢明ではないものの、モジュールにはぴったり合うような、非常に特化したユースケースをRedisで解決できるようになることを願っています。

APIがまだ柔軟に変更できる今、皆さんからのフィードバックが必要です。ぜひご意見をお聞かせください!

原文は Salvatore Sanfilippo により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。