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互換性だった。今日書かれたモジュールが、Redisコアがどれだけ変化したとしても、同じAPIのまま4年後にも動き続けるような互換性だ。さらにバイナリ互換性も求めた。4年前に作られたモジュールが、再コンパイルすら必要とせず、そのまま新しいバージョンのRedisにロードして期待通りに動作するようにしたかったのだ。
フライトが終わりテルアビブに着く頃には、すでに「modules」ブランチで動くものができていた。皆でAPIをどうすべきか議論し、最終的にRedisの内部を直接操作できることが不可欠な機能だということで全員が一致した。私たちが実現したかったのは、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をリリースしていくことができるのだ。
近いうちにModules Directoryも公開される予定だ。そこではredis-cliを使って、Redisプロトコルを話すサーバーに自分のモジュールを登録できる。残念ながら今回は間に合わなかったが、あと数週間の問題だ。
今後何が起こるのか、とてもとてもワクワクしている! モジュールはクライアントと同じようにバザールモデルを採用するので、「公式モジュール」というものは存在しない。良いものは必ず使われるようになるし、すべてRedisのサイトに掲載される予定だ。おそらくGitHubのスター数などでランク付けされるような形になるだろう。
多くのユーザーがモジュールエコシステムの一員となり、コアの中で解決するには賢明とは言えないが、モジュールにはうってつけの、非常に特化したユースケースをRedisで解決できるようにしてくれることを願っている。
APIがまだ柔軟に変更できる今、皆さんからのフィードバックが必要だ。ぜひ意見を聞かせてほしい!
記事をランダムに読む
コメント
ログインしてコメントする