Image for post
Image for post

DevOps — Retour sur un downtime Redis, Kubernetes en production

Jonathan Perucca
Nov 6, 2019 · 7 min read

Aujourd’hui, on va revenir sur une série de downtime que nous avons pu vivre chez Malt courant mai 2019. Ici, on ne s’attardera pas forcément sur le moyen de correction en urgence du downtime de production, mais plutôt sur l’exploration d’un problème survenu avec notre utilisation de redis à travers les yeux d’un binôme (un dev et un ops).

Contexte

Les équipes dev et ops ouvrent une visio-conférence commune, on est parti pour investiguer le problème.

Premier point de constat, toutes les applications remontent ne plus pouvoir interroger notre cluster redis.

Chez Malt, on utilise Redis comme un cache centralisé pour plusieurs notions, qu’elles soient techniques ou fonctionnelles. Dans le lot, nous y retrouvons la mise en cache des feature flags (un article de notre CTO parle de ce concept) ainsi que des cache applicatifs plus traditionnels. Plus spécifiquement nous utilisons Google Memory Store, qui est en réalité un Redis managé par Google.

L’investigation

Pas de charge CPU anormale, pas de consommation mémoire excessive. En revanche, un lot de socket timeout. On part du coup plutôt voir du côté de ce qu’effectuent les applications en relation avec redis.

Les métriques redis sont on ne peut plus claires, on observe des appels KEYS, qui bloquent l’ensemble de l’instance redis pendant plusieurs dizaines de secondes

chaque appel KEYS *unitaire* peut prendre jusqu’à 25s
chaque appel KEYS *unitaire* peut prendre jusqu’à 25s
chaque appel KEYS *unitaire* peut prendre jusqu’à 25s

Du coté des nos applications servant le trafic à nos utilisateurs, l’impact de performance se fait immédiatement ressentir:

le pic de latence est clairement ressenti par nos utilisateurs
le pic de latence est clairement ressenti par nos utilisateurs
le pic de latence est clairement ressenti par nos utilisateurs

KEYS vs SCAN

Le problème de KEYS

Ce que l’on peut retrouver dans la documentation de redis concernant l’opération “KEYS”

Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don’t use KEYS in your regular application code. If you’re looking for a way to find keys in a subset of your keyspace, consider using SCAN or sets.

Techniquement, KEYS va ramener un ensemble de clés correspondant à notre recherche mais avec une meilleure consistance que SCAN. On est sûr que même si une opération d’ajout de clés est effectuée pendant que l’opération KEYS était exécutée, l’ajout ne surviendra qu’une fois que KEYS sera terminée. On peut assimiler cette mécanique à un système de lock sur un système de persistance SQL. Cette opération représente une complexité de recherche linéaire (O(N))

La solution avec SCAN

On permet donc avec SCAN de libérer le thread de redis pour effectuer d’autres opérations. Le temps total de parcours d’un ensemble de clés entre KEYS et SCAN reste équivalent mais ne bloquera pas l’entièreté des commandes suivantes pendant l’opération SCAN.

La première recherche qu’on effectue : quelle portion de notre code utilise l’appel à “KEYS”.

Aucun résultat ne remonte, on passe donc du côté de nos librairies qui utilisent redis.

On utilise principalement redis à travers 2 librairies :

  • FF4J pour notre gestion de cache des feature flags
  • spring cache (couche d’abstraction de cache) avec spring-data-redis pour le système interne de cache.

FF4J

On s’aperçoit qu’une modification avait été faite sur cette classe sur la capacité de FF4J à nettoyer sa liste de feature du cache en utilisant l’opération “SCAN”.

On commence donc à regarder pour remplacer la portion de code utilisant l’opérateur “KEYS”, mais nous n’avions pas encore la certitude que ces portions de code de FF4J étaient bien les sources de nos problèmes. En recherchant les endroits d’invocations de ces méthodes dans FF4J, on s’appercoit que ce sont des méthodes uniquement utilisées pour la webapi de ff4j et le nombre d’appels ainsi que la volumétrie retournée par l’opération “KEYS” ne colle pas avec les timeout constatés (quand bien même les appels à KEYS sont bloquants)

Toutefois, nous avons tout de même préféré contribuer à ce remplacement d’opération qui reste à éviter en production. Voir ici la PR sur FF4J (disponible dans ff4j à partir de la release 1.8.2)

Spring-data-redis

Nous tombons en effet sur DefaultRedisCacheWriter qui utilise cette opération pour les opérations de nettoyage de cache (utilisé assez intensivement côté applicatif que ce soit dans notre code ou bien à d’autres moment comme pour les opérations transactionnelles impliquant une mise en cache des résultats retournés par certains bean spring).

Ce problème a déjà été remonté sur le projet spring-data-redis depuis mai 2018

En attendant le traitement de cette issue côté spring-data-redis et vu que les api des couches d’abstraction de spring (ici spring cache) sont bien faites pour la configuration du module de cache, nous avons pu nous permettre de ré-écrire une implémentation de ce RedisCacheWriter sans avoir à forker spring-data-redis pour remplacer ce bout de code.

Résultats perçus

ça va tout de suite beaucoup mieux!
ça va tout de suite beaucoup mieux!
ça va tout de suite beaucoup mieux!

L’utilisation de SCAN même lors d’opérations spécifiques d’administration nécessitant de parcourir l’ensemble des clés sur Redis pouvant encore toujours durer en cumulé plusieurs dizaines de secondes ne bloque effectivement plus autant et aussi longtemps les autres opérations liées au trafic web utilisateur: les pics de latence côté trafic web utilisateur sont désormais négligeables!

Quelques bonnes pratiques

Nos applications tournant sur Google Kubernetes Engine, nous avions configuré les health checks permettant de déterminer si une application est en “bonne santé” si l’endpoint /health exposé retournait simplement un code retour HTTP 200.

Et c’était tout.

Ces applications mettant plusieurs minutes à démarrer et à se préchauffer, un redémarrage de l’ensemble des instances d’un même service signifiait malheureusement une indisponibilité allongée.

La version de Kubernetes que nous utilisions au moment de l’incident exposait deux sondes pour vérifier cet état de santé: la liveness et la readiness probe, qui peuvent soit effectuer ces health checks HTTP, soit vérifier si un socket TCP de l’application est bien ouvert. Lorsque la sonde readiness d’une application est en échec, le trafic cesse d’être envoyé à cette instance. Lorsque la sonde liveness est par contre en échec, c’est un redémarrage de l’instance qui est initié!

Nous avions configuré la liveness et la readiness de manière quasi-similaire, or cet incident nous a permis de conclure que dans notre contexte particulier d’applications lentes à démarrer, ce redémarrage de nos applications n’est souhaitable qu’en dernier recours pour ne pas accuser d’effet domino dû à la défaillance d’une ou de plusieurs ressources communes comme Redis.

Désormais, la sonde liveness ne vérifie que si le socket TCP des applications est bel et bien ouvert, la sonde readiness est inchangée. Si un tel incident se reproduisait à l’avenir, le temps d’indisponibilité sera bien moindre.

Maîtriser la volumétrie de redis

Les performances de KEYS et SCAN étant liées au nombre de clés présentes, contrôler l’accroissement du nombre de ces clés est par conséquent une méthode préventive additionnelle qu’il nous fallait mettre en place.

Juste avant l’incident, notre base de données Redis contenait environ 1,5 million de clés. un peu moins de la moitié de ces clés n’avaient pas de date d’expiration (TTL), et parmi cette moitié seule une petite partie pouvait réellement justifier de ne pas être éphémère.

Nous avons donc modifié nos applications pour que l’absence d’utilisation de clés ayant un TTL devienne une exception et non plus la norme. Nous avons ainsi pu réduire la volumétrie de notre instance de moitié!

Image for post
Image for post

Conclusion

Que l’on parle de mouvement devops ou que l’on y affecte un autre nom, le principe d’avoir le point de vue et les connaissances à la fois d’un admin sys ainsi que d’un dev est une excellente collaboration pour ce type de cas précis.

Au final notre plateforme accusera moins de risque d’indisponibilité lors de mal-fonctions intermittentes de ressources partagées (comme Redis), cependant, nous pourrions aller encore plus loin en implémentant un pattern de circuit-breaker pour activer une solution de repli temporaire et garder la plateforme disponible, ce sera peut être le sujet d’un autre ticket ;)

Article co-écrit par

Olivier Boukili (ops) — https://github.com/oboukili

Jonathan Perucca (dev) — https://github.com/jonathan-perucca

nerds-malt

Tout ce qui se passe sous le capot par l’équipe R&D de Malt…

Jonathan Perucca

Written by

nerds-malt

Tout ce qui se passe sous le capot par l’équipe R&D de Malt (ex Hopwork). Vous pouvez retrouver nos projets Open Source sur Github. Ici nous parlerons de ce que nous faisons.

Jonathan Perucca

Written by

nerds-malt

Tout ce qui se passe sous le capot par l’équipe R&D de Malt (ex Hopwork). Vous pouvez retrouver nos projets Open Source sur Github. Ici nous parlerons de ce que nous faisons.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store