なぜ JDBC と R2DBC を比べるのか
どちらもDBアクセスのための手段ですが、性質は大きく異なります。
- JDBC: 古くからある標準APIで、SQL実行中はスレッドがブロックされます
- R2DBC: 非ブロッキングI/Oを前提に設計された新しい仕様で、I/O待ちの間にスレッドを他の処理に回せます。そのため高負荷環境ではスループット向上が期待できます
R2DBCは一般的にSpring WebFluxと組み合わせて利用されることが多いですが、Spring MVC+Kotlin Coroutinesでも違いが出るのか気になり、勉強がてら試してみました。
結論
今回の検証から言えることは、
- Spring MVCのような同期モデルでは、R2DBCのメリットはほとんどなく、積極的に選ぶ必要はない
- ただし、一部の条件のP95レイテンシではR2DBCが有利になることもある
検証環境
- MacBook Pro (Apple M3, メモリ16GB)
- Spring Boot 3.5(Kotlin Coroutines 有効)
- PostgreSQL 16(Docker上で稼働)
プロジェクト構成
検証用に作成したプロジェクトは以下のような構成です。
シンプルな三層構造です、OrderController
で/orders
エンドポイントを公開し、OrderService
を経由してOrderRepository
に処理を委譲します。
-
OrderController
...APIエンドポイント -
OrderService
...リポジトリを束ねる -
OrderRepository
...リポジトリのインターフェース-
JdbcOrderRepository
...JDBCを利用した実装(Dispatchers.IO
利用) -
R2dbcOrderRepository
...R2DBCを利用した実装
-
application.ymlのプロファイルで JdbcOrderRepository
/ R2dbcOrderRepository
を切り替えて動作を確認できるようにしています。
2つのリポジトリが実行するクエリは同じで以下のような条件で検索してページングを行います。
SELECT id, customer_id, created_at, total_amount, note
FROM orders
WHERE customer_id = :customerId
AND created_at BETWEEN :from AND :to
ORDER BY created_at DESC
LIMIT :size OFFSET :offset;
ソースコード全体は GitHub に公開していますので、詳細はこちらを参照してください。
検証シナリオ設計
-
orders
テーブルに 20万行相当のデータを投入 - 負荷テストツールのk6を利用し、指定した数の仮想ユーザーが60 秒間
/orders
へリクエストを投げ続ける - JDBC / R2DBC それぞれで2を行い、スループットを計測する
- 並行ユーザー数を変えて2~3を繰り返す
TomcatのmaxThreadsを200に設定していたため、その前後の負荷領域を観測できるよう10 → 300の範囲で検証を行いました。
コネクションプールはJDBC / R2DBCともに最大20。
結果
クエリが早すぎると非同期化メリットがなくなると思いインデックスを外した検証も行いました。
インデックスありの場合
JDBC | R2DBC | |||||
---|---|---|---|---|---|---|
並行ユーザー数 | 1秒あたりの処理件数(req/s) | 平均応答時間(ms) | P95応答時間(ms) | 1秒あたりの処理件数(req/s) | 平均応答時間(ms) | P95応答時間(ms) |
10 | 570.8 | 6.56 | 11.09 | 506.8 | 9.02 | 12.82 |
50 | 3723.5 | 3.10 | 7.40 | 3487.4 | 4.05 | 9.83 |
100 | 5903.4 | 5.86 | 15.26 | 4320.9 | 12.76 | 35.76 |
150 | 5924.8 | 13.09 | 31.35 | 4254.4 | 24.71 | 47.65 |
200 | 6041.2 | 19.54 | 43.06 | 3782.1 | 42.22 | 67.26 |
250 | 6076.1 | 26.21 | 55.96 | 4197.9 | 48.90 | 73.12 |
300 | 6149.8 | 32.88 | 65.23 | 4360.7 | 58.08 | 84.55 |
インデックスなしの場合
JDBC | R2DBC | |||||
---|---|---|---|---|---|---|
並行ユーザー数 | 1秒あたりの処理件数(req/s) | 平均応答時間(ms) | P95応答時間(ms) | 1秒あたりの処理件数(req/s) | 平均応答時間(ms) | P95応答時間(ms) |
10 | 451.9 | 11.75 | 16.96 | 354.3 | 17.84 | 20.81 |
50 | 589.8 | 73.92 | 119.31 | 502.1 | 88.14 | 139.50 |
100 | 607.6 | 153.92 | 386.18 | 508.6 | 185.41 | 286.74 |
150 | 521.8 | 276.97 | 717.36 | 505.6 | 285.58 | 736.69 |
200 | 526.8 | 369.33 | 859.68 | 489.9 | 397.73 | 863.02 |
250 | 509.4 | 480.93 | 1050 | 557.9 | 437.77 | 495.45 |
300 | 551.7 | 534.07 | 1050 | 504.6 | 584.66 | 1050 |
まとめ
今回の条件では平均やスループットでR2DBCの明確な優位は見られませんでした。一方で高並行時にはP95でR2DBCが良い場面も観測されたため、長尾(P99)抑制の可能性はありそうです。P99は未計測なので、今後あらためて検証してみます。