Failover rapido con Amazon Aurora PostgreSQL - Amazon Aurora

Le traduzioni sono generate tramite traduzione automatica. In caso di conflitto tra il contenuto di una traduzione e la versione originale in Inglese, quest'ultima prevarrà.

Failover rapido con Amazon Aurora PostgreSQL

Di seguito, vengono fornite informazioni su come garantire che il failover si verifichi il più rapidamente possibile. Per ripristinare rapidamente dopo il failover, è possibile utilizzare la gestione della cache del cluster per il cluster di database Aurora PostgreSQL. Per ulteriori informazioni, consulta Ripristino rapido dopo il failover con Cluster Cache Management per Aurora PostgreSQL.

Alcuni dei passaggi da eseguire affinché il failover venga eseguito rapidamente includono i seguenti:

  • Impostare i keepalive TCP (Transmission Control Protocol) con intervalli temporali brevi, per interrompere le query in esecuzione più lunghe prima che scada il timeout di lettura in caso di errore.

  • Impostare i timeout per la memorizzazione nella cache DNS (Domain Name System) Java in modo aggressivo. Questo garantisce che l'endpoint di sola lettura di Aurora possa scorrere correttamente nodi di sola lettura nei successivi tentativi di connessione.

  • Impostare le variabili di timeout usate nella stringa di connessione JDBC più basse possibile. Utilizzare oggetti di connessione separati per query di breve e lunga durata.

  • Utilizzare gli endpoint Aurora di lettura e scrittura forniti per connetterti al cluster.

  • Utilizzare le operazioni API RDS per testare la risposta dell’applicazione in caso di errori lato server. Inoltre, utilizzare uno strumento di eliminazione pacchetti per testare la risposta dell'applicazione per gli errori lato client.

  • Utilizza il driver JDBC di AWS per PostgreSQL per sfruttare al massimo di funzionalità di failover di Aurora PostgreSQL. Per ulteriori informazioni su AWS JDBC Driver for PostgreSQL e le istruzioni complete per utilizzarlo, consulta il repository GitHub di AWS JDBC Driver for PostgreSQL.

Questi sono descritti in modo dettagliato di seguito.

Impostazione dei parametri di keepalive TCP

Quando si imposta una connessione TCP, ad essa viene associato un set di timer. Quando il timer keepalive raggiunge lo zero, un pacchetto di esplorazione keepalive viene inviato all’endpoint di connessione. Se la sonda riceve una risposta, si può presumere che la connessione sia ancora attiva e in esecuzione.

Attivare i parametri keepalive TCP e impostarli in modo aggressivo garantisce che se il client non è in grado di connettersi al database, le eventuali connessioni attive vengono chiuse rapidamente. L'applicazione può quindi connettersi a un nuovo endpoint.

I seguenti parametri keepalive TCP devono essere impostati:

  • tcp_keepalive_time controlla il tempo, in secondi, dopo il quale viene inviato un pacchetto keepalive quando nessun dato è stato inviato dal socket. Le ACK non sono considerate dati. Consigliamo la seguente impostazione:

    tcp_keepalive_time = 1

  • tcp_keepalive_intvl controlla il tempo, in secondi, tra l'invio di successivi pacchetti keepalive dopo l'invio del pacchetto iniziale. Imposta questo tempo utilizzando il parametro tcp_keepalive_time. Consigliamo la seguente impostazione:

    tcp_keepalive_intvl = 1

  • tcp_keepalive_probes è il numero di esplorazioni keepalive non riconosciute che si verificano prima che l'applicazione venga notificata. Consigliamo la seguente impostazione:

    tcp_keepalive_probes = 5

Queste impostazioni dovrebbero notificare l'applicazione entro cinque secondi quando il database smette di rispondere. Se i pacchetti keepalive vengono spesso eliminati all'interno della rete dell'applicazione, è possibile impostare un valore tcp_keepalive_probes più elevato. Questo consente di avere più buffer in reti meno affidabili, sebbene aumenti il tempo necessario per rilevare un errore effettivo.

Per impostare parametri keepalive TCP su Linux
  1. Esegui il test della modalità di configurazione dei parametri keepalive TCP.

    A questo scopo, utilizza la riga di comando con i seguenti comandi. Questa è una configurazione consigliata a livello di sistema. In altre parole, influenza anche tutte le altre applicazioni che creano socket con l’opzione SO_KEEPALIVE attivata.

    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. Una volta trovata una configurazione che funziona per l'applicazione, mantenere le impostazioni aggiungendo le seguenti righe a /etc/sysctl.conf, comprese le eventuali modifiche apportate:

    tcp_keepalive_time = 1 tcp_keepalive_intvl = 1 tcp_keepalive_probes = 5

Configurare l'applicazione per il Failover rapido

Di seguito, è disponibile una discussione su diverse modifiche di configurazione per Aurora PostgreSQL che è possibile apportare per un failover rapido. Per ulteriori informazioni sulla configurazione e la configurazione del driver JDBC PostgreSQL, vedere la documentazione PostgreSQL JDBC Driver.

Ridurre i timeout della cache DNS

Quando l'applicazione prova a stabilire una connessione dopo un failover, il nuovo scrittore Aurora PostgreSQL sarà un lettore precedente Per individuarlo, utilizzare l'endpoint di sola lettura Aurora prima della completa propagazione degli aggiornamenti DNS. Impostare Time to Live (TTL) java DNS su un valore basso, ad esempio meno di 30 secondi, consente di effettuare il ciclo tra i nodi del lettore nei successivi tentativi di connessione.

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

Impostare una stringa di connessione Aurora PostgreSQL per un Failover rapido

Per usare il failover rapido di Aurora PostgreSQL, accertarsi che la stringa di connessione dell'applicazione contenga un elenco di host anziché un singolo host. Di seguito è riportata una stringa di connessione di esempio che può essere utilizzata per connettersi a un cluster Aurora PostgreSQL: In questo esempio, gli host sono in grassetto.

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

Per una migliore disponibilità e per evitare una dipendenza dall'API RDS, è opportuno mantenere un file con cui eseguire la connessione. Questo file contiene una stringa host che viene letta dall'applicazione quando si stabilisce una connessione al database. Questa stringa host contiene tutti gli endpoint Aurora disponibili per il cluster. Per altre informazioni sugli endpoint Aurora, consultare Gestione delle connessioni Amazon Aurora.

Ad esempio, potrebbe essere necessario archiviare gli endpoint in un file locale come mostrato di seguito.

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

L'applicazione legge da questo file per compilare la sezione host della stringa di connessione JDBC. Rinominare il cluster DB fa sì che questi endpoint siano modificati. Assicurati che l'applicazione gestisca questo evento nel caso in cui si verifichi.

Un'altra opzione è usare un elenco di nodi di istanze database, come illustrato di seguito.

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

Il vantaggio di questo approccio è che il driver di connessione PostgreSQL JDBC esegue un ciclo di tutti i nodi in questo elenco per individuare una connessione valida. Al contrario, quando si usano gli endpoint Aurora, vengono provati solo due nodi per ogni tentativo di connessione. Tuttavia, l'utilizzo dei nodi di istanza database presenta uno svantaggio. Se si aggiungono o si rimuovono nodi dal cluster e l'elenco degli endpoint di istanza diventa obsoleto, il driver di connessione potrebbe non trovare mai l'host corretto cui connettersi.

Per garantire che l'applicazione non rimanga in attesa troppo a lungo prima di connettersi a un host, imposta i seguenti parametri in modo aggressivo.

  • targetServerType: controlla se il driver si connette a un nodo di scrittura o lettura. Per garantire che le applicazioni si riconnetteranno solo a un nodo di scrittura, imposta il valore targetServerType su primary.

    I valori per il parametro targetServerType includono primary, secondary, any e preferSecondary. Il valore preferSecondary esegue prima un tentativo per stabilire una connessione a un lettore. Se non è possibile stabilire una connessione con il lettore, si connette allo scrittore.

  • loginTimeout: controlla per quanto tempo l'applicazione rimane in attesa prima di accedere al database dopo che è stata stabilita una connessione socket.

  • connectTimeout – Controlla quanto tempo il connettore attende per stabilire una connessione con il database.

In base al livello di aggressività desiderato per l'applicazione, puoi modificare altri parametri dell'applicazione per accelerare il processo di connessione:

  • cancelSignalTimeout: in alcune applicazioni, potrebbe essere necessario inviare un segnale di annullamento "massimo sforzo" su una query scaduta. Se questo segnale di annullamento si trova nel percorso di failover, valuta se impostarlo in modo aggressivo per evitare che venga inviato a un host morto.

  • socketTimeout – Questo parametro controlla quanto tempo il connettore attende per le operazioni di lettura. Questo parametro può essere usato come un "timeout di query" globale per assicurare che nessuna query attenda più a lungo di questo valore. Una buona pratica è quella di disporre di due gestori di connessione. Un gestore di connessione che esegue query di breve durata e imposta questo valore più basso. Un altro gestore di connessione, per query a esecuzione prolungata, con questo valore impostato molto più alto. Con questo approccio, è possibile fare affidamento sui parametri keepalive TCP per interrompere le query a esecuzioni prolungata se il server non funziona.

  • tcpKeepAlive: attivare questo parametro per garantire che i parametri keepalive TCP impostati siano rispettati.

  • loadBalanceHosts – Quando è impostato su true, l'applicazione del parametro si connette a un host casuale scelto da un elenco di host candidati.

Altre opzioni per ottenere la stringa host

È possibile ottenere la stringa host da diverse fonti, compresa la funzione aurora_replica_status e usando l'API di Amazon RDS.

In molti casi, occorre determinare chi è lo scrittore del cluster o trovare altri nodi di lettura nel cluster. A questo scopo, l'applicazione può connettersi a qualsiasi istanza database nel cluster database ed eseguire la query della funzione aurora_replica_status. Questa funzione può essere utilizzata per ridurre il tempo necessario per trovare un host cui connettersi. Tuttavia, in determinati scenari di errore di rete la funzione aurora_replica_status potrebbe mostrare informazioni obsolete o incomplete.

Un buon modo per assicurarsi che l'applicazione possa individuare un nodo cui connettersi è provare a connettersi all'endpoint di scrittura del cluster e quindi all'endpoint di lettura del cluster, fino a quando non è possibile stabilire una connessione leggibile. Questi endpoint non cambiano a meno che il cluster database non venga rinominato. Quindi, in genere possono essere lasciati come membri statici dell'applicazione o archiviati in un file di risorsa che viene letto dall'applicazione.

Una volta stabilita una connessione usando uno di questi endpoint, è possibile ottenere informazioni sul resto del cluster. A questo scopo, richiama la funzione aurora_replica_status. Ad esempio, il comando seguente recupera informazioni con 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)

Ad esempio, la sezione host della stringa di connessione potrebbe iniziare con entrambi gli endpoint del cluster di scrittura e di lettura, come mostrato di seguito.

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

In questo scenario, l'applicazione prova a stabilire una connessione a qualsiasi tipo di nodo, primario o secondario. Una volta connessa, una buona prassi è quella di esaminare innanzitutto lo stato di lettura-scrittura del nodo. A questo scopo, occorre eseguire la query del risultato del comando SHOW transaction_read_only.

Se il valore restituito della query è OFF, allora la connessione al nodo primario è andata a buon fine. Tuttavia, si supponga che il valore restituito sia ON e che l'applicazione richieda una connessione di lettura/scrittura. In questo caso, si può richiamare la funzione aurora_replica_status per determinare il server_id che ha session_id='MASTER_SESSION_ID'. Questa funzione fornisce il nome del nodo primario. Ciò è possibile il endpointPostfix descritto di seguito.

È importante sapere quando si esegue una connessione a una replica con dati obsoleti. Quando ciò avviene, la funzione aurora_replica_status potrebbe mostrare informazioni aggiornate. Una soglia di obsolescenza può essere impostata a livello di applicazione. Per la verifica, osservare la differenza tra l'ora del server e il valore last_update_timestamp. In generale, l'applicazione dovrebbe evitare di alternarsi tra due host a causa delle informazioni in conflitto restituite dalla funzione aurora_replica_status. L'applicazione dovrebbe provare prima tutti gli host conosciuti anziché seguire i dati restituiti da aurora_replica_status.

Elencare istanze utilizzando l'operazione API DescribeDBClusters, esempio in Java

L'elenco delle istanze può essere ricavato in maniera programmatica utilizzando l'AWS SDK for Java, in particolare l'operazione API DescribeDBClusters.

Di seguito è riportato un piccolo esempio di come eseguire questa operazione in 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(","));

Qui, pgJDBCEndpointStr contiene un elenco formattato di endpoint, come mostrato di seguito.

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

La variabile endpointPostfix può essere una costante impostata dall'applicazione. Oppure, può essere ottenuta dall’applicazione eseguendo la query dell’operazione API DescribeDBInstances per una singola istanza del cluster. Questo valore rimane costante all'interno di Regione AWS e per un singolo cliente. Pertanto, salva una chiamata API per mantenere semplicemente questa costante in un file di risorsa letto dall'applicazione. Nell'esempio precedente, è impostato sul valore seguente.

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

Ai fini della disponibilità, una buona norma è quella di usare per impostazione predefinita gli endpoint Aurora del cluster database se l'API non risponde o impiega troppo tempo a rispondere. Viene garantito l'aggiornamento degli endpoint nel tempo necessario per aggiornare il record DNS. L'aggiornamento del record DNS con un endpoint richiede in genere meno di 30 secondi. L’endpoint può essere memorizzato in un file di risorse utilizzato dall'applicazione.

Verifica del Failover

In tutti i casi è necessario disporre di un cluster di database contenente due o più istanze database.

Dal lato server, alcune operazioni API possono causare un'interruzione che può essere usata per testare la risposta delle applicazioni:

  • FailoverDBCluster: questa operazione tenta di promuovere una nuova istanza database nel cluster database per la scrittura

    Il seguente esempio di codice mostra come è possibile utilizzare failoverDBCluster per causare un'interruzione. Per ulteriori informazioni sulla configurazione di un client Amazon RDS, consulta Utilizzo di AWS SDK per Java.

    public void causeFailover() { final AmazonRDS rdsClient = AmazonRDSClientBuilder.defaultClient(); FailoverDBClusterRequest request = new FailoverDBClusterRequest(); request.setDBClusterIdentifier("cluster-identifier"); rdsClient.failoverDBCluster(request); }
  • RebootDBInstance: il failover non è garantito con questa operazione API. Tuttavia, arresta il database dello scrittore. Può essere utilizzata per testare la risposta dell'applicazione all'eliminazione delle connessioni. Il parametro ForceFailover non si applica ai motori Aurora. Utilizzare invece l'operazione API FailoverDBCluster.

  • ModifyDBCluster: la modifica del parametro Port causa un'interruzione quando i nodi del cluster iniziano ad ascoltare su una nuova porta. In generale, l'applicazione può rispondere innanzitutto a questo errore assicurandosi che solo l'applicazione controlli le modifiche alle porte. Inoltre, accertarsi che possa aggiornare in modo appropriato gli endpoint da cui dipende. A questo scopo, è necessario che qualcuno aggiorni manualmente la porta quando vengono apportate modifiche a livello di API. Oppure, è possibile utilizzare l'API RDS nell’applicazione per determinare se la porta è cambiata.

  • ModifyDBInstance: la modifica del parametro DBInstanceClasscausa un'interruzione.

  • DeleteDBInstance: l'eliminazione del primario (scrittore) causa la promozione di una nuova istanza database a scrittore nel cluster database.

Dal lato applicazione o client, se si usa Linux, è possibile testare in che modo l'applicazione risponde a improvvise eliminazioni di pacchetti. Ciò può essere fatto a seconda che porta, host o se i pacchetti keepalive TCP vengono inviati o ricevuti utilizzando il comando iptables.

Esempio di failover rapido in Java

Il seguente esempio di codice mostra come un'applicazione potrebbe configurare un gestore di driver di Aurora PostgreSQL.

L'applicazione richiama la funzione getConnection quando è richiesta una connessione. Una chiamata a getConnection può non riuscire a trovare un host valido. Un esempio è quando non viene trovato alcuno scrittore ma il parametro targetServerType è impostato su primary. In questo caso, l'applicazione chiamante deve semplicemente riprovare a richiamare la funzione.

Per evitare di spingere il comportamento dei tentativi continui nell'applicazione, puoi impacchettare questo tentativo di chiamata in un pool di connessioni. Con la maggior parte dei pool di connessioni, puoi specificare una stringa di connessione JDBC. Pertanto l'applicazione può richiamare getJdbcConnectionString e passarla al pool di connessioni. Ciò significa che con Aurora PostgreSQL si può utilizzare un failover più rapido.

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