刹那(せつな)の瞬き

Willkömmen! Ich heiße Setsuna. Haben Sie etwas Zeit für mich?

RustでSQLServerへの接続プールにbb8 / bb8-tiberiusを試してみた

バックエンドで Tiberius を利用する非同期な接続プールに bb8 があります。

bb8 は SQLServer 用のアダプタ bb8-tiberius と組み合わせて使用します。
※async-std ではなく tokio 用です。

....

接続プールの使用方法はシンプルなので、前述したサイトの説明に従ってコードを記述すれば問題ないと思います。

接続プールを構築する際、接続数の最大値を .max_size() で指定できるのですが、この値の増減による実行結果を確認してみました。

1. 環境とデータベース

Rustでasync/awaitに対応したTiberiusからSQLServerに接続する

※ここで構築した環境を引き続き利用します。

  • OS: KDE neon 5.21.4 (Ubuntu 20.04 ベース) / macOS Mojave v10.14.6
  • rustc 1.51.0
  • SQL Server 2019 (RTM-CU10) (KB5001090) - 15.0.4123.1 (X64) on Linux
  • データベース: my_test_db

2. プロジェクト

(1) 準備

適当なディレクトリにプロジェクトを作成します。(今回は ~/work/conn_pool)

$ cd ~/work
$ cargo new conn_pool
$ cd conn_pool
(2) ファイル

・Cargo.toml の編集

[dependencies] セクションに下記の内容を追加します。

[dependencies]
tiberius = { version = "0.5", features = ["chrono"] }
bb8 = "0.7"
bb8-tiberius = "0.5"
tokio = { version = "1.5", features = ["time"] }
futures = "0.3"
chrono = "0.4"

ソースコード: src/main.rs

下記のソースコードをコピーして src/main.rs を書き換えます。
※ソース中の conn_str の内容は、試す環境に合わせて変更してください。

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("#### Start ####");
    let started = std::time::Instant::now();

    let conn_str = "Server=tcp:localhost,1433;TrustServerCertificate=yes;Database=my_test_db;UID=sa;PWD=abcd1234$";
    let manager = bb8_tiberius::ConnectionManager::build(conn_str)?;
    let pool = bb8::Pool::builder().max_size(10).build(manager).await?;

    let mut threads = Vec::new();
    for idx in 0..20 {
        let pool_tmp = pool.clone();
        let handle = tokio::spawn(async move {
            println!("Thread #{}", idx);
            let mut client = pool_tmp.get().await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
            let result = client
                .simple_query("SELECT 番号,氏名,誕生日 FROM 会員名簿 ORDER BY 誕生日 DESC")
                .await
                .unwrap()
                .into_first_result()
                .await
                .unwrap();
            for row in result {
                println!(
                    "#{} | {} | {} | {} |",
                    idx,
                    row.get::<i32, _>("番号").unwrap(),
                    row.get::<&str, _>("氏名").unwrap().to_string(),
                    row.get::<chrono::NaiveDate, _>("誕生日")
                        .unwrap()
                        .format("%Y/%m/%d"),
                );
            }
        });
        threads.push(handle);
    }
    futures::future::join_all(threads).await;

    println!("#### Finish ####");
    println!("経過時間: {:?}", started.elapsed());
    Ok(())
}

このコードでは、接続プールの最大値を 10 とし、非同期タスクを 20 個実行します。
また、SQL クエリの実行時間が短いため、敢えて非同期タスクの中で 200 ミリ秒待機しています。
最後に、すべての非同期タスクが完了した後、処理の経過時間を表示します。

準備は以上です。

3. 実行結果

.max_size(10) として実行した結果です。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/conn_pool`
#### Start ####
Thread #0
Thread #2
Thread #1
Thread #3
Thread #4

・・・(ざっくり省略)・・・

#11 | 307 | 中井 雄樹 | 1984/02/29 |
#19 | 105 | 江口 美奈 | 1979/06/23 |
#16 | 307 | 中井 雄樹 | 1984/02/29 |
#11 | 105 | 江口 美奈 | 1979/06/23 |
#16 | 105 | 江口 美奈 | 1979/06/23 |
#### Finish ####
経過時間: 539.765411ms

4. 実行結果のまとめ

今回の処理では、経過時間は接続プールの最大接続数によって変化します。

最大接続数を 1 から 10 まで変化させた場合の経過時間をまとめました。
参考までに最大接続数を非同期タスク数と同じ 20 に設定した場合の結果も載せています。

最大接続数 1回目 2回目 3回目 4回目 5回目
.max_size(1) 4.243623s 4.17383s 4.180024s 4.175134s 4.174535s
.max_size(2) 2.416127s 2.159097s 2.159124s 2.155713s 2.17077s
.max_size(3) 1.773368s 1.544799s 1.553429s 1.552768s 1.58303s
.max_size(4) 1.178223s 1.141275s 1.13779s 1.14564s 1.154331s
.max_size(5) 938.821ms 943.597ms 945.557ms 933.896ms 948.629ms
.max_size(6) 942.27ms 935.486ms 941.867ms 946.894ms 936.995ms
.max_size(7) 822.035ms 737.501ms 744.878ms 743.485ms 752.166ms
.max_size(8) 750.744ms 741.996ms 744.562ms 731.633ms 747.467ms
.max_size(9) 754.524ms 741.767ms 743.779ms 752.383ms 739.935ms
.max_size(10) 538.341ms 539.604ms 542.781ms 541.15ms 537.545ms
.max_size(20) 349.346ms 355.685ms 352.035ms 354.031ms 349.371ms

非同期な接続プールとして特に問題なく、期待通りの結果になりました。

当然の事ですが、この結果は 1 接続あたりの処理数によって変化しています。
最大接続数が 20 の場合、1 接続 1 処理になるので、この結果が最速です。

また、最大接続数が 10 〜 19 の場合、1 接続あたり 1 または 2 処理になるので経過時間に大きな変化はありません。そのため、最大接続数 10 以外は省略しました。

4. 補足

データベースへの接続をここまで簡単に非同期タスクに渡せるのは本当に助かります。

最初は async-std に対応している SQLServer 用の接続プールを探してたのですが、現時点では見つけられませんでした。

bb8-tiberius は features の設定で async-std にも対応してるみたいなので、async-std でコードを書きたい場合は bb8 の対応や代替クレートを待つ感じでしょうか。

個人的には tokio でも特に問題ないので、このまま利用したいと思います。