パフォーマンス
本ドキュメントでは、ldbcとjdbcのパフォーマンス特性を詳細に分析し、どのような状況でldbcを選択すべきかについて技術的な観点から解説します。
エグゼクティブサマリー
ベンチマーク結果から、ldbcはjdbcと比較して約1.8〜2.1倍の高いスループットを示しています。この優位性は、Cats EffectのFiberベースの並行性モデルと非ブロッキングI/Oの実装に起因します。特に高並行性環境において、ldbcは優れたスケーラビリティを発揮します。
主要な発見事項
- パフォーマンス: ldbcはjdbcより約2倍高速(8スレッド環境)
- スケーラビリティ: スレッド数の増加に対してldbcは線形に近いスケーリングを実現
- リソース効率: メモリ使用量が大幅に削減(Fiber: 500バイト vs OSスレッド: 1MB)
- レイテンシ: 高負荷時でも安定した応答時間を維持
ベンチマーク実行環境
ハードウェア・ソフトウェア環境
- JDK: Amazon Corretto 21.0.6
- JVM: OpenJDK 64-Bit Server VM (21.0.6+7-LTS)
- メモリ: 4GB (ヒープサイズ: -Xms4G -Xmx4G)
- GC: G1GC (-XX:+UseG1GC -XX:MaxGCPauseMillis=200)
- MySQL: バージョン8.4.0(Docker環境)
JMHベンチマーク設定
@BenchmarkMode(Array(Mode.Throughput)) // スループット測定
@OutputTimeUnit(TimeUnit.SECONDS) // ops/s単位で出力
@State(Scope.Benchmark) // ベンチマークスコープ
@Fork(value = 1) // フォーク数: 1
@Warmup(iterations = 5) // ウォームアップ: 5回
@Measurement(iterations = 10) // 測定: 10回
@Threads(1) // スレッド数: 1-16で変動
測定条件
- 接続方式: 接続プーリングなし(単一接続を再利用)
- クエリタイプ:
- Statement: 動的SQL実行
- PreparedStatement: パラメータ化クエリ
- データサイズ: 500, 1000, 1500, 2000行
- 測定対象テーブル: 16カラム(各種データ型を含む)
- 数値型: Long, Short, Int, Float, Double, BigDecimal
- 文字列型: String (2種)
- 日時型: LocalDate, LocalTime, LocalDateTime (2種)
- 論理型: Boolean
スレッド数によるパフォーマンス比較
1スレッド環境
シングルスレッド環境では、ldbcとjdbcのパフォーマンス差は比較的小さくなります。これは、並行性の利点が発揮されないためです。
パフォーマンス比率(ldbc/jdbc):
- 500行: 1.43倍
- 1000行: 1.52倍
- 1500行: 1.48倍
- 2000行: 1.51倍
2スレッド環境
2スレッド環境から、ldbcの優位性が明確になり始めます。
パフォーマンス比率(ldbc/jdbc):
- 500行: 1.83倍
- 1000行: 1.48倍
- 1500行: 1.66倍
- 2000行: 1.75倍
4スレッド環境
4スレッド環境では、ldbcのスケーラビリティが顕著に現れます。
パフォーマンス比率(ldbc/jdbc):
- 500行: 1.89倍
- 1000行: 1.82倍
- 1500行: 1.87倍
- 2000行: 1.93倍
8スレッド環境
8スレッド環境で、ldbcは最も高いパフォーマンス優位性を示します。
パフォーマンス比率(ldbc/jdbc):
- 500行: 1.76倍
- 1000行: 2.01倍
- 1500行: 1.92倍
- 2000行: 2.09倍
16スレッド環境
16スレッド環境でも、ldbcは安定した高パフォーマンスを維持します。
パフォーマンス比率(ldbc/jdbc):
- 500行: 1.95倍
- 1000行: 2.03倍
- 1500行: 1.98倍
- 2000行: 2.12倍
技術的分析
Cats Effectのパフォーマンス特性
Cats Effect 3は、長時間稼働するバックエンドネットワークアプリケーション向けに最適化されています:
最適化対象
- 物理スレッド数: 8スレッド以上の環境
- 処理内容: ネットワークソケットI/O中心の処理
- 稼働時間: 数時間以上の長時間稼働アプリケーション
パフォーマンス指標
flatMap
操作:
- 旧世代Intel CPU: 約7ナノ秒
- 最新AMD CPU: 約3ナノ秒
- ボトルネック: 計算処理ではなく、スケジューリングとI/Oが主要因
ユーザー空間スケジューラー
- Work-Stealingアルゴリズム: スループット重視の設計
- 細粒度のプリエンプション: Kotlinコルーチンより柔軟なタスク切り替え
- スタック使用量: コルーチンインタープリターによる定常的なメモリ使用
他のランタイムとの比較
- Project Loom (Virtual Threads): 現時点でCats Effectが性能面で優位
- スレッドレスサスペンション: リソースの安全な管理と非同期中断をサポート
並行性モデルの違い
ldbc(Cats Effect 3)
ldbcはCats Effect 3のFiberベースの並行性モデルを採用しています:
// 非ブロッキングI/O操作
for {
statement <- connection.prepareStatement(sql)
_ <- statement.setInt(1, id)
resultSet <- statement.executeQuery()
result <- resultSet.decode[User]
} yield result
特徴:
- Fiber(グリーンスレッド): 軽量なユーザー空間スレッド
- メモリ使用量: 約300-500バイト/Fiber
- コンテキストスイッチ: ユーザー空間で完結(カーネル呼び出し不要)
-
CPUキャッシュ効率: スレッドアフィニティによる高いキャッシュヒット率
-
Work-Stealingスレッドプール:
- CPUコア毎の作業キュー(グローバル競合を回避)
- 動的負荷分散
- 自動yield挿入によるCPU飢餓防止
jdbc(従来のスレッドモデル)
jdbcは従来のOSスレッドとブロッキングI/Oを使用:
// ブロッキングI/O操作
Sync[F].blocking {
val statement = connection.prepareStatement(sql)
statement.setInt(1, id)
val resultSet = statement.executeQuery()
// スレッドがブロックされる
}
特徴:
- OSスレッド: ネイティブスレッド
- メモリ使用量: 約1MB/スレッド
- コンテキストスイッチ: カーネル呼び出しが必要
- 固定サイズスレッドプール
ネットワークI/O実装
ldbc - 非ブロッキングソケット
// fs2 Socketを使用した非ブロッキング読み取り
socket.read(8192).flatMap { chunk =>
// チャンク単位での効率的な処理
processChunk(chunk)
}
- ゼロコピー最適化: BitVectorによる効率的なバッファ管理
- ストリーミング: 大きな結果セットの効率的な処理
- タイムアウト制御: 細粒度のタイムアウト設定が可能
jdbc - ブロッキングソケット
// 従来のブロッキングI/O
val bytes = inputStream.read(buffer)
// スレッドがI/O完了までブロック
- バッファリング: 結果セット全体をメモリに読み込み
- スレッドブロッキング: I/O待機中はスレッドが使用不可
メモリ効率とGC圧力
ldbcのメモリ管理
- プリアロケートバッファ: 結果行用の再利用可能なバッファ
- ストリーミング処理: 必要に応じたデータフェッチ
- 不変データ構造: 構造共有による効率的なメモリ使用
jdbcのメモリ管理
- 一括読み込み: 結果セット全体をメモリに保持
- 中間オブジェクト: ボクシング/アンボクシングによるオーバーヘッド
- GC圧力: 一時オブジェクトによる頻繁なGC
使用シナリオ別推奨事項
ldbcを選択すべきケース
-
高並行性アプリケーション
- Webアプリケーション(高トラフィック)
- マイクロサービス
- リアルタイムデータ処理
-
リソース制約環境
- コンテナ環境(Kubernetes等)
- サーバーレス環境
- メモリ制限のある環境
-
スケーラビリティ重視
- 将来的な負荷増加が予想される
- 弾力的なスケーリングが必要
- クラウドネイティブアプリケーション
-
関数型プログラミング
- 純粋関数型アーキテクチャ
- 型安全性重視
- コンポーザビリティ重視
jdbcを選択すべきケース
-
レガシーシステム統合
- 既存のjdbcコードベース
- サードパーティライブラリ依存
- 移行コストが高い場合
-
シンプルなCRUD操作
- 低並行性
- バッチ処理
- 管理ツール
-
特殊なjdbc機能
- ベンダー固有の拡張機能
- 特殊なドライバー要件
パフォーマンスチューニング
Cats Effectのベストプラクティス
1. 適切なワークロードの理解
- I/O集約的なタスク: Cats Effectは非ブロッキングI/Oで最高のパフォーマンスを発揮
- CPU集約的なタスク: 専用のスレッドプールへの委譲を検討
- 測定の重要性: アプリケーション固有のコンテキストでパフォーマンスを測定
2. IO操作の最適化
// 細粒度のIO合成を活用
val optimized = for {
data <- fetchData() // 非ブロッキングI/O
_ <- IO.cede // 明示的な協調的yield
processed <- processData(data) // CPU集約的な処理
_ <- saveResult(processed) // 非ブロッキングI/O
} yield processed
// CPU集約的なタスクは専用プールで実行
val cpuBound = IO.blocking {
// 重い計算処理
}.evalOn(cpuBoundExecutor)
3. リソース管理
// Resourceを使用した安全なリソース管理
val dataSource = Resource.make(
createDataSource() // 取得
)(_.close()) // 解放
// 自動的なリソース解放を保証
dataSource.use { ds =>
// データソースを使用した処理
}
ldbc最適化設定
val datasource = MySQLDataSource
.build[IO]("localhost", 3306, "user")
.setPassword("password")
.setDatabase("db")
// パフォーマンス設定
.setUseServerPrepStmts(true) // サーバーサイドプリペアドステートメント
.setUseCursorFetch(true) // カーソルベースフェッチング
.setFetchSize(1000) // フェッチサイズ
.setSocketOptions(List(
SocketOption.noDelay(true), // TCP_NODELAY
SocketOption.keepAlive(true) // キープアライブ
))
.setReadTimeout(30.seconds) // 読み取りタイムアウト
スレッドプール設定
// Cats Effect 3のランタイム設定
object Main extends IOApp {
override def computeWorkerThreadCount: Int =
math.max(4, Runtime.getRuntime.availableProcessors())
override def run(args: List[String]): IO[ExitCode] = {
// アプリケーションロジック
}
}
まとめ
ldbcは、特に以下の条件を満たす場合に優れた選択肢となります:
- 高並行性: 多数の同時接続を処理する必要がある
- スケーラビリティ: 負荷に応じた柔軟なスケーリングが必要
- リソース効率: メモリ使用量を最小限に抑えたい
- 型安全性: コンパイル時の型チェックを重視
- 関数型プログラミング: 純粋関数型のアーキテクチャを採用
ベンチマーク結果が示すように、ldbcは8スレッド以上の環境でjdbcの約2倍のスループットを実現し、さらに高いスレッド数でも性能劣化が少ない優れたスケーラビリティを持っています。
Cats Effectの最適化による恩恵
ldbcが活用するCats Effect 3は、以下の点で最適化されています:
- 長時間稼働アプリケーション: 数時間以上の連続稼働で真価を発揮
- ネットワークI/O中心: データベースアクセスのような非ブロッキングI/Oに最適
- マルチコア活用: 8スレッド以上の環境で最高のパフォーマンス
これらの特性は、まさにデータベースアクセスライブラリであるldbcのユースケースと完璧に合致しており、現代的なクラウドネイティブアプリケーションや高トラフィックWebサービスにおいて、ldbcは強力な選択肢となるでしょう。
重要な指針: 「パフォーマンスは常にターゲットとなるユースケースと前提条件に相対的である」- 実際のアプリケーションコンテキストでの測定を推奨します。