PgBouncer のスループットを4倍に拡張した方法
(clickhouse.com)- ClickHouse Managed Postgres では、シングルスレッドの PgBouncer が1つの CPU コアに縛られる制約を避けるため、コア数に比例したマルチプロセスのフリートを標準構成として運用している
- すべてのプロセスが
so_reuseportで同じポートにバインドされ、カーネルが新規接続を分散するため、クライアントからは単一のエンドポイントに見える - 別接続で入ってくる Postgres のキャンセルリクエストが、セッションを保持していないプロセスに到着する可能性があるため、プロセスピアリングで実際のセッション所有者へ転送する
- 16-vCPU の
c7i.4xlargeでは、単一プロセスは約 87,000 TPS がピークだったが、16プロセスのフリートは約 336,000 TPS まで伸び、約4倍のスループットを記録した - 接続数が少ない場合は単一プロセスが同等または少し速いこともあるが、高い並行性では1つのコアがボトルネックになるため、接続予算をフリート全体に分け、Postgres を過剰接続にせずにスループットと接続上限を拡張する必要がある
単一プロセスが生む CPU ボトルネック
- PgBouncer はシングルスレッドであるため、システムの CPU 数に関係なく、1つのプロセスは1つのコアしか使わない
- 16-vCPU サーバーでも接続プーリング処理は1つのコアに集中し、残りの15コアはアイドル状態になる
- そのため Postgres の処理余力が尽きる前に、プーラーが全体のスループットを制限する
- ClickHouse Managed Postgres は、利用可能なコア数に比例して PgBouncer のプロセス数を決めるプロセスフリートを運用している
so_reuseport を使った接続分散
- フリート内のすべてのプロセスは
so_reuseportを有効化し、同じポートにバインドする - カーネルが受信接続を各プロセスに分散するため、クライアントは1つのエンドポイントに接続すればよく、背後で複数の PgBouncer が動作していることを知る必要はない
- PgBouncer のドキュメントでも、複数のシングルスレッドプロセスで複数コアを活用する方法として
so_reuseportを案内している
クエリキャンセルを保証するプロセスピアリング
- Postgres のキャンセルリクエストは、実行中のクエリ接続とは別の新しい接続を通じ、キャンセルキーとともに入ってくる
so_reuseportを使うと、カーネルが新しいキャンセル接続を、そのセッションを保持しているプロセスではなく別のプロセスに渡す可能性がある- リクエストを受けたプロセスが該当クエリを知らなければ、キャンセルは実行されない
- PgBouncer プロセス同士をピアリングすると、誤ったプロセスに到着したリクエストを実際にセッションを所有するプロセスへ転送できる
- これにより、どのプロセスがリクエストを受けても、フリート全体でクエリキャンセルが正常に動作する
トランザクションプーリングと接続予算
- プーリングはトランザクションモードで動作し、トランザクションがコミットされるとすぐにサーバー接続がプールへ返される
max_client_connとmax_db_connectionsはプロセス数で割り、各プロセスに割り当てる- 接続予算を分割すると、フリート全体の接続数を Postgres の安全な上限内に保ちながら、集約した接続上限を引き上げられる
- 単一プロセスは自身の
max_client_connを超える新規クライアントを、次のエラーとともに拒否するFATAL: no more connections allowed (max_client_conn)
同一ハードウェアでのベンチマーク
- 2つの構成は同一の AWS EC2 環境で測定した
- プーラーサーバー: 16-vCPU
c7i.4xlarge - Postgres: 別サーバー
- 負荷生成器: 3台目のサーバーで
pgbenchを使用 - ワークロード: 読み取り専用(select-only)、トランザクションプーリングモード
- プーラーサーバー: 16-vCPU
- 比較対象は PgBouncer の単一プロセスと16プロセスのフリートで、インスタンスタイプ・Postgres・ワークロードは同一
- クライアント接続を8から256まで増やしながら、スループットと16コアサーバーの CPU 使用率を測定した
| クライアント | 単一プロセス TPS | 単一プロセス サーバー CPU | フリート TPS | フリート サーバー CPU |
|---|---|---|---|---|
| 8 | 8,910 | 0.8% | 6,450 | 2.9% |
| 32 | 54,203 | 5.2% | 64,244 | 12.3% |
| 64 | 86,570 | 8.3% | 219,439 | 31.9% |
| 128 | 83,463 | 8.1% | 320,547 | 45.9% |
| 256 | 76,893 | 7.7% | 336,469 | 48.9% |
スループットと CPU 活用の結果
- 単一プロセスは約 87,000 TPS でピークに達した後、負荷が増えるほど性能が低下し、クライアント256では約 77,000 TPS を記録した
- 16プロセスのフリートはより多くのコアを活用しながら約 336,000 TPS まで増加し、単一プロセス比で約4倍のスループットを示した
pidstatの測定では、単一の PgBouncer プロセスは負荷中に CPU 約97%で1つのコアを使い切っていたが、16-vCPU サーバー全体の使用率は10%未満にとどまった- フリートはサーバー全体に処理を分散して約8コアを使用し、Postgres と負荷生成器が限界に達した時点でも追加の余力が残っていた
- クライアント256を維持した測定では、単一プロセスのサーバーが約9%、フリートのサーバーが約 52% CPU を継続的に使用した
- EC2 CloudWatch の外部測定では、単一プロセスインスタンスの平均 CPUUtilization は約16%、フリートは約60%だった
- CloudWatch の値はゲスト内部の測定より少し高いが、単一の PgBouncer が 16-vCPU の大部分を活用できないという違いは同じである
並行性に応じた構成選択
- 接続数が少ない場合は並列化する処理がなく、フリートの接続が複数プロセスに分散されるため、単一プロセスで十分、または少し速い場合がある
- クライアント8では単一プロセスが 8,910 TPS、フリートが 6,450 TPS を記録した
- 並行性が高まると、単一プロセスが占有する1つのコアがスループットの壁となり、フリートとの性能差が広がる
- PgBouncer より先に Postgres が限界に達する環境では、単一プロセスも妥当なデフォルトである
- プーラーがスループットを制限し始めたら、コア数に合わせたプロセスフリート、
so_reuseportベースのポート共有、プロセスピアリングを組み合わせて使うことでボトルネックを解消できる - すべての ClickHouse Managed Postgres サーバーは、この構成を標準で提供している
1件のコメント
Hacker Newsのコメント
https://github.com/yandex/odysseyを使えばよい。OdysseyはスケーラブルなPgBouncerである
PostgreSQL接続を1万超まで拡張した顧客も多く、今後はOdysseyやpgdogのような代替案も検討する予定である。ただ個人的には、PostgreSQLに1万を超える接続を置く方式は好まず、数百接続でも十分にスケールできると考えている
KubernetesでPgBouncerを運用すれば、1台のマシンで複数プロセスを立ち上げるのも簡単で、複数マシンに分散するのも容易である。
AzureがVMメンテナンスの過程でサーバ群全体に順次障害を起こすことがあるため、マルチマシン構成は特に有用である
ここで言うピアリングの概念は理解できるが、PostgreSQLで使ったことはない。
A) PostgreSQLにはこれを簡単に構成できるモードや設定があるのか。エラーが出なくなるまでキャンセル要求をピアへ順番に転送するのか、あるいは誤って受け取ったプロセスが正しいプロセスを識別できるメタデータがキャンセル要求に含まれているのか、という構造を想像している。
B) すべてのPostgreSQLプロセスが
so_reuseportでクライアント要求を受けるなら、ピア間通信には別の**プロセス間通信(IPC)**手段が必要に見えるが、実際にはどのような仕組みを使っているのか?PgBouncerのプロセスIDをキャンセルキーに含めれば、比較的簡単に実装できそうだ
26枚目のスライドから参照できる: https://www.pgevents.ca/events/pgconfdev2024/sessions/sessio...
PgBouncerメンテナーのJelteによる発表動画もある: https://www.youtube.com/watch?v=X-nCHcZ6vQU
pgdogを使っているが、必要な用途に非常によく合っている
PgBouncerがなぜキャンセル要求まで気にする必要があるのか気になる。PostgreSQLへそのまま渡し、キャンセルされたクエリに対してPostgreSQLが通常応答の代わりにエラーを返せば、その接続を担当するPgBouncerが処理すればよいのではないか?
PgBouncerはクライアントに偽ではあるが追跡可能なキャンセルPIDとシークレット値を渡し、キャンセル要求が戻ってくると実際のサーバー・プロセスのPIDとシークレット値を見つけて転送する。また、クライアントはクエリが実行中だと信じていても、PgBouncerはすでに終了して接続が再利用されたことを知っている場合があるため、その接続とPIDが本当にまだそのクエリを実行中かどうかも確認しなければならない
これはマイクロサービスで、接続プールを通じてPostgreSQLサーバーへのアクセスを制御する用途なのか? モノリシックなバックエンドなら必要なさそうに見える。
たいていのまともなバックエンドフレームワークには接続プールが内蔵されており、マイクロサービスが必要でも推奨でもないユースケースの98%はこれで解決できる
PgBouncerは素晴らしいソフトウェアである。広く活用しており、データベース運用をかなり簡単にしてくれる
so_reuseportを初めて知ったが興味深い。構成の要はこれとピアリングに見えるが、ピアリングはPgBouncerに組み込まれていて設定も簡単なのか?Kubernetesでもピアリングを使えるのか? この環境ではポートを再利用する必要はなさそうだが、各Podが個別の接続プールを持って独立して動作することになるのかも気になる
今46歳だが、23歳のときにPostgreSQLの重い接続モデルを見て衝撃を受けた記憶がある。その後も改善されていないのか?
ただし接続プールを使わないと、PostgreSQLはプロセスをforkしなければならないため、新規接続の作成には常に数十ミリ秒以上のコストがかかる。接続プールなしで書かれたアプリケーションも理想的ではないが、よく存在する。
アプリケーションフレームワークも変化しており、サーバーレスアーキテクチャではPostgreSQLが問題を抱え始める数万接続を生み出しうる。個人的には数百を超える接続は好まないが、今では十分あり得る状況である
Java互換性を確保する過程で、多くの企業が並行性処理の方式を大きく変えなければならなかった。PostgreSQLとSQLiteも似た時期に設計されたが、このようなスレッド論争が起こる前から高負荷システムを展開してきた業界のベテランたちが、古いハードウェア利用者まで支援しながら作ったシステムだった