Amazon Aurora
Aurora 用户指南
AWS 文档中描述的 AWS 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 AWS 服务入门

Amazon Aurora PostgreSQL 最佳实践

本主题提供使用 Amazon Aurora PostgreSQL 数据库集群或向其迁移数据的最佳实践和选项信息。

使用 Amazon Aurora PostgreSQL 进行快速故障转移

通过 Aurora PostgreSQL 有几个方法可以更快地执行故障转移。本部分将具体讨论以下每个方法:

  • 主动设置 TCP keepalive,确保运行时间较长、等待服务器响应的查询在发生故障的情况下在读取超时到期之前被终止。

  • 主动设置 Java DNS 缓存超时,确保 Aurora 只读终端节点能够在后续的连接尝试中在只读节点之间进行正常的循环切换。

  • 将在 JDBC 连接字符串中使用的超时变量设置得尽可能低。对短时间和长时间运行的查询使用不同的连接对象。

  • 使用提供的读取和写入 Aurora 终端节点建立到集群的连接。

  • 使用 RDS API 测试在服务器端发生故障时的应用程序响应,使用丢包工具测试在客户端发生故障时的应用程序响应。

设置 TCP Keepalive 参数

TCP keepalive 过程很简单:在设置 TCP 连接时,会关联一组定时器。当 keepalive 定时器计时到零,则发送一个 keepalive 探测数据包。如果收到 keepalive 探测的回复,则可认为连接仍在正常运行。

启用并主动设置 TCP keepalive 参数,可确保在客户端不再能够连接到数据库时,任何活动连接都能很快关闭。该操作使应用程序能够做出恰当的反应,例如选择要连接的新主机。

需要设置以下 TCP keepalive 参数:

  • tcp_keepalive_time 控制一个以秒为单位的时间,如果套接字未发送数据 (ACK 不视为数据) 的时间达到此时间段,则发送 keepalive 数据包。建议进行以下设置:

    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

  1. 在测试如何配置 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
  2. 找到对应用程序有效的配置后,必须通过在 /etc/sysctl.conf 中添加以下行 (包括所做的任何更改) 来保留这些设置:

    tcp_keepalive_time = 1 tcp_keepalive_intvl = 1 tcp_keepalive_probes = 5

有关在 Windows 上设置 TCP keepalive 参数的信息,请参阅 Things You May Want to Know About TCP Keepalive

配置应用程序以实现快速故障转移

本部分介绍您可进行的几项特定于 Aurora PostgreSQL 的配置更改。PostgreSQL JDBC 网站提供了有关 JDBC 驱动程序的常规设置和配置的文档。

减少 DNS 缓存超时

在故障转移后,在应用程序尝试建立连接时,新的 Aurora PostgreSQL 写入方是以前的读取方,可以在完全传播 DNS 更新之前使用 Aurora 只读 终端节点找到该写入方。将 Java DNS TTL 设置为较小的值有助于在后续连接尝试时在读取节点之间进行循环切换。

// 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=<masteruser>&password=<masterpw>&loginTimeout=2 &connectTimeout=2&cancelSignalTimeout=2&socketTimeout=60 &tcpKeepAlive=true&targetServerType=master&loadBalanceHosts=true

为获得最佳可用性并避免对 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 终端节点时,每次连接尝试将仅尝试两个节点。使用数据库实例节点的缺点是,如果对集群添加或删除节点,则实例终端节点列表就变得陈旧,连接驱动程序可能永远找不到应该连接的主机。

主动设置以下参数有助于确保应用程序不用等太长时间就能连接任意一台主机。

  • loginTimeout - 控制应用程序在建立套接字连接之后 等待多长时间才能登录到数据库。

  • connectTimeout - 控制套接字等待多长时间才能与数据库建立连接。

还可以修改其他应用程序参数,以加速连接过程,具体取决于应用程序需要多快建立连接。

  • cancelSignalTimeout - 在某些应用程序中,可能需要对已经超时的查询发送“尽力”取消信号。如果此取消信号在故障转移路径中,则应该考虑主动设置它,以免将此信号发送给已停止运行的主机。

  • socketTimeout - 该参数控制套接字在执行读取操作之前等待多长时间。该参数可用作全局“查询超时”,以确保任何查询等待的时间都不会超过该值。一种好方法是,采用两个连接处理程序,一个运行短时间存在的查询并将该值设置得较小,另一个处理长时间运行的查询并将该值设置得较大。这样,如果服务器出现故障,可以由 TCP keepalive 参数来终止长时间运行的查询。

  • tcpKeepAlive - 启用该参数可确保所设置的 TCP keepalive 参数有效。

  • targetServerType- 该参数可用于控制驱动程序是连接到读取 (从) 还是写入 (主) 节点。可能的值为:anymasterslave preferSlave。如果设置为 preferSlave 值,将首先尝试与读取节点建立连接,如果无法与读取节点建立连接,则转而连接写入节点。

  • 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 | vdl | highest_lsn_rcvd | cur_replay_latency | now | last_update_time -----------------------------------+--------------------------- -----------+-----------+------------------+--------------------+- ------------------------------+------- mynode-1 | 3e3c5044-02e2-11e7-b70d-95172646d6ca | 594220999 | 594221001 | 201421 | 2017-03-07 19:50:24.695322+00 | 2017-03-07 19:50:23+00 mynode-2 | 1efdd188-02e4-11e7-becd-f12d7c88a28a | 594220999 | 594221001 | 201350 | 2017-03-07 19:50:24.695322+00 | 2017-03-07 19:50:23+00 mynode-3 | MASTER_SESSION_ID | 594220999 | | | 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_idsession_id='MASTER_SESSION_ID'。此函数为您提供主节点的名称。您可以将此与下述“endpointPostfix”结合使用。

需要注意当连接数据过时的副本的情况。这时,aurora_replica_status 函数可能显示过时的信息。可以按应用程序级别设置陈旧阈值,通过查看服务器时间与 last_update_time 之间的差异可以检查此阈值。通常,您的应用程序应确保避免由于 aurora_replica_status 函数返回的冲突信息而在两个主机之间来回切换。也就是说,您的应用程序应先尝试所有已知主机,而不是盲目遵从 aurora_replica_status 函数返回的数据。

RDS API

您可以使用 AWS Java 软件开发工具包,特别是 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 对集群中的单个实例进行查询来获取。该值在一个区域中或对于单个客户将保持常量,因此,可省去一次 API 调用,只需将此常量保存在应用程序将读取的资源文件中。在上面的示例中,将其设置为:

.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com

考虑到可用性,如果 API 无响应或响应时间过长,默认使用数据库集群的 Aurora 终端节点是一个好方法。在终端节点更新 DNS 记录所用时间 (通常少于 30 秒) 之内,应确保其最新。这也仍然可以存储在应用程序使用的资源文件中。

测试故障转移

在任何情况下,数据库集群都必须包含不少于两个数据库实例。

在服务器端,某些 API 可以引起中断,您可以利用此中断来测试应用程序的响应情况:

  • FailoverDBCluster - 尝试将数据库集群中的新数据库实例提升为写入方

    public void causeFailover() { /* * See http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/basics.html for more details on setting up an RDS client */ 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 数据包时如何基于端口、主机做出响应。

快速故障转移示例

以下代码示例说明了应用程序如何设置 Aurora PostgreSQL 驱动程序管理器。当应用程序需要连接时,它会调用 getConnection(...)。在有些时候,例如找不到写入方,但 targetServerType 设置为“master”,而调用应用程序直接重试时,对该函数的调用可能无法找到有效主机。这些问题可以用一个连接池程序轻松解决,以免将重试操作推送到应用程序。大多数连接池程序允许指定 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 kill 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(",")); } }

排查存储问题

如果排序或索引创建操作所需的内存量超过可用内存量,则 Aurora PostgreSQL 会将多余的数据写入到存储空间。写入数据时,它会使用用于存储错误和消息日志的相同存储空间。如果您的排序或索引创建操作功能超过可用内存,则可能导致本地存储不足。如果您在存储空间不足的情况下运行 Aurora PostgreSQL 时遇到了问题,可以重新配置数据排序以使用更多内存,也可以缩短 PostgreSQL 日志文件的数据保留时间。有关更改日志保留期的更多信息,请参阅 PostgreSQL 数据库日志文件