CLOVER🍀

That was when it all began.

Redis/memcached互換のDragonflyをRedisとして試す

これは、なにをしたくて書いたもの?

Redis/memcached互換を実現しつつ、Redisの25倍高速と謳われているDragonflyというインメモリデータストアがあるようです。

Redis互換で25倍高速とする「Dragonfly」が登場。2022年の最新技術でインメモリデータストアを実装 - Publickey

ちょっと気になっていたので、今回はDragonflyをRedisとして使って試してみようと思います。

Dragonfly

Dragonflyは、RedisおよびmemcachedAPIと互換性があり、Redisの25倍高速と謳っているインメモリデータストアです。

Dragonfly is a modern in-memory datastore, fully compatible with Redis and Memcached APIs. Dragonfly implements novel algorithms and data structures on top of a multi-threaded, shared-nothing architecture. As a result, Dragonfly reaches x25 performance compared to Redis and supports millions of QPS on a single instance.

Dragonfly

GitHub - dragonflydb/dragonfly: A modern replacement for Redis and Memcached

マルチスレッドで動作するシェアードナッシングアーキテクチャ上で新しいアルゴリズムとデータ構造を実装したもの、ということらしいです。

CおよびC++で実装されているみたいですね。

ライセンスは、「Dragonfly Business Source License 1.1」。

https://github.com/dragonflydb/dragonfly/blob/v0.6.0/LICENSE.md

BSL 1.1をベースに追加条件を付けたライセンスで、サービスとして他者提供することは禁じたもののようです。

Redisおよびmemcachedとのベンチマーク比較は、こちらを参照。

Benchmarks

Dragonflyに関するバックグラウンドは、こちらに書かれています。

Background

設計上の決定事項は、こちら。

Design decisions

Redisとの互換性(Dragonfly 0.6.0時点)

DragonflyがサポートしているRedisのコマンドは、こちらに記載があります。
※情報確認時点でのDragonflyのバージョンは0.6.0

Platform

見るとわかりますが、DragonflyはすべてのRedisコマンドをサポートしているわけではないようです。

クラスタリングに関するコマンドもサポートしていません。

クラスタリングに関してはissueもありましたが、対応しそうな雰囲気はなさそうですね。当面は、シングルノードでの利用になるでしょう。

Do you support clustering? · Issue #165 · dragonflydb/dragonfly · GitHub

memcachedAPIに関する互換性はわかりませんでしたが、Redisの方だけでいいかなと思ったので特に追っていません。

情報を眺めるのはこれくらいにして、実際に使ってみましょう。

環境

今回は、Dragonflyを動作させる環境と、Dragonflyにアクセスする環境の2つでそれぞれ別に用意します。

Dragonflyは、こちらの環境で動作させます。Ubuntu Linux 22.0.4 LTSです。このサーバーのIPアドレスは、192.168.121.124とします。

$ uname -srvmpio
Linux 5.15.0-43-generic #46-Ubuntu SMP Tue Jul 12 10:30:17 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
Codename:       jammy

Dragonflyにアクセスする環境は、Ubuntu Linux 20.04 LTSです。

$ uname -srvmpio
Linux 5.4.0-122-generic #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
Codename:       focal

Dragonflyへのアクセスは、Javaから行うことにします。

$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-122-generic", arch: "amd64", family: "unix"

Dragonflyをインストールする

では、Dragonflyをインストールします。今回使うDragonflyは0.6.0です。

Dragonflyを使うには、Dockerコンテナを使う、ビルド済みのバイナリを使用する、ソースコードからビルドする、のどれかを選択します。

Running the server

今回は、ビルド済みバイナリを使用することにします。

Running the server / Releases

ダウンロード。

$ curl -OL https://github.com/dragonflydb/dragonfly/releases/download/v0.6.0/dragonfly-x86_64.tar.gz

展開。

$ tar xf dragonfly-x86_64.tar.gz

含まれているのは、単一のバイナリのみです。

$ ll
合計 8724
drwxrwxr-x 2 xxxxx xxxxx    4096  8月  8 23:00 ./
drwxr-x--- 5 xxxxx xxxxx    4096  8月  8 23:00 ../
-rw-r--r-- 1 xxxxx xxxxx    3226  7月 28 18:03 LICENSE.md
-rwxr-xr-x 1 xxxxx xxxxx 6141416  7月 28 18:11 dragonfly-x86_64*
-rw-rw-r-- 1 xxxxx xxxxx 2773780  8月  8 23:00 dragonfly-x86_64.tar.gz

クライアントも付属していませんね。シンプルにDragonflyサーバーのみです。

バージョン。

$ ./dragonfly-x86_64 --version
dragonfly v0.6.0-e8cbd41719843019893f3d8f93deff41446caf28
build time: 2022-07-28 09:04:04

ところで、このバイナリをUbuntu Linux 20.04 LTS(カーネル5.4.0)で起動しようとすると

$ uname -srvmpio
Linux 5.4.0-122-generic #138-Ubuntu SMP Wed Jun 22 15:00:31 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
Codename:       focal

起動に失敗します。カーネル5.10以降でないと動作しないようです。

$ ./dragonfly-x86_64 
E20220808 22:45:16.836228  1151 dfly_main.cc:210] Kernel 5.10 or later is supported. Exiting...

これは、Linuxのio-uringという新しいAPIを使うからのようなのですが。

Dragonfly runs on linux. It uses relatively new linux specific io-uring API for I/O, hence it requires Linux version 5.10 or later. Debian/Bullseye, Ubuntu 20.04.4 or later fit these requirements.

Running the server

GitHub - axboe/liburing

Ubuntu Linux 20.04.4であればOKのように書かれていますけどね…。

気を取り直して。Ubuntu Linux 22.04 LTSであれば、カーネル5.10以降なので

$ uname -srvmpio
Linux 5.15.0-43-generic #46-Ubuntu SMP Tue Jul 12 10:30:17 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
Codename:       jammy

起動に成功します。

$ ./dragonfly-x86_64

Redisのように起動時に情報が表示されたりはしません。

設定は、README.mdを見ると以下のようなものがあるらしいのですが。

Configuration

  • port
  • bind
  • requirepass
  • maxmemory
  • dir
  • dbfilename
  • memcache_port
  • keys_output_limit
  • dbnum
  • cache_mode
  • hz

実際には、ヘルプを見た方が良さそうです。

$ ./dragonfly-x86_64 --help
dragonfly-x86_64: a modern in-memory store.

Usage: dragonfly [FLAGS]


  Flags from facade/dragonfly_connection.cc:
    --http_admin_console (If true allows accessing http console on main TCP
      port); default: true;
    --tcp_nodelay (Configures dragonfly connections with socket option
      TCP_NODELAY); default: false;

  Flags from facade/dragonfly_listener.cc:
    --conn_threads (Number of threads used for handing server connections);
      default: 0;
    --conn_use_incoming_cpu (If true uses incoming cpu of a socket in order to
      distribute incoming connections); default: false;
    --tls (); default: false;
    --tls_client_cert_file (cert file for tls connections); default: "";
    --tls_client_key_file (key file for tls connections); default: "";


  Flags from server/dfly_main.cc:
    --bind (Bind address. If empty - binds on all interfaces. It's not advised
      due to security implications.); default: "";
    --pidfile (If not empty - server writes its pid into the file); default: "";
    --use_large_pages (If true - uses large memory pages for allocations);
      default: false;

  Flags from server/engine_shard_set.cc:
    --backing_prefix (); default: "";
    --cache_mode (If true, the backend behaves like a cache, by evicting entries
      when getting close to maxmemory limit); default: false;
    --hz (Base frequency at which the server updates its expiry clock and
      performs other background tasks. Warning: not advised to decrease in
      production, because it can affect expiry precision for PSETEX etc.);
      default: 1000;

  Flags from server/generic_family.cc:
    --dbnum (Number of databases); default: 16;
    --keys_output_limit (Maximum number of keys output by keys command);
      default: 8192;

  Flags from server/io_mgr.cc:
    --backing_file_direct (If true uses O_DIRECT to open backing files);
      default: false;

  Flags from server/list_family.cc:
    --list_compress_depth (Compress depth of the list. Default is no
      compression); default: 0;
    --list_max_listpack_size (Maximum listpack size, default is 8kb);
      default: -2;

  Flags from server/main_service.cc:
    --maxmemory (Limit on maximum-memory that is used by the database.0 - means
      the program will automatically determine its maximum memory usage);
      default: 0;
    --memcache_port (Memcached port); default: 0;
    --port (Redis port); default: 6379;

  Flags from server/server_family.cc:
    --dbfilename (the filename to save/load the DB); default: "dump";
    --dir (working directory); default: "";
    --requirepass (password for AUTH authentication); default: "";

  Flags from server/tiered_storage.cc:
    --tiered_storage_max_pending_writes (Maximal number of pending writes per
      thread); default: 32;


  Flags from helio/util/proactor_pool.cc:
    --proactor_threads (Number of io threads in the pool); default: 0;


  Flags from helio/util/uring/proactor.cc:
    --proactor_register_fd (If true tries to register file descriptors);
      default: false;
    --proactor_spin_limit (How many times to spin proactor loop before blocking
      on kernel); default: 10;

Try --helpfull to get a list of all flags or --help=substring shows help for
flags which include specified substring in either in the name, or description or
path.

今回はパスワード(--requirepass)を指定して起動することにします。

$ ./dragonfly-x86_64 --requirepass=dragonflypass


## こちらでも可
$ ./dragonfly-x86_64 --requirepass dragonflypass

これで、Dragonfly側の準備は完了です。

Redisクライアントからアクセスする

次に、Redisクライアントからアクセスしてみましょう。

今回は、Vert.xのRedisクライアントとSmallrye Mutiny Vert.x bindingsを使うことにします。

Redis Client | Eclipse Vert.x

Smallrye Mutiny Vert.x bindings

Maven依存関係等は、こちら。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.smallrye.reactive</groupId>
            <artifactId>smallrye-mutiny-vertx-redis-client</artifactId>
            <version>2.25.0</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.23.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

動作確認は、テストコードで行うことにします。

src/test/java/org/littlewings/dragonfly/RedisClientTest.java

package org.littlewings.dragonfly;

import java.time.Duration;
import java.util.List;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.helpers.test.AssertSubscriber;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.redis.client.Redis;
import io.vertx.mutiny.redis.client.RedisAPI;
import io.vertx.mutiny.redis.client.Response;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class RedisClientTest {

    // ここに、テストを書く!!
}

set/get、そしてdel、再度getを確認。

    @Test
    public void gettingStarted() {
        Vertx vertx = Vertx.vertx();

        Redis redis = Redis.createClient(vertx, "redis://:dragonflypass@192.168.121.124:6379/0");
        RedisAPI redisApi = RedisAPI.api(redis);

        // set
        Uni<Response> setResponse = redisApi.set(List.of("key1", "value1"));
        // set → get
        Uni<Response> getResponse =
                setResponse
                        .onItem()
                        .transformToUni(r -> redisApi.get("key1"));

        UniAssertSubscriber<Response> getAssertSubscriber =
                getResponse
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        Response actualGetResponse =
                getAssertSubscriber
                        .awaitItem()
                        .assertCompleted()
                        .getItem();

        assertThat(actualGetResponse).asString().isEqualTo("value1");

        // del
        Uni<Response> delResponse = redisApi.del(List.of("key1"));
        // del → get
        Uni<Response> emptyResponse =
                delResponse
                        .onItem()
                        .transformToUni(r -> redisApi.get("key1"));

        UniAssertSubscriber<Response> emptyAssertSubscriber =
                emptyResponse
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        Response actualEmptyResponse =
                emptyAssertSubscriber
                        .awaitItem()
                        .assertCompleted()
                        .getItem();

        assertThat(actualEmptyResponse).isNull();

        redisApi.close();

        vertx.closeAndAwait();
    }

expire付きのset、getでの確認、そして時間を置いてexpireが機能していることを確認。

    @Test
    public void assertExpire() {
        Vertx vertx = Vertx.vertx();

        Redis redis = Redis.createClient(vertx, "redis://:dragonflypass@192.168.121.124:6379/0");
        RedisAPI redisApi = RedisAPI.api(redis);

        // set with expire
        Uni<Response> setResponse = redisApi.set(List.of("key10", "value10", "EX", "3"));
        // set → get
        Uni<Response> getResponse =
                setResponse
                        .onItem()
                        .transformToUni(r -> redisApi.get("key10"));

        UniAssertSubscriber<Response> getAssertSubscriber =
                getResponse
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        Response actualGetResponse =
                getAssertSubscriber
                        .awaitItem()
                        .assertCompleted()
                        .getItem();

        assertThat(actualGetResponse).asString().isEqualTo("value10");

        // get
        Uni<Response> getWithDelayResponse =
                Uni
                        .createFrom()
                        .nullItem()
                        .onItem()
                        .delayIt()
                        .by(Duration.ofSeconds(5L))
                        .onItem().transformToUni(n -> redisApi.get("key10"));

        UniAssertSubscriber<Response> getWithDelayAssertSubscriber =
                getWithDelayResponse
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        Response actualGetWithDelayResponse =
                getWithDelayAssertSubscriber
                        .awaitItem()
                        .assertCompleted()
                        .getItem();

        assertThat(actualGetWithDelayResponse).isNull();

        redisApi.close();
        vertx.closeAndAwait();
    }

興味本意ですが、SmallRye Mutiny視点でMulti。msetやmgetを確認しています。

    @Test
    public void assertMulti() {
        Vertx vertx = Vertx.vertx();

        Redis redis = Redis.createClient(vertx, "redis://:dragonflypass@192.168.121.124:6379/0");
        RedisAPI redisApi = RedisAPI.api(redis);

        // mset
        Uni<Response> msetResponse = redisApi.mset(List.of("key11", "value11", "key22", "value22", "key33", "value33"));
        // mset → mget
        Uni<Response> mgetResponse =
                msetResponse.onItem().transformToUni(r -> redisApi.mget(List.of("key11", "key22", "key33")));

        Multi<String> multiMgetResponse =
                mgetResponse
                        .onItem()
                        .transformToMulti(item -> item.toMulti()).onItem().transform(item -> item.toString());

        AssertSubscriber<String> mgetAssertSubscriber =
                multiMgetResponse
                        .subscribe()
                        .withSubscriber(AssertSubscriber.create(10));

        List<String> actualMgetResponse =
                mgetAssertSubscriber
                        .awaitItems(3)
                        .assertCompleted()
                        .getItems();

        assertThat(actualMgetResponse).hasSize(3).containsExactly("value11", "value22", "value33");

        // del
        Uni<Response> delResponse = redisApi.del(List.of("key11", "key22", "key33"));
        // del → mget
        Uni<Response> emptyResponse =
                delResponse.onItem().transformToUni(r -> redisApi.mget(List.of("key11", "key22", "key33")));

        UniAssertSubscriber<Response> emptyAssertSubscriber =
                emptyResponse
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        Response actualEmptyResponse =
                emptyAssertSubscriber
                        .awaitItem()
                        .assertCompleted()
                        .getItem();

        assertThat(actualEmptyResponse)
                .hasSize(3)
                .containsOnlyNulls();

        redisApi.close();

        vertx.closeAndAwait();
    }

いずれも、問題なく動作しました。

まとめ

Redis/memcached互換だというDragonflyを試してみました。

DragonflyのWebサイトを見ているとRedisと完全互換という謳い文句に見えるのですが、意外とそうでもなかったりするようなので情報確認
としては良い機会になりました。