Déployer un service hautement disponible grâce à Redis Replication (Publié dans Linux Pratique n°140)
Redis est un système de stockage de données en mémoire de type NoSQL orienté hautes performances. Il est souvent utilisée comme base de données, système de cache ou de messages. Nous verrons dans cet article comment déployer un service hautement disponible grâce à Redis Replication.
1 Notre premier serveur Redis
Pour démarrer, commençons avec un mon serveur Redis. L’installation sera présentée pour Ubuntu 22.04 et devrait fonctionner sans trop d’adaptations pour Debian ou RHEL-like.
1.1 Installation
Redis est disponible dans les dépôts classiques de la distribution. Une configuration minimale peut être faite en installant simplement les paquets et en activant le service systemd.
sudo apt -y install redis-server sudo systemctl enable --now redis-server
Vérifions maintenant que nous parvenons à nous connecter :
redis-cli 127.0.0.1:6379> ping PONG
La CLI permet également de récupérer avec la commande INFO un certain nombre d’éléments sur le fonctionnement du serveur. Celle-ci s’appelle seule ou avec un argument pour obtenir les informations relatives à une catégorie particulière.
127.0.0.1:6379> INFO Server # Server redis_version:6.0.16 redis_git_sha1:00000000 redis_git_dirty:0 redis_build_id:a3fdef44459b3ad6 redis_mode:standalone os:Linux 5.15.0-73-generic x86_64 arch_bits:64 multiplexing_api:epoll atomicvar_api:atomic-builtin gcc_version:11.2.0 process_id:596 run_id:7ea0cf9f46b211a64874d7a1c0a115be78c42e98 tcp_port:6379 uptime_in_seconds:61483 uptime_in_days:0 hz:10 configured_hz:10 lru_clock:8771018 executable:/usr/bin/redis-server config_file:/etc/redis/redis.conf io_threads_active:0
Redis gère plusieurs types de données. Il est possible d’utiliser des chaînes de caractères, des ensembles, des hashs, des listes ainsi que d’autres types de données. Pour procéder à une première vérification de bon fonctionnement, nous allons ainsi écrire une donnée de type string, récupérer la valeur en indiquant la clé puis effacer cette donnée.
127.0.0.1:6379> set foo bar OK 127.0.0.1:6379> get foo "bar" 127.0.0.1:6379> del foo (integer) 1 127.0.0.1:6379> get foo (nil)
A ce stade, notre serveur est fonctionnel, toutefois sa configuration est très basique. Redis n’écoute que sur l’interface localhost ce qui est incompatible avec la notion de réplication que nous mettrons en place et il est préférable que systemd prenne en charge le service de manière explicite. Nous allons entreprendre donc nos premières modifications du fichier de configuration, /etc/redis/redis.conf et remplacer les paramètres bind et supervised par les valeurs suivantes :
bind 0.0.0.0 supervised systemd protected-mode
Redémarrons le service redis et vérifions :
sudo systemctl restart redis-server sudo netstat -lataupen |grep ":6379" tcp 0 0 0.0.0.0:6379 0.0.0.0:* LISTEN 115 39165 3267/redis-server 0
En pratique, la plupart des options de Redis peuvent être définies avec la commande CONFIG SET. C’est peut-être moins classique ou confortable qu’un fichier de configuration mais c’est ce qui permet à Redis d’être entièrement reparamétré sans qu’il ne soit nécessaire de relancer le service et donc sans aucune interruption de service.
1.2 Gérer la persistance
Redis est une base de données en mémoire. Par conséquent si le service est arrêté, quelle qu’en soit la raison, les données sont perdues. Pour assurer la persistance des données, Redis permet d’utiliser deux mécanismes. Dans le premier, appelé RDB, Redis va générer des snapshots à intervalles réguliers de la base de données. Cela se configure avec save suivi de deux indicateurs, le premier consiste en une sauvegarde après n secondes si au moins un certain nombre d’écritures ont eu lieu. Ce paramètre est en outre multivalué .La politique par défaut est celle-ci :
save 900 1 save 300 10 save 60 10000
Généralement, on souhaite avoir au moins un snapshot récent même s’il y a peu d’écritures, d’où celui réalisé après 15 minutes à partir du moment où il y a eu une écriture. A l’inverse, en cas de forte charge, on souhaite également avoir des snapshots fréquents pour minimiser la perte de données, d’où le snapshot toutes les minutes dès 10000 clés modifiées. Ces seuils sont naturellement à adapter en fonction de l’activité de la base et afin de minimiser la perte de données admissible.
Dans le second mécanisme, AOF pour Append Only File, la persistance est gérée via l’utilisation d’un fichier de logs qui va historiser chaque écriture. Pour un maximum de fiabilité, les deux modes peuvent être utilisés conjointement. Naturellement, cela a un impact sur la consommation mémoire et les I/O disques car pour chaque écriture en base, une seconde est faite pour le journal AOB.
Pour utiliser l’AOF, ajoutons ces deux lignes dans le fichier redis.conf :
appendonly yes appendfilename "appendonly.aof" appendfsync everysec
1.3 Un minimum de sécurité
Par défaut, Redis n’est pas joignable via le réseau. Pour que la mise en œuvre de la réplication à suivre soit possible, le service écoute désormais sur l’ensemble des interfaces réseau. Bien qu’il soit possible de restreindre à une ou plusieurs interfaces, l’accès au service à des fins de test s’est réalisé sans authentification. En définissant un mot de passe sur le paramètre requirepass, toute opération sur Redis nécessitera au préalable de s’authentifier avec la commande AUTH suivie du mot de passe. Dans la configuration redis.conf nous ajoutons donc :
requirepass SECRET
Et nous pouvons vérifier :
# redis-cli 127.0.0.1:6379> AUTH SECRET OK
Pour assurer une meilleur sécurité, du TLS devrait être mis en œuvre et les ACL Redis 6 devraient être utilisées.
2 Assurer la haute disponibilité
2.1 Les mécanismes
Redis Replication est un système maître/esclave permettant de répliquer de manière asynchrone les données du master sur les slaves. En cas de perte de connectivité avec le master, les slaves vont tenter de resynchroniser l’écart de données dès que la connexion redevient disponible. Si cela n’est pas possible une resynchronisation complète est réalisée. Un slave peut servir de source de réplication à un autre slave et les répliquas peuvent servir de serveur en lecture pour répartir la charge et donc augmenter la performance.
En cas de perte du master, une procédure de failover doit être mise en œuvre pour promouvoir un slave comme master. Tant que cela n’est pas fait, le fonctionnement est en mode dégradé si ce n’est totalement interrompu. Redis Sentinel utilisé en complément permet de superviser le fonctionnement de Redis et en cas de panne du master, de promouvoir un nouveau master et de reconfigurer les repliquas pour qu’ils se répliquent depuis ce nouveau master. Dans ce type d’architecture, il est recommandé de disposer d’au moins 3 serveurs pour gérer le quorum.
Avec Redis Replication il est fortement recommandé d’activer la persistance. En effet, sans persistance et en cas de redémarrage du master, il se retrouvera vide de toute donnée. Dans un second temps, les slaves vont s’empresser de répliquer cela les vidant de leurs données.
Pour assurer la montée en charge horizontale, Redis Cluster offre des possibilités complémentaires. Dans ce mode, il s’agit toujours d’avoir une architecture master/slave mais avec gestion et failover automatique. L’espace de clés est divisé de sorte que chaque groupe de serveur ne gère qu’une partition de la base de données, cela s’appelle du sharding. La taille de la base de données peut ainsi être plus importante et la charge peut être répartie sur plus de nœuds. Un minimum de 6 nœuds est toutefois requis pour Redis Cluster ce qui prédispose ce mode à des infrastructures relativement conséquentes.
De fait dans cet article j’aborderai uniquement Redis Replication.
2.2 Configuration
La configuration mise en œuvre sur le premier serveur que nous appellerons db01 devra être reprise à l’identique sur nos deux slaves. Le serveur DB01 sera le master par défaut. Pour que la suite soit aisément compréhensible, voici les serveurs et adresses IP que je vais utiliser :
- db01 : 192.168.69.81
- db02 : 192.168.69.82
- db03 : 192.168.69.83
Sur chacun de nos serveurs, nous allons définir la valeur de masterauth. En effet, nous avons exigé précédemment un mot de passe via requirepass, il faut donc s’authentifier pour avoir les droits pour répliquer :
masterauth SECRET
Et enfin, sur chaque slave, on indique depuis quel serveur notre slave doit se répliquer :
replicaof db01.morot.local 6379
Il suffit de redémarrer nos serveurs, il n’y a rien de plus à faire.
2.3 Vérification de la réplication
Dans un premier temps, envoyons la commande INFO Replication sur notre master. Si la configuration est correcte, alors on doit pouvoir avoir un rôle master, visualiser les deux slaves comme connectés ainsi que leurs adresses IP et enfin en fonctionnement nominal un statut online :
127.0.0.1:6379> AUTH SECRET OK 127.0.0.1:6379> INFO Replication # Replication role:master connected_slaves:2 slave0:ip=192.168.69.83,port=6379,state=online,offset=1106,lag=0 slave1:ip=192.168.69.82,port=6379,state=online,offset=1106,lag=0 master_replid:75b55df27cafe6bfd7aec3114bfd63e77d45a8cb master_replid2:0000000000000000000000000000000000000000 master_repl_offset:1106 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:1106
A l’inverse, sur un slave, le rôle doit correspondre et le serveur doit être connecté au slave (master_link_status). Si master_sync_in_progress est à 0 alors la réplication est terminée. La section master_replid indiqué le dataset de ReplicationID qui doit être identique avec le master lorsque les masters et slaves sont synchronisés. Si une synchronisation était en cours, nous aurions des champs master_sync supplémentaires pour suivre la volumétrie à répliquer et les performances de réplication.
127.0.0.1:6379> INFO Replication # Replication role:slave master_host:db01.morot.local master_port:6379 master_link_status:up master_last_io_seconds_ago:3 master_sync_in_progress:0 slave_repl_offset:1288 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:75b55df27cafe6bfd7aec3114bfd63e77d45a8cb master_replid2:0000000000000000000000000000000000000000 master_repl_offset:1288 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:1288
2.4 Failover
Voyons désormais ce qu’il se passe lors d’une panne du master. J’ai simulé un crash du master en coupant violemment la VM qui porte le service. Sur chacun de mes slaves, la réplication indique que le lien avec le master est « down » avec le délai depuis lequel le lien est rompu.
127.0.0.1:6379> INFO Replication # Replication role:slave master_host:db01.morot.local master_port:6379 master_link_status:down master_last_io_seconds_ago:-1 master_sync_in_progress:0 slave_repl_offset:3472 master_link_down_since_seconds:42
S’il s’agit d’un simple redémarrage du serveur master, la réplication devrait remonter dès que le serveur sera up. En cas de panne majeure, le service rendu est interrompu. Il faut manuellement reconfigurer les slaves pour rétablir le service.
Sur db03, nous allons donc indiquer que sa source de réplication est db02 :
127.0.0.1:6379> replicaof db02.morot.local 6379 OK
Pour autant, cela ne fait pas de db02 un master par magie. D’autant que Redis supporte la réplication d’un slave depuis un autre slave. Il faut donc indiquer que db02 est un master ce qui se configure en indiquant qu’il est le répliqua d’aucun serveur :
127.0.0.1:6379> replicaof no one OK 127.0.0.1:6379> info replication # Replication role:master connected_slaves:1 slave0:ip=192.168.69.83,port=6379,state=online,offset=3472,lag=0
2.5 Le retour du master
Se pose ensuite la question de la remise en service du serveur db01 lorsqu’il sera réparé. Avec le failover précédent il va se retrouver master sans slave et il sera aussi désynchronisé des autres serveurs Redis. Il est donc indispensable de le remettre en synchronisation avec le master qui a été promu. Cela se fait de la même façon que lors du failover précédent :
127.0.0.1:6379> replicaof db02.morot.local 6379 OK
Et c’est tout. Si toutefois vous tenez à remettre le serveur redis master « nominal » en tant que master, il faut alors :
- Le resynchroniser sur le master et s’assurer que la réplication est terminée
- Définir les slaves en état nominal (db02 et db03) comme replicaof db01
- Indiquer que db01 n’est plus le répliqua d’aucun serveur.
Cela reste toutefois une opération à réaliser en période de maintenance car une courte rupture du service lors de la bascule est à prévoir.
3 Failover automatique avec Sentinel
3.1 Configuration
Nous venons de le voir, la réplication est simple à mettre en œuvre ou à reconfigurer. Elle a toutefois un énorme inconvénient en cas de perte du master, elle est manuelle. Nous allons donc mettre en œuvre un système complémentaire appelé Redis Sentinel qui viendra gérer automatiquement la promotion d’un nouveau master et la reconfiguration des slaves.
Commençons par installer sentinel :
sudo apt -y install redis-sentinel
Et configurer le service pour qu’il communique sur le réseau et non plus uniquement sur localhost. Au niveau firewall, le port 26379 devra autoriser les flux. Cela se fait via le fichier dédié /etc/redis/sentinel.conf :
bind 0.0.0.0 port 26379
Ajoutons un mot de passe de connexion, il s’agit du mot de passe d’authentification sur Sentinel :
requirepass SECRET
Enfin démarrons la configuration des règles de bascule. Nous monitorons l’adresse IP du master, 192.168.69. Un quorum de 2 serveurs est exigé, c’est à dire que si au moins deux slaves s’accordent sur le fait que le master est injoignable alors une bascule est enclenchée.
sentinel monitor mymaster 192.168.69.81 6379 2 sentinel auth-pass mymaster SECRET
Le master est considéré comme injoignable après une minute :
sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1
Une redémarrage du service sentinel plus tard, vérifions l’état de notre service en se connectant via redis-cli sur le port de Sentinel cette fois :
redis-cli -p 26379 127.0.0.1:26379> AUTH SECRET OK 127.0.0.1:26379> info sentinel # Sentinel sentinel_masters:1 sentinel_tilt:0 sentinel_running_scripts:0 sentinel_scripts_queue_length:0 sentinel_simulate_failure_flags:0 master0:name=mymaster,status=ok,address=192.168.69.81:6379,slaves=2,sentinels=4
Nous retrouvons bien le bon nombre de slaves. Si on souhaite des informations détaillées sur les slaves il est possible d’envoyer la commande sentinel slaves mymaster et pour les instances sentinel : sentinel sentinels mymaster. La sortie étant peu lisible, il s’agit de la configuration en mode clé valeur dans la base Redis, je ne vais pas l’inclure ici.
3.2 Test de bascule
Sur notre master, simulons une panne en arrêtant le service avec systemctl stop redis-server. En parallèle, regardons les logs de Sentinel dans /var/log/redis/redis-sentinel.log. Par lisibilité, je vais filtrer quelques peu les logs.
En premier lieu nous avons un event sdown, cela signifie que sentinel a détecté que le master n’était plus joignable :
37873:X 18 Jun 2023 21:33:04.343 # +sdown master mymaster 192.168.69.81 6379
Ensuite, nous avons un odown, c’est à dire que le serveur est vu comme injoignable par au moins le nombre de serveur du quorum, comme nous avons indiqué 2, il faut au moins les deux serveurs pour qu’une promotion d’un nouveau soit réalisée :
37873:X 18 Jun 2023 21:33:05.420 # +odown master mymaster 192.168.69.81 6379 #quorum 3/2
db03 a été élu comme nouveau master :
37873:X 18 Jun 2023 21:34:17.255 # +selected-slave slave 192.168.69.83:6379 192.168.69.83 6379 @ mymaster 192.168.69.81 6379 37873:X 18 Jun 2023 21:34:17.437 # +promoted-slave slave 192.168.69.83:6379 192.168.69.83 6379 @ mymaster 192.168.69.81 6379
db02 est reconfiguré comme slave de db03 :
37873:X 18 Jun 2023 21:34:17.453 * +slave-reconf-sent slave 192.168.69.82:6379 192.168.69.82 6379 @ mymaster 192.168.69.81 6379 37873:X 18 Jun 2023 21:34:17.510 * +slave-reconf-inprog slave 192.168.69.82:6379 192.168.69.82 6379 @ mymaster 192.168.69.81 6379 37873:X 18 Jun 2023 21:34:18.623 * +slave slave 192.168.69.82:6379 192.168.69.82 6379 @ mymaster 192.168.69.83 6379
Enfin, on peut le confirmer via la cli redis sur db03 :
127.0.0.1:6379> INFO REPLICATION # Replication role:master connected_slaves:1 slave0:ip=192.168.69.82,port=6379,state=online,offset=481996,lag=1Félicitations, Redis s’est automatiquement reconfiguré sans intervention. Que se passe-t-il désormais pour notre ancien master ? Au redémarrage de redis, il est devenu un slave. Cela se voit d’ailleurs dans les logs sentinel des autres serveurs. Il est possible de forcer via la cli sentinel un failover vers un autre serveur :127.0.0.1:26379> SENTINEL failover mymaster OK 127.0.0.1:26379> INFO sentinel # Sentinel sentinel_masters:1 sentinel_tilt:0 sentinel_running_scripts:0 sentinel_scripts_queue_length:0 sentinel_simulate_failure_flags:0 master0:name=mymaster,status=ok,address=192.168.69.82:6379,slaves=2,sentinels=4Il n’est toutefois pas possible d’indiquer expressément quel serveur sera promu. Une manière de le contrôler est d’utiliser la directive slave-priority pour favoriser un serveur avec une priorité plus faible.
4 HAProxy
Dans un monde idéal, votre client Redis aura un support de Sentinel. Il saura alors contacter sentinel pour connaître le master et s’y connecter. En récupérant la liste des serveurs, il sera en mesure de distribuer les requêtes en lecture sur les slaves pour répartir la charge. En cas d’anomalie du master, il saura enfin gérer le failover.
En pratique, on ne peut pas toujours connaître le niveau de support des clients notamment lorsque l’on fournit la plateforme à des développeurs tiers. Dans ce cas précis, je recommande d’utiliser le classique HAProxy qui sait tout faire en matière de répartition de charge que je vous laisse installer via les packages de la distribution.
Le cas particulier qu’il faut néanmoins gérer c’est la détection du master. De manière simple, si le port de destination répond, on considère souvent que le service est fourni par le backend. Dans notre cas, il va falloir identifier le serveur dont le rôle est le master, souvenez-vous nous avions l’information avec INFO Replication avec la CLI Redis. Il faut donc définir un TCP Check qui réalisera une authentification, enverra cette même commande est aura comme backend active celui dont le rôle est master :
frontend ft_redis
bind 0.0.0.0:6379 name redis
default_backend bk_redisbackend bk_redis
option tcp-check
tcp-check send AUTH\ SECRET\r\n
tcp-check send PING\r\n
tcp-check expect string +PONG
tcp-check send INFO\ Replication\r\n
tcp-check expect string role:master
tcp-check send QUIT\r\n
tcp-check expect string +OK
server r1 db01:6379 check inter 10s
server r2 db02:6379 check inter 10s
server r3 db03:6379 check inter 10sConclusion
C’est en terminé pour ce tour d’horizon de la haute disponibilité d’un service Redis Replication avec Sentinel. J’invite le lecteur curieux à compléter ce parcours pour utiliser les ACL et ajouter ce niveau de sécurité. J’ai volontairement écarté ce point pour légèrement simplifier la configuration et rester ciblé sur le sujet.