Dans mon travail quotidien, j’accompagne les équipes de différentes organisations et je les aide à relever leurs défis AWS. L’une des équipes que j’ai récemment soutenues utilisait Amazon ElasticCache pour Redis comme couche de stockage/cache pour sa charge de travail principale. Ils validaient leur configuration de production et testaient plusieurs scénarios de panne. Dans cet article, je vais partager quelques-unes des leçons apprises. Gardez à l’esprit que les cas décrits dans cet article sont très spécifiques au contexte et peuvent ne pas refléter votre cas d’utilisation, donc mon conseil est de toujours faire vos propres tests.
Architecture initiale
L’équipe a créé un service basé sur REST en utilisant API Gateway, AWS Lambda et Amazon ElastiCache pour Redis.
Amazon ElastiCache pour Redis a été configuré avec le mode cluster activé, car cela a donné à l’équipe la flexibilité de mettre à l’échelle les actions de lecture et d’écriture. Le cluster initial existait d’un seul fragment, avec la possibilité d’évoluer vers le haut/bas si nécessaire.
Lorsque le client Redis écrit et lit à partir du cluster, il vérifie d’abord à quelle partition appartiennent les données et envoie la commande de lecture ou d’écriture au nœud de cette partition particulière.
Désormais, lorsque vous configurez ElastiCache Redis avec le « mode cluster » activé dans AWS, la méthode recommandée pour vous connecter au cluster à partir de votre client consiste à vous connecter via le point de terminaison de configuration du cluster. Le point de terminaison de configuration du cluster garde une trace de toutes les modifications apportées au cluster Redis et peut vous fournir une topologie à jour.
La fonction AWS Lambda avait été écrite en Java et, par conséquent, l’équipe a examiné différents pilotes Redis basés sur Java et a finalement décidé d’utiliser Jedis, qui est un client Java pour Redis conçu pour les performances et la facilité d’utilisation. L’une des raisons pour lesquelles ils ont choisi d’utiliser Jedis était qu’il s’agissait d’une bibliothèque légère. L’instanciation de la connexion client prend entre 200 et 600 ms (selon les paramètres de mémoire Lambda), ce qui a un impact sur le démarrage à froid de la fonction Lambda :
private JedisRedisService() {
logger.info("Instantiating Redis Cluster connection");
HostAndPort hostAndPort = new HostAndPort(REDIS_HOST, Integer.valueOf(REDIS_PORT));
ConnectionPoolConfig connectionPoolConfig = new ConnectionPoolConfig();
connectionPoolConfig.setMaxTotal(5);
connectionPoolConfig.setMaxIdle(5);
DefaultJedisClientConfig defaultJedisClientConfig = DefaultJedisClientConfig.builder()
.connectionTimeoutMillis(JedisCluster.DEFAULT_TIMEOUT)
.socketTimeoutMillis(JedisCluster.DEFAULT_TIMEOUT)
.blockingSocketTimeoutMillis(JedisCluster.DEFAULT_TIMEOUT)
.user(REDIS_USERNAME)
.password(REDIS_PASSWORD)
.ssl(true)
.build();
jedisCluster = new JedisCluster(
hostAndPort,
defaultJedisClientConfig,
JedisCluster.DEFAULT_MAX_ATTEMPTS,
connectionPoolConfig
);
logger.info("Instantiated Redis Cluster connection");
}
Jedis s’est avéré être le pilote le plus rapide pour des scénarios spécifiques basés sur le post « Optimiser les performances du client Redis pour Amazon ElastiCache et MemoryDB » sur le blog AWS. Les performances d’un cache sont très importantes, mais nous voulions également examiner différents aspects du pilote.
Attendez-vous à l’inattendu
Werner Vogels déclare souvent que vous devez toujours vous attendre à l’inattendu si vous construisez dans le cloud. Les choses peuvent casser, et il est préférable de s’y préparer autant que possible.
Les pannes sont inévitables et tout finira par tomber en panne avec le temps : des routeurs aux disques durs, des systèmes d’exploitation aux unités de mémoire corrompant les paquets TCP, des erreurs passagères aux pannes permanentes. C’est une évidence, que vous utilisiez du matériel de la plus haute qualité ou des composants les moins chers. – Source: « 10 leçons tirées de 10 ans d’Amazon Web Services »
Pour tester comment Jedis gérerait certains scénarios « d’échec », nous avons décidé que nous voulions tester, au moins, les choses que nous pourrait attendre.
Source: AWS
Deux scénarios auxquels nous pouvions penser étaient :
- Une défaillance du nœud principal.
- Une mise à niveau de maintenance qui ferait pivoter et mettrait à jour les nœuds vers une nouvelle version de Redis.
Nous avons effectué ces tests avec la version 4.2.3 de Jedis, et plus tard, nous avons réessayé avec la version 4.3.1 car il y avait quelques correctifs et améliorations concernant les clusters Redis.
Pour tester l’impact, nous avons créé une fonction Lambda simple qui effectue OBTENIR opérations sur un ensemble spécifique de clés dans notre cache. Comme nous voulions tester dans des circonstances « réelles », nous avons créé un petit test de charge avec Artillerie.io qui envoyait un flux constant de demandes.
En réponse, nous représentons la valeur de la méthode get et listons les nœuds disponibles dans le cluster :
{
"nodes": [
"test-cluster-0001-001.test-cluster.ifrpha.euw1.cache.amazonaws.com:6379",
"test-cluster-0001-002.test-cluster.ifrpha.euw1.cache.amazonaws.com:6379",
"test-cluster-0001-003.test-cluster.ifrpha.euw1.cache.amazonaws.com:6379"
],
"test": "test value"
}
Basculement du nœud principal
Étant donné qu’ElastiCache pour Redis était la principale source de données du service, l’équipe souhaitait voir l’impact d’une défaillance du nœud principal. Le service était censé avoir une charge de travail lourde en lecture et notre petit cluster de test se composait d’un nœud principal et de deux nœuds répliqués. L’équipe s’attendait à avoir une configuration de production similaire lors du lancement du service. Tester une panne de nœud principal est assez simple dans ElastiCache pour Redis. Depuis la console AWS, vous pouvez déclencher le basculement à l’aide d’un bouton :
Soit depuis la ligne de commande en exécutant :
aws elasticache test-failover /
--replication-group-id "test-cluster" /
--node-group-id "0001"
Une fois le basculement déclenché, nous pouvions suivre sa progression dans les journaux d’événements ElastiCache. Le journal des événements peut être trouvé dans la console AWS ou à partir de l’AWS CLI en exécutant :
aws elasticache describe-events --max-items 5
Cela se traduira par une réponse JSON ou basée sur une table qui contient une liste d’événements dans l’ordre dans lequel ils se sont produits.
Identifiant source | Taper | Date | Événement |
---|---|---|---|
cluster-de-test-0001-001 | cluster de cache | 12 novembre 2022, 14:43:40 (UTC+01:00) | Récupération terminée pour les nœuds de cache 0001. |
cluster-de-test-0001-001 | cluster de cache | 12 novembre 2022, 14:33:22 (UTC+01:00) | Récupération des nœuds de cache 0001. |
cluster de test | groupe de réplication | 12 novembre 2022, 14:31:59 (UTC+01:00) | Basculement vers le nœud de réplica test-cluster-0001-002 terminé. |
cluster de test | groupe de réplication | 12 novembre 2022, 14:30:56 (UTC+01:00) | API de basculement de test appelée pour le groupe de nœuds 0001. |
Comme vous pouvez le voir dans le journal des événements ci-dessus, le cluster effectue d’abord un basculement. Après quoi, il récupère le nœud et le rajoute au cluster. Si vous regardez attentivement, vous pouvez voir que le temps entre le déclenchement de l’action de basculement et le basculement réel prend environ une minute. Nous avons effectué ce test plusieurs fois et nous avons vu cela varier de près d’une minute à jusqu’à deux minutes.
Nous avons remarqué qu’à partir du moment où le nœud principal a été supprimé, notre service a commencé à générer des erreurs. Dans les journaux CloudWatch, nous avons remarqué plusieurs erreurs liées à Jedis :
Cluster retry deadline exceeded.: redis.clients.jedis.exceptions.JedisClusterOperationException
redis.clients.jedis.exceptions.JedisClusterOperationException: Cluster retry deadline exceeded.
at redis.clients.jedis.executors.ClusterCommandExecutor.executeCommand(ClusterCommandExecutor.java:88)
at redis.clients.jedis.UnifiedJedis.executeCommand(UnifiedJedis.java:148)
at redis.clients.jedis.UnifiedJedis.get(UnifiedJedis.java:570)
Et:
2022-10-16 19:03:28 7e6d77f1-eea2-4c9e-ab3c-da108d1c9b16 DEBUG ClusterCommandExecutor - Failed connecting to Redis: null
redis.clients.jedis.exceptions.JedisConnectionException: Failed to connect to any host resolved for DNS name.
at redis.clients.jedis.DefaultJedisSocketFactory.connectToFirstSuccessfulHost(DefaultJedisSocketFactory.java:63) ~[task/:?]
at redis.clients.jedis.DefaultJedisSocketFactory.createSocket(DefaultJedisSocketFactory.java:87) ~[task/:?]
at redis.clients.jedis.Connection.connect(Connection.java:180) ~[task/:?]
at redis.clients.jedis.Connection.initializeFromClientConfig(Connection.java:338) ~[task/:?]
at redis.clients.jedis.Connection.<init>(Connection.java:53) ~[task/:?]
at redis.clients.jedis.ConnectionFactory.makeObject(ConnectionFactory.java:71) ~[task/:?]
La durée totale des exceptions dans notre journal a fini par prendre environ six minutes. Comme la charge était la même pendant le basculement, nous n’avons remarqué aucun démarrage à froid Lambda pendant le basculement.
Pendant ce temps, notre service répondait principalement par des erreurs et ne servait pas nos clients :
Trouver ce qui en était la cause n’était pas si simple, car Jedis n’écrit pas beaucoup d’informations de journal lorsque le niveau de journalisation est défini sur débogage. D’après ce que nous avons pu dire du code source de Jedis, il semble que lorsque le client Jedis se connecte au point de terminaison de configuration, il récupère tous les hôtes connus. Il stocke cela dans un cache local et détermine le nœud principal à partir de l’ensemble de nœuds. Une fois que le nœud principal est connu, il exécutera des opérations sur le nœud principal. Pour ce faire, il se connecte au nom de domaine/entrée généré par Amazon et lorsque cette entrée est indisponible/inaccessible, il semble qu’il n’utilise pas le point de terminaison de configuration pour redécouvrir les nœuds ou récupérer la nouvelle topologie de cluster. Nous avons d’abord pensé que cela avait à voir avec la mise en cache DNS de la JVM, nous avons donc également essayé de désactiver la mise en cache, mais nous n’avons vu aucun effet après ce changement :
{
java.security.Security.setProperty("networkaddress.cache.ttl","0");
java.security.Security.setProperty("networkaddress.cache.negative.ttl", "0");
}
À partir du code source de Jedis, nous avons appris qu’un peu de randomisation est appliqué une fois que le nœud est marqué comme « indisponible ». Cela semble se produire uniquement lorsque la tentative de connexion a expiré après plusieurs tentatives. Il…