これは、なにをしたくて書いたもの?
Redis/memcached互換を実現しつつ、Redisの25倍高速と謳われているDragonflyというインメモリデータストアがあるようです。
Redis互換で25倍高速とする「Dragonfly」が登場。2022年の最新技術でインメモリデータストアを実装 - Publickey
ちょっと気になっていたので、今回はDragonflyをRedisとして使って試してみようと思います。
Dragonfly
Dragonflyは、RedisおよびmemcachedのAPIと互換性があり、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.
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とのベンチマーク比較は、こちらを参照。
Dragonflyに関するバックグラウンドは、こちらに書かれています。
設計上の決定事項は、こちら。
Redisとの互換性(Dragonfly 0.6.0時点)
DragonflyがサポートしているRedisのコマンドは、こちらに記載があります。
※情報確認時点でのDragonflyのバージョンは0.6.0
見るとわかりますが、DragonflyはすべてのRedisコマンドをサポートしているわけではないようです。
クラスタリングに関するコマンドもサポートしていません。
クラスタリングに関してはissueもありましたが、対応しそうな雰囲気はなさそうですね。当面は、シングルノードでの利用になるでしょう。
Do you support clustering? · Issue #165 · dragonflydb/dragonfly · GitHub
memcachedのAPIに関する互換性はわかりませんでしたが、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コンテナを使う、ビルド済みのバイナリを使用する、ソースコードからビルドする、のどれかを選択します。
今回は、ビルド済みバイナリを使用することにします。
ダウンロード。
$ 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.
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
を見ると以下のようなものがあるらしいのですが。
- 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を使うことにします。
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と完全互換という謳い文句に見えるのですが、意外とそうでもなかったりするようなので情報確認
としては良い機会になりました。