Basculement rapide avec Amazon Aurora PostgreSQL - Amazon Aurora

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Basculement rapide avec Amazon Aurora PostgreSQL

Vous apprendrez ensuite comment faire en sorte que le basculement se produise aussi rapidement que possible. Pour récupérer rapidement après un basculement, vous pouvez utiliser la gestion de cache en cluster pour votre cluster de base de données Aurora PostgreSQL. Pour plus d’informations, consultez Récupération rapide après basculement avec la gestion des caches de clusters pour Aurora PostgreSQL.

Voici quelques-unes des mesures que vous pouvez prendre pour que le basculement soit rapide :

  • Définissez des keepalives TCP (Transmission Control Protocol) de courte durée, afin d'arrêter les requêtes plus longues avant l'expiration du délai de lecture en cas d'échec.

  • Définissez les délais de mise en cache du système de nom de domaine (DNS) de Java de manière agressive. Cela permet de s'assurer que le point de terminaison en lecture seule d'Aurora peut correctement passer en revue les nœuds en lecture seule lors des tentatives de connexion ultérieures.

  • Définition des variables d'expiration utilisées dans la chaîne de connexion JDBC à des valeurs aussi faibles que possible. Utilisation d'objets de connexion distincts pour les requêtes à exécution courte et longue.

  • Utilisez les points de terminaison Aurora en lecture et en écriture qui sont fournis pour vous connecter au cluster.

  • Utilisez les opérations de l'API RDS pour tester la réponse de l'application en cas de défaillance du serveur. Utilisez également un outil de suppression de paquets pour tester la réponse de l'application en cas de défaillance côté client.

  • Utilisez le pilote AWS JDBC pour tirer pleinement parti des fonctionnalités de basculement d'Aurora PostgreSQL. Pour plus d'informations sur le pilote AWS JDBC et des instructions complètes pour son utilisation, consultez le référentiel de pilotes JDBC Amazon Web Services (AWS). GitHub

Ces opérations sont traitées plus en détail ci-après.

Définition des paramètres TCP keepalive

Lorsque vous établissez une connexion TCP, un ensemble de temporisateurs est employé avec la connexion. Lorsque le temporisateur keepalive atteint zéro, un paquet de test keepalive est envoyé au point de terminaison de la connexion. Si le test reçoit une réponse, vous pouvez supposer que la connexion est toujours en cours.

L'activation des paramètres TCP keepalive et leur réglage agressif garantissent que si votre client ne peut pas se connecter à la base de données, toute connexion active sera rapidement fermée. L'application peut alors se connecter à un nouveau point de terminaison.

Assurez-vous de définir les paramètres TCP keepalive suivants :

  • tcp_keepalive_time contrôle le délai en secondes au bout duquel un paquet keepalive est envoyé lorsqu'aucune donnée n'a été envoyée par le socket. Les ACK ne sont pas considérés comme des données. Nous vous recommandons de définir les paramètres suivants :

    tcp_keepalive_time = 1

  • tcp_keepalive_intvl contrôle l'intervalle en secondes entre l'envoi des paquets keepalive suivants après l'envoi du paquet initial. Réglez cette heure à l'aide du paramètre tcp_keepalive_time. Nous vous recommandons de définir les paramètres suivants :

    tcp_keepalive_intvl = 1

  • tcp_keepalive_probes est le nombre de tests keepalive non reconnus qui ont lieu avant que l'application ne soit informée. Nous vous recommandons de définir les paramètres suivants :

    tcp_keepalive_probes = 5

Ces paramètres doivent informer l'application dans les cinq secondes après que la base de données a cessé de répondre. Si les paquets keepalive sont souvent abandonnés dans le réseau de l'application, vous pouvez définir une valeur tcp_keepalive_probes plus élevée. Cela permet d'augmenter la mémoire tampon dans les réseaux moins fiables, mais augmente le temps nécessaire à la détection d'une panne réelle.

Pour définir des paramètres TCP keepalive sous Linux
  1. Testez comment configurer vos paramètres TCP keepalive.

    Nous vous recommandons de le faire en utilisant la ligne de commande avec les commandes suivantes. Cette configuration suggérée est valable pour l'ensemble du système. En d'autres termes, cela affecte également toutes les autres applications qui créent des sockets avec l'option SO_KEEPALIVE activée.

    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. Lorsque vous avez trouvé une configuration qui fonctionne pour votre application, rendez persistants les paramètres suivants en ajoutant les lignes suivantes à /etc/sysctl.conf, avec les modifications que vous avez effectuées :

    tcp_keepalive_time = 1 tcp_keepalive_intvl = 1 tcp_keepalive_probes = 5

Configuration de votre application pour le basculement rapide

Vous trouverez ci-dessous une discussion sur plusieurs changements de configuration d'Aurora PostgreSQL que vous pouvez mettre en œuvre pour un basculement rapide. Pour en savoir plus sur l'installation et la configuration du pilote JDBC PostgreSQL, consultez la documentation Pilote JDBC PostgreSQL.

Réduction des délais d'expiration du cache du DNS

Lorsque votre application tente d'établir une connexion après un basculement, la nouvelle instance Aurora PostgreSQL en écriture sera une ancienne instance en lecture. Vous pouvez la trouver en utilisant le point de terminaison Aurora en lecture seule avant que les mises à jour DNS ne se soient entièrement propagées. En fixant la durée de vie (TTL) du DNS java à une valeur faible, inférieure à 30 secondes par exemple, on facilite le passage d'un nœud de lecteur à l'autre lors des tentatives de connexion ultérieures.

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

Définition d'une chaîne de connexion Aurora PostgreSQL pour le basculement rapide

Pour utiliser le basculement rapide d'Aurora PostgreSQL, assurez-vous que la chaîne de connexion de votre application comporte une liste d'hôtes au lieu d'un seul. Voici un exemple de chaîne de connexion que vous pouvez utiliser pour vous connecter à un cluster Aurora PostgreSQL. Dans cet exemple, les hôtes sont en gras.

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

Pour une meilleure disponibilité et pour éviter une dépendance à l'API RDS, nous vous recommandons de garder un fichier pour vous connecter. Ce fichier contient une chaîne d'hôte que votre application lit lorsque vous établissez une connexion avec la base de données. Cette chaîne hôte possède tous les points de terminaison Aurora disponibles pour le cluster. Pour plus d'informations sur les points de terminaison Aurora, consultez Gestion des connexions Amazon Aurora.

Par exemple, vous pouvez stocker vos points de terminaison dans un fichier local comme indiqué ci-dessous.

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

Votre application lit ce fichier pour renseigner la section hôte de la chaîne de connexion JDBC. Le changement de nom du cluster de base de données entraîne la modification de ces points de terminaison. Assurez-vous que votre application gère cet événement s'il se produit.

Vous pouvez également utiliser une liste de nœuds d'instance de base de données, comme suit.

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

L'avantage de cette approche est que le pilote de connexion JDBC PostgreSQL boucle sur tous les nœuds de cette liste pour trouver une connexion valide. En revanche, lorsque vous utilisez les points de terminaison Aurora, seuls deux nœuds sont essayés à chaque tentative de connexion. L'utilisation des nœuds d'instance de base de données présente toutefois un inconvénient. Si vous ajoutez ou supprimez des nœuds dans votre cluster et que la liste des points de terminaison devient obsolète, le pilote de connexion ne trouvera peut-être jamais l'hôte correct auquel il doit se connecter.

Pour faire en sorte que votre application n'attende pas trop longtemps pour se connecter à un hôte donné, définissez les paramètres suivants de manière agressive :

  • targetServerType : contrôle si le pilote se connecte à un nœud en écriture ou en lecture. Pour vous assurer que vos applications se reconnectent uniquement à un nœud d'écriture, définissez la valeur de targetServerType sur primary.

    Les valeurs pour le paramètre targetServerType incluent primary, secondary, any et preferSecondary. La valeur preferSecondary tente d'abord d'établir une connexion avec une instance en lecture. Elle se connecte à l'instance en écriture si aucune connexion à l'instance en lecture ne peut être établie.

  • loginTimeout : contrôle la durée d'attente de votre application pour se connecter à la base de données après qu'une connexion socket a été établie.

  • connectTimeout – contrôle la durée d'attente du socket pour établir une connexion à la base de données.

Vous pouvez modifier d'autres paramètres d'application pour accélérer le processus de connexion, selon le degré d'énergie que vous voulez attribuer à votre application :

  • cancelSignalTimeout : dans certaines applications, vous voudrez peut-être envoyer un signal d'annulation le meilleur possible pour une requête qui a expiré. Si ce signal d'annulation se trouve dans votre chemin de basculement, pensez à le définir de manière énergique pour éviter de l'envoyer à un hôte mort.

  • socketTimeout – Ce paramètre contrôle la durée d'attente du socket pour les opérations de lecture. Ce paramètre peut servir de délai de requête global afin d'assurer que la durée d'attente des requêtes ne dépasse jamais sa valeur. Une bonne pratique consiste à avoir deux gestionnaires de connexion. Un gestionnaire de connexion exécute des requêtes à courte durée de vie et fixe cette valeur plus bas. Dans un autre gestionnaire de connexion, pour les requêtes de longue durée, cette valeur est beaucoup plus élevée. Avec cette approche, vous pouvez compter sur les paramètres TCP keepalive pour arrêter les requêtes de longue durée si le serveur tombe en panne.

  • tcpKeepAlive : activez ce paramètre pour garantir que les paramètres TCP keepalive que vous définissez sont respectés.

  • loadBalanceHosts – Lorsque ce paramètre est défini sur true, l'application se connecte à un hôte aléatoire choisi dans une liste d'hôtes candidats.

Autres options pour obtenir la chaîne hôte

Vous pouvez obtenir la chaîne hôte à partir de plusieurs sources, notamment de la fonction aurora_replica_status et en utilisant l'API Amazon RDS.

Dans de nombreux cas, vous devez déterminer quelle est l'instance en écriture du cluster ou trouver d'autres nœuds en lecture dans le cluster. Pour ce faire, votre application peut se connecter à n'importe quelle instance de base de données dans le cluster de base de données et interroger la fonction aurora_replica_status. Vous pouvez utiliser cette fonction pour réduire le temps nécessaire à la recherche d'un hôte auquel se connecter. Toutefois, dans certains scénarios de défaillance du réseau, la aurora_replica_status fonction peut afficher out-of-date ou ne pas fournir des informations complètes.

Une bonne manière de s'assurer que votre application peut trouver un nœud auquel se connecter est d'essayer de se connecter au point de terminaison de l'instance en écriture du cluster, puis au point de terminaison de l'instance en lecture du cluster. Vous faites cela jusqu'à ce que vous puissiez établir une connexion lisible. Ces points de terminaison ne changent pas, sauf si vous renommez votre cluster de base de données. Ainsi, vous pouvez généralement les laisser en tant que membres statiques de votre application ou les stocker dans un fichier de ressources que votre application lit.

Une fois que vous avez établi une connexion à l'aide de l'un de ces points de terminaison, vous pouvez obtenir des informations sur le reste du cluster. Pour ce faire, appelez la fonction aurora_replica_status. Par exemple, la commande suivante récupère des informations avec 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)

Par exemple, la section hosts (hôtes) de votre chaîne de connexion peut commencer par les points de terminaison des clusters de l'instance en écriture et en lecture, comme indiqué ci-dessous.

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

Dans ce scénario, votre application tente d'établir une connexion à un type de nœud, principal ou secondaire. Dès que votre application est connectée, nous vous conseillons de commencer par examiner le statut en lecture-écriture du nœud. Pour ce faire, recherchez le résultat de la commande SHOW transaction_read_only.

Si la valeur de retour de la requête est OFF, vous êtes bien connecté au nœud principal. Toutefois, supposons que la valeur de retour soit ON et que votre application nécessite une connexion en lecture/écriture. Dans ce cas, vous pouvez appeler la fonction aurora_replica_status pour déterminer le server_id qui possède session_id='MASTER_SESSION_ID'. Cette fonction vous donne le nom du nœud principal. Vous pouvez l'utiliser avec la fonction endpointPostfix décrite ci-après.

Assurez-vous de savoir ce que vous faites lorsque vous vous connectez à un réplica disposant de données obsolètes. Dans ce cas, la aurora_replica_status fonction peut afficher out-of-date des informations. Vous pouvez définir un seuil d'instabilité au niveau de l'application. Pour le vérifier, vous pouvez regarder la différence entre l'heure du serveur et la valeur last_update_timestamp. En général, votre application doit éviter de basculer entre deux hôtes en raison de conflits d'informations renvoyées par la fonction aurora_replica_status. Votre application devrait d'abord essayer tous les hôtes connus au lieu de suivre les données renvoyées par aurora_replica_status.

Liste des instances utilisant l'opération de l'API DescribeDBClusters, exemple en Java

Vous pouvez rechercher par programmation la liste des instances en utilisant le AWS SDK for Java, et plus précisément l'opération d'API DescribeDBClusters.

Voici un petit exemple de la façon dont vous pouvez procéder en 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(","));

Ici, pgJDBCEndpointStr contient une liste formatée de points de terminaison, comme indiqué ci-dessous.

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

La variable endpointPostfix peut être une constante que votre application définit. Votre application peut aussi l'obtenir en interrogeant l'opération API DescribeDBInstances pour une seule instance de votre cluster. Cette valeur reste constante au sein d'un client Région AWS et pour un client individuel. Cela permet donc d'économiser un appel d'API pour simplement conserver cette constante dans un fichier de ressources que votre application lit. Dans l'exemple précédent, elle est définie comme suit.

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

Pour une meilleure disponibilité, nous vous conseillons d'utiliser par défaut les points de terminaison Aurora de votre cluster de bases de données si l'API ne répond pas ou est trop longue à répondre. Les points de terminaison sont toujours mis à jour dans le délai requis pour mettre à jour l'enregistrement DNS. La mise à jour de l'enregistrement DNS avec un point de terminaison prend généralement moins de 30 secondes. Vous pouvez stocker le point de terminaison dans un fichier de ressources que votre application consomme.

Test du basculement

Dans tous les cas, vous devez avoir un cluster de bases de données avec deux ou plusieurs instances de base de données.

Du côté serveur, certaines opérations d'API peuvent provoquer une panne qui peut servir à tester la manière dont votre application répond :

  • FailoverDBCluster : cette opération tente de promouvoir une nouvelle instance de base de données dans votre cluster de base de données en tant qu'instance en écriture.

    L'exemple de code suivant montre comment vous pouvez utiliser failoverDBCluster pour provoquer une panne. Pour plus de détails sur la configuration d'un client Amazon RDS, consultez la section Utilisation du AWS SDK for Java.

    public void causeFailover() { final AmazonRDS rdsClient = AmazonRDSClientBuilder.defaultClient(); FailoverDBClusterRequest request = new FailoverDBClusterRequest(); request.setDBClusterIdentifier("cluster-identifier"); rdsClient.failoverDBCluster(request); }
  • RebootDBInstance  : le basculement n'est pas garanti avec cette opération d'API. Cependant, il ferme la base de données sur l'instance en écriture. Vous pouvez l'utiliser pour tester la façon dont votre application réagit à l'abandon des connexions. Ce paramètre ForceFailover ne s'applique pas aux moteurs Aurora. Utilisez plutôt l'opération d'API FailoverDBCluster.

  • ModifyDBCluster : la modification du paramètre Port entraîne une panne lorsque les nœuds du cluster commencent à écouter sur un nouveau port. En général, votre application peut répondre à cette défaillance en s'assurant que seule votre application contrôle les changements de port. Assurez-vous également qu'elle peut mettre à jour de manière appropriée les points de terminaison dont elle dépend. Vous pouvez le faire en demandant à quelqu'un de mettre à jour manuellement le port lorsqu'il apporte des modifications au niveau de l'API. Vous pouvez également le faire en utilisant l'API RDS dans votre application pour déterminer si le port a changé.

  • ModifyDBInstance : la modification du paramètre DBInstanceClass provoque une panne.

  • DeleteDBInstance : la suppression de l'instance principale (écriture) entraîne la promotion d'une nouvelle instance de base de données en tant qu'instance en écriture dans votre cluster de base de données.

Du côté de l'application ou du client, si vous utilisez Linux, vous pouvez tester la façon dont l'application réagit aux rejets soudains de paquets. Vous pouvez le faire en fonction du port, de l'hôte ou si des paquets TCP keepalive sont envoyés ou reçus en utilisant la commande iptables.

Exemple de basculement rapide en Java

L'exemple de code suivant montre comment une application peut configurer un gestionnaire de pilotes Aurora PostgreSQL.

L'application appelle la fonction getConnectionlorsqu'elle a besoin d'une connexion. Un appel à getConnection peut ne pas parvenir à trouver un hôte valide. Par exemple, si aucune instance en écriture n'est trouvée mais que le paramètre targetServerType est défini sur primary. Dans ce cas, l'application appelante doit simplement réessayer d'appeler la fonction.

Pour éviter d'imposer le comportement de relance à l'application, vous pouvez envelopper cet appel de relance dans une fonction de regroupement de connexions. Avec la plupart des fonctions de regroupement de connexions, vous pouvez spécifier une chaîne de connexion JDBC. Votre application peut donc appeler getJdbcConnectionString et la transmettre à la fonction de regroupement de connexions. Cela signifie que vous pouvez utiliser un basculement plus rapide avec 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(",")); } }