메뉴
Amazon Relational Database Service
사용 설명서 (API Version 2014-10-31)

Amazon Aurora PostgreSQL 모범 사례

이 주제에서는 Amazon Aurora PostgreSQL DB 클러스터 사용 또는 이 클러스터로의 데이터 마이그레이션과 관련된 모범 사례와 옵션에 대해 설명합니다.

Amazon Aurora PostgreSQL을 사용한 빠른 장애 조치

Aurora PostgreSQL을 사용하여 빠른 장애 조치를 실행하는 몇 가지 방법이 있습니다. 이 단원에서는 다음과 같은 방법을 다룹니다.

  • TCP keepalive를 적극적으로 설정하여, 서버 응답을 기다리며 장기 실행 중인 쿼리가 장애 발생 시 읽기 제한 시간이 만료되기 전 제거되도록 합니다.

  • Java DNS 캐싱 제한 시간을 적극적으로 설정하여, Aurora 읽기 전용 엔드포인트가 후속 연결 시도에서 읽기 전용 노드를 적절히 순환할 수 있도록 합니다.

  • JDBC 연결 문자열에 사용되는 제한 시간 변수를 가능한 낮게 설정합니다. 단기 및 장기 실행 중인 쿼리에 별도의 연결 객체를 사용합니다.

  • 제공된 읽기 및 쓰기 Aurora 엔드포인트를 사용하여 클러스터에 대한 연결을 설정합니다.

  • RDS API를 사용하여 서버 측 장애에 대한 애플리케이션 응답을 테스트하고 패킷 폐기 도구를 사용하여 클라이언트 측 장애에 대한 애플리케이션 응답을 테스트합니다.

TCP Keepalives 파라미터 설정

TCP keepalive 프로세스는 단순합니다. TCP 연결을 설정하면 타이머 집합이 연결됩니다. keepalive 타이머가 0에 도달하면 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

이 설정은 데이터베이스가 응답을 중단하고 5초 안에 애플리케이션에 알려야 합니다. keepalive 패킷이 애플리케이션의 네트워크 내에서 자주 폐기되는 경우, tcp_keepalive_probes 값을 높게 설정할 수 있습니다. 이렇게 하면 실제 장애를 탐지하는 데 걸리는 시간이 늘어나지만 비교적 안정적이지 못한 네트워크에서 버퍼가 추가될 수 있습니다.

Linux에서 TCP keepalive 파라미터 설정

  1. TCP keepalive 파라미터의 구성 방법을 테스트할 때는 다음 명령을 명령줄에 입력하는 방법이 좋습니다. 여기에서 제안하는 구성은 시스템 전체에 해당하는 것으로, SO_KEEPALIVE 옵션을 켜고 소켓을 생성하는 다른 모든 애플리케이션에 영향을 미칩니다.

    Copy
    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에 다음 줄(사용자가 변경한 내용 포함)을 추가하여 이러한 설정을 유지해야 합니다.

    Copy
    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 구성 변경을 다룹니다. JDBC 드라이버의 일반 설정과 구성에 대한 설명서는 PostgreSQL JDBC 사이트에서 확인할 수 있습니다.

DNS 캐시 제한 시간 축소

애플리케이션이 장애 조치 이후 연결을 설정할 때는 이전 리더가 새로운 Aurora PostgreSQL 라이터로 사용됩니다. 이전 리더는 DNS 업데이트가 완전히 전파되기 전에 Aurora 읽기 전용 엔드포인트를 사용해 찾을 수 있습니다. java DNS TTL 값을 낮게 설정하면 후속 연결 시도 시 리더 노드 사이에 순환이 이루어집니다.

Copy
// 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 클러스터에 연결하는 데 사용할 수 있는 연결 문자열의 예는 이와 같습니다.

Copy
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 엔드포인트에 대한 자세한 내용은 Aurora 엔드포인트 단원을 참조하십시오. 예를 들어, 다음과 같이 파일에 로컬로 엔드포인트를 저장할 수 있습니다.

Copy
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 인스턴스 노드 목록을 사용하는 것입니다.

Copy
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 엔드포인트를 사용하면 연결 시도 한 회당 두 개의 노드만 시도합니다. DB 인스턴스 사용의 단점은 사용자가 클러스터에 노드를 추가하거나 제거하고 인스턴스 엔드포인트 목록이 무효로 되면 연결 드라이버가 연결할 올바른 호스트를 찾을 수 없다는 것입니다.

다음 파라미터를 적극적으로 설정하면 애플리케이션을 한 호스트에 연결하기 위해 너무 오래 기다리지 않도록 할 수 있습니다.

  • loginTimeout - 소켓 연결이 설정된 애플리케이션이 데이터베이스에 로그인하기까지 대기하는 시간을 제어합니다.

  • connectTimeout - 소켓이 데이터베이스에 연결을 설정하기까지 대기하는 시간을 제어합니다.

애플리케이션을 얼마나 적극적으로 설정하고 싶은지에 따라, 다른 애플리케이션 파라미터를 수정하여 연결 프로세스의 속도를 높일 수 있습니다.

  • cancelSignalTimeout - 일부 애플리케이션에서는 시간 초과 쿼리에 "최대한" 취소 신호를 보내야 할 수 있습니다. 이 취소 신호가 장애 조치 경로에 있는 경우, 잘못된 호스트로 이 신호가 전달되지 않도록 적극적으로 설정하는 것을 고려해야 합니다.

  • socketTimeout - 이 파라미터는 소켓이 읽기 작업을 대기하는 시간을 제어합니다. 쿼리가 이 값보다 길게 대기하지 않도록 이 파라미터를 전역 "쿼리 제한 시간"으로 사용할 수 있습니다. 한 가지 좋은 방법은, 수명이 짧은 쿼리를 실행하는 연결 핸들러를 두고 이 값을 낮게 설정하는 한편, 장기 실행 쿼리를 위한 다른 연결 핸들러를 두고 이 값을 훨씬 높게 설정하는 것입니다. 그러면 서버가 다운될 경우 TCP keepalive 파라미터를 이용해 장기 실행 쿼리를 삭제할 수 있습니다.

  • tcpKeepAlive - 이 파라미터를 활성화하면 사용자가 설정한 TCP keepalive 파라미터가 적용됩니다.

  • targetServerType- 이 파라미터를 사용하면 드라이버의 읽기(슬레이브) 또는 쓰기(마스터) 노드 연결 여부를 제어할 수 있습니다. 가능한 값은 any, master, slave , preferSlave입니다. preferSlave 값은 먼저 리더와의 연결 설정을 시도하지만 리더 연결을 설정할 수 없는 경우 폴백하고 라이터에 연결합니다.

  • loadBalanceHosts - 이 파라미터를 true로 설정하면 애플리케이션을 후보 호스트 목록에서 선택한 임의의 호스트에 연결합니다.

호스트 문자열을 가져올 수 있는 다른 옵션

replica_host_status 테이블, Amazon RDS API 사용을 비롯해 여러 소스로부터 호스트 문자열을 가져올 수 있습니다.

애플리케이션이 DB 클러스터의 DB 인스턴스에 연결하고 replica_host_status 테이블을 쿼리하여 클러스터 라이터가 누구인지 파악하거나 클러스터에서 다른 리더 노드를 찾아낼 수 있습니다. 이 상태 테이블을 사용하면 연결할 호스트를 찾는 데 걸리는 시간을 줄일 수 있으나 일부 시나리오에서 replica_host_status 테이블이 일부 네트워크 장애 시나리오에서 오래되거나 불완전한 정보를 표시할 가능성이 있습니다.

애플리케이션이 연결할 노드를 찾도록 보장하는 좋은 방법 중 하나는 클러스터 라이터엔드포인트 에 대한 연결을 시도한 후 읽기 가능한 연결을 설정할 수 있을 때까지 클러스터 리더엔드포인트 연결을 시도하는 것입니다. 사용자가 DB 클러스터 이름을 바꾸지 않는 한 이러한 엔드포인트는 변경되지 않기 때문에, 일반적으로 애플리케이션의 정적 멤버로 남아 있거나 애플리케이션이 읽어 들이는 리소스 파일에 저장될 수 있습니다.

이러한 엔드포인트 중 하나를 사용하여 연결을 설정하면 상태 테이블을 쿼리하여 나머지 클러스터에 대한 정보를 가져올 수 있습니다. 예를 들어, 다음 명령은 aurora_replica_status 함수를 사용해 정보를 가져옵니다.

Copy
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)

따라서 이를테면 연결 문자열의 호스트 섹션은 라이터와 리더 클러스터 엔드포인트로 시작할 수 있습니다.

Copy
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_time 사이의 차이를 통해 확인할 수 있습니다. 일반적으로, 애플리케이션은 replica_host_status 테이블에서 서로 충돌하는 정보로 인한 두 호스트 사이의 플립플롭을 피해야 합니다. 다시 말해 애플리케이션이 맹목적으로 replica_host_status 테이블을 따르기보다는, 먼저 최선을 다해 알려진 모든 호스트를 시도해야 합니다.

API

AWS Java SDK, 특히 DescribeDbClusters API를 사용하여 프로그래밍 방식으로 인스턴스 목록을 찾을 수 있습니다. java 8에서 이와 같이 검색하는 방법의 예는 다음과 같습니다.

Copy
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에 다음과 같은 형식의 엔드포인트 목록이 포함됩니다.

Copy
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 호출을 저장하여 애플리케이션이 읽어 들이는 리소스 파일에서 이 상수를 유지할 것입니다. 위 예제에서는 다음과 같이 설정됩니다.

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

가용성을 보장하기 위해 API가 응답하지 않거나 응답 시간이 너무 길 경우 DB 클러스터의 Aurora 엔드포인트를 사용하는 것을 기본값으로 설정하는 것이 좋습니다. 엔드포인트는 DNS 레코드를 업데이트하는 데 걸리는 시간 내에(일반적으로 30초 이내) 최신 상태가 됩니다. 이 역시 애플리케이션이 사용하는 리소스 파일에 저장할 수 있습니다.

장애 조치 테스트

어떤 경우든 DB 클러스터에 >= 2 DB 인스턴스가 포함되어야 합니다.

서버 측에서 특정 API가 애플리케이션의 응답 방식을 테스트하는 데 사용할 수 있는 중단을 유발할 수 있습니다.

  • FailoverDBCluster - DB 클러스터의 새로운 DB 인스턴스를 라이터로 승격하려고 합니다.

    Copy
    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 - 포트를 변경하면 클러스터의 노드가 새 포트에서 수신을 시작할 경우 중단을 유발합니다. 일반적으로 애플리케이션은 사용자의 애플리케이션만 포트 변경을 제어하고 해당 엔드포인트를 적절히 업데이트할 수 있는지 확인하고, 누군가 API 수준에서 변경할 때 포트를 수동으로 업데이트하도록 하거나, 애플리케이션에서 RDS API를 쿼리하여 포트가 변경되었는지 판단하는 방식으로 이와 같은 장애에 대응할 수 있습니다.

  • ModifyDBInstance - DBInstanceClass 수정 시 중단이 발생합니다

  • DeleteDBInstance - 마스터/라이터를 삭제하면 새 DB 인스턴스가 DB 클러스터의 라이터로 승격됩니다

Linux 사용 시 애플리케이션/클라이언트 측에서 포트 및 호스트 기반 패킷의 갑작스러운 폐기 또는 tcp keepalive 패킷이 전송되지 않거나 iptables를 이용해 수신되는 경우 애플리케이션이 어떻게 대응하는지 테스트할 수 있습니다.

빠른 장애 조치 예제

다음 코드 샘플은 애플리케이션이 Aurora PostgreSQL 드라이버 관리자를 설정하는 방식을 보여줍니다. 애플리케이션이 연결해야 할 때 getConnection(...)을 호출할 것입니다. 라이터를 찾을 수 없으나 targetServerType이 "마스터"로 설정된 경우 등에는 이 함수에 대한 호출이 유효한 호스트를 찾는 데 실패할 수 있으며, 애플리케이션이 호출을 다시 시도해야 합니다. 이 경우 애플리케이션에 재시도 동작을 푸시하지 않도록 연결 풀러로 쉽게 래핑될 수 있습니다. 대다수 연결 풀러에서 JDBC 연결 문자열을 지정할 수 있으므로 애플리케이션이 getJdbcConnectionString(...)을 호출하고 연결 풀러로 이를 전달하여 Aurora PostgreSQL에서 빠른 장애 조치를 활용할 수 있습니다.

Copy
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(",")); } }

관련 주제

이 페이지에서: