はじめに
皆さんは、「劔"Tsurugi"」というデータベースをご存知ですか?
純国産のインメモリデータベースで、ハードウェアの性能に応じてデータベースの性能もにスケールさせることができる将来有望なデータベースです。
詳しくは公式サイトをご覧ください。
なぜ今なのか
2023年にNEDOのプレスリリースが出てからずっと気になっていたのですが、アプリケーションと連携させるためのSDKがJavaやCしかなく、Pythonしかさわれない貧弱エンジニアの私にとってなかなか手が出せませんでした。
しかしついに!
2026年4月6日にPythonのSDKが発表されました!
Python SDKが出るのも時間の問題だと思っていましたが、ついに使えるようになりました。
ということで、この記事を書くに至りました。
検証環境
今回はDocker等のコンテナを使わず、直接DBをインストールしました。
なお全ての計測は、1回のウォームアップののち、10回の計測を行いその平均値で評価を行いました。
以下の設定が素人考えであり、厳密な検証ができる状態になっているかは怪しいので、有識者の方はぜひご意見いただけますと幸いです
ハードウェア
- Ubuntu 22.04.2 LTS
- Intel(R) Xeon(R) Silver 4316 CPU @ 2.30GHz
- DDR4-3200(Registered / Buffered ECC) 32GB x 16 = 512GB
PostgreSQLの設定
- version: PostgreSQL 14.22
デフォルトだと、永続性を担保しておらず、その分高速で動いてしまうため有利となり、メモリ使用量については不利になるので以下を変更します
shared_buffers = 16GB
fsync = on
synchronous_commit = on
Tsurugiの設定
- version: 1.10.0
同じく永続性の部分を明記します(デフォルトでSTOREDのようなのでしなくても良い)
[sql]
commit_response=STORED
事前準備
テーブルの作成
今回はIoTのセンサーデータが来る想定で作成しました。
両者ともSQL文でテーブル作成が可能です。
TimezoneについてはTsurugi側がTIMESTAMPTZに対応していないため、TIMESTAMP型としています。
DROP TABLE IF EXISTS sensor_data;
CREATE TABLE IF NOT EXISTS sensor_data (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
temperature DOUBLE PRECISION NOT NULL,
humidity DOUBLE PRECISION NOT NULL,
pressure DOUBLE PRECISION NOT NULL,
created_at TIMESTAMP NOT NULL
);
単一接続でバッチINSERT
まずはシンプルに並列化をせず、多くのデータをバッチでINSERTするケースを検証しました。
ソースコードは本記事最後のGitHubにおいています。
今回は実用性を重視するために、PostgreSQL側は実際の運用で使用するような最適な実装(asyncpg)を用いています。Tsurugi側はasyncではないので正直相当不利ですが、実務で利用する上ではasyncpgはデフォルトのようなものなので、我慢してもらいましょう...
- 1コミットあたりのデータ数 = 100
- コミット数 = 100
PostgreSQL
average total=2.157006s ops/sec=4,636.06 avg=0.2157ms
min total=2.076781s ops/sec=4,815.14 avg=0.2077ms
max total=2.327186s ops/sec=4,297.04 avg=0.2327ms
Tsurugi
average total=1.517899s ops/sec=6,588.05 avg=0.1518ms
min total=1.343995s ops/sec=7,440.51 avg=0.1344ms
max total=1.680932s ops/sec=5,949.08 avg=0.1681ms
| 実装 | average total | average ops/sec | sec/op |
|---|---|---|---|
| PostgreSQL | 2.16s | 4,636.06 | 0.22ms |
| Tsurugi | 1.52s | 6,588.05 | 0.15ms |
ぬおー早い!
同期/非同期でだいぶ不利ですが、それでもよく頑張ってくれました。
並列接続でバッチINSERT
準公式のTsurugi vs PostgreSQLのベンチマークでは、大量かつ複雑なトランザクションで比較を行なっており、Tsurugiの強みはそこにあります。
DBにおける"大量"は、複数のトランザクションが同時並行的に生じるという意味に近いです。
そこで並列化を行い、複数のコネクションからの多くのデータをバッチでINSERTするケースを検証しました。
同じくソースコードは本記事最後のGitHubにおいています。
- 1コミットあたりのデータ数 = 100
- コミット数 = 100
- 並列数(
ThreadPoolExecutor) = 32
PostgreSQL
average total=19.982135s ops/sec=16,014.30 avg=0.0624ms
min total=19.171534s ops/sec=16,691.41 avg=0.0599ms
max total=22.303964s ops/sec=14,347.23 avg=0.0697ms
Tsurugi
average total=96.673199s ops/sec=3,310.12 avg=0.3021ms
min total=74.815632s ops/sec=4,277.18 avg=0.2338ms
max total=114.424572s ops/sec=2,796.60 avg=0.3576ms
| 実装 | average total | average ops/sec | sec/op |
|---|---|---|---|
| PostgreSQL | 19.98s | 16,014.30 | 0.06ms |
| Tsurugi | 96.67s | 3,310.12 | 0.30ms |
残念なことに、並列数を上げると能力が上がるはずのTsurugiが大きく遅くなってしまいました。
おそらくですが、ここで同期/非同期の差が出てきてしまったのではないかと思われます。
そもそも同期型のTsurugi SDKをThread Pool Executorで無理やり回しているので、その部分で相当無理が出てしまったのではないだろうかと思われます。
計測時間も大きく揺らいでいることからも、Python自体の能力が相当足を引っ張ってしまっており、うまく性能が引き出せませんでした。
FastAPIで呼び出す
発想を変えて、そもそもDBはREST APIの内部で呼び出すよね、ってことで先ほどの並列をFastAPIを介することで実現してみました。
つまり1トランザクションを1REST APIに入れ、そのAPIエンドポイントを並列に叩くということです。
しかし結局同期であることは変わらないので、FastAPIの強みであるasync defはTsurugiでは使えません。
実際にFastAPIで実装し、k6でベンチマークを取ってみました。
- 1コミットあたりのデータ数 = 100
- コミット数 = 100
まずは実装確認でVUs(=worker)を1にして検証。Tsurugiのほうが若干早いはず。
VUs = 1
PostgreSQL
http_req_duration..............: avg=1.89s min=1.6s med=1.89s max=2.28s p(90)=2.16s p(95)=2.22s
{ expected_response:true }...: avg=1.89s min=1.6s med=1.89s max=2.28s p(90)=2.16s p(95)=2.22s
http_req_failed................: 0.00% 0 out of 16
http_reqs......................: 16 0.527219/s
Tsurugi
http_req_duration..............: avg=1.7s min=1.43s med=1.57s max=2.87s p(90)=1.93s p(95)=2.16s
{ expected_response:true }...: avg=1.7s min=1.43s med=1.57s max=2.87s p(90)=1.93s p(95)=2.16s
http_req_failed................: 0.00% 0 out of 18
http_reqs......................: 18 0.58755/s
うまくいってそうですね。では並列化します。
VUs = 32
PostgreSQL
http_req_duration..............: avg=2.6s min=1.52s med=2.87s max=4.16s p(90)=3.44s p(95)=3.59s
{ expected_response:true }...: avg=2.6s min=1.52s med=2.87s max=4.16s p(90)=3.44s p(95)=3.59s
http_req_failed................: 0.00% 0 out of 3699
http_reqs......................: 3699 12.219043/s
ここまで並列しても大きく遅くならないのはさすがですね...
Tsurugi
http_req_duration..............: avg=3.77s min=2s med=3.09s max=13.75s p(90)=4.48s p(95)=7.89s
{ expected_response:true }...: avg=3.77s min=2s med=3.09s max=13.75s p(90)=4.48s p(95)=7.89s
http_req_failed................: 0.00% 0 out of 2556
http_reqs......................: 2556 8.401758/s
やはりPostgreSQLに軍配が上がってしまいました。
先ほどのThreads型よりは大きく速度が上がっていますが、やはり同期/非同期がボトルネックになってしまっているんだと思います...
もともとJavaやCでの実装が想定されているでしょうから、有識者の方はぜひそちらで検証してください...
もしかするとこの記事を見て非同期APIが開発されると最高ですね!
おまけ
このまま終わるわけにはいかないので、準公式ベンチマークのこれを同じトランザクションにしてPythonで実装してみました。
当記事ではJavaを使用し、RTX/OCC/LTX等をうまく使ってTsurugiの最適化を行なっていますが、Pythonではおそらく難しいので、単純なトランザクションとして比較しています。
ソースコードも記事一番下のGitHubから。
- ONLINE_WORKERS = 32
- RUN_SECONDS = 300
- ONLINE_SLEEP_SEC = 0.1
- BATCH_INTERVAL_SEC = 10
- MAX_RETRY = 3
- CARD_ID_MIN = 1
- CARD_ID_MAX = 100_000
PostgreSQL
elapsed=301.939s
{'online_success': 5429, 'online_fail': 0, 'batch_success': 0, 'batch_fail': 28}
online_tps=17.98 batch_success=0
Tsurugi
elapsed=315.067s
{'online_success': 18555, 'online_fail': 0, 'batch_success': 10, 'batch_fail': 14}
online_tps=58.89 batch_success=10
素晴らしいですね!
やはり複雑なトランザクションではPythonでもTsurugiに軍配が上がるとわかりました!
終わりに
PythonはCなどに比べて遅い言語と言われます(私は思っていません)が、それでも遅いなりに裏側でCやRustを使ってなんとか高速化しています。
今回のTsurugi Python APIも裏はRustです(Tsurugi-Tubakuro)
正直なところ、PythonでTsurugiのパフォーマンスが引き出せるのかとても不安でした。
今回のベンチマークも、結果的にはPythonの能力が足を引っ張っている結果となっていました。
しかし、そもそもWeb APIを作る上では不利なPythonですが、昨今の生成AIブームに乗っかって、REST APIをPythonで書くケースも増えてきました。
複雑でかつ多並列トランザクションにおいては、そんなPythonでもPostgreSQLに勝つことができるわけです。
全てにおいて最強ではないにせよ、純国産DBとしてTsurugiがDBスタンダードに肩を並べることを願っています。
メニーコア・メニーメモリは正義!
検証用ソースコード
本記事は生成AIを使用していませんが、検証用のソースコードはChatGPTに手伝ってもらいました🤖