Conmutación por error rápida con Amazon Aurora PostgreSQL - Amazon Aurora

Conmutación por error rápida con Amazon Aurora PostgreSQL

A continuación puede ver información sobre cómo asegurarse de que la conmutación por error se produzca lo más rápido posible. Para recuperarse rápidamente tras una conmutación por error, puede utilizar la administración de caché del clúster para el clúster de bases de datos de Aurora PostgreSQL. Para obtener más información, consulte Recuperación rápida después de una conmutación por error con la administración de caché del clúster para Aurora PostgreSQL.

Algunos de los pasos que puede adoptar para que la conmutación por error pase rápidamente son los siguientes:

  • Establezca keepalives del protocolo de control de transmisión (TCP, por sus siglas en inglés) con plazos cortos para detener las consultas que se ejecutan durante más tiempo antes de que venza el tiempo de espera de lectura en caso de que se produzca un error.

  • Establezca tiempos de espera para que el almacenamiento en caché del sistema de nombres de dominio (DNS, por sus siglas en inglés) de Java sea agresivo. Esto ayuda a garantizar que el punto de conexión de solo lectura de Aurora pueda desplazarse correctamente por nodos de solo lectura en posteriores intentos de conexión.

  • Establezca las variables de tiempo de inactividad, que se utilizan en la cadena de la conexión de JDBC, con los valores más bajos posibles. Utilice objetos de conexión independientes para consultas de ejecución corta y prolongada.

  • Utilice los puntos de conexión de Aurora de lectura y escritura que se proporcionan para establecer una conexión al clúster.

  • Utilice las operaciones de la API de RDS para probar la respuesta de la aplicación en caso de errores del lado del servidor. Además, puede usar una herramienta para supresión de paquetes para probar la respuesta de la aplicación ante errores del lado del cliente.

  • Utilice el controlador JDBC de AWS para aprovechar al máximo las capacidades de conmutación por error de Aurora PostgreSQL. Para obtener más información sobre el controlador JDBC de AWS e instrucciones completas para utilizarlo, consulte el repositorio GitHub del controlador JDBC de Amazon Web Services (AWS).

A continuación, se analizan con más detalle.

Configuración de parámetros Keepalive de TCP

Si configura una conexión TCP, se asocia una serie de temporizadores a la conexión. Cuando el temporizador keepalive llega a cero, se envía un paquete de sondeo keepalive al punto de conexión. Si recibe una respuesta al sondeo, puede presuponer que la conexión sigue en funcionamiento.

Al habilitar parámetros keepalive de TCP y configurarlos agresivamente, se garantiza que si su cliente no puede conectarse a la base de datos, se cierre rápidamente cualquier conexión activa. A continuación, la aplicación se puede conectar a un nuevo punto de conexión.

Tiene que establecer los siguientes parámetros keepalive de TCP:

  • tcp_keepalive_time controla el tiempo, en segundos, después del cual se envía un paquete keepalive si el socket no ha enviado datos. Los ACK no se consideran datos. Recomendamos la siguiente configuración:

    tcp_keepalive_time = 1

  • tcp_keepalive_intvl controla el tiempo, en segundos, entre el envío de posteriores paquetes keepalive después de enviar el paquete inicial. Establezca esta hora con el parámetro tcp_keepalive_time. Recomendamos la siguiente configuración:

    tcp_keepalive_intvl = 1

  • tcp_keepalive_probes es la cantidad de sondeos keepalive sin confirmar que tienen lugar antes de que se produzca la notificación a la aplicación. Recomendamos la siguiente configuración:

    tcp_keepalive_probes = 5

Esta configuración debería realizar una notificación a la aplicación en un plazo de cinco segundos cuando la base de datos deja de responder. Puede establecer un valor de tcp_keepalive_probes más elevado si los paquetes keepalive se suprimen con frecuencia dentro de la red de la aplicación. Esto incrementa el tiempo que se tarda en detectar un error real, pero ofrece más capacidad de búfer en redes menos fiables.

Para configurar parámetros keepalive de TCP en Linux
  1. Pruebe cómo configurar los parámetros keepalive de TCP.

    Recomendamos hacerlo con la línea de comandos y los siguientes comandos. Esta configuración sugerida es para todo el sistema. En otras palabras, también afecta a todas las demás aplicaciones que crean sockets con la opción SO_KEEPALIVE activada.

    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 vez que haya encontrado una configuración que funcione para su aplicación, esta configuración debe almacenarse de forma persistente agregando las siguientes líneas a /etc/sysctl.conf, incluido cualquier cambio que haya realizado:

    tcp_keepalive_time = 1 tcp_keepalive_intvl = 1 tcp_keepalive_probes = 5

Configuración de su aplicación para una conmutación por error rápida

A continuación, encontrará un análisis de varios cambios de configuración para Aurora PostgreSQL que puede realizar para lograr una conmutación por error rápida. Para obtener más información sobre la configuración y configuración del controlador de JDBC de PostgreSQL, consulte la documentación del controlador de JDBC de PostgreSQL.

Reducción de los tiempos de espera de la caché de DNS

Cuando su aplicación trate de establecer una conexión después de una conmutación por error, el nuevo escritor Aurora PostgreSQL será un lector anterior. Puede encontrarlo mediante el punto de conexión de solo lectura de Aurora, antes de que las actualizaciones de DNS se hayan propagado por completo. Establecer el tiempo de vida (TTL, por sus siglas en inglés) de DNS de Java en un valor bajo (como debajo de 30 segundos) ayuda a desplazarse por los nodos del lector en intentos de conexión 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");

Configuración de una cadena de conexión de Aurora PostgreSQL para conmutaciones por error rápidas

Para utilizar la conmutación por error rápida de Aurora PostgreSQL, asegúrese de que la cadena de conexión de su aplicación tenga una lista de hosts en lugar de un solo host. Mostramos aquí una cadena de conexión de ejemplo que podría utilizar para conectarse a un clúster de Aurora PostgreSQL: En este ejemplo, los hosts aparecen en negrita.

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 una mejor disponibilidad y evitar depender de la API de RDS, le recomendamos que mantenga un archivo con el que conectarse. Este archivo contiene una cadena de host desde la que su aplicación lee cuando establece una conexión con la base de datos. Esta cadena de host tiene todos los puntos de conexión de Aurora disponibles para el clúster. Para obtener más información acerca de los puntos de conexión de Aurora, consulte Administración de conexiones de Amazon Aurora.

Por ejemplo, puede almacenar sus puntos de conexión en un archivo local como se muestra a continuación.

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

Su aplicación lee de ese archivo para rellenar la sección host de la cadena de conexión de JDBC. Cambiar el nombre del clúster de base de datos provoca que estos puntos de conexión también cambien. Asegúrese de que la aplicación administre este evento, si se produce.

Otra opción consiste en usar una lista de nodos de instancia de base de datos, del siguiente modo.

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

El beneficio de este enfoque es que el controlador de la conexión JDBC de PostgreSQL recorre todos los nodos de esta lista para encontrar una conexión válida. Por el contrario, cuando se usan los puntos de conexión de Aurora, solo se prueban dos nodos en cada intento de conexión. Sin embargo, el uso de nodos de instancias de base de datos tiene un inconveniente. Si agrega o elimina nodos de su clúster y la lista de puntos de conexión de la instancia se queda obsoleta, el controlador de la conexión podría no encontrar nunca el host correcto al que conectarse.

Configure los siguientes parámetros, de manera agresiva, para contribuir a garantizar que su aplicación no tenga que esperar demasiado para conectarse a cualquier host:

  • targetServerType: controla si el controlador se conecta a un nodo de escritura o lectura. Para asegurarse de que las aplicaciones se vuelvan a conectar solo a un nodo de escritura, establezca el valor targetServerType en primary.

    Los valores posibles para el parámetro targetServerType incluyen primary, secondary, any y preferSecondary. El valor preferSecondary primero intenta establecer una conexión con un lector. Se conecta al escritor si no se puede establecer una conexión con el lector.

  • loginTimeout: controla cuánto tiempo espera su aplicación para iniciar sesión en la base de datos después de que se haya establecido una conexión de socket.

  • connectTimeout: controla cuánto tiempo espera el socket para establecer una conexión con la base de datos.

Puede modificar otros parámetros de la aplicación para acelerar el proceso de conexión, en función del grado de agresividad deseado en la aplicación.

  • cancelSignalTimeout: en algunas aplicaciones, sería conveniente enviar la “mejor” señal de cancelación para una consulta cuyo tiempo se haya agotado. Si esta señal de cancelación se encuentra en su ruta de conmutación por error, debería plantearse configurarla agresivamente para evitar enviar esta señal a un host inoperativo.

  • socketTimeout: este parámetro controla cuánto tiempo espera el socket a que se produzcan las operaciones de lectura. Es posible usar este parámetro como tiempo de espera de consulta "global" para garantizar que nunca se supere este valor. Una práctica recomendada es tener dos controladores de conexión. Un controlador de conexión ejecuta consultas de corta duración y establece un valor más bajo. Y otro controlador de conexión, para consultas de larga duración, tiene este valor más alto. De este modo, puede confiar en que los parámetros keepalive de TCP detengan las consultas de larga duración si el servidor deja de funcionar.

  • tcpKeepAlive: active este parámetro para asegurarse de que se respeten los parámetros keepalive de TCP configurados.

  • loadBalanceHosts: cuando se establece en true, este parámetro hace que la aplicación se conecte a un alojamiento aleatorio elegido entre una lista de alojamientos candidatos.

Otras opciones para la obtención de la cadena de host

Puede obtener la cadena de host de diferentes lugares, entre incluida la función aurora_replica_status, y mediante la API de Amazon RDS.

En muchos casos, debe determinar quién es el escritor del clúster o encontrar otros nodos de lector del clúster. Para ello, su aplicación puede conectarse a cualquier instancia de base de datos del clúster de base de datos y consultar la función aurora_replica_status. Puede utilizar esta función para reducir la cantidad de tiempo que se tarda en encontrar un host al que conectarse. Sin embargo, en ciertos escenarios de fallo de red, la función aurora_replica_status podría mostrar información obsoleta o incompleta.

Una buena manera de garantizar que la aplicación pueda encontrar un nodo al cual conectarse es intentar conectarse al punto de conexión del escritor del clúster y, a continuación, al punto de conexión del lector del clúster. Hágalo hasta que pueda establecer una conexión legible. Estos puntos de conexión no cambian a menos que cambie el nombre del clúster de base de datos. En general, pueden dejarse como miembros estáticos de su aplicación o almacenarse en un archivo de recursos desde el que lea la aplicación.

Después de establecer una conexión con uno de estos puntos de conexión, puede llamar a la función para obtener información sobre el resto del clúster. Para ello, llame a la función aurora_replica_status. Por ejemplo, el siguiente comando obtiene información 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)

Por ejemplo, la sección hosts de su cadena de conexión podría empezar con los puntos de conexión del clúster del escritor y del lector, tal como se muestra a continuación.

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

Ante esta situación, su aplicación tratará de establecer una conexión a cualquier tipo de nodo, principal o secundario. Cuando la aplicación esté conectada, una buena práctica es examinar en primer lugar el estado de lectura-escritura del nodo. Para ello, consulte el resultado del comando SHOW transaction_read_only.

Si el valor de retorno de la consulta es OFF, significa que se ha conectado correctamente al nodo principal. Sin embargo, supongamos que el valor devuelto es ON y la aplicación requiere una conexión de lectura/escritura. En este caso, puede llamar a la función aurora_replica_status para determinar el server_id que tiene session_id='MASTER_SESSION_ID'. Esta función le proporciona el nombre del nodo principal. Puede usar esto con endpointPostfix, tal como se describe a continuación.

Asegúrese de estar al tanto cuando se conecte a una réplica con datos obsoletos. Cuando esto ocurre, la función aurora_replica_status podría mostrar información desactualizada. Puede establecer un umbral de obsolescencia en el nivel de aplicación. Para comprobarlo, puede ver la diferencia entre la hora del servidor y el valor de last_update_timestamp. En general, su aplicación debe evitar pasar de un alojamiento a otro debido a la información conflictiva devuelta por la función aurora_replica_status. Su aplicación debe probar primero todos los hosts conocidos en lugar de seguir los datos devueltos por aurora_replica_status.

Enumeración de instancias mediante la operación de la API DescribeDBClusters (ejemplo en Java)

Mediante programación, puede consultar la lista de instancias con AWS SDK for Java, concretamente con la operación de la API DescribeDBClusters.

Se muestra aquí un breve ejemplo de cómo podría hacer esto 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(","));

Aquí, pgJDBCEndpointStr contiene una lista con formato de puntos de conexión, tal como se muestra a continuación.

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 puede ser una constante que establece su aplicación. O bien, su aplicación puede obtenerla consultando la operación de la API DescribeDBInstances para una sola instancia de su clúster. Este valor es constante dentro de un Región de AWS y para un cliente individual. Por lo tanto, guarda una llamada a la API para mantener esta constante en un archivo de recursos desde el que su aplicación lee. En el ejemplo anterior, se establece en lo siguiente.

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

Para fines de disponibilidad, una práctica recomendada sería utilizar, de manera predeterminada, los puntos de conexión de Aurora de su clúster de bases de datos si la API no respondiera o tardara demasiado en responder. Se garantiza que los puntos de enlace estén actualizados en el tiempo que se tarda en actualizar el registro de DNS. La actualización del registro DNS con un punto de conexión suele tardar menos de 30 segundos. Puede almacenar el punto de conexión en un archivo de recursos que consume la aplicación.

Prueba de conmutación por error

En todos los casos, debe tener un clúster de bases de datos con dos o más instancias de base de datos en él.

Desde el lado del servidor, algunas operaciones de la API pueden causar una interrupción del servicio que se puede usar para probar cómo responden sus aplicaciones:

  • FailoverDBCluster: esta operación trata de promover a escritor una instancia de base de datos nueva en su clúster de bases de datos.

    En el siguiente ejemplo de código se muestra cómo se puede utilizar failoverDBCluster para provocar una interrupción. Para obtener más detalles sobre la configuración de un cliente de Amazon RDS, consulte Uso del SDK de AWS para Java.

    public void causeFailover() { final AmazonRDS rdsClient = AmazonRDSClientBuilder.defaultClient(); FailoverDBClusterRequest request = new FailoverDBClusterRequest(); request.setDBClusterIdentifier("cluster-identifier"); rdsClient.failoverDBCluster(request); }
  • RebootDBInstance: la conmutación por error no está garantizada en esta operación de la API. Sin embargo, cierra la base de datos del escritor. Puede usarla para probar cómo responde la aplicación ante caídas de las conexiones. El parámetro ForceFailover no se aplica a motores de Aurora. En su lugar, use la operación de la API FailoverDBCluster.

  • ModifyDBCluster: la modificación del parámetro Port causa una interrupción cuando los nodos del clúster comienzan a escuchar en un puerto nuevo. En general, su aplicación puede responder primero a este error asegurándose de que solo su aplicación controle los cambios en los puertos. Además, asegúrese de que pueda actualizar adecuadamente los puntos de conexión de los que depende. Para hacerlo, pida a alguien que actualice manualmente el puerto cuando realice modificaciones en el nivel de la API. O puede hacerlo mediante la API de RDS de la aplicación para determinar si el puerto ha cambiado.

  • ModifyDBInstance: modificar el parámetro DBInstanceClass provoca una interrupción.

  • DeleteDBInstance: eliminar el principal (escritor) provoca que una instancia de base de datos nueva se promueva al escritor en su clúster de bases de datos.

Desde el lado de la aplicación o del cliente, si está utilizando Linux, puede probar cómo responde la aplicación ante supresiones repentinas de paquetes. Puede hacerlo en función del puerto o el host o de si se envían o reciben paquetes TCP keepalive mediante el comando iptables.

Ejemplo de conmutaciones por error rápidas en Java

El siguiente ejemplo de código ilustra cómo una aplicación podría configurar un administrador del controlador de Aurora PostgreSQL.

La aplicación llamará a la función getConnection cuando necesite una conexión. Una llamada a getConnection puede no encontrar un host válido. Un ejemplo es cuando no se encuentra ningún escritor, pero el parámetro targetServerType está establecido en primary. En este caso, la aplicación de llamada simplemente debe volver a intentar llamar a la función.

Esto puede integrarse fácilmente en un concentrador de conexiones para evitar forzar el reintento hacia la aplicación. Con la mayoría de los concentradores de conexiones, puede especificar una cadena de conexión JDBC. Para que su solicitud pueda llamar a getJdbcConnectionString y pasarlo al concentrador de conexiones. Esto significa que puede usar una conmutación por error más rápida con 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(",")); } }