CloudStructures 1.0 - StackExchange.Redis対応、RedisInfoタブ(Glimpse)

CloudStructures、というRedisライブラリを以前に作ってたわけなのですが(CloudStructures - ローカルとクラウドのデータ構造を透過的に表現するC# + Redisライブラリ)、2013年末にGlimpseプラグインを追加してから一切音沙汰がなかった。私お得意の作るだけ作って放置パターンか!と思いきや、ここにきて突然の大更新。APIも破壊的大変更祭り。バージョンもどどーんと上げて1.0。ほぅ……。

一番大きいのが、ベースにしてるライブラリがBookSleeveからStackExchange.Redisになりました。StackExchange.RedisはBookSleeveの後継で、そしてAzure Redis Cacheのドキュメントでもマニュアルに使用されているなど、今どきの.NETにおけるRedisクライアントのデファクトの位置にあると言ってよいでしょう。当然、移行する必要があったんですが腰が重くて……。

APIを変えた理由は、以前は「ローカルとクラウドのデータ構造を透過的に表現」というのに拘ってIAsyncCollection的に見せるのに気を配ってたんですが、Redis本来のコマンド表現と乖離があって、かなり使いづらかったんでやめたほうがいーな、と。メソッド名が変わっただけで使い方は一緒なんですが、とりあえずRedisのコマンド名が露出するようになりました。ま、このほうが全然イイですね。抽象化なんて幻想だわさ、特に名前の。

CloudStructuresの必要性

こんな得体のしれない野良ライブラリなんて使いたくねーよ、StackExchange.Redisを生で使えばいいじゃん。と、思う気持ちは至極当然でまっとうな感覚だと思います。私もそう思う。そして、単純なStringGet/Setぐらいしか使わないならそれでOKです、本当にただのキャッシュストアとして使うだけならば。しかし、本気でRedisを使い倒す、本気でRedisの様々なデータ構造を活用していこうとすると、StackExchange.Redisを生で使うのは限界が来ます。戻り値のオブジェクトへのマッピングすらないので、そこら中にSeiralize/Deserializeしなければならなくなる。ADO.NETのDbDataReaderを生で使うようなもので、そうなったら普通はなんかラップするよね?ADO.NETにはDapperのようなMicro ORMからEntity FrameworkのようなフルセットのORMまである。StackExchange.Redisが生ADO.NETを志向するならば(これは作者も言明していて、付随機能は足さない方針のようです)ならば、そこにO/R(Object/Redis)マッパーが必要なのは自然のことで、それがCloudStructuresです。

CloudStructuresが提供するのは自動シリアライズ/デシリアライズ、キーからの分散コネクション(シャーディング)、コマンドのロギング、Web.configからの接続管理、そしてGlimpse用の各種可視化プロファイラーです。元々、というか今もCloudStructuresはうちの会社でかなりヘヴィに使ってて(このことは何度か記事でも推してます、技評のグラニがC#にこだわる理由とか)、コマンドのロギングとかは執拗に拘ってます。今回はそうした長い利用経験から、やっぱイケてない部分も沢山あったので徹底的に見直しました。

シャーディングは、StackExchange.RedisはそもそもConnectionMultiplexerという形で内部で複数の台への接続を抱えられるんですが、これはどちらかというと障害耐性的な機能(Master/Slaveや障害検知時の自動昇格など)が主なので、Memcached的なクライアントサイドでの分散はBookSleeveの時と変わらず持っていません。なので引き続きシャーディングはCloudStructures側の機能として提供しています。

そもそもRedisが必要かどうかだと、んー、私としては規模に関わらず絶対に入れたほうがいいと思ってます。RDBMSの不得意なところを綺麗に補完できるので、RDBMSだけで頑張るよりも、ちょっとしたとこに使ってやると物凄く楽になると思います。導入もAzure Redis CacheやAWSのElastiCache for Redisのようにマネージドのキャッシュサービスが用意されているので、特にクラウド環境ならば簡単に導入できますしね。

使い方の基本

RedisSettingsまたはRedisGroupを保持して、各データ構造用のクラスをキー付きで作って、メソッド(全部async)を呼ぶ、です。

// 設定はスタティックに保持しといてください
public static class RedisServer
{
    public static readonly RedisSettings Default = new RedisSettings("127.0.0.1");
}

// こんなクラスがあるとして
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// RedisStringという型を作って... (RedisSettings.Default.String<Person>("key")でも作れます)
var redis = new RedisString<Person>(RedisServer.Default, "test-string-key");

// コマンドをがしがし呼ぶ、何があるかはIntelliSenseで分かる
await redis.Set(new Person { Name = "John", Age = 34 });

// 取得もそのまま呼ぶ 
var copy = await redis.Get();

// Listも同じ感じ
var list = new RedisList<Person>(RedisServer.Default, "test-list-key");
await list.LeftPush(new[] { new Person { Name = "Tom" }, new Person { Name = "Mary" } });

var persons = await list.Range(0, 10);

難しいところはなく、割と直感的。StackExchange.Redisのままだと、特にListが辛かったりするんで、相当楽になれるかな。

メソッド名は基本的にStackExchange.Redisのメソッド名からデータ構造名のプリフィックスとAsyncサフィックスを抜いたものになってます。例えばSetAddAsync()はRedisSet.Add()になります。SetAddなんて最悪ですからね、そのまんま扱いたくない名前。Asyncをつけるかどうかはちょっと悩ましかったんですが、まぁ全部Asyncだしいっか、と思ったんで抜いちゃいました。

他の特徴としては全てのセット系のメソッドにRedisExpiryという引数を足してます。これは、SetのついでにExpireを足すって奴ですね。標準だとStringSetぐらいにしかないんですが、元々Redisは個別にExpireを呼べば自由につけれるので、自動でセットでつけてくれるような仕組みにしました。なんだかんだでExpireはつける必要があったりして、今までは毎回Task.WhenAllでまとめててたりしてたんですがすっごく面倒だったので、これで相当楽になれる、かな?

RedisExpiryはTimeSpanかDateTimeから暗黙的変換で生成されるので、明示的に作る必要はありません。

var list = new RedisList<int>(settings);
await list.LeftPush(1, expiry: TimeSpan.FromSeconds(30));
await list.LeftPush(10, expiry: DateTime.Now.AddDays(1));

こんな感じ。これは、StackExchange.RedisがRedisKey(stringかbyte[]から暗黙的に変換可能)やRedisValue(基本型に暗黙的/明示的に変換可能)な仕組みなので、それに似せてみました。違和感なく繋がるのではないかな。

コンフィグ

Web.configかapp.cofingから設定情報を引っ張ってこれます。トランスフォームとかもあるので、なんのかんのでWeb.configは重宝しますからねー、あると嬉しいんじゃないでしょうか。

<configSections>
    <section name="cloudStructures" type="CloudStructures.CloudStructuresConfigurationSection, CloudStructures" />
</configSections>

<cloudStructures>
    <redis>
        <group name="Cache">
            <!-- Simple Grouping(key sharding) -->
            <add connectionString="127.0.0.1,allowAdmin=true" db="0" />
            <add connectionString="127.0.0.1,allowAdmin=true" db="1" />
        </group>
        <group name="Session">
            <!-- Full option -->
            <add connectionString="127.0.0.1,allowAdmin=true" db="2" valueConverter="CloudStructures.GZipJsonRedisValueConverter, CloudStructures"  commandTracer="Glimpse.CloudStructures.Redis.GlimpseRedisCommandTracer, Glimpse.CloudStructures.Redis" />
        </group>
    </redis>
</cloudStructures>

こんな感じに定義して、

public static class RedisGroups
{
    // load from web.config
    static Dictionary<string, RedisGroup> configDict = CloudStructures.CloudStructuresConfigurationSection
        .GetSection()
        .ToRedisGroups()
        .ToDictionary(x => x.GroupName);

    // setup group
    public static readonly RedisGroup Cache = configDict["Cache"];
    public static readonly RedisGroup Session = configDict["Session"];

    static RedisGroups()
    {
        // 後述しますがGlimpseのRedisInfoを有効にする場合はここで登録する
        Glimpse.CloudStructures.Redis.RedisInfoTab.RegisiterConnection(new[] { Cache, Session });
    }
}

こんな風にstatic変数に詰めてやると楽に扱えます。

シリアライズ

CloudStructuresは基本型(int, double, string, etc)はそのまま格納し、オブジェクトはシリアライザを通します。シリアライザとして標準ではJSONシリアライザのJilを使っています。理由は、速いから。JilはJSON.NETと違って、JsonReader/Writerも提供しないし、複雑なカスタムオプションもフォールバックもありません(多少の(特にDateTime周りの)オプションはありますが)。単純にJSONをシリアライズ/デシリアライズする、もしくはdynamicで受け取る。それだけです。まぁ、CloudStructuresの用途には全然合ってる。

以前はprotobuf-netを使っていたんですが、今後はやめようと思ってます。理由は、DataMemberをつけて回るのが面倒だから、ではなくて、空配列/空文字列/nullのハンドリングが凄く大変だったり(ネストしたオブジェクトの空配列がデシリアライズしたらnullになってた、とかね……これは正直ヤバすぎた)、バージョニング(特にEnumの!)が辛かったり、型がないとデシリアライズできないのでちょっとしたDumpすらできなかったりと、実運用上クリティカルすぎる案件が多くてそろそろもう無理。

かわりに、ではないですが圧縮することを提案します。CloudStructuresは標準でGZipJsonRedisValueConverterというものも用意していまして、それに差し替えることでJSONをGZipで圧縮して格納/展開します。圧縮は、特にデカい配列を突っ込んだりするときに物凄く効きます。めちゃくちゃ容量縮みます。protobufにせよmsgpackにせよ、シリアライザは圧縮、ではないんで、バイナリフォーマットとして小さくはなっても、配列にたいしてめちゃくちゃ縮むとかそういうことは起こり得ません(勿論、別にmsgpack+GZipとか併用するのは構わないけれど)。

圧縮の欠点は圧縮なんで、圧縮/解凍にそれなりにパフォーマンスを取られること。と、いうわけでCloudStructuresではLZ4で圧縮するものも用意しました。LZ4はfastest compression algorithmということで、GZipと比べて数倍、圧縮/解凍が速い、です(ただしサイズ自体はGZipよりは縮まない)。この手の用途ではかなり適しているかなー、と。LZ4のライブラリはLZ4 for .NETを用いてます。

インストールはNuGetから入れてもらった後に、LZ4JsonRedisValueConverterに差し替えるだけ。

ふつーはそのまま生JSON、気にしたいけど色々入れたくない人はGZip、エクストリームに頑張ってみたい人はLZ4を選べばいいと思います。更にもっとやりたい人はObjectRedisValueConverterBaseを継承して、自作のRedisValueConverterを作ってみてくださいな。

Glimpseプラグイン

もはやASP.NET開発でGlimpseは絶対に欠かせません。使わないのはありえないレベル。あ、MiniProfilerはもういらないので投げ捨てましょう。というわけでCloudStructuresはGlimpse用のプラグインをしっかり用意してあります。相当気合入れて作りこんであるので、これのためにもRedis使うならCloudStructuresで触るべき、と言えます。マジで。

インストールはNuGetから本体とは別に。それとGlimpseを使う場合は、commandTracerにGlimpseRedisCommandTracerを渡しておいてあげてください。またRedisInfoで情報を出す場合、接続文字列でallowAdminをtrueにしておく必要があります。

<add connectionString="127.0.0.1,allowAdmin=true" db="0" commandTracer="Glimpse.CloudStructures.Redis.GlimpseRedisCommandTracer, Glimpse.CloudStructures.Redis" />

まず、Timeline。

コマンドの並列実行具合がしっかりタイムラインで確認できます。

Redisタブ。

コマンド名、キー名、送受信オブジェクトのダンプとサイズ、Expire時間と処理にかかった時間、そしてキーとコマンドで重複して発行してたら警告。これを見れば一回のページリクエストの中でどうRedisを使ったかが完全に分かるようになってます。不足してる情報は一切なし、とにかく全部出せる仕組みにしました。

最後にRedisInfoタブ。RedisInfoタブを使うには、最初に言ったallowAdmin=trueにすることと、もう一つ、最初に情報表示に使うRedisGroupを登録しておく必要があります。

Glimpse.CloudStructures.Redis.RedisInfoTab.RegisiterConnection(new[] { Cache, Session });

ServerInfoからCmdStat、コンフィグ、クライアント側のコンフィグやコネクションの状態を全部表示します。全部。全部。出せそうな情報は全部収集してきてます。こういうの何気に結構地味に相当大事だったりしますのよ、特にRedisサーバーの情報やコンフィグなんて普段は見ないですからね、こうして超絶カジュアルに見れるっての、かなりありがたい。

まとめ

そんなわけで凄く良くなったんで、かなりお薦めデス。ネーミングが直球なものしか付けないことの多い私にしては、CloudStructuresってライブラリ名はかなりカッコイイという点でもお薦めですね!

問題は旧CloudStructuresとの互換性が、かなり無いので既に使ってる場合は移行が大変ってことデスネ。うちはどうしたんだって?移行してないよ!どーしようかなぁ、うーん、そこはちょっとかなり悩ましい……。

まぁCloudStructuresを使うかどうかはともかくとして、RedisはC#界隈でももっとばんばん使われて欲すぃですねー、そして使うならStringGet/Setだけじゃもったいない。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive