Failover rápido com o Amazon Aurora PostgreSQL - Amazon Aurora

Failover rápido com o Amazon Aurora PostgreSQL

A seguir, você vai aprender a garantir que o failover ocorra o mais rápido possível. Para se recuperar rapidamente após o failover, você pode usar o gerenciamento de cache de cluster para o cluster de banco de dados do Aurora PostgreSQL. Para obter mais informações, consulte Recuperação rápida após failover com o gerenciamento de cache do cluster para o Aurora PostgreSQL.

Veja algumas das etapas que podem ser adotadas para que o failover seja executado rapidamente:

  • Definir os keepalives do Transmission Control Protocol (TCP) com períodos curtos para interromper a execução de consultas por mais tempo antes que o tempo limite de leitura expire em caso de falha.

  • Definir tempos limites agressivamente para o armazenamento em cache do Sistema de Nomes de Domínio (DNS) em Java. Isso ajuda a garantir que o endpoint somente leitura do Aurora possa percorrer corretamente os nós somente leitura em tentativas de conexão posteriores.

  • Definir as variáveis de tempo limite usadas na string de conexão JDBC com o valor mais baixo possível. Usar objetos de conexão separados para consultas de curta e longa execução.

  • Usar os endpoints do Aurora de leitura e gravação fornecidos para conectar com o cluster.

  • Usar as operações da API do RDS para testar a resposta da aplicação em caso de falhas do lado do servidor. Além disso, usar uma ferramenta de descarte de pacotes para testar a resposta da aplicação para falhas do lado do cliente.

  • Use o Driver JDBC para PostgreSQL da AWS para aproveitar ao máximo as funcionalidades de failover do Aurora PostgreSQL. Para obter mais informações sobre o Driver JDBC para PostgreSQL da AWS e as instruções completas para usá-lo, consulte o Repositório do GitHub do Driver JDBC para PostgreSQL da AWS.

Isso é abordado em mais detalhes a seguir.

Definir parâmetros de keepalives de TCP

Quando você configura uma conexão TCP, um conjunto de timers é associado à conexão. Quando o timer de keepalive chegar a zero, um pacote de sondagem keepalive é enviado ao endpoint de conexão. Se receber uma resposta, você poderá assumir que a conexão ainda está ativa.

Ativar parâmetros de keepalive de TCP e defini-los de forma agressiva garante que, se o seu cliente não conseguir se conectar ao banco de dados, qualquer conexão ativa será rapidamente encerrada. Depois, a aplicação pode se conectar a um novo endpoint.

Defina os seguintes parâmetros de keepalive de TCP:

  • tcp_keepalive_time controla o tempo, em segundos, após o qual um pacote de keepalive é enviado quando nenhum dado é enviado pelo soquete. ACKs não são considerados dados. Recomendamos a seguinte configuração:

    tcp_keepalive_time = 1

  • tcp_keepalive_intvl controla o tempo, em segundos, entre o envio de pacotes de keepalive subsequentes depois que o pacote inicial é enviado. Defina esse tempo usando o parâmetro tcp_keepalive_time. Recomendamos a seguinte configuração:

    tcp_keepalive_intvl = 1

  • tcp_keepalive_probes é o número de sondagens de keepalive não confirmadas que ocorrem antes de a aplicação ser notificada. Recomendamos a seguinte configuração:

    tcp_keepalive_probes = 5

Essas configurações devem notificar a aplicação em até cinco segundos depois que o banco de dados para de responder. Se os pacotes de keepalive forem frequentemente descartados dentro na rede da aplicação, você poderá definir um valor de tcp_keepalive_probes maior. Isso permite mais buffer em redes menos confiáveis, embora aumente o tempo necessário para detectar uma falha real.

Como definir parâmetros de keepalive de TCP no Linux
  1. Teste como configurar seus parâmetros de keepalive de TCP.

    Recomendamos fazer isso usando a linha de comando com os comandos a seguir. Essa configuração sugerida abrange todo o sistema. Em outras palavras, ela também afeta todas as outras aplicações que criam soquetes com a opção SO_KEEPALIVE ativada.

    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. Depois de encontrar uma configuração que funcione para a sua aplicação, mantenha essas configurações adicionando as seguintes linhas a /etc/sysctl.conf, incluindo as alterações feitas:

    tcp_keepalive_time = 1 tcp_keepalive_intvl = 1 tcp_keepalive_probes = 5

Configurar a aplicação para failover rápido

A seguir, você encontrará uma discussão sobre as várias alterações de configuração possíveis do Aurora PostgreSQL para obter um failover rápido. Para saber mais sobre a instalação e configuração do driver JDBC PostgreSQL, consulte a documentação do Driver JDBC PostgreSQL.

Reduzir tempos limite de cache do DNS

Quando a aplicação tentar estabelecer uma conexão após um failover, o novo gravador do Aurora PostgreSQL será um leitor anterior. Você pode encontrá-lo usando o endpoint somente leitura do Aurora antes que as atualizações de DNS sejam totalmente propagadas. Definir a vida útil (TTL) do DNS em Java com um valor baixo, como inferior a 30 segundos, ajuda a percorrer os nós de leitor em tentativas de conexão posteriores.

// 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");

Definir uma string de conexão do Aurora PostgreSQL para failover rápido

Para usar o failover rápido do Aurora PostgreSQL, verifique se a string de conexão da aplicação tem uma lista de hosts, em vez de apenas um host. Veja a seguir um exemplo de string de conexão que você pode usar para se conectar a um cluster do Aurora PostgreSQL. Neste exemplo, os hosts estão em negrito.

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

Para obter a melhor disponibilidade e evitar uma dependência para com a API do RDS, recomendamos manter um arquivo para se conectar com ele. Esse arquivo contém uma string de host que é lida pela aplicação quando você estabelece uma conexão com o banco de dados. Essa string de host tem todos os endpoints do Aurora disponíveis para o cluster. Para obter mais informações sobre endpoints do Aurora, consulte Gerenciamento de conexões do Amazon Aurora.

Por exemplo, você pode armazenar os endpoints em um arquivo local, conforme mostrado a seguir.

myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432, myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432

A aplicação faz a leitura desse arquivo para preencher a seção de host da string de conexão JDBC. Renomear o cluster de banco de dados faz com que esses endpoints sejam alterados. Verifique se a aplicação lidará com esse evento se ele ocorrer.

Outra opção é usar uma lista de nós de instância de banco de dados da seguinte maneira:

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

A vantagem dessa abordagem é que o driver da conexão JDBC PostgreSQL faz um loop em todos os nós dessa lista para encontrar uma conexão válida. Entretanto, quando você usa os endpoints do Aurora, apenas dois nós são testados em cada tentativa de conexão. Mas há uma desvantagem em usar nós de instância de banco de dados. Se você adicionar ou remover nós do cluster e a lista de endpoints de instância ficar obsoleta, o driver de conexão talvez nunca encontre o host correto com o qual se conectar.

Para ajudar a garantir que a aplicação não espere muito tempo para se conectar a qualquer host, defina os seguintes parâmetros de forma agressiva:

  • targetServerType: controla se o driver se conecta a um nó de gravação ou leitura. Para garantir que as aplicações se reconectem somente a um nó de gravação, defina o valor targetServerType como primary.

    Os valores para o parâmetro targetServerType incluem primary, secondary, any e preferSecondary. O valor preferSecondary primeiro tenta estabelecer uma conexão com um leitor. Ele se conectará ao gravador se nenhuma conexão com o leitor puder ser estabelecida.

  • loginTimeout: controla o tempo de espera para a aplicação acessar o banco de dados depois de estabelecida uma conexão de soquete.

  • connectTimeout: controla o tempo de espera do soquete para estabelecer uma conexão com o banco de dados.

Você pode modificar outros parâmetros de aplicações para acelerar o processo de conexão, dependendo do nível de agressividade que você deseja que a aplicação tenha:

  • cancelSignalTimeout: em algumas aplicações ativas, convém enviar um sinal de cancelamento de “melhor esforço” em uma consulta que atingiu o tempo limite. Se esse sinal de cancelamento estiver no seu caminho de failover, considere uma definição agressiva para evitar enviar esse sinal a um host inativo.

  • socketTimeout: esse parâmetro controla por quanto tempo o soquete aguarda operações de leitura. Esse parâmetro pode ser usado como um "tempo limite de consulta" global para garantir que nenhuma consulta espere mais do que esse valor. É recomendável ter dois manipuladores de conexão. Um que execute consultas de curta duração e defina um valor mais baixo. Outro que execute consultas de longa duração e defina um valor bem mais alto. Com essa abordagem, você pode depender de parâmetros de keepalive de TCP para interromper consultas de longa duração caso o servidor fique inoperante.

  • tcpKeepAlive: ative esse parâmetro para garantir que os parâmetros de keepalive de TCP que você definir sejam respeitados.

  • loadBalanceHosts: quando definido como true, esse parâmetro faz com que a aplicação se conecte a um host aleatório escolhido de uma lista de hosts candidatos.

Outras opções para obter a string do host

Você pode obter a string de host de várias origens, incluindo a função aurora_replica_status e usando a API do Amazon RDS.

Em muitos casos, você precisa determinar quem é o gravador do cluster ou localizar outros nós de leitor no cluster. Para fazer isso, sua aplicação pode se conectar a qualquer instância de banco de dados no cluster de banco de dados e consultar a função aurora_replica_status. Você pode usar essa função para reduzir o tempo necessário para localizar um host ao qual se conectar. No entanto, em determinados cenários de falha de rede, a função aurora_replica_status pode exibir informações desatualizadas ou incompletas.

Uma boa maneira de garantir que a aplicação possa encontrar um nó ao qual se conectar é tentar se conectar ao endpoint do gravador do cluster e depois ao endpoint do leitor do cluster. Você deve fazer isso até estabelecer uma conexão legível. Esses endpoints não mudam, a menos que você renomeie o cluster de banco de dados. Por isso, você geralmente pode deixá-los como membros estáticos da aplicação ou armazená-los em um arquivo de recursos do qual a aplicação lê conteúdo.

Depois de estabelecer uma conexão usando um desses endpoints, você poderá obter informações sobre o restante do cluster. Para fazer isso, chame a função aurora_replica_status. Por exemplo, o comando a seguir recupera informações com 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)

Por exemplo, a seção de hosts da string de conexão pode começar com os endpoints de cluster de gravador e leitor, conforme mostrado a seguir.

myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432, myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432

Nesse cenário, a aplicação tenta estabelecer uma conexão com qualquer tipo de nó, primário ou secundário. Quando a aplicação estiver conectada, é recomendável primeiro examinar o status de leitura/gravação do nó. Para fazer isso, consulte o resultado do comando SHOW transaction_read_only.

Se o valor de retorno da consulta for OFF, você já se conectou com êxito ao nó primário. No entanto, vamos supor que o valor de retorno seja ON e sua aplicação exija uma conexão de leitura/gravação. Nesse caso, você pode chamar a função aurora_replica_status para determinar o server_id que tem session_id='MASTER_SESSION_ID'. Essa função fornece o nome do nó primário. Você pode usá-la com o endpointPostfix descrito a seguir.

Tenha atenção quando você se conectar a uma réplica que tem dados obsoletos. Quando isso acontece, a função aurora_replica_status pode mostrar informações desatualizadas. Você pode definir um limite de desatualização no nível da aplicação. Para verificar isso, você pode observar a diferença entre a hora do servidor e o valor last_update_timestamp. Em geral, a aplicação deve evitar a mudança entre dois hosts devido a informações conflitantes retornadas pela função aurora_replica_status. Sua aplicação deve tentar todos os hosts conhecidos primeiro, em vez de seguir os dados retornados por aurora_replica_status.

Listar instâncias usando a operação de API DescribeDBCluster, exemplo em Java

Você pode localizar programaticamente a lista de instâncias usando AWS SDK for Java, especificamente a operação de API DescribeDBClusters.

Veja a seguir um pequeno exemplo de como você pode fazer isso no 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(","));

Aqui, pgJDBCEndpointStr contém uma lista formatada de endpoints, conforme mostrado a seguir.

my-node1.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432, my-node2.cksc6xlmwcyw.us-east-1-beta.rds.amazonaws.com:5432

A variável endpointPostfix pode ser uma constante definida pela sua aplicação. Ou sua aplicação pode obtê-la consultando a operação de API DescribeDBInstances para uma única instância em seu cluster. Esse valor permanece constante em uma Região da AWS e para um cliente individual. Por isso, ele salva uma chamada de API para simplesmente manter isso constante em um arquivo de recursos do qual sua aplicação lê. No exemplo anterior, isso é definido como o seguinte.

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

Para fins de disponibilidade, é recomendável usar como padrão os endpoints do Aurora do seu cluster de banco de dados, se a API não estiver respondendo ou se estiver demorando muito para responder. Existe a garantia de que os endpoints permanecerão atualizados durante o tempo necessário para atualizar o registro DNS. A atualização do registro de DNS com um endpoint normalmente leva menos de 30 segundos. Você pode armazenar o endpoint em um arquivo de recursos consumido pela aplicação.

Testar o failover

Em todos os casos, você deve ter um cluster de banco de dados que contenha duas ou mais instâncias de banco de dados.

Do lado do servidor, certas operações de API podem causar uma interrupção que pode ser usada para testar como as aplicações respondem:

  • FailoverDBCluster: essa operação tenta promover uma nova instância de banco de dados a gravador no cluster de banco de dados.

    O exemplo de código a seguir mostra como você pode usar o failoverDBCluster para causar uma interrupção. Para obter mais detalhes sobre a configuração de um cliente Amazon RDS, consulte Usar o AWS SDK for Java.

    public void causeFailover() { final AmazonRDS rdsClient = AmazonRDSClientBuilder.defaultClient(); FailoverDBClusterRequest request = new FailoverDBClusterRequest(); request.setDBClusterIdentifier("cluster-identifier"); rdsClient.failoverDBCluster(request); }
  • RebootDBInstance: o failover não é garantido com essa operação de API. No entanto, ele desativa o banco de dados no gravador. Você pode usá-lo para testar como sua aplicação responde a uma queda de conexões. O parâmetro ForceFailover não se aplica a mecanismos do Aurora. Em vez disso, use a operação de API FailoverDBCluster.

  • ModifyDBCluster: modificar o parâmetro Port causará uma interrupção quando os nós no cluster começarem a escutar em uma porta nova. Em geral, sua aplicação pode responder a essa falha primeiro garantindo que somente ela controle as alterações de porta. Além disso, verifique se ela consegue atualizar adequadamente os endpoints dos quais depende. Para isso, você pode especificar que alguém atualize manualmente a porta ao fazer modificações no nível da API. Ou você pode fazer isso usando a API do RDS na aplicação para determinar se a porta foi alterada.

  • ModifyDBInstance: modificar o parâmetro DBInstanceClass causa uma interrupção.

  • DeleteDBInstance: excluir o primário (gravador) faz com que uma nova instância de banco de dados seja promovida a gravador no cluster de banco de dados.

Do lado da aplicação ou do cliente, se usar o Linux, você poderá testar como a aplicação responde a perdas súbitas de pacotes. Você pode fazer isso com base na porta, no host ou em se os pacotes de keepalive de TCP são enviados ou recebidos usando o comando iptables.

Exemplo de failover rápido em Java

O exemplo de código a seguir mostra como uma aplicação pode configurar um gerenciador de driver do Aurora PostgreSQL.

A aplicação chama getConnection quando precisa de uma conexão. Uma chamada para getConnection pode não conseguir encontrar um host válido. Um exemplo é quando nenhum gravador é encontrado, exceto o parâmetro targetServerType definido como primary. Nesse caso, a aplicação da chamada deve simplesmente tentar chamar a função novamente.

Para evitar enviar o comportamento de repetição à aplicação, você pode compactar essa nova tentativa em um agrupador de conexões. Com a maioria dos agrupadores de conexão, você pode especificar uma cadeia de conexão JDBC. Assim, sua aplicação pode chamar em getJdbcConnectionString e passá-lo para o agrupador de conexões. Isso significa que você pode usar um failover mais rápido com o 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(",")); } }