本記事は オルトプラス Advent Calendar 2023 の12/10の記事です。
こんにちわ、エンジニアのピスキーと申します。
株式会社オルトプラスでは、主にメタバース空間など大勢のプレイヤーがフィールド上を動き回れるリアルタイム通信サーバーの設計・実装・運用をしています。
では早速Surreal DBの話をしていきましょう!
Surreal DBの紹介
リアルタイム通信サーバーを開発しているのですが、出来るだけ低遅延かつ高可用性を持たせた実装を目指すとなると、外部データとのやりとりには、Read/Writeが高速かつ、使いやすさとスケールの余地があるデータベースが必須となってきます。日々色々なDBを試してはいるのですが、少し前に、SurrealDBのサイトを見て、理想に近いというか、”The ultimate multi-model database”と銘打っているのも頷ける機能を持っており、
これからの成長にも期待しているプロダクトです。
めでたく今年の9/3にv1.0.0をリリースされたのもあり、本腰を入れて色々と触って感じたことなどを紹介していきたいと思います。
昨今よく見かけるようになった、クエリレイヤーと、ストレージレイヤーとで動かす"New SQL"と呼ばれるカテゴリーに入るプロダクトです。
ここ7~8年のDBの世界では、timeseries系、Graph系、vector系DBの、使用割合が増加していますが、SurrealDBはGraph系に属すると思います。
現時点で実装されている機能の紹介をしようと思いましたが、まさにultimateを感じるほどの機能が実装されていて、ここでは紹介しきれない数があるのですが、(機能リストはこちら)
私の目から見た気になる機能を一部あげてみます。
- Live Query
- スキーマフル、スキーマレス両対応
- オンメモリ、ブラウザ、ローカルストレージ、分散ストレージと、ストレージレイヤの多様さ
- Materialize Table
- 新しく実装された、柔軟なクエリ言語、SurrealQL
- 柔軟な認可機能(RBAC)
- 複合的なRecord ID
などなど、機能一覧ページ見てるだけで、ワクワクしてしまうのは私だけでしょうか?
コミットログを追うと、開発は、8年くらい前からGo言語で書き始め、2年くらい前にRustでの書き直しが始まり、そこから一気に開発者も増え、開発が加速しているように見えました。開発者の情熱を感じます。
いくら魅力的な機能があっても、使用に耐えるRead/Write性能に達していないと、そもそも組み込めないので、
実際に、動かしてみて、使用に耐える性能があるのか、軽いベンチマークをして、試していきたいと思います。
まずは動かしてみる
手元のローカル環境でも気軽に動かせるSurreal DBですが、実際にkurbenates(eks)上でちゃんと動くのかと確かめるため、Surreal DB用のPodをたててみようと思います。
Amazon Linux 2023 AMIベースのノード上で、dockerhub上のこちらのイメージで試してみましたが、
動作に必須ライブラリのバージョンが足らず動作しなかったので、
以下のようなDockerfileを用意し、イメージを作り、そちらを使用しました。
FROM debian:bookworm-slim as runtime
WORKDIR /app
RUN apt-get update -y
RUN apt-get install curl -y
RUN curl -sSf https://install.surrealdb.com | sh
CMD ["/usr/local/bin/surreal"]
これであれば、k8sのPodにargsを設定し、自由な引数をつけて、起動できるようになりました。
すでに、awesomeレポジトリ(https://github.com/surrealdb/awesome-surreal)もあり、
Management interfacesの項目に
GUIアプリのプロジェクトがいくつかあり、今回はSurrealist
を使わせていただきました。
すでに十分な機能を備えていて、
データの確認や、後述するSurrealQLの流し込みなどに使用しました。
k8s上にSurreal DBのPodをたて、ローカルの8000ポートにポートフォワードして使いましたが、ローカル環境にSurrealDBを立てての使用も問題なく動くと思います。
まずは、テーブルまで作っていきたいと思います。
まずは、DBを起動させます。
起動時に、rootのコマンドを設定することができるので、
初回起動時はそちらで設定して、後ほどSurrealQLで更新するのが手軽で良いかと思います。
$ surreal start --user root --pass hogehoge
まずは、テーブルを作っていきます。
Surreal DBの基本構造は、
- ネームスペース
- データベース
- テーブル
- データベース
となっており、どの階層でもユーザー認可を設定することができますが、
今回は、workerというユーザを作成し、全てのデータベースへの作業ができるROOT上にOWNERのロールを割り当てました。
まずネームスペースを作成し、
そのあとデータベースを作成し、
workerユーザを作成し、
Roomというテーブルを作成したのが、以下のSurrealQLです。
# ネームスペース作成
DEFINE NAMESPACE altplus;
USE NS altplus;
# データベース作成
DEFINE DATABASE alt_dev1;
DEFINE USER worker ON ROOT PASSWORD "hogehoge" ROLES OWNER;
# ネームスペースとデータベースの利用を宣言
USE NS altplus;
USE DATABASE alt_dev1;
# Roomテーブルの作成
DEFINE TABLE room SCHEMAFULL;
# Roomテーブルの各フィールドを作成
DEFINE FIELD name ON TABLE room TYPE string;
DEFINE FIELD user_count ON TABLE room TYPE int DEFAULT 0;
DEFINE FIELD room_type ON TABLE room TYPE int DEFAULT 0;
DEFINE FIELD registered_at ON TABLE room TYPE datetime DEFAULT time::now();
# Roomテーブルのroom_typeフィールドのインデックスを作成
DEFINE INDEX room_type_idx ON TABLE room FIELDS room_type;
# Roomテーブルのroom_typeフィールドの値"2"でセレクトしたMateriarizeテーブルを作成
DEFINE TABLE type_2_room AS SELECT * FROM room WHERE room_type = 2;
補足ですが、Materiarizeテーブルというのは、
既存のテーブルから、生成する仮想的なテーブルであり、
あらかじめ事前計算できるところを済ませたり、
元のデーブルに変更があれば、その変更後に遅延・バッチ処理で、Materializeテーブルにも反映するという機能を持ったテーブルで、そちらも試してみたいと思います。
SurrealistでDBにコネクトし、上記のSurrealQLをSurrealist経由で流し、
もろもろ作成していきます。
もちろんsurrealdbコマンドでDBに接続し、クエリを流す方法でも問題ありません。
データを投入する準備が整いました。
データCreateのサンプル
コンソールから接続し、SurrealQLを使っても、データのインサートできますが、
実態に即した状態で扱いたいので、
コードはRustで書き、別のkubernates上のPodから動かします。
Createのサンプルです。
まずは、dbのコネクションを取得します。
use surrealdb::Surreal;
use surrealdb::engine::remote::ws::{Client, Ws};
let db:Surreal<Client> = Surreal::new::<Ws>("x.x.x.x:8000").await?;
db.signin(Root {
username: "worker",
password: "hogehoge",
})
.await?;
db.use_ns("altplus").use_db("alt_dev1").await?;
let id = 1;
let room_type = 1;
let sql = format!("CREATE room:{} SET name = $name, user_count = 0, room_type = {}, registered_at = time::now();",room_type);
# bindでsql内のプレースホルダーを書き換えられます。
let mut created = db.query(sql.as_str())
.bind(("name", format!("room-{}", id).as_str()))
.await?;
# もしエラーがあればここで補足します。
created.take_errors().iter().for_each(|(a, b)| {
println!("usize: {:?} / error: {:?}", a, b);
});
特にエラーもなく、データがインサートできました。
提供されているSDKでは、
以上のような感じで、SurrealQLを、テキストとして渡す方法もあれば、
serdeのSerialize、Deserializeを利用してstructをデータモデルとして扱う方法もあります。
ストレージレイヤ別ベンチマーク
ここからが割と主題なのですが、私が何より気になるのは、どのストレージレイヤだとどのくらいの速度が出るのかで、かなり使用するユースケースが変わってくるので、以下のパターンでベンチマークをとりました。
今回は、単体のDBでのベンチマーク比較を主に行いたかったので、
以下のストレージレイヤー層で色々試していきました。
比較対象として、AWSのDynamoDBとRDSも参考までに載せています。
ストレージレイヤー
- サーバーノード (AWS EC2 - m7g.large(4cpu/メモリ8G))
- SurrealDB - オンメモリ
- SurrealDB - Pod上のSSDローカルストレージ(Rocks DB)
- SurrealDB - k8sのPersistant Volume(EBS)(Rocks DB)
- dynamodb - オンデマンド WCU/RCUともに10000
- aurora - 4RCU, 4WCU (8Gメモリ想定)
クライアントノード:サーバーと同じVPC内のAWS EC2 - m7g.4xlarge(16cpu)
負荷試験コードはrustで書き、
並行処理にはtokioを利用しています。
並列数100の場合は、100個のtokioジョブを作成し、各ジョブ毎に100件処理させ、
ジョブの開始から終了までの時間を測っています。
db接続に使ったcrateは、
surrealの場合は、surreal = "1.0.0"
dynamodbには、aws-sdk-dynamodb = "0.39.0"
auroraには、sqlx = "0.7"
です。
並列数1 / createリクエスト 10000件
1回目 | 2回目 | 3回目 | |
---|---|---|---|
SurrealDB - in memory | 19.109550372s | 19.090285499s | 19.537268307s |
SurrealDB - local | 20.745024212s | 19.52897024s | 20.101149721s |
SurrealDB - k8s PV | 21.117914907s | 20.70480329s | 20.649204981s |
dynamo | 35.866170692s | 38.643057592s | 45.175309964s |
aurora | 638.614518338s | 621.93083527s | 612.54474268s |
並列数100 / createリクエスト 100 x 100件
1回目 | 2回目 | 3回目 | |
---|---|---|---|
SurrealDB - in memory | 3.021720151s | 3.149559572s | 3.194944794s |
SurrealDB - local | 2.910413722s | 2.821561787s | 2.910569897s |
SurrealDB - k8s PV | 3.105589627s | 2.988515222s | 3.232857147s |
dynamo | 3.697921859s | 3.658924116s | 4.248436712s |
aurora | 7.15170037s | 6.93647926s | 7.520541267s |
並列数1 / updateリクエスト 10000件
1回目 | 2回目 | 3回目 | |
---|---|---|---|
SurrealDB - in memory | 20.120560684s | 20.675735649s | 19.918918987s |
SurrealDB - local | 20.737257577s | 20.129442358s | 20.17949441s |
SurrealDB - k8s PV | 21.089498462s | 21.229720747s | 20.453517148s |
dynamo | 56.103865107s | 55.069498882s | 55.401576083s |
aurora | 524.442847505s | 520.864850163s | 522.75982571s |
並列数100 / updateリクエスト 100 x 100件
1回目 | 2回目 | 3回目 | |
---|---|---|---|
SurrealDB - in memory | 3.021720151s | 3.149559572s | 3.194944794s |
SurrealDB - local | 3.031745974s | 2.992591341s | 2.720599114s |
SurrealDB - k8s PV | 3.031745974s | 2.992591341s | 2.720599114s |
dynamo | 3.86846267s | 4.517719577s | 4.482463953s |
aurora | 7.235311923s | 7.358347137s | 6.202709701s |
並列数100 並列数100 / selectリクエスト 100件
- room_type: 均等に振り分けた0,1,2,3 の内の2を抽出一つ
- materialize table (事前計算済みのテーブル)
room_typeへのインデックスは設定しました。
surreal(Index有)
dynamo(GSI有)
aurora(Index有)
1回目 | 2回目 | 3回目 | |
---|---|---|---|
SurrealDB - in memory | 3.032248285s | 3.265638103s | 3.418871636s |
SurrealDB - in memory - materialize table | 2.688563825s | 2.614341477s | 2.614341477s |
SurrealDB - local | 3.275499019s | 3.198799498s | 3.161353797s |
SurrealDB - local - materialize table | 2.930275651s | 2.686727312s | 2.771640347s |
SurrealDB - k8s PV | 3.144671684s | 3.428465474s | 3.06669175s |
SurrealDB - k8s PV - materialize table | 2.810007887s | 2.775199883s | 3.055664716s |
dynamo | 4.082166519s | 4.165618013s | 4.131040662s |
aurora | 5.304212309s | 5.296920372s | 5.65588013s |
総論
まず思ったのは、dynamodbより全スコアで上回った点です。
正直dynamoよりは遅いだろうと踏んでいたのですが、4コアマシンでなかなかの性能が出ています。
materialize tableがいいですね。これは積極的に使用していった方が良さそうです。
思ったよりもストア層の差異が出なかったのも面白いですね。
in memoryががっつり早いかなと思っていたのですが、そこまでの性能比は出なかったなという印象です。
これは今後の開発にも使っていきたいと思わせる結果でした。
分散ストレージでの検証・性能評価も近いうちに行っていこうと思います。
おまけ
あとSurrealDBが推していて、私も最も気になってる機能の一つで、
Live Queryという、データの追加・更新・削除の通知をテーブル単位で(Materializeテーブルも含まれる!)購読できる機能があり、そちらも試してみたかったのですが、RustSDKのコードを覗いてみても、まだ一部未実装な部分があり、(その部分はレポジトリを見ると、Issueがあり、PRもマージ済みで次期バージョンで入るといいなと思っていますが)、そちらも登場次第、触っていきたいと思っています。
jsのsdkにはあったので、手元の環境で、軽く試してみましたが、
レコードの追加・更新・削除の購読では、一件の漏れもなく補足できて、
書き込みの処理速度よりも早く、購読の処理が実現できていました。
この機能は、データ更新と連動して、別のコンポーネントを直接低遅延で動かそうなので、
用途も色々考えられ、とても期待しています。