Amazon Aurora PostgreSQL による高速フェイルオーバー
以下では、フェイルオーバーが可能な限り速く実行する方法について説明します。フェイルオーバー後に迅速に回復するには、Aurora PostgreSQL DB クラスターのクラスターキャッシュ管理を使用できます。詳細については、「Aurora PostgreSQL のクラスターキャッシュ管理によるフェイルオーバー後の高速リカバリ」を参照してください。
フェイルオーバーを高速に実行するための手順には、次のようなものがあります。
-
Transmission Control Protocol (TCP) のキープアライブを短い時間に設定することで、障害発生時、読み取りタイムアウトになる前に長時間実行中のクエリを停止します。
-
Java ドメインネームシステム (DNS) キャッシュのタイムアウトを積極的に設定します。これにより、Aurora の読み取り専用エンドポイントでは、その後の接続試行で読み取り専用ノードを適切に循環させることができるようになります。
-
JDBC 接続文字列で使用されるタイムアウト可変をできるだけ低く設定します。短期間の実行クエリと長期間の実行クエリに対して、別々の接続オブジェクトを使用します。
-
指定された Aurora エンドポイントの読み書きを使用してクラスターに接続します。
-
RDS API オペレーションを使用して、サーバー側の障害に対するアプリケーションの応答をテストします。また、パケットドロップツールを使用して、クライアント側の障害に対するテストアプリケーションの応答をテストします。
-
AWS JDBC ドライバーを使用して、Aurora PostgreSQL のフェイルオーバー機能を最大限に活用します。AWS JDBC ドライバーおよびその使用方法の詳細については、「Amazon Web Services (AWS) JDBC ドライバー GitHub リポジトリ
」を参照してください。
これらについて、以下で詳しく説明します。
TCP キープアライブパラメータの設定
TCP 接続を設定すると、接続には一連のタイマーが関連付けられます。キープアライブタイマーがゼロになると、エンドポイントにキープアライブプローブパケットが送信されます。プローブが応答を受信した場合、接続は引き続き稼働中であると推測できます。
TCP キープアライブパラメータを有効化して積極的に設定することで、クライアントがデータベースに接続できない場合は、有効な接続が速やかに切断されます。その後、アプリケーションは新しいエンドポイントに接続できます。
以下の TCP キープアライブパラメータを設定する必要があります。
-
tcp_keepalive_time
は、ソケットからデータが送信されない場合に、キープアライブパケットが送信される時間を秒単位で制御します。ACK はデータとはみなされません。次の設定が推奨されます。tcp_keepalive_time = 1
-
tcp_keepalive_intvl
は、最初のパケットが送信されてから後続のパケットが送信されるまでの時間を秒単位で制御します。この時間は、tcp_keepalive_time
パラメータで設定します。次の設定が推奨されます。tcp_keepalive_intvl = 1
-
tcp_keepalive_probes
は、アプリケーションに通知される前に発生する、認知されていないキープアライブプローブ数です。次の設定が推奨されます。tcp_keepalive_probes = 5
この設定で、データベースの応答が停止してから 5 秒後以内にアプリケーションに通知します。アプリケーションネットワークでキープアライブがドロップする頻度が高い場合には、tcp_keepalive_probes
値をより高く設定できます。これにより実際の障害を検出するまでの時間が増えますが、信頼性が低いネットワークでもバッファを確保できます。
Linux で TCP キープアライブパラメータを設定するには
-
TCP キープアライブパラメータの設定方法をテストします。
そのためには、以下のコマンドをコマンドラインで使用することをお勧めします。この推奨設定はシステム全体に適用されます。つまり、
SO_KEEPALIVE
オプションをオンにしてソケットを作成する他のすべてのアプリケーションにも影響します。sudo sysctl net.ipv4.tcp_keepalive_time=1 sudo sysctl net.ipv4.tcp_keepalive_intvl=1 sudo sysctl net.ipv4.tcp_keepalive_probes=5
-
アプリケーションに適した設定を見つけたら、以下の行を
/etc/sysctl.conf
に追加して、変更した内容を含めてこの設定を維持します。tcp_keepalive_time = 1 tcp_keepalive_intvl = 1 tcp_keepalive_probes = 5
高速フェイルオーバー用にアプリケーションを設定する
以下では、Aurora PostgreSQL の高速フェイルオーバーに使える設定変更についていくつか説明します。PostgreSQL JDBC ドライバーのセットアップと設定の詳細については、PostgreSQL JDBC ドライバー
DNS キャッシュタイムアウトの短縮
フェイルオーバー後にアプリケーションが接続を確立しようとすると、新しい Aurora PostgreSQL ライターは前のリーダーになります。これは DNS の更新が完全に伝達される前に、Aurora の読み取り専用エンドポイントを使用して検索できます。Java DNS の有効期限 (TTL) を 30 秒以下のような低い値に設定すると、その後のリーダーノード間における接続試行のサイクルに役立ちます
// Sets internal TTL to match the Aurora RO Endpoint TTL java.security.Security.setProperty("networkaddress.cache.ttl" , "1"); // If the lookup fails, default to something like small to retry java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "3");
Aurora PostgreSQL 接続文字列を高速フェイルオーバー用に設定する
Aurora PostgreSQL 高速フェイルオーバーを使用するには、アプリケーションの接続文字列が、単一のホストではなく、ホストの一覧になっている必要があります。Aurora PostgreSQL クラスターに接続するために使用できる接続文字列の例を次に示します。この例では、ホストは太字表示されています。
jdbc:postgresql://myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432, myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432 /postgres?user=<primaryuser>&password=<primarypw>&loginTimeout=2 &connectTimeout=2&cancelSignalTimeout=2&socketTimeout=60 &tcpKeepAlive=true&targetServerType=primary
最大限の可用性と RDS API への依存を回避するため、接続用のファイルを維持することをお勧めします。このファイルには、データベース接続の確立時にアプリケーションが読み込んだホスト文字列が含まれています。このホストの文字列には、クラスターで使用できるすべての Aurora エンドポイントがあります。Aurora エンドポイントの詳細については、「Amazon Aurora エンドポイント接続」を参照してください。
例えば、以下のようにエンドポイントをローカルファイルに保存できます。
myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432, myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432
アプリケーションは、このファイルを読み込んで JDBC 接続文字列のホストセクションを入力します。DB クラスターの名前を変更すると、これらのエンドポイントが変更されます。このイベントが発生した場合は、アプリケーションによってこのイベントが処理されることを確認してください。
また、以下のように DB インスタンスノードの一覧を使用する方法もあります。
my-node1.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432, my-node2.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432, my-node3.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432, my-node4.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432
この方法の利点は、PostgreSQL JDBC 接続ドライバーがこの一覧のすべてのノードをループして有効な接続を検索することです。一方、Aurora エンドポイントを使用する場合、1 回の接続試行で 2 つのノードのみが試行されます。ただし、DB インスタンスノードを使用することにはデメリットがあります。クラスターにノードを追加、または削除すると、インスタンスエンドポイントの一覧が古くなり、接続ドライバーが接続する接続ホストを見つけることができない場合があります。
以下のパラメータ設定を活用すると、アプリケーションがいずれか 1 つのホストに接続する際、必要以上に待機しないようにできます。
-
targetServerType
- ドライバーが書き込みノードと読み取りノードのどちらに接続するかを制御します。アプリケーションが書き込みノードにのみ再接続されるようにするには、targetServerType
の値をprimary
に設定します。targetServerType
パラメータの値は、primary
、secondary
、any
、およびpreferSecondary
です。preferSecondary
の値を指定すると、最初にリーダーへの接続の確立が試行されます。リーダーへの接続がない場合は、ライターに接続されます。 -
loginTimeout
- ソケット接続の確立後、アプリケーションがデータベースにログインするための待機時間を制御します。 -
connectTimeout
- ソケットがデータベースに接続を確立するまでの待機時間を制御します。
その他のアプリケーションパラメータを変更して、アプリケーションの希望する積極性に応じて、接続プロセスを高速化できます。
-
cancelSignalTimeout
- 一部のアプリケーションでは、タイムアウトがあるクエリで「ベストエフォート」型キャンセル信号を送信できます。このキャンセル信号がフェイルオーバーパスにある場合は、デッドホストにこの信号を送信しないように、この信号を積極的に設定することを検討してください。 -
socketTimeout
- このパラメータは、ソケットが読み取り操作で待機する時間を制御します。このパラメータは、グローバルな「クエリタイムアウト」として使用でき、すべてのクエリがこの値以上待機しないことを確保します。2 つの接続ハンドラーを用意することをお勧めします。1 つの接続ハンドラーが短期間のクエリを実行するため、この値を低く設定します。長時間クエリを実行する別の接続ハンドラーでは、この値を非常に高く設定します。この方法により、サーバーがダウンした場合に、TCP キープアライブパラメータによって長期間実行しているクエリを停止できます。 -
tcpKeepAlive
- このパラメータを有効にすると、設定した TCP キープアライブパラメータが優先されるようにします。 -
loadBalanceHosts
-true
に設定すると、このパラメータは、選択可能なホストの一覧からランダムに選択されたホストにアプリケーションを接続します。
ホスト文字列を取得するためのその他のオプション
aurora_replica_status
関数を含めたいくつかの出典から、また Amazon RDS API を使用することで、ホスト文字列を取得できます。
多くの場合、クラスターのライターを特定するか、クラスター内の他のリーダーノードを検索する必要があります。そのために、アプリケーションでは、DB クラスター内の任意の DB インスタンスに接続し、aurora_replica_status
関数にクエリします。この関数を使用することで、接続先のホストを検索する時間を短縮できます。ただし、特定のネットワーク障害シナリオでは、aurora_replica_status
関数によって、最新ではない情報や不完全な情報を表示することがあります。
アプリケーションが接続先のノードを確実に見つけられるようにするために、クラスターライターエンドポイントへの接続を試行し、次にクラスターリーダーエンドポイントに接続するのが良い方法です。読み取り可能な接続を確立できるようになるまで、この操作を行います。これらのエンドポイントは、DB クラスターの名前を変更しない限り変更されません。したがって、一般的には、アプリケーションの静的メンバーとして残すか、アプリケーションが読み込むリソースファイルに保管することができます。
これらのエンドポイントの 1 つを使用して接続を確立したら、残りのクラスターの情報を取得できます。これを行うには、aurora_replica_status
関数を呼び出します。例えば、次のコマンドは aurora_replica_status
で情報を取得します。
postgres=> SELECT server_id, session_id, highest_lsn_rcvd, cur_replay_latency_in_usec, now(), last_update_timestamp FROM aurora_replica_status(); server_id | session_id | highest_lsn_rcvd | cur_replay_latency_in_usec | now | last_update_timestamp -----------+--------------------------------------+------------------+----------------------------+-------------------------------+------------------------ mynode-1 | 3e3c5044-02e2-11e7-b70d-95172646d6ca | 594221001 | 201421 | 2017-03-07 19:50:24.695322+00 | 2017-03-07 19:50:23+00 mynode-2 | 1efdd188-02e4-11e7-becd-f12d7c88a28a | 594221001 | 201350 | 2017-03-07 19:50:24.695322+00 | 2017-03-07 19:50:23+00 mynode-3 | MASTER_SESSION_ID | | | 2017-03-07 19:50:24.695322+00 | 2017-03-07 19:50:23+00 (3 rows)
例えば、接続文字列の [hosts] (ホスト) セクションは、ライタークラスターとリーダークラスターの両方のエンドポイントから始まる場合があります。
myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432, myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432
このシナリオでは、アプリケーションは、プライマリまたはセカンダリの任意のノードタイプに接続の確立を試行します。アプリケーションが接続されたら、最初にノードの読み書き状態を調べることをお勧めします。そのためには、コマンド SHOW
transaction_read_only
の結果をクエリします。
クエリの戻り値が OFF
の場合、プライマリノードへの接続に成功しています。ただし、戻り値が ON
で、アプリケーションによって読み書きの接続を要求しているとします。この場合、aurora_replica_status
関数を呼び出すことで、session_id='MASTER_SESSION_ID'
を持つ server_id
を判断できます。この関数は、プライマリノードの名前を表示します。後で説明する endpointPostfix
と一緒に使用できます。
古いデータを持つレプリカに接続する場合は注意が必要です。この場合、aurora_replica_status
関数は、最新ではない情報を表示することがあります。古さのしきい値はアプリケーションレベルで設定できます。これを確認するには、サーバーの時間と last_update_timestamp
の値との差で判断できます。一般に、アプリケーションでは、aurora_replica_status
関数から返される情報が矛盾することによる 2 つのホスト間での急変を避ける必要があります。アプリケーションでは aurora_replica_status
から返されるデータに従うのではなく、最初にすべての既知のホストを試行する必要があります。
DescribeDBClusters API オペレーションを使用したインスタンスのリスト化 (Java の例)
インスタンスの一覧をプログラムで検索するには、AWS SDK for Java
以下に、Java 8 でこれを行う方法の一例を紹介します。
AmazonRDS client = AmazonRDSClientBuilder.defaultClient(); DescribeDBClustersRequest request = new DescribeDBClustersRequest() .withDBClusterIdentifier(clusterName); DescribeDBClustersResult result = rdsClient.describeDBClusters(request); DBCluster singleClusterResult = result.getDBClusters().get(0); String pgJDBCEndpointStr = singleClusterResult.getDBClusterMembers().stream() .sorted(Comparator.comparing(DBClusterMember::getIsClusterWriter) .reversed()) // This puts the writer at the front of the list .map(m -> m.getDBInstanceIdentifier() + endpointPostfix + ":" + singleClusterResult.getPort())) .collect(Collectors.joining(","));
ここで、以下に示すように pgJDBCEndpointStr
にはエンドポイントの形式一覧が含まれています。
my-node1.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432, my-node2.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432
この変数 endpointPostfix
には、アプリケーションが設定する定数を指定できます。または、アプリケーションがクラスター内の単一インスタンスの DescribeDBInstances
API オペレーションをクエリすることで取得することもできます。この値は、AWS リージョン 内と個人のお客様では定数のままです。そのため、アプリケーションが読み取るリソースファイルにこの定数を保存するだけで、API 呼び出しを節約できます。前述の例では、以下のように設定されています。
.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com
API が応答しないか、応答するまでの時間が長すぎる場合、可用性を高めるために DB クラスターの Aurora エンドポイントをデフォルトで使用するように設定することをお勧めします。エンドポイントは、DNS レコードを更新するためにかかる時間内で最新状態に保たれることが保証されます。エンドポイントでの DNS レコードの更新は、通常 30 秒以内に完了します。エンドポイントは、アプリケーションが消費するリソースファイルに保存できます。
フェイルオーバーのテスト
いずれの場合も、2 つ以上の DB インスタンスを含む DB クラスターが必要です。
サーバー側では、特定の API オペレーションによって障害が発生する場合があり、アプリケーションがどのように応答するかテストするために使用できます。
-
FailoverDBCluster - このオペレーションは、DB クラスターの新規の DB インスタンスをライターに昇格させようと試行します。
次のコード例は、
failoverDBCluster
を使用して停止を引き起こす方法を示しています。Amazon RDS クライアントの設定の詳細については、AWS SDK for Java の使用を参照してください。public void causeFailover() { final AmazonRDS rdsClient = AmazonRDSClientBuilder.defaultClient(); FailoverDBClusterRequest request = new FailoverDBClusterRequest(); request.setDBClusterIdentifier("cluster-identifier"); rdsClient.failoverDBCluster(request); }
-
RebootDBInstance - フェイルオーバーはこの API オペレーションでは保証されません。ただし、ライターのデータベースは停止します。これを使用すると、接続が切断された際にアプリケーションがどのように応答するかをテストできます。
ForceFailover
パラメータは Aurora エンジンには適用されません。代わりに、FailoverDBCluster
API オペレーションを使用します。 -
ModifyDBCluster -
Port
パラメータを変更すると、新しいポートでクラスター内のノードがリッスンを開始すると停止します。一般に、アプリケーションがポートの変更のみを制御することで、この障害に対応できます。また、依存するエンドポイントを適切に更新できることを確認します。これは、API レベルで変更が加えられた際に、手動でポートを更新することで実現できます。または、アプリケーションで RDS API を使用し、ポートが変更されたかどうかを確認できます。 -
ModifyDBInstance—
DBInstanceClass
パラメータを変更すると停止します。 -
DeleteDBInstance - プライマリ (ライター) を削除すると、新規の DB インスタンスが DB クラスターでライターに昇格します。
アプリケーション側またはクライアント側では、Linux を使用する場合、突然のパケットドロップに対してアプリケーションがどのように応答するかテストできます。これは、iptables コマンドを使用して、ポート、ホスト、または TCP キープアライブパケットの送受信の有無に基づき行うことができます。
高速フェイルオーバーの例 (Java)
以下のコード例では、アプリケーションがどのように Aurora PostgreSQL ドライバーマネージャーをセットアップできるかを示しています。
アプリケーションは、接続が必要になると getConnection
関数を呼び出します。getConnection
への呼び出しでは、有効なホストが見つからないことがあります。一例として、ライターは見つかりませんが、targetServerType
パラメータが primary
に設定されています。この場合、呼び出し元のアプリケーションは、単に関数の呼び出しを再試行する必要があります。
この再試行動作を接続プーラーにまとめることで、再試行動作によってアプリケーションに与える影響を回避できます ほとんどの接続プーラーでは、JDBC 接続文字列を指定できます。したがって、アプリケーションは getJdbcConnectionString
を呼び出し、それを接続プーラーに渡すことができます。これにより、Aurora PostgreSQL でより高速なフェイルオーバーを使用できるようになります。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.joda.time.Duration; public class FastFailoverDriverManager { private static Duration LOGIN_TIMEOUT = Duration.standardSeconds(2); private static Duration CONNECT_TIMEOUT = Duration.standardSeconds(2); private static Duration CANCEL_SIGNAL_TIMEOUT = Duration.standardSeconds(1); private static Duration DEFAULT_SOCKET_TIMEOUT = Duration.standardSeconds(5); public FastFailoverDriverManager() { try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } /* * RO endpoint has a TTL of 1s, we should honor that here. Setting this aggressively makes sure that when * the PG JDBC driver creates a new connection, it will resolve a new different RO endpoint on subsequent attempts * (assuming there is > 1 read node in your cluster) */ java.security.Security.setProperty("networkaddress.cache.ttl" , "1"); // If the lookup fails, default to something like small to retry java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "3"); } public Connection getConnection(String targetServerType) throws SQLException { return getConnection(targetServerType, DEFAULT_SOCKET_TIMEOUT); } public Connection getConnection(String targetServerType, Duration queryTimeout) throws SQLException { Connection conn = DriverManager.getConnection(getJdbcConnectionString(targetServerType, queryTimeout)); /* * A good practice is to set socket and statement timeout to be the same thing since both * the client AND server will stop the query at the same time, leaving no running queries * on the backend */ Statement st = conn.createStatement(); st.execute("set statement_timeout to " + queryTimeout.getMillis()); st.close(); return conn; } private static String urlFormat = "jdbc:postgresql://%s" + "/postgres" + "?user=%s" + "&password=%s" + "&loginTimeout=%d" + "&connectTimeout=%d" + "&cancelSignalTimeout=%d" + "&socketTimeout=%d" + "&targetServerType=%s" + "&tcpKeepAlive=true" + "&ssl=true" + "&loadBalanceHosts=true"; public String getJdbcConnectionString(String targetServerType, Duration queryTimeout) { return String.format(urlFormat, getFormattedEndpointList(getLocalEndpointList()), CredentialManager.getUsername(), CredentialManager.getPassword(), LOGIN_TIMEOUT.getStandardSeconds(), CONNECT_TIMEOUT.getStandardSeconds(), CANCEL_SIGNAL_TIMEOUT.getStandardSeconds(), queryTimeout.getStandardSeconds(), targetServerType ); } private List<String> getLocalEndpointList() { /* * As mentioned in the best practices doc, a good idea is to read a local resource file and parse the cluster endpoints. * For illustration purposes, the endpoint list is hardcoded here */ List<String> newEndpointList = new ArrayList<>(); newEndpointList.add("myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432"); newEndpointList.add("myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432"); return newEndpointList; } private static String getFormattedEndpointList(List<String> endpoints) { return IntStream.range(0, endpoints.size()) .mapToObj(i -> endpoints.get(i).toString()) .collect(Collectors.joining(",")); } }