Amazon Aurora PostgreSQL 的快速故障转移
接下来,您可以学习如何确保故障转移尽快进行。要在故障转移后快速恢复,您可以对 Aurora PostgreSQL 数据库集群使用集群缓存管理。有关更多信息,请参阅通过 Aurora PostgreSQL 的集群缓存管理提供故障转移后的快速恢复。
为使故障转移快速执行,可以采取的一些步骤包括:
-
将传输控制协议(TCP)keepalive 设置为较短的时间范围,以便在出现故障时,在读取超时到期之前停止运行时间较长的查询。
-
前瞻性地设置 Java 域名系统(DNS)缓存的超时。这样做有助于确保 Aurora 只读端点在以后进行连接尝试时,能够在只读节点之间进行正常的循环切换。
-
将在 JDBC 连接字符串中使用的超时变量设置得尽可能低。对短时间和长时间运行的查询,使用单独的连接对象。
-
使用提供的读取和写入 Aurora 端点连接到集群。
-
使用 RDS API 操作测试在服务器端发生故障时的应用程序响应。此外,使用丢包工具测试在客户端发生故障时的应用程序响应。
-
使用 AWS JDBC 驱动程序充分利用 Aurora PostgreSQL 的故障转移功能。有关 AWS JDBC 驱动程序的更多信息及其完整使用说明,请参阅 Amazon Web Services (AWS) JDBC Driver GitHub repository
。
下文将详述其中这些内容。
设置 TCP keepalive 参数
在设置 TCP 连接时,会有一组计时器与该连接相关联。当 keepalive 计时器计时到零时,将向连接端点发送一个 keepalive 探测数据包。如果探测收到回复,则可认为连接仍在正常运行。
开启并前瞻性地设置 TCP keepalive 参数,可确保在客户端无法连接到数据库时,任何活动的连接都能很快关闭。然后,应用程序可以连接到新的端点。
确保设置以下 TCP keepalive 参数:
-
tcp_keepalive_time
控制一个以秒为单位的时间,如果套接字未发送数据的时间达到此时间段,则发送 keepalive 数据包。不将 ACK 视为数据。建议进行以下设置:tcp_keepalive_time = 1
-
tcp_keepalive_intvl
控制发送初始数据包后到发送后续 keepalive 数据包之间的时间,以秒为单位。应使用tcp_keepalive_time
参数设置此时间。建议进行以下设置:tcp_keepalive_intvl = 1
-
tcp_keepalive_probes
是在应用程序收到通知之前未获确认的 keepalive 探测包的数量。建议进行以下设置:tcp_keepalive_probes = 5
这些设置应在数据库停止响应的五秒内通知应用程序。如果在应用程序的网络中经常出现 keepalive 数据包丢包,则可以设置较大的 tcp_keepalive_probes
值。这样做允许在不太可靠的网络中使用更多缓冲区,尽管它增加了检测实际故障所需的时间。
在 Linux 上设置 TCP keepalive 参数
-
测试如何配置 TCP keepalive 参数。
我们建议使用命令行以及以下命令来执行此操作。这个建议的配置是系统范围的。换句话说,它还会影响在
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 连接字符串的主机部分。重命名数据库集群将导致这些端点发生更改。如果发生此事件,请确保您的应用程序处理该事件。
另一个选择是使用数据库实例节点的列表,如下所示。
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
参数的值包括primary
、secondary
、any
和preferSecondary
。值为preferSecondary
将首先尝试与读取器建立连接。如果无法与读取器建立连接,则它连接到写入器。 -
loginTimeout
– 控制应用程序在建立套接字连接之后等待多长时间才能登录到数据库。 -
connectTimeout
– 控制套接字等待多长时间才能与数据库建立连接。
还可以修改其他应用程序参数以加速连接过程,具体取决于应用程序需要多快建立连接:
-
cancelSignalTimeout
– 在某些应用程序中,可能需要对已经超时的查询发送尽力取消信号。如果此取消信号在故障转移路径中,则考虑前瞻性地设置它,以免将此信号发送给已停止运行的主机。 -
socketTimeout
– 该参数控制套接字在执行读取操作之前等待多长时间。该参数可用作全局“查询超时”,以确保任何查询等待的时间都不会超过该值。一个好的做法是使用两个连接处理程序。一个连接处理程序运行短时查询并将该值设置为较低的值。另一个连接处理程序用于长时间运行的查询,应将此值设置为高得多。采用这种方法,如果服务器出现故障,可以依靠 TCP keepalive 参数来停止长时间运行的查询。 -
tcpKeepAlive
– 开启该参数可确保所设置的 TCP keepalive 参数有效。 -
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
函数来确定具有 session_id='MASTER_SESSION_ID'
的 server_id
。此函数为您提供主节点的名称。您可以将其与 endpointPostfix
结合使用,如下文所述。
当您连接到具有陈旧数据的副本时,请确保您知道这一情况。这时,aurora_replica_status
函数可能会显示过时的信息。您可以在应用程序级别设置陈旧阈值。要检查这一点,您可以查看服务器时间和 last_update_timestamp
值之间的差异。一般来说,您的应用程序应避免由于 aurora_replica_status
函数返回的冲突信息而在两台主机之间来回切换。您的应用程序应该首先尝试所有已知主机,而不是遵从由 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 不响应或响应时间过长,则原定设置使用数据库集群的 Aurora 端点是一个好方法。在端点更新 DNS 记录所用时间之内,应确保其最新。使用端点更新 DNS 记录通常可在不到 30 秒的时间内完成。您可以将此端点存储在应用程序使用的资源文件中。
测试故障转移
在任何情况下,数据库集群都必须包含两个或更多数据库实例。
在服务器端,某些 API 操作可能引起中断,您可以利用此中断来测试应用程序的响应情况:
-
FailoverDBCluster – 此操作尝试将数据库集群中的新数据库实例提升为写入器。
以下代码示例显示了如何可以使用
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 – 删除主实例(写入器)会导致数据库集群中的新数据库实例提升为写入器。
从应用程序或客户端,如果您使用 Linux,您可以测试应用程序如何响应突然的数据包丢失。您可以根据端口、主机或是否使用 iptables 命令发送或接收 TCP keepalive 数据包来执行上述操作。
快速故障转移 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(",")); } }