Redis 互換で25倍高速の Dragonfly を .NET で使ってみる

Redis はメモリ上でデータを管理するインメモリデータベースの一つで、主に高速なデータアクセスを要求される場面で使われています。当社でも Web API の高速化のため、キャッシュ用のデータベースとして Redis を使っています。

そんな Redis と互換性があり、25倍高速な「Dragonfly」が登場しました。開発元からアナウンスされていますが、ざっくり言うと「Redis は10年以上前のテクノロジーで最適化されたまま停滞していた」 ➡ 「マルチコアCPUやメモリ量など、現在のハードウェアに最適化して再実装して25倍高速に」ということらしいです。GitHubのリポジトリはこちら。宇宙最速を謳っています。

性能の検証は公式にベンチマークが掲載されていますし、いくつかの記事でも実際に検証しているものが見つかりましたのでそちらにお任せします。ということで今回は、「互換性あるって言っても実際どうなの?」「乗り換えるにはどうすれば?」という疑問を解決するため検証してみました。

RedisとDragonfly の環境を構築する

今回は docker-compose で環境構築します。1ファイルでもできなくはないですが、通常同時に使うことはないので別々に作成します。

まずは Redis。検証環境では既に6379のポートが使用中だったため16379に割り当てました。

version: '3'

services:
  redis:
    image: redis:latest
    container_name: redis
    volumes:
      - "./data:/data"
    ports:
      - 16379:6379

同様に Dragonfly も作成します。こちらのポートは26379にしました。

version: '3'

services:
  redis:
    image: docker.dragonflydb.io/dragonflydb/dragonfly:latest
    container_name: dragonfly
    volumes:
      - "./data:/data"
    ports:
      - 26379:6379

それぞれ起動します。

docker-compose up -d

無事起動しました。

Recreating redis ... done
Pulling redis (docker.dragonflydb.io/dragonflydb/dragonfly:latest)...
latest: Pulling from dragonflydb/dragonfly
d7bfe07ed847: Pull complete
55b840b10d9d: Pull complete
4f4fb700ef54: Pull complete
8b6940899aa8: Pull complete
ff09696caacb: Pull complete
Digest: sha256:c3c8da0fef408a80445ca7c89a81cc01d10dbb6a03d4ff0bb48bad9e1a4343ff
Creating dragonfly ... done

ASP.NET Core Web API で検証する

環境はVisual Studio 2022です。プロジェクトテンプレートで ASP.NET Core Web API を選択し、プロジェクトを作成します。

Nuget で以下のパッケージを追加します。

  • StackExchange.Redis.Extensions.AspNetCore
  • StackExchange.Redis.Extensions.Core
  • StackExchange.Redis.Extensions.System.Text.Json

テンプレートのコードを流用し、Redis を使うように修正します。まずは IRedisClientFactory をDIします。余談ですが、StackExchange.Redis.Extensions が IRedisClientFactory をDIするように変更されていことに気が付きました。8.0 からでしょうか。※7.0では IRedisCacheClient をDIしていた。

using StackExchange.Redis.Extensions.Core.Configuration;
using StackExchange.Redis.Extensions.System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var redisConfiguration = builder.Configuration.GetSection("Redis").Get<RedisConfiguration>();
builder.Services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Controller で IRedisClientFactory を受け取り、Getでキャッシュを使うように修正します。

using Microsoft.AspNetCore.Mvc;
using StackExchange.Redis.Extensions.Core.Abstractions;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private IRedisClientFactory _redisClientFactory { get; set; }

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(IRedisClientFactory redisClientFactory, ILogger<WeatherForecastController> logger)
        {
            _redisClientFactory = redisClientFactory;
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public async Task<IEnumerable<WeatherForecast>> GetAsync()
        {
            var redisClient = _redisClientFactory.GetDefaultRedisClient();
            var weatherForecasts = await redisClient.Db0.GetAsync<WeatherForecast[]>("WeatherForecasts");
            if (weatherForecasts is not null)
            {
                return weatherForecasts;
            }

            weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();

            await redisClient.Db0.AddAsync<WeatherForecast[]>("WeatherForecasts", weatherForecasts);

            return weatherForecasts;
        }
    }
}

appsettings.json に接続情報を追記します。まずは Redis に接続するようにポートに16379を指定しておきます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Redis": {
    "AllowAdmin": false,
    "Ssl": false,
    "ConnectTimeout": 6000,
    "ConnectRetry": 2,
    "Hosts": [
      {
        "Host": "localhost",
        "Port": "16379"
      }
    ],
    "Database": 0
  },
  "AllowedHosts": "*"
}

この状態でデバッグ実行し、Swaggerから /WeatherForecast を呼び出します。1回目の実行はキャッシュがないため新しく WeatherForecast が生成されます。この API は date は現在時刻をベースに、その他はランダムに設定されるため、呼ばれる度に異なるレスポンスを返しますが、2回目以降何度読んでも同じレスポンスが返ってくるためキャッシュが効いていることが確認できます。

[
  {
    "date": "2022-07-13T12:01:41.958173+09:00",
    "temperatureC": -11,
    "temperatureF": 13,
    "summary": "Bracing"
  },
  {
    "date": "2022-07-14T12:01:41.9586412+09:00",
    "temperatureC": 17,
    "temperatureF": 62,
    "summary": "Warm"
  },
  {
    "date": "2022-07-15T12:01:41.9586444+09:00",
    "temperatureC": 53,
    "temperatureF": 127,
    "summary": "Balmy"
  },
  {
    "date": "2022-07-16T12:01:41.9586447+09:00",
    "temperatureC": -13,
    "temperatureF": 9,
    "summary": "Freezing"
  },
  {
    "date": "2022-07-17T12:01:41.9586449+09:00",
    "temperatureC": -12,
    "temperatureF": 11,
    "summary": "Cool"
  }
]

さて、本題の互換性や乗り換えの検証です。appsettings.json を編集し、Dragonfly へ切り替えます。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Redis": {
    "AllowAdmin": false,
    "Ssl": false,
    "ConnectTimeout": 6000,
    "ConnectRetry": 2,
    "Hosts": [
      {
        "Host": "localhost",
        "Port": "26379"
      }
    ],
    "Database": 0
  },
  "AllowedHosts": "*"
}

こちらも同様に、2回目以降同じレスポンスが返ります。Redis互換を謳うだけはあり、プログラムや依存ライブラリなど一切変更することなく、接続情報の設定だけで切り替えられました。今回は切り替えながら検証したかったためRedis と Dragonfly を同時に起動していましたが、Redis を落として同じポートで Dragonfly を起動したら設定情報すら変える必要はありません。

[
  {
    "date": "2022-07-13T13:38:14.2334898+09:00",
    "temperatureC": 40,
    "temperatureF": 103,
    "summary": "Hot"
  },
  {
    "date": "2022-07-14T13:38:14.2339419+09:00",
    "temperatureC": 20,
    "temperatureF": 67,
    "summary": "Sweltering"
  },
  {
    "date": "2022-07-15T13:38:14.2339445+09:00",
    "temperatureC": -13,
    "temperatureF": 9,
    "summary": "Freezing"
  },
  {
    "date": "2022-07-16T13:38:14.2339447+09:00",
    "temperatureC": 14,
    "temperatureF": 57,
    "summary": "Balmy"
  },
  {
    "date": "2022-07-17T13:38:14.2339448+09:00",
    "temperatureC": 7,
    "temperatureF": 44,
    "summary": "Balmy"
  }
]

まとめ

Redis 互換という謳い文句通り、アプリケーションに何の変更を加えることもなく、入れ替えることができました。これで25倍高速となれば入れ替えない理由はありません。もちろんシステム構成を含む環境によって実測は大きくことなりますし、API としてどの程度高速になるかは測定・検証する必要があります。これまで Redis を採用したような場面で、これからは Dragonfly を選択肢に加えてみてはいかがでしょうか。

※ライセンスがBSL 1.1になっているため注意しましょう。

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA