Amazon Aurora PostgreSQL 的快速容錯移轉 - Amazon Aurora

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

Amazon Aurora PostgreSQL 的快速容錯移轉

以下,您可以瞭解如何確保儘可能快速地進行容錯移轉。若要在容錯移轉後快速復原,您可以對 Aurora PostgreSQL 資料庫叢集使用叢集快取管理。如需詳細資訊,請參閱Aurora PostgreSQL 的容錯移轉後使用叢集快取管理快速復原

您可以採取,以便快速地執行容錯移轉的一些步驟包括下列步驟:

  • 以短時間範圍設定傳輸控制通訊協定 (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_keepalives_idle 控制時間 (以秒為單位),如果通訊端經過此時間後未傳送任何資料,則傳送保持連線封包。ACK 並不視為資料。我們建議您使用下列設定:

    tcp_keepalives_idle = 1

  • tcp_keepalives_interval 控制時間 (以秒為單位),此時間為傳送初始封包之後傳送後續保持連線封包的時間。請使用 tcp_keepalives_idle 參數設定此時間。我們建議您使用下列設定:

    tcp_keepalives_interval = 1

  • tcp_keepalives_count 為通知應用程式之前發生的未確認保持連線探測的數量。我們建議您使用下列設定:

    tcp_keepalives_count = 5

這些設定應該在資料庫停止回應的五秒鐘內通知應用程式。如果在應用程式網路內保持連線封包經常遭到捨棄,則可以設定較高的 tcp_keepalives_count 值。這麼做雖然會增加偵測實際故障所需的時間,但在較不可靠的網路中可允許更多緩衝。

若要在 Linux 上設定 TCP 保持連線參數
  1. 測試如何設定 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
  2. 找到適合您應用程式的組態之後,請將下列各行 (包括您所做的任何變更) 新增至 /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 連線字串的主機區段。重新命名資料庫叢集會造成這些端點變更。如果發生這種情況,請確定您的應用程式會處理該情況。

另一個選項是使用資料庫執行個體節點的清單,如下所示。

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 端點時,每次嘗試連線時,只會嘗試兩個節點。但是,使用資料庫執行個體節點有一個缺點。如果從您的叢集新增或移除節點,而執行個體端點的清單變得過時,則連線驅動程式可能永遠找不到要連線的正確主機。

積極地設定下列參數,以協助確保您的應用程式不會在連線至任何主機時等候過久。

  • targetServerType – 控制驅動程式是否連接至寫入或讀取節點。若要確保您的應用程式只會重新連線至寫入節點,請將 targetServerType 值設定為 primary

    targetServerType 參數的值包括 primarysecondaryanypreferSecondary。此 preferSecondary 值會先嘗試與讀取器建立連線。如果沒有讀取器連線可以建立,則會連線至寫入器。

  • loginTimeout – 控制通訊端連線建立之後,您的應用程式等待登入資料庫的時間長短。

  • connectTimeout – 控制通訊端等候資料庫連線建立的時間長短。

視您希望應用程式的積極程度而定,可以修改其他應用程式參數以加速連線程序。

  • cancelSignalTimeout – 在某些應用程式中,您可能希望對已逾時的查詢傳送「最大努力」取消信號。如果此取消信號位於您的容錯移轉路徑中,您應該考慮積極地設定它,以避免將此信號傳送至已停止執行的主機。

  • socketTimeout – 此參數可控制通訊端等候讀取操作的時間長短。此參數可用做為全域「查詢逾時」,以確保任何查詢的等候時間均不超過於此值。一個好的做法是擁有兩個連線處理程序。一個連接處理程序運行短期查詢,並將此值設定為較低的值。另一個連線處理常式 (針對長時間執行的查詢) 的設定要高得多。使用此種方法,您可以仰賴 TCP 保持連線參數,當伺服器關閉時停止長時間執行的查詢。

  • tcpKeepAlive – 啟用此參數以確保系統會遵守您所設定的 TCP 保持連線參數。

  • loadBalanceHosts – 設為 true 時,此參數會將應用程式連接至自候選主機清單選擇的隨機主機。

用於取得主機字串的其他選項

您可以從數個來源 (包括 aurora_replica_status 函數和透過使用 Amazon RDS API) 取得主機字串。

在許多情況下,您需要判斷誰是叢集的寫入器,或尋找叢集中的其他讀取器節點。為了這麼做,您的應用程式可以連線至資料庫叢集中的任何資料庫執行個體,並查詢 aurora_replica_status 函數。您可以使用此函數來減少尋找要連接之主機所花費的時間量。然而在某些網路故障情況下,aurora_replica_status 函數可能會顯示過時或不完整的資訊。

為了確保應用程式可以找到要連線的節點,最好嘗試先連線至叢集寫入器端點,接著再連線至叢集讀取器端點。您可以執行此操作,直到您可以建立可讀連線為止。除非您重新命名資料庫叢集,否則這些端點不會變更。因此一般而言,您可以將它們保留為您應用程式的靜態成員,或存放在您應用程式會從中讀取的資源檔案中。

使用其中一個端點建立連線之後,您可以取得叢集其餘部分的資訊。若要執行此作業,請呼叫 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)

例如連線字串的主機區段可以從寫入器和讀取器叢集端點開始,如下所示。

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 函數來確定 server_id 具有 session_id='MASTER_SESSION_ID'。此函數提供主要節點的名稱。您可以將其與 endpointPostfix 搭配使用,說明如下。

當您連線到具有過時資料的複本時,請確保您曉得此種情況。發生此情況時,aurora_replica_status 函數可能會顯示過時的資訊。您可以在應用程式層級設定失效的臨界值。若要檢查這一點,您可以查看伺服器時間與 last_update_timestamp 值之間的差異。一般來說,由於 aurora_replica_status 函數傳回的資訊會衝突,您的應用程式應該確保避免在兩個主機之間反覆執行。您的應用程式應該先嘗試所有已知主機,而不是遵循 aurora_replica_status 函數傳回的資料。

使用 DescribeDBClusters API 操作列出執行個體的 Java 範例

您可以使用 適用於 Java 的 AWS SDK (特別是 DescribeDBClusters API 操作),以程式設計方式尋找執行個體的清單。

這裡是您在 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 未回應或回應時間太長,理想的做法是預設為使用資料庫叢集的 Aurora 端點。在更新 DNS 記錄耗用的時間內會保證這些端點是最新的。使用端點更新 DNS 記錄需要的時間通常不到 30 秒。您可以將端點儲存在應用程式取用的資源檔案中。

測試容錯移轉

在所有情況下,您必須擁有包含兩個或多個資料庫執行個體的資料庫叢集。

從伺服器端,某些 API 操作可能會導致中斷,可用來測試您的應用程式如何回應:

  • FailoverDBCluster – 此作業嘗試將資料庫叢集中的新資料庫執行個體提升為寫入器。

    下列程式碼範例顯示如何使用 failoverDBCluster 來造成中斷。如需設定 Amazon RDS 用戶端的詳細資訊,請參閱使用適用於 Java 的 AWS 開發套件

    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 – 刪除主要 (寫入器) 執行個體會導致資料庫叢集的新資料庫執行個體被提升為寫入器。

如果您使用的是 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(",")); } }