話題の記事

最大同時接続数を1に制限したRDSにRDS Proxyを構成して基本動作を押さえよう

RDS Proxyの動作について〜入門編〜
2020.07.03

CX事業本部@大阪の岩田です。

社内で需要がありそうだったので、RDS Proxyの基本動作について簡単にまとめてみました。クライアントからの最大同時接続数を1に設定したRDSに対してRDS Proxyを構成し、クライアントアプリケーションに見立てたEC2からいくつかのパターンで接続を試行した結果をまとめています。

環境

今回検証に利用した環境です。

  • RDS for PostgreSQL 11.8-R1
    • インスタンスクラス db.t3.micro
    • max_connections: 9 バックグラウンドでrdsadminユーザー、rdsproxyadminユーザーがDBに接続するのを考慮して9に設定しています。今回の環境であればmax_connectionsを9に設定することで非マスターユーザーからの同時接続数を1に制限することができます。
  • RDS Proxy
    • エンジンの互換性: PostgreSQL
    • 接続プールの最大接続数: 100
    • 接続借用タイムアウト: 10秒

その他通信に必要になるセキュリティグループやサブネットは適宜設定しています。また、検証のため事前にproxy_userという非マスターユーザーを作成しています。

RDS Proxyを利用するメリット

まずは簡単におさらいから。RDS Proxyを利用するメリットの1つとして、アプリケーションからみた論理的なDB接続数を実際のDB接続数より多く見せかけることができる というメリットがあります。例えば2つのクライアントが同時にRDS Proxyに接続している場合、アプリケーションからみたDB接続数は2つですが、RDS Proxyがコネクションプールをよしなに管理することで、実際のRDS Proxy <-> RDS間のDB接続は1つしか消費しない。といった構成が可能になります。このあたりの詳細はこちらの資料で詳しく解説しているので、よければ参考にして下さい。

RDS Proxyなしで接続

まずは意図通りに設定できているか、確認の意味を含めてRDS Proxyを介さずにEC2から直接RDSに接続してみます。

psql -h <RDSのエンドポイント> -U proxy_user template1

​ 一応max_connectionsを確認しましょう。

template1=> show max_connections ;
 max_connections
-----------------
 9
(1 row)

OKですね。現在の接続状況を確認してみます。

template1=> select usename,count(*) from pg_stat_activity  group by usename;
    usename    | count
---------------+-------
               |     4
 proxy_user    |     1
 rdsproxyadmin |     2
 rdsadmin      |     2
(4 rows)

合計9つのDB接続があります。

では、現在psqlで接続中のシェルとは別のシェルからpsqlで接続を試みましょう。

psql -h <RDSのエンドポイント> -U proxy_user template1
Password for user proxy_user:
psql: FATAL:  remaining connection slots are reserved for non-replication superuser and rds_superuser connections

同時接続数の上限を超過したため、接続エラーとなりました。意図通り動作していそうです。

RDS Proxyありで接続

ここからはpsqlの接続先をRDS Proyxのエンドポイントに変更していくつかのパターンを試してみます。

トランザクションなしの場合

まず1つ目のシェルからpsqlでRDS Proxyに接続します。

$ psql -h <RDS Proxyのエンドポイント> -U proxy_user template1
Password for user proxy_user:
psql (11.5, server 11.8)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

template1=>

接続できたら先程と同様に現在の接続状況を確認してみます。

template1=> select usename,count(*) from pg_stat_activity  group by usename;
    usename    | count
---------------+-------
               |     4
 proxy_user    |     1
 rdsproxyadmin |     2
 rdsadmin      |     2
(4 rows)

1つ目のシェルからRDS Proxyに接続したまま、2つ目のシェルからもRDS Proxyに接続してみましょう。

$ psql -h <RDS Proxyのエンドポイント> -U proxy_user template1
Password for user proxy_user:
psql (11.5, server 11.8)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

template1=>

おっ!今度は最大同時接続数超過のエラーにならずに接続できました。

1つ目のpsql接続からSQLを実行してみます。

template1=> select client_addr,client_port from pg_stat_activity where usename = 'proxy_user';
client_addr  | client_port
--------------+-------------
 172.31.13.49 |       12873
(1 row)

同様に2つ目のpsql接続からもSQLを実行してみます。

template1=> select client_addr,client_port from pg_stat_activity where usename = 'proxy_user';
client_addr  | client_port
--------------+-------------
 172.31.13.49 |       12873
(1 row)

無事に実行できました。1つ目のシェル、2つ目のシェル共どちらから確認しても、DBユーザー:proxy_userの使用しているDB接続はクライアントのIPアドレス、ポート番号が同一であり、EC2 <-> RDS Proxy間の2つのpsql接続はRDS Proxy <-> RDS間の1つの接続を共有していることが分かります。

今度は1つ目のpsql接続、2つ目のpsql接続連続でSQLを実行してみましょう。

まず1つ目

select pg_sleep(3);

間髪あけずに2つ目でも同じSQLを実行

select pg_sleep(3);

1つ目のpsqlでは約3秒後にSQLのレスポンスが返却されます。

template1=> select pg_sleep(3);
 pg_sleep
----------

(1 row)

2つ目のpsqlでは約6秒後にSQLのレスポンスが返却されます。

template1=> select pg_sleep(3);
 pg_sleep
----------

(1 row)

1つ目のpsqlがpg_sleep(3)を実行している3秒間はバックエンドのRDS Proxy <-> RDSの接続が1つ目のpsqlに割り当てられているため、2つ目のpsqlはWAIT状態になっていることが分かります。

いい感じですね!アプリケーションに対してRDSの最大同時接続数以上の可用性を提供できそうです。

トランザクションあり

トランザクションを利用するとどうなるでしょうか?先ほどと同様psqlからRDS Proxyに対して2つの接続を確立し、まず1つ目のpsqlでBEGINします。

template1=> begin;
BEGIN

2つ目のpsqlでもBEGINしてみましょう。

template1=> begin;
ERROR:  Timed-out waiting to acquire database connection

今度は約10秒後にタイムアウトエラーとなります。1つ目のpsqlがトランザクション実行中のため、2つ目のpsqlにDB接続を割り当てることができず、RDS Proxyのパラメータ「接続借用タイムアウト」で設定した10秒経過後にエラーが返却されたというわけです。実験が終わったので、1つめのpsqlのトランザクションはROLLBACKしておきましょう。

ロングトランザクションを実行するようなワークロードはRDS Proxyの恩恵を受けづらいことが分かります。

トランザクションなし & Prepared Statement利用

今度はRDS Proyの注意点である「ピン留め」の動作を確認してみます。「ピン留め」とはRDS ProxyにプールされたDB接続が特定のクライアントに固定され、複数のクライアント間で共有できなくなる現象のことです。詳細は公式ドキュメントをご参照下さい。

https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/rds-proxy.html#rds-proxy-pinning

先ほどと同様psqlからRDS Proxyに対して2つの接続を確立し、1つ目のpsql接続からPrepared Statementを利用してみましょう。

template1=> prepare test_stmt as select now() where 1 = $1;
PREPARE
template1=> execute test_stmt (2);
 now
-----
(0 rows)

このまま2つ目のpsql接続からSQLを発行してみましょう

template1=> select now();
FATAL:  Request returned an error: remaining connection slots are reserved for non-replication superuser and rds_superuser connections
SSL connection has been closed unexpectedly
The connection to the server was lost. Attempting reset: Succeeded.

今度は即座にエラーが返却されました。1つの目のpsqlにRDS Proxy <-> RDS間の接続が「ピン留め」されたことで、2つ目のpsql接続用に割り当てるためのDB接続がコネクションプールから枯渇したためです。新たにRDS Proxy <-> RDS間のDB接続をOPENしようとしても、最大同時接続数に到達しているため新規のDB接続がOPENできずエラーが返却されるのです。

1つ目のpsql接続からPrepared StatementをDEALLOCATEするとどうでしょう?

template1=> deallocate test_stmt ;
DEALLOCATE

再度2つ目のpsql接続からSQLを発行します。

template1=> select now();
FATAL:  Request returned an error: remaining connection slots are reserved for non-replication superuser and rds_superuser connections
SSL connection has been closed unexpectedly
The connection to the server was lost. Attempting reset: Succeeded.

エラーです。ピン留め恐ろしや。。。

Lambdaを使うユースケースでは稀かもしれませんが、EC2やECSでアプリを稼働させる場合は何らかのアプリケーションフレームワークを利用することが多いでしょう。アプリケーションフレームワークはセキュリティ対策等の理由でDBアクセス時に自動的にPrepared Statementを利用することも多いと思います。このあたりの特性をしっかり理解しておかないとRDS Proxyのメリットを享受できないばかりか、環境を改悪することにもなりかねないので、このあたりの挙動にはよーく注意しておきたいですね。何らかのアプリケーションフレームワーク on Fargate & RDSの環境にRDS Proxyを追加導入した結果、レイテンシーを悪化させただけで何もメリットが得られなかった。なんて悲しい事件が起きそうです。

まとめ

RDS Proxyの基本動作について簡単にまとめてました。次回はもう少し掘り下げた内容でピン留め周りの挙動を検証してみようと思います。お楽しみに!