PostgreSQLコネクションプールの調査

この記事は [NTTコムウェア Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nttcomware) 12日目の記事です。

## はじめに
こんにちは、NTTコムウェアの川本です。

データベース、主にオープンソースのDBMSであるPostgreSQL関連の機能・性能の検証や技術サポートなどをしています。またPostgreSQLやその周辺ツールの開発コミュニティ活動も行っています。

PostgreSQLはクライアントから接続を受けるとそのクライアントの対応を行うバックエンドプロセスを生成するアーキテクチャのため、コネクションプーラーを用いることでプロセス生成のオーバーヘッドを減らすことが出来ます。

これまでも興味はあったのですが、自分で確認したことがなかったので、今回はPostgreSQLのコネクションプーラーとして知られる、PgBouncerやPgpool-IIを利用するとpgbenchのスコアがどう変化するのか検証してみることにしました。

## 調査内容

ノートPC上のVirtualBox仮想マシンで、PostgreSQLとコネクションプーラーを同じサーバ上に構築した場合と、別のサーバ上に構築した場合について、pgbenchの実行結果を比較します。

コネクションプーラーの効果を確認したいため、全ての調査パターンでpgbenchの`-C`オプションを使用した以下のコマンドを実行しています。
```
pgbench -h <IPaddr> -p <port> -C -c 50 -d postgres -U postgres 2>/dev/null
```

:::note warn
物理サーバでなく、リソースも十分な環境でないため、あくまで同一条件におけるpgbenchスコア比較の参考情報で、ある程度の傾向をみるものになります。
:::

### 使用するソフトウェア

- RockyLinux 9.2
- PostgreSQL 16.6 (PostgreSQLコミュニティRPM)
- Pgpool-II 4.5.5 (Pgpool-IIコミュニティRPM)
- PgBouncer 1.23.1 (PostgreSQLコミュニティRPM)

### 調査パターン

* 前提
   * PostgreSQLはサーバ1で動作します
   * pgbenchはサーバ2から実行します
   * コネクションプーラーをサーバ1に構築する場合、PostgreSQLとコネクションプーラー間の接続はTCP/IPとします
   * コネクションプーラーをサーバ2に構築する場合、pgbenchとコネクションプーラー間の接続はTCP/IP接続とします
   * コネクションプーラーありの場合はコネクション作成のためにpgbenchを2回実行してから測定を行います
   * 各パターンで3回ずつpgbenchを実行してその平均で比較します
   * Pgpool-IIはコネクションプーラーとして以外にも多くの機能を持っていますが、今回はコネクションプーラー以外の機能はオフにします
   * SELinux, firewallは無効にしています
   * 各パターンを実行するまえに`pgbench -i postgres`を実行します

1. サーバ1のPostgreSQLに対して、サーバ2からpgbenchを実行
1. サーバ1のPgpool-IIに対して、サーバ2からpgbenchを実行
1. サーバ1のPgBouncerに対して、サーバ2からpgbenchを実行
1. サーバ2のPgpool-IIに対して、サーバ2からpgbenchを実行
1. サーバ2のPgBouncerに対して、サーバ2からpgbenchを実行

### 環境構築(主な設定)

* postgresql.conf
```
max_connections = 100
autovacuum = off
checkpoint_timeout = 1d
max_wal_size = 20GB
```

* pgpool.conf
```
backend_clustering_mode = 'raw'
backend_hostname0 = '192.168.56.101'
process_management_mode = static
num_init_children = 70
max_pool = 1
child_life_time = 0
load_balance_mode = off
```

* pgbouncer.ini
```
[databases]
postgres = host=192.168.56.101 port=5432 dbname=postgres

[pgbouncer]
pool_mode = session
max_client_conn = 70
min_pool_size = 70
max_db_connections = 70
max_user_connections = 70
```

## 測定結果

### パターン1 コネクションプーラーなし

* 1回目
```
latency average = 1724.629 ms
average connection time = 23.403 ms
tps = 28.991736 (including reconnection times)
```
* 2回目
```
latency average = 1859.303 ms
average connection time = 24.779 ms
tps = 26.891790 (including reconnection times)
```
* 3回目
```
latency average = 1795.491 ms
average connection time = 24.227 ms
tps = 27.847542 (including reconnection times)
```

* TPSの平均値は `27.91`

### パターン2 DBサーバにPgpool-II

* 1回目
```
latency average = 2537.307 ms
average connection time = 34.866 ms
tps = 19.705932 (including reconnection times)
```

* 2回目
```
latency average = 2676.883 ms
average connection time = 36.525 ms
tps = 18.678444 (including reconnection times)
```

* 3回目
```
latency average = 2628.731 ms
average connection time = 35.851 ms
tps = 19.020584 (including reconnection times)
```

* TPSの平均値は `19.13`

### パターン3 DBサーバにPgBouncer

* 1回目
```
latency average = 1721.377 ms
average connection time = 21.145 ms
tps = 29.046513 (including reconnection times)
```

* 2回目
```
latency average = 1672.062 ms
average connection time = 20.577 ms
tps = 29.903203 (including reconnection times)
```

* 3回目
```
latency average = 1667.409 ms
average connection time = 20.640 ms
tps = 29.986648 (including reconnection times)
```

* TPSの平均値は `29.65`

### パターン4 APサーバにPgpool-II

* 1回目
```
latency average = 1463.757 ms
average connection time = 15.081 ms
tps = 34.158680 (including reconnection times)
```

* 2回目
```
latency average = 1444.447 ms
average connection time = 14.457 ms
tps = 34.615316 (including reconnection times)
```

* 3回目
```
latency average = 1542.194 ms
average connection time = 15.306 ms
tps = 32.421334 (including reconnection times)
```

* TPSの平均値は `33.73`

### パターン5 APサーバにPgBouncer

* 1回目
```
latency average = 762.229 ms
average connection time = 1.728 ms
tps = 65.597076 (including reconnection times)
```

* 2回目
```
latency average = 793.870 ms
average connection time = 1.769 ms
tps = 62.982612 (including reconnection times)
```

* 3回目
```
latency average = 807.646 ms
average connection time = 1.783 ms
tps = 61.908296 (including reconnection times)
```

* TPSの平均値は `63.50`

## 結果確認

各パターンにおけるTPSは以下のようになりました。

| No. | パターン | TPS | 1との比較 |
| --- | --- | --- | :---: |
| 1 | コネクションプーラーなし | 27.91 | 1.00 |
| 2 | DBサーバにPgpool-II | 19.13 | 0.69 |
| 3 | DBサーバにPgBouncer | 29.65 | 1.06 |
| 4 | APサーバにPgpool-II | 33.73 | 1.21 |
| 5 | APサーバにPgBouncer | 63.50 | 2.28 |

想像以上に差があって驚きました。
DBサーバ上にコネクションプーリングを目的としてPgpool-IIを置くのはよくないということと、コネクションプーリングとしてPgBouncerは大きな効果を持つことがわかりました。

また、動作させるサーバを分けることで、ここまで結果が変わるということも学びになりました。
とはいえ、今回自分が動作確認した環境は動作が安定しているとは口が裂けてもいえないので、システムへの導入を検討する場合は実環境での確認を行う必要があると思います。

## おわりに

PostgreSQL界で有名なコネクションプーラーについて、効果を確認しました。

結果から見るとPgBouncerの効果が目を引きますが、Pgpool-IIはクラスタリング機能、ロードバランス機能やクエリキャッシュ機能といった今回使用しなかった機能も持ち合わせている分内部で行う処理も多いため、コネクションプーリング機能のみを取り出して評価するのは不利だったということは考慮しなければなりません。

自分の興味からの検証でしたが、予想外の結果も得られて勉強になりました。

※ 記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。