Haute disponibilité avec Patroni

Formation HAPAT

Dalibo SCOP

24.09

29 août 2024

Sur ce document

Formation Formation HAPAT
Titre Haute disponibilité avec Patroni
Révision 24.09
ISBN N/A
PDF https://dali.bo/hapat_pdf
EPUB https://dali.bo/hapat_epub
HTML https://dali.bo/hapat_html
Slides https://dali.bo/hapat_slides

Licence Creative Commons CC-BY-NC-SA

Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :

  • Paternité
  • Pas d’utilisation commerciale
  • Partage des conditions initiales à l’identique

Marques déposées

PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.

Versions de PostgreSQL couvertes

Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 12 à 16.

Généralités sur la haute disponibilité

Introduction

  • Définition de « Haute Disponibilité »
  • Contraintes à définir
  • Contraintes techniques
  • Solutions existantes

Définitions

  • RTO / RPO
  • Haute Disponibilité de données / de service

RTO / RPO

Haute disponibilité de service

  • Continuité d’activité malgré incident
  • Redondance à tous les niveaux
    • réseaux, stockage, serveurs, administrateurs…
    • réplication des données
  • Automatisation des bascules

Haute disponibilité des données

  • Perte faible ou nulle de données après incident
    • redonder les données
    • garantir les écritures à plusieurs endroits
  • Contradictoire avec la haute disponibilité de service
    • arbitrage possible avec une complexité et un budget plus importants

Sauvegardes

  • Composant déjà présent
  • Travail d’optimisation à effectuer
  • RTO de quelques minutes possibles
  • RPO de quelques minutes (secondes ?) facilement
  • Et ne pas oublier de tester

Sauvegarde PITR

  • Sauvegarde incrémentale binaire
  • Optimiser la sauvegarde complète
  • Optimiser la restauration complète (RTO)
  • Ajuster l’archivage au RPO désiré

PITR et redondance par réplication physique

Outils PITR

  • Barman
  • pgBackRest

Bilan PITR

  • Utiliser un outil libre issu de l’écosystème PostgreSQL
  • Fiabilise l’architecture
  • Facilite la mise en œuvre et l’administration
  • Couvre déjà certains besoins de disponibilité
  • Nécessite une intervention humaine
  • Nécessite une supervision fiable

Réplication physique

  • Réplique les écritures via les journaux de transactions
  • Entretient une ou plusieurs instances clones
  • Intégrée à PostgreSQL
  • Facilité de mise en œuvre
  • Réduit RPO/RTO par rapport au PITR
  • Plus de matériel
  • Architecture et maintenance plus complexes
  • Haute disponibilité des données

Réplication et RPO

  • Réplication asynchrone ou synchrone
  • Nécessite un réseau très fiable et performant
  • Asynchrone : RPO dépendant du volume d’écriture
    • RPO < 1s hors maintenance et chargement en masse
  • Synchrone : RPO = 0
    • 2 secondaires minimum
    • impact sur les performances !

Réplication et RTO

  • Bascule manuelle
  • Promotion d’une instance en quelques secondes

Bilan sur la réplication

  • 0 ≤ RPO < PITR
  • RTO = prise en charge + 30s
  • Simple à mettre en œuvre
  • Investissement financier et humain plus important

Bascule automatisée

  • Détection d’anomalie et bascule automatique
  • HA de service :
    • réduit le temps de prise en charge
  • Plusieurs solutions en fonction du besoin
  • Beaucoup de contraintes !

Prise de décision

  • La détection d’anomalie est naïve !
  • L’architecture doit pouvoir éviter un split-brain
  • Solutions éprouvées :
    • fencing
    • quorum
    • watchdog
    • SBD
  • Solutions le plus souvent complémentaires.

Mécanique de fencing

  • Isole un serveur/ressource
    • électriquement
    • arrêt via IPMI, hyperviseur
    • coupe les réseaux
  • Utile :
    • pour un serveur muet ou fantôme (rogue node)
    • lorsque l’arrêt d’une ressource est perturbé
  • Déclenché depuis un des nœuds du cluster
  • Nécessite une gestion fine des droits
  • Supporté par Pacemaker, embryonnaire dans Patroni

Mécanique d’un Quorum

  • Chaque serveur possède un ou plusieurs votes
  • Utile en cas de partition réseau
  • La partition réseau qui a le plus de votes détient le quorum
  • La partition qui détient le quorum peut héberger les ressources
  • La partition sans quorum doit arrêter toute ressource
  • Attention au retour d’une instance dans le cluster
  • Supporté par Pacemaker et Patroni (via DCS)

Mécanique du watchdog

  • Équipement matériel intégré partout
    • au pire : softdog (moins fiable)
  • Compte à rebours avant redémarrage complet du serveur
  • À ré-armer par un composant applicatif du serveur, périodiquement
  • Permet de déclencher du self-fencing rapide et fiable
    • meilleure réactivité de l’agrégat
  • « Fencing du pauvre », complémentaire du quorum
  • Patroni et Pacemaker : oui

Storage Base Death

  • L’une des méthodes historique de fencing
  • Un ou plusieurs disques partagés
    • où les nœuds s’échangent des messages
  • Un watchdog par nœud
  • Message poison pill pour demander à un nœud distant de s’auto-fencer
  • Self-fencing en cas de perte d’accès aux disques…
    • …si Pacemaker confirme lui aussi une anomalie
  • Patroni : émulé

Bilan des solutions anti-split-brain

À minima, une architecture fiable peut se composer au choix :

  • fencing actif ou SBD
  • 1 watchdog par serveur + quorum
  • L’idéal : tous les configurer
  • Désactiver les services au démarrage

Implication et risques de la bascule automatique

  • Un collègue peu loquace de plus : un automate
    • et tout doit passer à présent par lui
  • Complexification de l’administration
    • Formation + Tests + Documentation + Communication
    • sinon : erreurs humaines plus fréquentes
    • au final : est-ce plus fiable ?
  • Opérations post-bascule toujours à faire

Reconstruction automatique des instances

  • mécanisme supporté par Patroni
  • risque des:
    • perte d’informations
    • sur-incident
  • préférer une reconstruction manuelle

Questions

N’hésitez pas, c’est le moment !

Patroni : Architecture

Au menu

  • Patroni
  • Réplication PostgreSQL
  • DCS

Patroni

  • Un démon patroni par instance PostgreSQL
  • Les démons patroni coopèrent entre eux
  • Chaque démon administre son instance PostgreSQL locale
    • et impose de passer par lui

Réplication PostgreSQL

Patroni configure la réplication physique entre les instances pour :

  • assurer la réplication des données
    • en mode synchrone et/ou asynchrone
    • aussi en cascade
  • maintenir la réplication après bascule

DCS

  • Stockage distribué par consensus
  • écritures distribuées et atomiques
  • source de vérité de l’agrégat
  • hautement disponible

Deux grappes de serveurs

Agrégats de serveurs pour Patroni

Questions

  • C’est le moment !

Réplication physique : rappels

PostgreSQL

Introduction

  • Patroni repose sur la réplication physique de PostgreSQL
  • la haute disponibilité implique les équipes techniques
  • les équipes techniques doivent comprendre la mécanique

Au menu

  • Mise en place de la réplication physique
  • Promotion
  • Retour à l’état stable

Mise en place de la réplication par streaming

  • Réplication en flux
  • Un processus du serveur primaire discute avec un processus du serveur secondaire
    • d’où un lag moins important
  • Asynchrone ou synchrone
  • En cascade

Serveur primaire (1/2) - Configuration

Dans postgresql.conf :

  • wal_level = replica (ou logical)
  • max_wal_senders = X
    • 1 par client par streaming
    • défaut : 10
  • wal_sender_timeout = 60s

Serveur primaire (2/2) - Authentification

  • Le serveur secondaire doit pouvoir se connecter au serveur primaire
  • Pseudo-base replication
  • Utilisateur dédié conseillé avec attributs LOGIN et REPLICATION
  • Configurer pg_hba.conf :
   host replication user_repli 10.2.3.4/32   scram-sha-256
  • Recharger la configuration

Serveur secondaire (1/4) - Copie des données

Copie des données du serveur primaire (à chaud !) :

  • Copie généralement à chaud donc incohérente !
  • Le plus simple : pg_basebackup
    • simple mais a des limites
  • Idéal : outil PITR
  • Possible : rsync, cp
    • ne pas oublier pg_backup_start()/pg_backup_stop() !
    • exclure certains répertoires et fichiers
    • garantir la disponibilité des journaux de transaction

Serveur secondaire (2/4) - Fichiers de configuration

  • postgresql.conf & postgresql.auto.conf
    • paramètres
  • standby.signal (dans PGDATA)
    • vide

Serveur secondaire (2/4) - Paramètres

  • primary_conninfo (streaming) :
primary_conninfo = 'user=postgres host=prod port=5434
 passfile=/var/lib/postgresql/.pgpass
 application_name=secondaire2 '
  • Optionnel :
    • primary_slot_name
    • recovery_command
    • wal_receiver_timeout

Serveur secondaire (3/4) - Démarrage

  • Démarrer PostgreSQL
  • Suivre dans les traces que tout va bien

Processus

Sur le primaire :

  • walsender ... streaming 0/3BD48728

Sur le secondaire :

  • walreceiver streaming 0/3BD48728

Promotion

  • Attention au split-brain !
  • Vérification avant promotion
  • Promotion : méthode et déroulement
  • Retour à l’état stable

Attention au split-brain !

  • Si un serveur secondaire devient le nouveau primaire
    • s’assurer que l’ancien primaire ne reçoit plus d’écriture
  • Éviter que les deux instances soient ouvertes aux écritures
    • confusion et perte de données !

Vérification avant promotion

  • Primaire :
# systemctl stop postgresql-14
$ pg_controldata -D /var/lib/pgsql/14/data/ \
| grep -E '(Database cluster state)|(REDO location)'
Database cluster state:               shut down
Latest checkpoint's REDO location:    0/3BD487D0
  • Secondaire :
$ psql -c 'CHECKPOINT;'
$ pg_controldata -D /var/lib/pgsql/14/data/ \
| grep -E '(Database cluster state)|(REDO location)'
Database cluster state:               in archive recovery
Latest checkpoint's REDO location:    0/3BD487D0

Promotion du standby : méthode

  • Shell :
    • pg_ctl promote
  • SQL :
    • fonction pg_promote()
  • Déclenchement par fichier :
    • promote_trigger_file(<=v15)

Promotion du standby : déroulement

Une promotion déclenche :

  • déconnexion de la streaming replication (bascule programmée)
  • rejeu des dernières transactions en attente d’application
  • choix d’une nouvelle timeline du journal de transaction
  • suppression du fichier standby.signal
  • nouvelle timeline et fichier .history
  • ouverture aux écritures

Opérations après promotion du standby

  • VACUUM ANALYZE conseillé
    • calcul d’informations nécessaires pour autovacuum

Retour à l’état stable

  • Si un standby a été momentanément indisponible, reconnexion directe possible si :
    • journaux nécessaires encore présents sur primaire (slot, wal_keep_size/wal_keep_segments)
    • journaux nécessaires présents en archives (restore_command)
  • Sinon
    • « décrochage »
    • reconstruction nécessaire

Retour à l’état stable, suite

  • Synchronisation automatique une fois la connexion rétablie
  • Mais reconstruction obligatoire :
    • si le serveur secondaire était plus avancé que le serveur promu (« divergence »)
  • Reconstruire les serveurs secondaires à partir du nouveau principal :
    • rsync, restauration PITR, plutôt que pg_basebackup
    • pg_rewind
  • Reconstruction : manuelle !
  • Tablespaces !

Conclusion

  • La réplication physique native est très robuste
  • Patroni peut automatiser :
    • la mise en réplication
    • la promotion
    • le raccrochage d’un ancien primaire

Installation de PostgreSQL depuis les paquets communautaires

Travaux pratiques

Sur Rocky Linux 8 ou 9

Réplication asynchrone en flux avec un seul secondaire

Promotion de l’instance secondaire

Retour à la normale

Sur Debian 12

Réplication asynchrone en flux avec un seul secondaire

Promotion de l’instance secondaire

Retour à la normale

Travaux pratiques (solutions)

Sur Rocky Linux 8 ou 9

Sur Debian 12

etcd : Architecture et fonctionnement

Au menu

  • L’algorithme Raft
  • Implémentation Raft dans etcd
  • Installation etcd
  • Fonctionnalités d’etcd
  • Tâches d’administrations d’un cluster etcd

L’algorithme Raft

  • Algorithme de consensus
  • Replicated And Fault Tolerant
  • Leader-based requests

Raft : Journal et machine à états

  • Machine à états : leader, follower ou candidate
  • Journal des événements :
    • répliqué depuis le leader
    • même résultat sur tous les nœuds
  • Mandats :
    • commence par une élection du leader (unique)
    • numérotés, croissants

Élection d’un leader

  • Heart beats depuis le leader vers les followers
  • Si pas de nouvelles du leader :
    • démarrage de l’élection d’un nouveau leader
  • Promotion par consensus entre les membres
  • Si échec de l’élection, attente aléatoire
  • Tolérance de panne à partir de 3 nœuds

Réplication du journal

  • Mécanisme de réplication
  • Structure des journaux
  • Protection contre les incohérences
    • n° de mandat + index

Sécurité & cohérence

Lors d’une élection:

  • les votants refusent tout candidat en retard par rapport à eux

Majorité et tolérance de panne

  • Nombre impair de nœuds total recommandé
  • Majorité : quorum = (nombre de nœuds / 2) + 1
  • Tolérance de panne : nombre de nœuds - quorum
  • Pas de quorum : pas de réponse au client

Tolérance de panne : Tableau récapitulatif

Nombre de nœuds Majorité Tolérance de panne
2 2 0
3 2 1
4 3 1
5 3 2
6 4 2
7 4 3
8 5 3

Interaction avec les clients

  • Toutes les communications passent par le leader
  • Protection contre l’exécution en double des requêtes
  • Le leader valide les entrées non commitées après son élection
  • Le leader vérifie qu’il est bien leader avant de répondre à une demande de lecture

Raft en action

Démo interactive :

Mécanique d’etcd

  • Serveur de stockage distribué par consensus
    • clé/valeur
  • Multiples nœuds
  • Nécessite 3 nœuds minimum pour avoir une tolérance de panne
  • Haute disponibilité et tolérance de panne
  • Élection par un quorum

etcd V2 et V3

  • Abandon de l’API REST au profit de gRPC
  • endpoints différents pour la supervision
  • Commandes différentes dans etcdctl (ETCDCTL_API)
  • Nouvelles fonctionnalités (ou implémentations différentes):
    • transactions
    • leases
    • quorum & lecture linéarisées / sérialisées
  • Possibilité d’activer le protocole v2 :
    • deux bases de données séparées, une par version de l’API
  • Utilisez l’API v3
    • supportée par Patroni depuis la version 2.0.0 (septembre 2020)

etcd et Raft

  • Implémente l’algorithme Raft
    • hautement disponible: élections et réplication par consensus
    • tolérance de panne possible qu’à partir de 3 nœuds
    • différence : requêtes sur un followers
  • Nombre de membres impair, recommandé entre 3 et 7
  • Mutualisation du cluster etcd pour plusieurs clusters Patroni

Modes de défaillance d’etcd

  • Indisponibilité :
    • du leader
    • d’un nombre de membres inférieur ou égal à la tolérance de panne
    • d’un nombre de membres supérieur à la tolérance de panne
  • Partition réseau
  • Échec lors de l’initialisation du cluster (bootstrap)

Mise en œuvre d’etcd

Contraintes matérielles et logicielles

  • VMs dédiées !
  • Dimensionner la mémoire pour les données + l’OS
    • quota par défaut : 2 Go (largement supérieur à ce que Patroni utilise)
    • préconisation : 2 à 8 Go
    • attention à l’overcommit / OOM killer
  • Des disques rapides pour ne pas ralentir le cluster lors des COMMIT
    • utiliser du SSD
    • tester le stockage avec fio
  • Un réseau fiable (beaucoup d’échanges entre nœuds etcd)
  • Exemple chez CoreOS : proc dual core, 2 Go de RAM, 80 Go de SSD.

Installation des binaires

  • RedHat/EL depuis le dépôt PGDG :
    • dnf install etcd
    • firewalld
  • Debian/Ubuntu :
    • apt-get install etcd (Debian 11)
    • apt-get install etcd-server etcd-client (Debian 12)
  • Installation depuis les sources (Github)

Configuration etcd

Trois méthodes de configuration différentes:

  • En arguments à l’exécutable etcd
  • En variables d’environnement lues par l’exécutable etcd
  • Dans un fichier YAML pointé --config-file

Services etcd

Configurations employées par les principaux services etcd:

  • Service etcd.service
  • Variables d’environnements…
  • … chargées depuis un fichier :
    • /etc/etcd/etcd.conf (paquet PGDG Red Hat & dérivées)
    • /etc/default/etcd (Debian et dérivées)
  • Installation manuelle
    • créer le fichier de service
    • adapter la configuration

Démarrage du cluster

  • systemctl start etcd
    • attente du quorum dès démarrage
  • Premier contact avec etcdctl
  • Création automatique du data-dir

Utilisation d’etcd

Stockage distribué

  • Manipulation des clés avec : get, put, del (ou l’API)
  • Anciennes versions des clés accessibles… jusqu’au passage d’une purge
  • Lectures linéarisées et sérialisées
  • Patroni utilise etcd comme serveur de configurations distribuées pour :
    • stocker des informations sur l’état du cluster
    • stocker la configuration dynamique de Patroni

Notion de bail

  • Notion de lease dans etcd
    • associe une durée de validité (TTL) à une clé
    • le client peut renouveler le bail
    • etcd détruit la clé à l’expiration du bail
  • Utilisation par Patroni
    • permet d’identifier le leader Patroni et de garantir que la leader key expire une fois le TTL expiré

Unicité des clés

  • Création d’une transaction (etcdctl txn)
  • Création conditionnelle d’une clé
    • action si la clé n’existe pas
    • action si la clé existe
  • Permet à Patroni de garantir qu’il n’y a qu’un seul leader

Maintenances

Authentification

  • Activation de l’authentification requise
    • etcdctl auth enable
  • user : pour l’authentification
    • etcdctl user [add|del|get|list]
    • etcdctl user [grant-role|revoke-role]
  • role : conteneur pour les droits, assigné aux utilisateurs
    • etcdctl role [add|del|get|list]
    • etcdctl role [grant-permission|revoke-permission]

Chiffrement des communications

  • Communications avec les clients :
    • --cert-file
    • --key-file
    • --client-cert-auth
    • --trusted-ca-file
  • Communications au sein du cluster etcd :
    • --peer-cert-file
    • --peer-key-file
    • --peer-client-cert-auth
    • --peer-trusted-ca-file

Sauvegarde et restauration

  • Nécessité de recréer le cluster
  • Sauvegarde des données
    • etcdctl … snapshot save …
    • copie de la base de données ($ETCD_DATA_DIRECTORY/member/snap/db)
  • Restauration
    • nouveau cluster
    • etcdctl snapshot restore …
  • Démarrer le nouveau cluster

Remplacement de membre

Pour remplacer un membre sans risquer de perdre le quorum :

  • d’abord retirer l’ancien membre
  • puis ajouter le nouveau membre
  • et jamais l’inverse !

Autres tâches de maintenance

  • Journal Raft et espace de stockage :
    • rétention
    • quota
    • compactage manuelle et automatique

Supervision et métrologie

  • endpoint /debug
  • endpoint /metrics
    • destiné à prometheus
    • Grafana dispose d’une source de données Prometheus
    • surveiller
      • présence d’un leader, nombre d’élections
      • statistiques sur les consensus, performances du stockage et du réseau
      • l’utilisation du quota
  • endpoint /health

Questions

  • C’est le moment !

Quiz

Travaux pratiques

Raft

Nous allons utiliser le simulateur Raftscope.

Les 5 nœuds portent tous le dernier numéro de mandat qu’ils ont connu. Le leader est cerclé de noir.

Le timeout de chaque follower se décrémente en permanence et est réinitialisé quand un message de heart beat du leader est reçu.

Le journal de chaque nœud est visible sur la droite pour suivre la propagation des informations de chaque request.

Il est possible de mettre en pause, d’accélérer ou ralentir.

Les actions se font par clic droit sur chaque nœud :

  • stop / resume / restart pour stopper/(re)démarrer un nœud ;
  • time out sur un nœud provoque une élection ;
  • request est une interrogation client (à faire uniquement sur le leader).
  • Observer la première élection et l’échange des heart beats par la suite.
  • Mettre en défaut le leader (stop) et attendre une élection.
  • Lancer plusieurs écritures (requests) sur le leader.
  • Remettre en route l’ancien leader.
  • Arrêter deux nœuds, dont le leader, et observer le comportement du cluster.
  • Qu’en est-il de la tolérance de panne ?
  • Arrêter un nœud secondaire (pour un total de 3 nœuds arrêtés).
  • Tester en soumettant des écritures au primaire.
  • Rallumer un nœud pour revenir à 3 nœuds actifs dont un leader.
  • Éteindre le leader. Tenter de soumettre des écritures.
  • Que se passe-t-il ?

Installation d’etcd sous Debian

  • Installer etcd sur les 3 nœuds e1, e2 et e3.
  • Supprimer le nœud etcd créé sur chaque serveur.
  • Configurer un cluster etcd sur les 3 nœuds e1, e2 et e3.
  • Démarrez le cluster etcd

Installation d’etcd sous Rocky Linux 9

  • Installer etcd sur les 3 nœuds e1, e2 et e3.
  • Configurer un cluster etcd sur les 3 nœuds e1, e2 et e3.
  • Activez et démarrez le cluster etcd

etcd : manipulation (optionnel)

Ces exercices utilisent l’API v3 d’etcd.

But : manipuler la base de données distribuée d’Etcd.

  • Depuis un nœud, utiliser etcdctl put pour écrire une clef foo à la valeur bar.
  • Récupérer cette valeur depuis un autre nœud.
  • Modifier la valeur à baz.
  • Créer un répertoire food contenant les clés/valeurs poisson: bar et vin: blanc.
  • Récupérer toutes les clefs du répertoire food en exigeant une réponse d’un quorum.

But : constater le comportement d’Etcd conforme à l’algorithme Raft.

  • Tout en observant les logs de etcd et la santé de l’agrégat, procéder au fencing du leader avec virsh suspend <nom machine>.
  • Geler le nouveau leader de la même manière et voir les traces du nœud restant.

Travaux pratiques (solutions)

Raft

  • Observer la première élection et l’échange des heart beats par la suite.

Les nœuds portent au départ le numéro de mandat 1, il n’y a pas de leader :

élection etcd - étape 1

Il faut attendre que l’un des nœuds ait atteint son timeout pour qu’il propose une élection. Ici, c’est le nœud S2 qui déclenche l’élection :

élection etcd - étape 2

Les petites pastilles sur ce nœud S2 représentent le nombre de votes lui étant favorable. Un vote sur cinq est pour le moment validé : le sien. Les autres nœuds de l’agrégat reçoivent donc la candidature de S2 et y répondent tous favorablement :

élection etcd - étape 3

Nous voyons le nombre de vote augmenter au fur et à mesure que S2 les reçoit :

élection etcd - étape 4

Finalement, S2 remporte l’élection, crée le mandat n°2 et envoie son premier message de keep alive :

élection etcd - étape 5

Les autres se raccrochent à lui et entretiennent chacun leur time out.

  • Mettre en défaut le leader (stop) et attendre une élection.

Le même phénomène se produit et l’on arrive au mandat 3. Ici, le timeout du nœud S1 expire en premier :

failover etcd - étape 1

S1 déclenche une élection :

failover etcd - étape 2

Les autres nœuds accordent leur vote à S1, sauf S2 qui est éteint :

failover etcd - étape 3

S1 devient leader :

failover etcd - étape 4
  • Lancer plusieurs écritures (requests) sur le leader.

Elles apparaissent sur la droite dans le journal au fur et à mesure que le leader les diffuse. La diffusion se fait en plusieurs étapes. Écrite et émission depuis S1 :

Requête etcd - étape 1

Bonne réception des nœuds S3, S4 et S5 :

Requête etcd - étape 2

S0 ayant reçu une majorité d’acquittement, il valide la valeur auprès des autres nœuds :

Requête etcd - étape 3

Tous les nœuds ont validé la valeur :

Requête etcd - étape 4
  • Remettre en route l’ancien leader.

Celui-ci redémarre à 2 (en tant que follower) :

failback etcd - étape 1

Il bascule sur le mandat 3 au premier heart beat reçu, et commence à remplir son journal :

failback etcd - étape 2

Il rattrape ensuite son journal avec autant d’échange avec le leader que nécessaire :

failback etcd - étape 3 failback etcd - étape 4 failback etcd - étape 5

  • Arrêter deux nœuds, dont le leader, et observer le comportement du cluster.
  • Qu’en est-il de la tolérance de panne ?

Le quorum demeure au sein du cluster, il est donc toujours possible de déclencher une élection. Comme précédemment, les trois nœuds restants s’accordent sur un leader. Dans notre exemple, tous les nœuds déclenchent leur propre élection en même temps :

second failover etcd - étape 1

Chaque nœud ayant voté pour lui-même, il n’y a pas de consensus sur le nœud à élire, les nœuds redeviennent followers. L’un d’entre eux voit son timeout atteint, ce qui donne lieu à une seconde élection :

second failover etcd - étape 2

Elle conduit ici à l’élection du nœud S2 :

second failover etcd - étape 3

La tolérance de panne est maintenant nulle.

  • Arrêter un nœud secondaire (pour un total de 3 nœuds arrêtés).
  • Tester en soumettant des écritures au primaire.

Le cluster continue de fonctionner : on peut lui soumettre des écritures (requests) :

perte quorum etcd - étape 1

Elles sont bien répliquées mais ne sont pas exécutées (ou commitées) par le leader :

perte quorum etcd - étape 2

Pour cela, il faut que les écritures aient été répliquées vers la majorité des nœuds du cluster. Cela signifie que le client reste en attente de confirmation du traitement de sa demande.

  • Rallumer un nœud pour revenir à 3 nœuds actifs dont un leader.
  • Éteindre le leader. Tenter de soumettre des écritures.
  • Que se passe-t-il ?

L’un des deux nœuds survivants lance une élection :

Tentative d’élection sans quorum etcd - étape 1

L’autre le suit :

Tentative d’élection sans quorum etcd - étape 2

Mais comme le quorum de 3 nœuds (moitié de 5 plus 1) n’est pas obtenu, l’élection échoue. Les time out continuent d’expirer et de nouvelles élections sont lancées :

Tentative d’élection sans quorum etcd - étape 3

Faute de leader, il devient impossible de soumettre des écritures. Tout client s’appuyant dessus reçoit une erreur. Il faut attendre le retour en ligne d’un nœud et l’élection d’un nouveau leader.

Installation d’etcd sous Debian

  • Installer etcd sur les 3 nœuds e1, e2 et e3.

Les paquets etcd sont disponibles dans les dépôts officiels de Debian 12. L’installation consiste simplement à installer les deux paquets etcd-server et etcd-client :

# apt-get install -y etcd-server etcd-client
  • Supprimer le nœud etcd créé sur chaque serveur.

Afin de respecter la politique des paquets Debian, l’installation du paquet etcd-server crée automatiquement une instance etcd locale. Nous devons la détruire avant de pouvoir construire notre cluster etcd à trois nœuds.

Sur chaque serveur etcd, exécuter les commandes suivantes :

# systemctl stop etcd.service
# rm -Rf /var/lib/etcd/default
  • Configurer un cluster etcd sur les 3 nœuds e1, e2 et e3.

La configuration sous Debian se situe dans le fichier /etc/default/etcd où doivent être renseignés les paramètres etcd sous leur forme de variables d’environnements.

Voici le fichier de configuration commenté du nœud e1, veillez à adapter toutes les adresses IP 10.x.y.z :

# nom du nœud au sein du cluster
ETCD_NAME=e1

# emplacement du répertoire de données
ETCD_DATA_DIR=/var/lib/etcd/acme

# interface et port d'écoute des autres nœuds
ETCD_LISTEN_PEER_URLS=http://10.0.0.11:2380

# interfaces et port d'écoute des clients
ETCD_LISTEN_CLIENT_URLS=http://10.0.0.11:2379,http://127.0.0.1:2379,http://[::1]:2379

# interface et port d'écoute à communiquer aux clients
ETCD_ADVERTISE_CLIENT_URLS=http://10.0.0.11:2379

#######################################
## Initialisation du cluster et du nœud

# création du nœud au sein d'un nouveau cluster
ETCD_INITIAL_CLUSTER_STATE=new

# interface et port à communiquer aux autres nœuds
ETCD_INITIAL_ADVERTISE_PEER_URLS=http://10.0.0.11:2380

# liste des nœuds composant le cluster
ETCD_INITIAL_CLUSTER=e1=http://10.0.0.11:2380,e2=http://10.0.0.12:2380,e3=http://10.0.0.13:2380
  • Démarrez le cluster etcd

Une fois les trois fichiers de configuration établis, nous pouvons démarrer le cluster en exécutant la commande suivante sur tous les serveurs etcd :

# systemctl start etcd

La commande suivante doit désormais fonctionner sur n’importe quel nœud :

$ etcdctl -wjson endpoint status | jq
[
  {
    "Endpoint": "127.0.0.1:2379",
    "Status": {
      "header": {
        "cluster_id": 11628162814576028000,
        "member_id": 13786016400334385000,
        "revision": 95,
        "raft_term": 24
      },
      "version": "3.4.23",
      "dbSize": 61440,
      "leader": 2266733444126347800,
      "raftIndex": 112,
      "raftTerm": 24,
      "raftAppliedIndex": 112,
      "dbSizeInUse": 53248
    }
  }
]

L’état du cluster est visible grace à la commande suivante :

$ etcdctl endpoint status -w table --cluster
+-----------------------+------------------+---------+---------+-----------+[..]+--------+
|       ENDPOINT        |        ID        | VERSION | DB SIZE | IS LEADER |[..]| ERRORS |
+-----------------------+------------------+---------+---------+-----------+[..]+--------+
| http://10.0.0.11:2379 | 9e80988e833ccb43 |  3.5.13 |   20 kB |     false |[..]|        |
| http://10.0.0.13:2379 | a10d8f7920cc71c7 |  3.5.13 |   29 kB |      true |[..]|        |
| http://10.0.0.12:2379 | abdc532bc0516b2d |  3.5.13 |   20 kB |     false |[..]|        |
+-----------------------+------------------+---------+---------+-----------+[..]+--------+

Installation d’etcd sous Rocky Linux 9

  • Installer etcd sur les 3 nœuds e1, e2 et e3.

Les paquets etcd sont disponibles depuis les dépôts PGDG pour les distributions Red Hat 9 & dérivées (comme ici Rocky Linux 9). Il nous faut donc au préalable configurer ce dépôt :

# dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/\
pgdg-redhat-repo-latest.noarch.rpm

Il est désormais possible d’installer le paquet etcd en activant le dépôt pgdg-rhel9-extras :

# dnf --enablerepo=pgdg-rhel9-extras install -y etcd
  • Configurer un cluster etcd sur les 3 nœuds e1, e2 et e3.

La configuration sur cette distribution se situe dans le fichier /etc/etcd/etcd.conf où sont renseignés les paramètres etcd sous leur forme de variables d’environnements.

Ci-après les différents paramètres à modifier pour le nœud e1, veillez à adapter toutes les adresses IP 10.x.y.z :

ETCD_NAME=e1
ETCD_DATA_DIR=/var/lib/etcd/acme
ETCD_LISTEN_PEER_URLS=http://10.0.0.11:2380
ETCD_LISTEN_CLIENT_URLS=http://10.0.0.11:2379,http://127.0.0.1:2379,http://[::1]:2379
ETCD_ADVERTISE_CLIENT_URLS=http://10.0.0.11:2379
ETCD_INITIAL_CLUSTER_STATE=new
ETCD_INITIAL_ADVERTISE_PEER_URLS=http://10.0.0.11:2380
ETCD_INITIAL_CLUSTER=e1=http://10.0.0.11:2380,e2=http://10.0.0.12:2380,e3=http://10.0.0.13:2380
  • Activez et démarrez le cluster etcd

Une fois les trois fichiers de configuration établis, nous pouvons démarrer le cluster en exécutant la commande suivante sur tous les serveurs etcd :

# systemctl start etcd

Afin qu’etcd démarre automatiquement avec le serveur, il est nécessaire d’activer le service :

# systemctl enable etcd

Une fois les trois fichiers de configuration établis, la commande suivante doit fonctionner sur n’importe quel nœud :

$ etcdctl -wjson endpoint status | jq
[
  {
    "Endpoint": "127.0.0.1:2379",
    "Status": {
      "header": {
        "cluster_id": 11628162814576028000,
        "member_id": 13786016400334385000,
        "revision": 95,
        "raft_term": 24
      },
      "version": "3.4.23",
      "dbSize": 61440,
      "leader": 2266733444126347800,
      "raftIndex": 112,
      "raftTerm": 24,
      "raftAppliedIndex": 112,
      "dbSizeInUse": 53248
    }
  }
]

L’état du cluster est visible grace à la commande suivante :

$ etcdctl endpoint status -w table --cluster
+-----------------------+------------------+---------+---------+-----------+[..]+--------+
|       ENDPOINT        |        ID        | VERSION | DB SIZE | IS LEADER |[..]| ERRORS |
+-----------------------+------------------+---------+---------+-----------+[..]+--------+
| http://10.0.0.11:2379 | 9e80988e833ccb43 |  3.5.13 |   20 kB |     false |[..]|        |
| http://10.0.0.13:2379 | a10d8f7920cc71c7 |  3.5.13 |   29 kB |      true |[..]|        |
| http://10.0.0.12:2379 | abdc532bc0516b2d |  3.5.13 |   20 kB |     false |[..]|        |
+-----------------------+------------------+---------+---------+-----------+[..]+--------+

etcd : manipulation (optionnel)

Ces exercices utilisent l’API v3 d’etcd.

But : manipuler la base de données distribuée d’Etcd.

  • Depuis un nœud, utiliser etcdctl put pour écrire une clef foo à la valeur bar.
$ etcdctl put foo bar
OK
  • Récupérer cette valeur depuis un autre nœud.
$ etcdctl get foo
foo
bar
  • Modifier la valeur à baz.
$ etcdctl put foo baz
OK
  • Créer un répertoire food contenant les clés/valeurs poisson: bar et vin: blanc.
$ etcdctl put food/poisson bar
OK

$ etcdctl put food/vin blanc
OK

$ etcdctl get --keys-only --prefix food/
food/poisson

food/vin
  • Récupérer toutes les clefs du répertoire food en exigeant une réponse d’un quorum.
$ etcdctl get --consistency="l" --keys-only --prefix food/
 /food/poisson

 /food/vin

But : constater le comportement d’Etcd conforme à l’algorithme Raft.

  • Tout en observant les logs d’etcd et la santé de l’agrégat, procéder au fencing du leader avec virsh suspend <nom machine>.

Le leader peut être identifié avec la commande suivante :

# etcdctl -wtable --endpoints http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 endpoint status

Dans notre correctif, e1 est actuellement leader. Dans une fenêtre sur e2 et e3, laisser défiler le journal :

# journalctl -fu etcd

Depuis une session dans le serveur hôte, interroger e2 ou e3 continuellement à propos de la santé de l’agrégat avec par exemple :

$ watch -n1 "curl -s -XGET http://10.0.0.12:2379/health | jq"
{
  "health": "true",
}

Depuis le serveur hôte, cette commande interrompt brutalement la machine virtuelle e1 (mais sans la détruire) :

# virsh destroy e1
Domain 'e1' destroyed

Il est aussi possible de suspendre la machine avec la commande suivante :

# virsh suspend e1
Domain 'e1' suspended

La commande inverse est alors virsh resume e1.

Notez que la machine est suspendue, donc encore présente, mais plus du tout exécutée par l’hyperviseur. Cette présence inactive peut parfois créer des situations déstabilisantes pour les autres machines virtuelles ayant toujours des connexions TCP en suspend. Aussi, après son réveil, sauf présence de chrony ou ntpsec, l’horloge de cette machine virtuelle nécessite une intervention manuelle afin de la resynchroniser.

Dans les traces de e3, nous trouvons :

INFO: 49fc71338f77c1c4 is starting a new election at term 3
INFO: 49fc71338f77c1c4 became candidate at term 4
INFO: 49fc71338f77c1c4 received MsgVoteResp from 49fc71338f77c1c4 at term 4
INFO: 49fc71338f77c1c4 [logterm: 3, index: 9] sent MsgVote request to 97e570ef03022438 at term 4
INFO: 49fc71338f77c1c4 [logterm: 3, index: 9] sent MsgVote request to be3b2f45686f7694 at term 4
INFO: raft.node: 49fc71338f77c1c4 lost leader be3b2f45686f7694 at term 4

L’identifiant 49fc71338f77c1c4 est celui de e3 lui-même. Ces traces nous indiquent :

  • le déclenchement de l’élection avec un nouveau mandat n°4 ;
  • le vote de e3 pour lui-même ;
  • les demandes de vote vers e1 et e2.
  • la perte du leader e1 (identifiant be3b2f45686f7694) durant le mandat n°4

Suivent alors les messages suivants:

INFO: 49fc71338f77c1c4 received MsgVoteResp from 97e570ef03022438 at term 4
INFO: 49fc71338f77c1c4 has received 2 MsgVoteResp votes and 0 vote rejections
INFO: 49fc71338f77c1c4 became leader at term 4
INFO: raft.node: 49fc71338f77c1c4 elected leader 49fc71338f77c1c4 at term 4

Seul e2 (ici 97e570ef03022438) répond au vote, mais cette réponse suffit à atteindre le quorum (de 2 sur 3) et valider l’élection de e3 comme nouveau leader.

En interrogeant l’état de e3, nous confirmons bien qu’il est leader :

$ etcdctl -wtable endpoint status
+----------------+------------------+---------+---------+-----------+-[…]
|    ENDPOINT    |        ID        | VERSION | DB SIZE | IS LEADER | […]
+----------------+------------------+---------+---------+-----------+-[…]
| 127.0.0.1:2379 | 49fc71338f77c1c4 |  3.4.23 |   20 kB |      true | […]
+----------------+------------------+---------+---------+-----------+-[…]
  • Geler le nouveau leader de la même manière et voir les traces du nœud restant.

L’état de l’agrégat tombe en erreur suite à cette perte du quorum :

$ curl -s -XGET http://10.0.0.12:2379/health | jq
{
  "health": "false"
}

Dans les traces de e2, nous constatons qu’il tente en boucle une élection, envoie des messages mais faute de réponse, n’obtient jamais d’accord pour l’élection :

15:36:31 INFO: 97e570ef03022438 is starting a new election at term 4
15:36:31 INFO: 97e570ef03022438 became candidate at term 5
15:36:31 INFO: 97e570ef03022438 received MsgVoteResp from 97e570ef03022438 at term 5
15:36:31 INFO: 97e570ef03022438 [logterm: 4, index: 1192] sent MsgVote request to 49fc71338f77c1c4 at term 5
15:36:31 INFO: 97e570ef03022438 [logterm: 4, index: 1192] sent MsgVote request to be3b2f45686f7694 at term 5
15:36:31 INFO: raft.node: 97e570ef03022438 lost leader 49fc71338f77c1c4 at term 5
15:36:33 INFO: 97e570ef03022438 is starting a new election at term 5
15:36:33 INFO: 97e570ef03022438 became candidate at term 6
15:36:33 INFO: 97e570ef03022438 received MsgVoteResp from 97e570ef03022438 at term 6
15:36:33 INFO: 97e570ef03022438 [logterm: 4, index: 1192] sent MsgVote request to 49fc71338f77c1c4 at term 6
15:36:33 INFO: 97e570ef03022438 [logterm: 4, index: 1192] sent MsgVote request to be3b2f45686f7694 at term 6
15:36:34 INFO: 97e570ef03022438 is starting a new election at term 6
15:36:34 INFO: 97e570ef03022438 became candidate at term 7
15:36:34 INFO: 97e570ef03022438 received MsgVoteResp from 97e570ef03022438 at term 7
15:36:34 INFO: 97e570ef03022438 [logterm: 4, index: 1192] sent MsgVote request to 49fc71338f77c1c4 at term 7
15:36:34 INFO: 97e570ef03022438 [logterm: 4, index: 1192] sent MsgVote request to be3b2f45686f7694 at term 7

La situation revient à la normale dès qu’un des deux autres revient en ligne avec par exemple la commande virsh start e3.

Patroni : Mise en œuvre

PostgreSQL

Au menu

  • Architecture générale
  • Patroni
  • Proxy, VIP et poolers de connexions

Architecture générale

  • Haute disponibilité de service
  • Instances gérées uniquement par Patroni
  • Nécessite deux agrégats de serveurs :
    • DCS (etcd)
    • Patroni
  • Synchronisation des horloges
    • attention aux snapshots !

Définitions

Patroni permet de :

  • créer / mettre en réplication
  • maintenir
  • superviser

des serveurs PostgreSQL en haute disponibilité.

Mécanismes mis en œuvre

  • Réplication physique et outils standards (pg_rewind, pg_basebackup)
  • Sauvegarde PITR (barman, pgBackRest…)
  • Gestionnaire de service : Patroni
  • DCS : Serveur de configurations distribuées (etcd ou autres)

Bascule automatique

  • split-brain
  • leader lock
  • heart beat
  • Promotion automatique

Définition : split-brain

  • 2 primaires sollicités en écriture
  • Arbitrage très difficile
  • Perte de données
  • Indisponibilité du service

Leader lock de Patroni

  • Verrou attribué au primaire de manière unique
  • Communication entre les nœuds Patroni
    • comparaison des LSN
  • Nouveau primaire :
    • nouvelle timeline
    • nouvelle chaîne de connexions
    • les secondaires se raccrochent au primaire

Heartbeat

  • Tous les nœuds Patroni s’annoncent à etcd
    • primaire ou follower
  • si perte de contact avec le leader :
    • timeout sur les followers et bascule

Bootstrap de nœud

  • Création du premier nœud
    • initdb
    • pgBackRest depuis sauvegarde PITR
  • Création / Reconstruction de réplica
    • pg_basebackup depuis primaire
    • pgBackRest depuis sauvegarde PITR (delta !)

Répartition sur deux sites

  • Prévoir la perte d’un des 2 sites
  • Quorum impossible
    • au pire, passage en read only !

Répartition sur trois sites

  • 3 sites pour un quorum
  • Tolérance de panne accrue
  • Changement de site lors d’une bascule
  • 3ᵉ site en standby en cas de perte de 2 sites

Installation

  • Paquets disponibles pour les distributions EL
  • Paquets disponibles pour les distributions Debian
  • Installez PostgreSQL avec Patroni !

Sur Entreprise Linux

  • Utilisation des dépôts communautaires PGDG
  • Nécessite d’activer le dépôt EPEL

Sur Debian et dérivés

  • Paquet patroni disponible
  • Versions plus à jour dans les dépôts PGDG communautaires
    • gérées par les mainteneurs Debian officiels

Installation manuelle

  • Utilisation du gestionnaire de paquets Python pip
  • Installe Patroni mais aussi ses dépendances

Configurations

  • Configuration statique
    • stockée dans le fichier de configuration YAML de chaque nœud
    • recharge par patronictl reload
  • Configuration dynamique
    • stockée dans le DCS
    • initialisée depuis la section bootstrap.dcs du fichier de configuration YAML
    • modifiable ensuite par patronictl edit-config
    • prise en compte immédiatement si possible
    • copiée dans $PGDATA/patroni.dynamic.json à intervalle régulier
  • Variables d’environnement

Paramètres globaux du cluster

Patroni définit trois paramètres globaux au cluster :

  • name
  • namespace
  • scope

Configuration du DCS

  • DCS supportés : etcd, Consul, ZooKeeper, Exhibitor, Kubernetes
  • avec etcd ou etcd3 :
    • host
    • protocol
    • username, password
    • cacert, cert, key

Configuration de Patroni

Le paramétrage de Patroni est :

  • initialisé depuis la section bootstrap.dcs
  • conservé à la racine du YAML dynamique

Création des instances

Il est possible d’indiquer à Patroni comment construire les instances primaires et secondaires d’un agrégat. Les sections concernées sont :

  • bootstrap.method et bootstrap.initdb
  • postgresql.create_replica_methods
  • scripts bootstrap.post_bootstrap et bootstrap.post_init

Configuration de PostgreSQL

  • Configuration de l’agrégat et des instances
  • Configuration dynamique initialisée depuis la section bootstrap.dcs.postgresql (parameters, pg_hba, pg_ident)
  • Configuration statique conservée dans la section postgresql du YAML dynamique
    • postgresql.authentication
    • postgresql.parameters
    • postgresql.pg_hba
    • postgresql.pg_ident
    • postgresql.callbacks

Agrégat de secours

  • Initialisé depuis la section bootstrap.dcs.standby_cluster
  • Lie deux agrégats Patroni distincts :
    • un agrégat contenant une instance primaire
    • un agrégat ne contenant que des instances secondaires
  • Réplication en cascade vers l’agrégat standby

Slots de réplication

Initialisés depuis les sections suivantes :

  • bootstrap.dcs.slots
  • bootstrap.dcs.ignore_slots

Journaux applicatifs

  • Par défaut dans le fichier de trace système
  • Quelques paramètres de la section log :
    • type : format des traces (pain ou json)
    • level : niveau de log
    • format : format des lignes
    • dir : répertoire où placer les journaux
    • file_num : nombre de journaux à conserver
    • file_size : taille maximale d’un journal avant rotation

API REST de Patroni

Patroni expose une API REST :

  • utile à son administration, par ex. via le CLI patronictl
  • utilisée pour la communication inter-démons
  • section restapi :
    • connect_address
    • listen
    • authentication (username, password)
    • SSL (certfile, keyfile, keyfile_password, cafile, verify_client)

API REST pour le CLI patronictl

Une configuration spécifique est réservée au CLI patronictl :

  • section : ctl
    • insecure
    • certfile
    • keyfile
    • keyfile_password
    • cacert

Configuration du watchdog

  • méthode de protection anti split-brain
  • section watchdog :
    • mode : off, automatic ou required
    • device
    • safety_margin

Marqueurs d’instance

Patroni supporte différents marqueurs dans la section tags :

  • nofailover
  • clonefrom
  • noloadbalance
  • replicatefrom
  • nosync
  • failover_priority

Agrégat multinœud Citus

  • Permet de simplifier le déploiement d’un cluster Citus
  • Paramètres :
    • group
    • database

Variables d’environnement

  • Tous les paramètres existent en tant que variables d’environnement
  • Deux sont utiles au quotidien :
    • PATRONICTL_CONFIG_FILE
    • PATRONI_SCOPE

CLI patronictl

  • Interagit avec l’agrégat
  • Depuis n’importe quelle machine

Consultation d’état

  • list : liste les membres d’un agrégat par ordre alphabétique
  • topology : affiche la topologie d’un agrégat

Consulter la configuration du cluster

  • show-config : affiche la configuration dynamique

Modifier la configuration du cluster

  • edit-config : édite la configuration dynamique de l’agrégat
    • ni fichier de conf, ni ALTER SYSTEM !
  • Si redémarrage : à demander explicitement

Commandes de bascule

  • switchover : promotion d’un secondaire
  • failover : bascule par défaillance du primaire

Contrôle de la bascule automatique

  • pause, resume
  • history
  • reinit

Changements de configuration

  • reload
    • attention aux pending restart
  • restart

endpoints de l’API REST

L’API REST permet de :

  • Contrôler du rôle du serveur
  • S’informer sur l’état d’un nœud ou du cluster
  • Manipuler le cluster

Proxy, VIP et Poolers de connexions

  • Connexions aux réplicas
  • Chaîne de connexion
  • HAProxy
  • Keepalived

Connexions aux réplicas

  • Réplication asynchrone, synchrone et remote apply.
  • API REST de Patroni

Chaîne de connexion

  • Chaînes de connexion multihôtes
  • 2 formats différents : URL ou clé=valeur
  • Sélection du type de nœuds grâce à target_session_attrs

HAProxy

  • Répartiteur de charge (round-robin)
  • Check HTTP sur l’API REST de Patroni
    • \primary
    • \replica
  • Page Web pour les statistiques
  • Attention à sa disponibilité !

Keepalived

  • Monte la VIP sur le serveur de l’instance primaire
  • Utilisation de l’API REST de Patroni

Questions

  • C’est le moment !

Quiz

Travaux pratiques

Patroni : installation

Sur les machines créées précédemment :

  • installer PostgreSQL sur les 2 nœuds depuis les dépôts PGDG
  • installer Patroni sur les 2 nœuds depuis les dépôts PGDG

Avec Debian, ne pas utiliser l’intégration de Patroni dans la structure de gestion multiinstance proposée par postgresql-common afin de se concentrer sur l’apprentissage.

  • configurer et démarrer le cluster Patroni acme sur les 2 nœuds
  • observer les traces de chaque nœud Patroni
  • observer la topologie de l’agrégat avec patronictl.
  • Ajouter le 3ème nœud à l’agrégat.
  • Déterminer le primaire via l’API Patroni en direct avec curl.
  • Quels sont les slots de réplication sur le leader ?
  • Forcer l’utilisation du watchdog dans la configuration de Patroni. Que se passe-t-il ?
  • Donner les droits à l’utilisateur postgres sur le fichier /dev/watchdog. Après quelques secondes, que se passe-t-il ?

Patroni : utilisation

  • Se connecter depuis l’extérieur à la base postgres, d’un des nœuds.
  • Comment obtenir une connexion en lecture et écrire ?
  • Créer une table :

     CREATE TABLE insertions (
       id     int         GENERATED ALWAYS AS IDENTITY,
       d      timestamptz DEFAULT now(),
       source text        DEFAULT inet_server_addr()
     );
  • Insérer une ligne toutes les secondes, à chaque fois dans une nouvelle connexion au primaire.

  • Dans une autre fenêtre, afficher les 20 dernières lignes de cette table.

  • Stopper le nœud leader Patroni.
  • Que se passe-t-il dans la topologie, et dans les requêtes ci-dessus ?
  • Arrêter les processus du nouveau primaire. Il ne reste qu’un nœud actif. Que se passe-t-il ?
  • Arrêter deux nœuds du cluster etcd. Que se passe-t-il ?
  • Redémarrer les nœuds etcd.
  • Relancer un des nœuds Patroni.
  • Sur le troisième nœud (arrêté), détruire le PGDATA. Relancer Patroni.
  • Forcer un failover vers le nœud p1.
  • Modifier les paramètres shared_buffers et work_mem. Si besoin, redémarrer les nœuds.

Travaux pratiques Debian 12 (solutions)

Patroni : installation

Sur les machines créées précédemment :

  • installer PostgreSQL sur les 2 nœuds depuis les dépôts PGDG

Si cette étape a déjà été réalisée, arrêter le service PostgreSQL et supprimer le cluster.

# systemctl stop postgresql@16-main
$ pg_dropcluster 16 main

Activation du dépôt PGDG, à exécuter sur nœuds p1 et p2 :

# apt update
[…]

# apt install postgresql-common
[…]

# /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
This script will enable the PostgreSQL APT repository on apt.postgresql.org on
your system. The distribution codename used will be bookworm-pgdg.

Using keyring /usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg
Writing /etc/apt/sources.list.d/pgdg.sources ...

Running apt-get update […]
Reading package lists... Done

You can now start installing packages from apt.postgresql.org.

Have a look at https://wiki.postgresql.org/wiki/Apt for more information;
most notably the FAQ at https://wiki.postgresql.org/wiki/Apt/FAQ

Désactiver la création automatique d’une instance durant l’installation :

mkdir -p /etc/postgresql-common/createcluster.d
echo create_main_cluster = false > /etc/postgresql-common/createcluster.d/custom.conf

Installation de PostgreSQL 16 :

# apt install postgresql-16
[…]
Setting up postgresql-16 (16.2-1.pgdg120+2) ...
  • installer Patroni sur les 2 nœuds depuis les dépôts PGDG

Les dépôts PGDG fournissent le paquet patroni :

# apt install patroni
  • configurer et démarrer le cluster Patroni acme sur les 2 nœuds

Avec Debian, ne pas utiliser l’intégration de Patroni dans la structure de gestion multiinstance proposée par postgresql-common afin de se concentrer sur l’apprentissage.

La configuration de Patroni se déroule dans le fichier /etc/patroni/config.yml. Dans le doute, ce fichier est indiqué par la commande systemctl status patroni.

Sur les nœuds p1 et p2, commencer par générer un fichier de configuration comme base de travail :

PATH="$PATH:/usr/lib/postgresql/16/bin" patroni \
    --generate-sample-config /etc/patroni/config.yml

Dans ce fichier, éditer les variables suivantes :

  • scope: "acme" : le scope, ou nom, du cluster. Ce nom est utilisé au sein du DCS comme préfixe de toutes les clés ;
  • name: "<hostname>" : nom de la machine. Cette valeur doit être différente pour chaque nœud ;
  • log.level: INFO : dans le cadre de ce TP, il est aussi possible de positionner le niveau de log à DEBUG ;
  • log.dir: '/var/log/patroni/acme' : pour les besoins du TP, afin de bien séparer les journaux de Patroni et PostgeSQL, nous demandons à Patroni d’écrire lui-même ses journaux dans ce répertoire ;
  • restapi : vérifier que l’adresse IP d’écoute est correcte pour chaque serveur ;
  • bootstrap.dcs.postgresql.use_pg_rewind: false : il est préférable de désactiver par défaut l’utilisation de pg_rewind. Cette fonctionnalité ne doit être activée que sur certains environnements.
  • postgresql.datadir: "/var/lib/postgresql/16/main" : emplacement du PGDATA ;
  • postgresql.pg_hba: : vérifier la cohérence des règles pour les clients et la réplication. Éventuellement, facilitez-vous la vie pour le reste du TP ;
  • postgresql.bindir: "/usr/lib/postgresql/16/bin" : chemin vers les binaires de PostgreSQL ;
  • postgresql.authentication.replication.password : positionner le mot de passe à attribuer au rôle replicator ;
  • postgresql.authentication.superuser.password : positionner le mot de passe à attribuer au rôle postgres ;
  • postgresql.listen et postgresql.connect_address : vérifier que les adresses IP sont correctes ;

Une fois ce modèle complété, la section dédiée au DCS doit encore être ajoutée. Collecter les adresses IP des nœuds etcd et ajouter à la configuration la section etcd3 sur le modèle suivant :

etcd3:
  hosts:
  - 10.0.0.11:2379
  - 10.0.0.12:2379
  - 10.0.0.13:2379

Cette configuration est minimale. Libre à vous de modifier la façon dont les instances sont créées (activation des checksums, collation par défaut, etc), ajouter des règles, activer l’authentification etcd, …

Pour ce TP, comme nous plaçons les journaux de Patroni dans des fichiers, nous recommandons de faire de même pour PostgreSQL en ajoutant ces paramètres :

postgresql:
  parameters:
    logging_collector: on
    log_destination: stderr

Ce paramétrage a pour seul but de faciliter ce TP. Ce n’est pas une recommandation pour un serveur en production. Aucune gestion de rotation, rétention ou externalisation n’est ici en place. Il est tout aussi possible de se reposer sur journald.

Voici un exemple de configuration obtenue sur le nœud p1 :

scope: 'acme'
name: p1

etcd3:
  hosts:
  - 10.0.0.11:2379
  - 10.0.0.12:2379
  - 10.0.0.13:2379

log:
  format: '%(asctime)s %(levelname)s: %(message)s'
  level: INFO
  max_queue_size: 1000
  traceback_level: ERROR
  dir: '/var/log/patroni/acme'

restapi:
  connect_address: 10.0.0.21:8008
  listen: 10.0.0.21:8008

# The bootstrap configuration. Works only when the cluster is not yet initialized.
# If the cluster is already initialized, all changes in the `bootstrap` section are ignored!
bootstrap:
  # This section will be written into <dcs>:/<namespace>/<scope>/config after initializing
  # new cluster and all other cluster members will use it as a `global configuration`.
  # WARNING! If you want to change any of the parameters that were set up
  # via `bootstrap.dcs` section, please use `patronictl edit-config`!
  dcs:
    loop_wait: 10
    retry_timeout: 10
    ttl: 30
    postgresql:
      parameters:
        hot_standby: 'on'
        max_connections: 100
        max_locks_per_transaction: 64
        max_prepared_transactions: 0
        max_replication_slots: 10
        max_wal_senders: 10
        max_worker_processes: 8
        track_commit_timestamp: 'off'
        wal_keep_size: 128MB
        wal_level: replica
        wal_log_hints: 'on'
      use_pg_rewind: false
      use_slots: true

postgresql:
  authentication:
    replication:
      password: 'pass'
      username: replicator
    superuser:
      password: 'pass'
      username: postgres
  bin_dir: '/usr/lib/postgresql/16/bin'
  connect_address: 10.0.0.21:5432
  data_dir: '/var/lib/postgresql/16/main'
  listen: 10.0.0.21:5432
  parameters:
    password_encryption: scram-sha-256
    logging_collector: on
    log_destination: stderr
  pg_hba:
  - local all all trust
  - host all all all trust
  - host replication replicator all scram-sha-256

tags:
  clonefrom: true
  failover_priority: 1
  noloadbalance: false
  nosync: false

Assurez-vous que ce fichier de configuration est bien accessible à l’utilisateur postgres et créer le répertoire nécessaire aux journaux applicatifs :

chmod 0644 /etc/patroni/config.yml
install -o postgres -g postgres -d /var/log/patroni/acme

Nous pouvons désormais valider la configuration :

patroni --validate /etc/patroni/config.yml

Le code retour de la commande est 0 si tout est valide. Sinon, la commande affiche les avertissements appropriés.

Il est maintenant possible de démarrer le service Patroni. Nous commençons d’abord sur p1 pour créer l’instance :

# systemctl start patroni

Puis nous démarrons le service sur p2, qui va donc créer le secondaire :

# systemctl start patroni
  • observer les traces de chaque nœud Patroni

Sur p1, nous trouvons les messages suivants dans le journal /var/log/patroni/acme/patroni.log :

INFO: Selected new etcd server http://10.20.0.11:2379
INFO: No PostgreSQL configuration items changed, nothing to reload.
INFO: Lock owner: None; I am p1
INFO: trying to bootstrap a new cluster
INFO: postmaster pid=5541
INFO: establishing a new patroni heartbeat connection to postgres
INFO: running post_bootstrap
WARNING: Could not activate Linux watchdog device: Can't open watchdog device: [Errno 2] No such file or directory: '/dev/watchdog'
INFO: initialized a new cluster
INFO: no action. I am (p1), the leader with the lock

Le démon Patroni démarre, choisi un serveur etcd, se saisit du leader lock et initialise l’instance PostgreSQL locale. Les sorties standard et d’erreur des commandes exécutées par Patroni n’est pas capturée vers le journal de ce dernier. Ces commandes sont donc capturées par journald et associées au service patroni. Nous y retrouvons par exemple la sortie de initdb :

# journalctl -u patroni
[…]
patroni: The files belonging to this database system will be owned by user "postgres".
patroni: This user must also own the server process.
patroni: The database cluster will be initialized with locale "C.UTF-8".
[…]
patroni: Success. You can now start the database server using:
patroni:     /usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/16/main -l logfile start

Sur p2, nous trouvons dans le journal correspondant :

INFO: Selected new etcd server http://10.0.0.13:2379
INFO: No PostgreSQL configuration items changed, nothing to reload.
INFO: Lock owner: p1; I am p2
INFO: trying to bootstrap from leader 'p1'
INFO: replica has been created using basebackup
INFO: bootstrapped from leader 'p1'
INFO: postmaster pid=4551
INFO: Lock owner: p1; I am p2
INFO: establishing a new patroni heartbeat connection to postgres
INFO: no action. I am (p2), a secondary, and following a leader (p1)

Comme sur p1, le démon Patroni démarre et choisi un serveur etcd, mais il découvre le leader lock appartient déjà à p1. L’instance PostgreSQL locale n’existant pas, Patroni décide de la créer depuis celle de p1.

Dans les deux cas, la configuration par défaut des journaux applicatifs de PostgreSQL les places dans le répertoire PGDATA/log, donc ici /var/lib/postgresql/16/main/log, équivalent à ~postgres/16/main/log.

  • observer la topologie de l’agrégat avec patronictl.

L’utilisation de patronictl nécessite un fichier de configuration permettant de déterminer le nom du cluster et idéalement les nœuds du DCS. Sur les machines p1 et p2, nous pouvons utiliser directement le fichier de configuration de Patroni /etc/patroni/config.yml. La commande devient :

$ patronictl -c /etc/patroni/config.yml topology
+ Cluster: acme (7349612307776631369) -----+----+-----------+-------+
| Member | Host      | Role    | State     | TL | Lag in MB | Tags  |
+--------+-----------+---------+-----------+----+-----------+-------+
| p1     | 10.0.0.21 | Leader  | running   |  1 |           | […]   |
| + p2   | 10.0.0.22 | Replica | streaming |  1 |         0 | […]   |
+--------+-----------+---------+-----------+----+-----------+-------+

Nous constatons que p1 héberge bien l’instance primaire et p2 la secondaire.

Pour simplifier cette commande, il est possible de positionner dans votre environnement le variable PATRONICTL_CONFIG_FILE. Par exemple :

$ export PATRONICTL_CONFIG_FILE=/etc/patroni/config.yml
$ patronictl topology
[…]

Pour la positionner automatiquement, vous pouvez par exemple créer le fichier /etc/profile.d/99-patroni.sh avec le contenu suivant :

cat <<EOF > /etc/profile.d/99-patroni.sh
PATRONICTL_CONFIG_FILE=/etc/patroni/config.yml
export PATRONICTL_CONFIG_FILE
EOF
chmod +x /etc/profile.d/99-patroni.sh
  • Ajouter le 3ème nœud à l’agrégat.

Répéter les étapes précédentes sur le serveur p3 :

  • installation des dépôts PGDG ;
  • installation de PostgreSQL et Patroni ;
  • création du fichier de configuration de Patroni ;
  • démarrage de l’instance sur p3.

Après l’ajout du troisième nœud, la topologie est la suivante :

$ patronictl topology
+ Cluster: acme (7349612307776631369) -----+----+-----------+------+
| Member | Host      | Role    | State     | TL | Lag in MB | Tags |
+--------+-----------+---------+-----------+----+-----------+------+
| p1     | 10.0.0.21 | Leader  | running   |  1 |           | […]  |
| + p2   | 10.0.0.22 | Replica | streaming |  1 |         0 | […]  |
| + p3   | 10.0.0.23 | Replica | streaming |  1 |         0 | […]  |
+--------+-----------+---------+-----------+----+-----------+------+

Il est conseillé lors des tests de garder une fenêtre répétant l’ordre régulièrement :

$ watch -n1 patronictl topology
  • Déterminer le primaire via l’API Patroni en direct avec curl.

La configuration est visible de l’extérieur :

$ curl -s http://p1:8008/patroni | jq
{
  "state": "running",
  "postmaster_start_time": "[…]",
  "role": "master",
  "server_version": 160002,
  "xlog": {
    "location": 50534136
  },
  "timeline": 1,
  "replication": [
    {
      "usename": "replicator",
      "application_name": "p2",
      "client_addr": "10.0.0.22",
      "state": "streaming",
      "sync_state": "async",
      "sync_priority": 0
    },
    {
      "usename": "replicator",
      "application_name": "p3",
      "client_addr": "10.0.0.23",
      "state": "streaming",
      "sync_state": "async",
      "sync_priority": 0
    }
  ],
  "dcs_last_seen": 1711217105,
  "tags": {
    "clonefrom": true,
    "failover_priority": 1
  },
  "database_system_identifier": "7349612307776631369",
  "patroni": {
    "version": "3.2.2",
    "scope": "acme",
    "name": "p1"
  }
}

De manière plus précise :

$ curl -s http://p1:8008/cluster |\
jq '.members[] | select ((.role == "leader" ) and ( .state == "running")) | { name }'
{
  "name": "p1"
}
  • Quels sont les slots de réplication sur le leader ?

Par défaut, les slots de réplications portent le nom des nœuds réplicas.

$ curl -s http://p1:8008/cluster | jq '.members[] | select (.role == "replica") | { name }'
{
  "name": "p2"
}
{
  "name": "p3"
}

On peut le vérifier en se connectant à PostgreSQL sur le serveur primaire (ici p1) :

$ sudo -iu postgres
$ psql -c "SELECT slot_name FROM pg_replication_slots"
 slot_name
-----------
 p2
 p3
  • Forcer l’utilisation du watchdog dans la configuration de Patroni. Que se passe-t-il ?

Ajoutez la section watchdog dans le fichier de configuration de Patroni /etc/patroni/config.yml en positionnant mode: required :

watchdog:
  mode: required
  # device: /dev/watchdog
  safety_margin: 5

Il nous faut recharger la configuration de Patroni :

$ patronictl reload acme
+ Cluster: acme (7349612307776631369) --------+----+-----------+
| Member | Host      | Role    | State        | TL | Lag in MB |
+--------+-----------+---------+--------------+----+-----------+
| p1     | 10.0.0.21 | Replica | start failed |    |   unknown |
| p2     | 10.0.0.22 | Replica | start failed |    |   unknown |
| p3     | 10.0.0.23 | Leader  | running      |  6 |           |
+--------+-----------+---------+--------------+----+-----------+
Are you sure you want to reload members p1, p2, p3? [y/N]: y
Reload request received for member p1 and will be processed within 10 seconds
Reload request received for member p2 and will be processed within 10 seconds
Reload request received for member p3 and will be processed within 10 seconds

Suite à cette modification, le cluster Patroni se retrouve sans primaire. Dans les journaux applicatif de p3, ici précédemment marqué Leader, nous trouvons :

INFO: Reloading PostgreSQL configuration.
INFO: Lock owner: p3; I am p3
ERROR: Configuration requires watchdog, but watchdog could not be configured.
INFO: Demoting self (immediate)
INFO: Leader key released
INFO: Demoting self because watchdog could not be activated
INFO: Lock owner: None; I am p3
INFO: not healthy enough for leader race
INFO: starting after demotion in progress
INFO: closed patroni connections to postgres
INFO: postmaster pid=561
INFO: establishing a new patroni heartbeat connection to postgres
WARNING: Watchdog device is not usable
INFO: Dropped unknown replication slot 'p1'
INFO: Dropped unknown replication slot 'p2'
INFO: following a different leader because i am not the healthiest node
  • Donner les droits à l’utilisateur postgres sur le fichier /dev/watchdog. Après quelques secondes, que se passe-t-il ?

Un simple chown sur le device /dev/watchdog de chaque VM peut suffire, au moins le temps de ce TP. Néanmoins, il ne survivrait pas au e de la machine.

Le fichier de service de Patroni propose de systématiquement modifier les droits sur ce device décommentant la ligne suivante :

#ExecStartPre=-/usr/bin/sudo /bin/chown postgres /dev/watchdog

Une autre solution est de configurer udev afin qu’il modifie les droits sur ce fichier automatiquement à chaque démarrage :

cat <<'EOF' > /etc/udev/rules.d/99-watchdog.rules
# give writes on watchdog device to postgres
SUBSYSTEM=="misc", KERNEL=="watchdog", ACTION=="add", RUN+="/bin/chown postgres /dev/watchdog"

# Or a better solution using ACL:
#SUBSYSTEM=="misc", KERNEL=="watchdog", ACTION=="add", RUN+="/bin/setfacl -m u:postgres:rw- /dev/watchdog"
EOF

Une fois les droits positionnés, l’un des nœuds Patroni devrait finalement réussir à activer le watchdog et ainsi devenir leader et promouvoir son instance PostgreSQL :

INFO: i6300ESB timer activated with 25 second timeout, timing slack 15 seconds
INFO: promoted self to leader by acquiring session lock
INFO: Lock owner: p2; I am p2
INFO: updated leader lock during promote
INFO: Lock owner: p2; I am p2
INFO: no action. I am (p2), the leader with the lock

Patroni : utilisation

  • Se connecter depuis l’extérieur à la base postgres, d’un des nœuds.

La connexion extérieure peut se faire depuis la machine hôte des VM. Il est nécessaire d’y installer un client PostgreSQL, par exemple postgresql-client. Pour se connecter à l’instance p1, utiliser l’une des commandes suivantes :

$ psql -h p1 -p 5432 -U postgres -d postgres 
$ psql -h 10.0.0.21 postgres postgres

En cas de problème, il faut regarder :

  • les règles de firewall ;
  • les traces du nœud PostgreSQL concerné, plus instructives que le simple message d’erreur du client ;
  • le fichier de configuration pg_hba.conf.
  • Comment obtenir une connexion en lecture et écrire ?

Il est nécessaire de se connecter à l’instance primaire pour réaliser des écritures en base. La chaîne de connexion permettant d’indiquer plusieurs nœuds, nous pouvons y préciser tous les nœuds du cluster. Afin de sélectionner le nœud primaire, il suffit d’ajouter le paramètre target_session_attrs=primary ou target_session_attrs=read-write.

Par exemple :

psql "host=p1,p2,p3 user=postgres target_session_attrs=primary"
psql "postgresql://postgres@p1,p2,p3/postgres?target_session_attrs=read-write"

Pour une connexion en lecture seule, nous utilisons par exemple :

psql "postgresql://postgres@p1,p2,p3:5432/postgres?target_session_attrs=prefer-standby"
  • Créer une table :

     CREATE TABLE insertions (
       id     int         GENERATED ALWAYS AS IDENTITY,
       d      timestamptz DEFAULT now(),
       source text        DEFAULT inet_server_addr()
     );
  • Insérer une ligne toutes les secondes, à chaque fois dans une nouvelle connexion au primaire.

  • Dans une autre fenêtre, afficher les 20 dernières lignes de cette table.

Dans la colonne source de la table créée, la valeur par défaut fait appel à la fonction inet_server_addr() qui retourne l’adresse IP du serveur PostgreSQL sur lequel nous sommes connectés.

Lancer une insertion toutes les secondes :

watch -n1 'psql -X -d "host=p1,p2,p3 user=postgres target_session_attrs=primary" \
                -c "INSERT INTO insertions SELECT;"'

Lire les vingt dernières lignes de la table :

watch -n1 'psql -X -d "host=p1,p2,p3 port=5432 user=postgres" \
                -c "SELECT * FROM insertions ORDER BY d DESC LIMIT 20"'

Bien entendu, nous obtenons toujours le même nœud dans la colonne source, ici p1 :

 id  |               d               |    source
-----+-------------------------------+---------------

 313 | 2024-03-05 15:40:03.118307+00 | 10.0.0.21/32
 312 | 2024-03-05 15:40:02.105116+00 | 10.0.0.21/32
 311 | 2024-03-05 15:40:01.090775+00 | 10.0.0.21/32
 310 | 2024-03-05 15:40:00.075847+00 | 10.0.0.21/32
 309 | 2024-03-05 15:39:59.061759+00 | 10.0.0.21/32
 308 | 2024-03-05 15:39:58.048074+00 | 10.0.0.21/32

(20 lignes)
  • Stopper le nœud leader Patroni.
  • Que se passe-t-il dans la topologie, et dans les requêtes ci-dessus ?

Sur p1 :

# systemctl stop patroni

Après l’arrêt de p1, p3 prend le rôle de leader :

$ patronictl -c /etc/patroni/config.yml topology
+ Cluster: acme (7349612307776631369) ---+----+-----------+------+
| Member | Host      | Role    | State   | TL | Lag in MB | Tags |
+--------+-----------+---------+---------+----+-----------+------+
| p3     | 10.0.0.23 | Leader  | running |  2 |           | […]  |
| + p1   | 10.0.0.21 | Replica | stopped |    |   unknown | […]  |
| + p2   | 10.0.0.22 | Replica | running |  1 |         0 | […]  |
+--------+-----------+---------+---------+----+-----------+------+

Les insertions échouent le temps de la bascule, ici pendant environ 2 secondes, puis continuent depuis l’autre nœud :

 id  |               d               |    source
-----+-------------------------------+---------------

 445 | 2024-03-24 15:50:17.840394+00 | 10.0.0.23/32
 444 | 2024-03-24 15:50:16.823004+00 | 10.0.0.23/32
 431 | 2024-03-24 15:50:14.755045+00 | 10.0.0.21/32
 430 | 2024-03-24 15:50:13.740541+00 | 10.0.0.21/32
  • Arrêter les processus du nouveau primaire. Il ne reste qu’un nœud actif. Que se passe-t-il ?

Le leader Patroni a changé à nouveau :

$ patronictl -c /etc/patroni/config.yml topology
+ Cluster: acme (7349612307776631369) --+----+-----------+------+
| Member | Host      | Role   | State   | TL | Lag in MB | Tags |
+--------+-----------+--------+---------+----+-----------+------+
| p2     | 10.0.0.22 | Leader | running |  6 |           | […]  |
+--------+-----------+--------+---------+----+-----------+------+
  • Arrêter deux nœuds du cluster etcd. Que se passe-t-il ?

Sur e1 et e2 :

# systemctl stop etcd

Il n’y a plus de quorum etcd garantissant une référence. Le cluster Patroni se met en lecture seule et les insertions tombent en échec puisqu’elles exigent une connexion ouverte en écriture :

psql: error: connection to server at "p1" (10.0.0.21), port 5432 failed: Connection refused
        Is the server running on that host and accepting TCP/IP connections?
connection to server at "p2" (10.0.0.22), port 5432 failed: server is in hot standby mode
connection to server at "p3" (10.0.0.23), port 5432 failed: Connection refused
        Is the server running on that host and accepting TCP/IP connections?
  • Redémarrer les nœuds etcd.

Les écritures reprennent.

  • Relancer un des nœuds Patroni.

Dans le cadre de cette correction p2 est l’actuel leader, nous redémarrons donc Patroni sur p1 :

# systemctl restart patroni

L’instance sur p1 se raccroche en secondaire :

$ patronictl -c /etc/patroni/config.yml topology
+ Cluster: acme (7349612307776631369) -----+----+-----------+------+
| Member | Host      | Role    | State     | TL | Lag in MB | Tags |
+--------+-----------+---------+-----------+----+-----------+------+
| p2     | 10.0.0.22 | Leader  | running   |  7 |           | […]  |
| + p1   | 10.0.0.21 | Replica | streaming |  7 |         0 | […]  |
+--------+-----------+---------+-----------+----+-----------+------+
  • Sur le troisième nœud (arrêté), détruire le PGDATA. Relancer Patroni.

Le PGDATA de nos instances se trouve dans /var/lib/postgresql/16/main. Supprimons ce PGDATA sur p3, le nœud restant :

rm -rf /var/lib/postgresql/16/main

Relançons Patroni sur ce nœud :

# systemctl start patroni

Nous observons dans les journaux de Patroni et PostgreSQL que l’instance est recrée et se raccroche à p2 :

LOG:  starting PostgreSQL 16.2 (Debian 16.2-1.pgdg120+2) […]
LOG:  listening on IPv4 address "10.0.0.23", port 5432
LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
LOG:  database system was interrupted while in recovery at log time […]
HINT:  If this has occurred more than once some data might be corrupted and you might need to choose an earlier recovery target.
LOG:  entering standby mode
LOG:  starting backup recovery with redo LSN 0/45405620, checkpoint LSN 0/4FD4D788, on timeline ID 7
LOG:  redo starts at 0/45405620
LOG:  completed backup recovery with redo LSN 0/45405620 and end LSN 0/509EA568
LOG:  consistent recovery state reached at 0/509EA568
LOG:  database system is ready to accept read-only connections
LOG:  started streaming WAL from primary at 0/50000000 on timeline 7

Le temps de la reconstruction de zéro, il est possible de voir l’évolution de l’état de p3 de creating replica à streaming :

$ export PATRONICTL_CONFIG_FILE=/etc/patroni/config.yml
$ patronictl topology
+ Cluster: acme (7349612307776631369) ------------+----+-----------+------+
| Member | Host      | Role    | State            | TL | Lag in MB | Tags |
+--------+-----------+---------+------------------+----+-----------+------+
| p2     | 10.0.0.22 | Leader  | running          |  7 |           | […]  |
| + p1   | 10.0.0.21 | Replica | streaming        |  7 |         0 | […]  |
| + p3   | 10.0.0.23 | Replica | creating replica |    |   unknown | […]  |
+--------+-----------+---------+------------------+----+-----------+------+

/* Après un certain temps */

$ patronictl topology
+ Cluster: acme (7349612307776631369) -----+----+-----------+------+
| Member | Host      | Role    | State     | TL | Lag in MB | Tags |
+--------+-----------+---------+-----------+----+-----------+------+
| p2     | 10.0.0.22 | Leader  | running   |  7 |           | […]  |
| + p1   | 10.0.0.21 | Replica | streaming |  7 |         0 | […]  |
| + p3   | 10.0.0.23 | Replica | streaming |  7 |         0 | […]  |
+--------+-----------+---------+-----------+----+-----------+------+
  • Forcer un failover vers le nœud p1.
$ patronictl failover
Current cluster topology
+ Cluster: acme (7349612307776631369) -----+----+-----------+------+
| Member | Host      | Role    | State     | TL | Lag in MB | Tags |
+--------+-----------+---------+-----------+----+-----------+------+
| p1     | 10.0.0.21 | Replica | streaming |  7 |         0 | […]  |
+--------+-----------+---------+-----------+----+-----------+------+
| p2     | 10.0.0.22 | Leader  | running   |  7 |           | […]  |
+--------+-----------+---------+-----------+----+-----------+------+
| p3     | 10.0.0.23 | Replica | streaming |  7 |         0 | […]  |
+--------+-----------+---------+-----------+----+-----------+------+
Candidate ['p1', 'p3'] []: p1
Are you sure you want to failover cluster acme, demoting current leader p2? [y/N]: y
[…] Successfully failed over to "p1"
+ Cluster: acme (7349612307776631369) ---+----+-----------+------+
| Member | Host      | Role    | State   | TL | Lag in MB | Tags |
+--------+-----------+---------+---------+----+-----------+------+
| p1     | 10.0.0.21 | Leader  | running |  7 |           | […]  |
+--------+-----------+---------+---------+----+-----------+------+
| p2     | 10.0.0.22 | Replica | stopped |    |   unknown | […]  |
+--------+-----------+---------+---------+----+-----------+------+
| p3     | 10.0.0.23 | Replica | running |  7 |         0 | […]  |
+--------+-----------+---------+---------+----+-----------+------+

$ patronictl topology
+ Cluster: acme (7349612307776631369) -----+----+-----------+------+
| Member | Host      | Role    | State     | TL | Lag in MB | Tags |
+--------+-----------+---------+-----------+----+-----------+------+
| p1     | 10.0.0.21 | Leader  | running   |  8 |           | […]  |
| + p2   | 10.0.0.22 | Replica | streaming |  8 |         0 | […]  |
| + p3   | 10.0.0.23 | Replica | streaming |  8 |         0 | […]  |
+--------+-----------+---------+-----------+----+-----------+------+
  • Modifier les paramètres shared_buffers et work_mem. Si besoin, redémarrer les nœuds.

Pour modifier la configuration, nous devons utiliser patronictl, plutôt qu’éditer directement les fichiers de configuration. Une alternative est de modifier la configuration statique, dans le fichier YAML “/etc/patroni/config.yml”. Cette méthode facilite leur maintenance, mais impose que le contenu soit identique sur tous les nœuds, ce qui est généralement le cas dans un déploiement industrialisé.

La commande patronictl edit-config appelle l’éditeur par défaut, souvent vi, vim ou nano. Vous pouvez modifier la variable d’environnement EDITOR pour pointer sur votre éditeur favori.

Éditons la configuration dynamique et ajoutons les deux paramètres :

$ export PATRONICTL_CONFIG_FILE=/etc/patroni/config.yml
$ patronictl edit-config
[…]
--- 
+++ 
@@ -12,6 +12,8 @@
     wal_keep_size: 128MB
     wal_level: replica
     wal_log_hints: 'on'
+    shared_buffers: 300MB
+    work_mem: 50MB
   use_pg_rewind: false
   use_slots: true
 retry_timeout: 10


Apply these changes? [y/N]: y
Configuration changed

Les nœuds sont à redémarrer à cause de la modification de shared_buffers :

$ patronictl topology
+ Cluster: acme (7349612307776631369) -----+----+------+-----------------+------+
| Member | Host      | Role    | State     | TL | Lag… | Pending restart | Tags |
+--------+-----------+---------+-----------+----+------+-----------------+------+
| p1     | 10.0.0.21 | Leader  | running   |  8 |      | *               | […]  |
| + p2   | 10.0.0.22 | Replica | streaming |  8 |    0 | *               | […]  |
| + p3   | 10.0.0.23 | Replica | streaming |  8 |    0 | *               | […]  |
+--------+-----------+---------+-----------+----+------+-----------------+------+

Commandons le redémarrage de PostgreSQL sur les trois nœuds :

$ patronictl restart acme
+ Cluster: acme (7349612307776631369) -----+----+------+-----------------+------+
| Member | Host      | Role    | State     | TL | Lag… | Pending restart | Tags |
+--------+-----------+---------+-----------+----+------+-----------------+------+
| p1     | 10.0.0.21 | Leader  | running   |  8 |      | *               | […]  |
+--------+-----------+---------+-----------+----+------+-----------------+------+
| p2     | 10.0.0.22 | Replica | streaming |  8 |    0 | *               | […]  |
+--------+-----------+---------+-----------+----+------+-----------------+------+
| p3     | 10.0.0.23 | Replica | streaming |  8 |    0 | *               | […]  |
+--------+-----------+---------+-----------+----+------+-----------------+------+
When should the restart take place (e.g. […])  [now]: 
Are you sure you want to restart members p1, p2, p3? [y/N]: y
Restart if the PostgreSQL version is less than provided (e.g. 9.5.2)  []: 
Success: restart on member p1
Success: restart on member p2
Success: restart on member p3

Vérifions que le paramétrage a bien été modifié :

$ for h in p1 p2 p3; do echo -ne $h:; psql -Xtd "host=$h user=postgres" -c "show shared_buffers"; done
p1: 300MB

p2: 300MB

p3: 300MB

Noter que le contenu des modifications est tracé dans un fichier patroni.dynamic.json dans le PGDATA :

$ jq . patroni.dynamic.json
{
  "loop_wait": 10,
  "postgresql": {
    "parameters": {
      "hot_standby": "on",
      "max_connections": 100,
      "max_locks_per_transaction": 64,
      "max_prepared_transactions": 0,
      "max_replication_slots": 10,
      "max_wal_senders": 10,
      "max_worker_processes": 8,
      "track_commit_timestamp": "off",
      "wal_keep_size": "128MB",
      "wal_level": "replica",
      "wal_log_hints": "on",
      "shared_buffers": "300MB",
      "work_mem": "50MB"
    },
    "use_pg_rewind": false,
    "use_slots": true
  },
  "retry_timeout": 10,
  "ttl": 30
}

Travaux pratiques Rocky 9 (solutions)

Patroni : installation

Sur les machines créées précédemment :

  • installer PostgreSQL sur les 2 nœuds depuis les dépôts PGDG

Si cette opération a déjà été réalisée précédemment, stopper le service PostgreSQL, le désactiver et supprimer le répertoire de données de l’instance.

# systemctl stop postgresql-16
# systemctl disable postgresql-16
# rm -Rf /var/lib/pgsql/16/data

Activation du dépôt PGDG, à exécuter sur nœuds p1 et p2 :

# dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/\
pgdg-redhat-repo-latest.noarch.rpm

# dnf install -y postgresql16-server
[…]
Installed:
    postgresql16-16.2-1PGDG.rhel9.x86_64
    postgresql16-libs-16.2-1PGDG.rhel9.x86_64
    postgresql16-server-16.2-1PGDG.rhel9.x86_64

Complete!
  • installer Patroni sur les 2 nœuds depuis les dépôts PGDG

Les dépôts PGDG fournissent le paquet patroni :

dnf install -y epel-release
dnf install -y patroni-etcd
  • configurer et démarrer le cluster Patroni acme sur les 2 nœuds

La configuration de Patroni se déroule dans le fichier /etc/patroni/patroni.yml.

Sur les nœuds p1 et p2, commencer par générer un fichier de configuration comme base de travail :

PATH="$PATH:/usr/pgsql-16/bin" patroni \
    --generate-sample-config /etc/patroni/patroni.yml

Dans ce fichier, éditer les variables suivantes :

  • scope: "acme" : le scope, ou nom, du cluster. Ce nom est utilisé au sein du DCS comme préfixe de toutes les clés ;
  • name: "<hostname>" : nom de la machine. Cette valeur doit être différente pour chaque nœud ;
  • log.level: INFO : dans le cadre de ce TP, il est aussi possible de positionner le niveau de log à DEBUG ;
  • log.dir: '/var/log/patroni/acme' : pour les besoins du TP, afin de bien séparer les journaux de Patroni et PostgeSQL, nous demandons à Patroni d’écrire lui-même ses journaux dans ce répertoire ;
  • restapi : vérifier que l’adresse IP d’écoute est correcte pour chaque serveur ;
  • bootstrap.dcs.postgresql.use_pg_rewind: false : il est préférable de désactiver par défaut l’utilisation de pg_rewind. Cette fonctionnalité ne doit être activée que sur certains environnements.
  • postgresql.datadir: "/var/lib/pgsql/16/data" : emplacement du PGDATA ;
  • postgresql.pg_hba: : vérifier la cohérence des règles pour les clients et la réplication. Éventuellement, facilitez-vous la vie pour le reste du TP ;
  • postgresql.bindir: "/usr/pgsql-16/bin" : chemin vers les binaires de PostgreSQL ;
  • postgresql.authentication.replication.password : positionner le mot de passe à attribuer au rôle replicator ;
  • postgresql.authentication.superuser.password : positionner le mot de passe à attribuer au rôle postgres ;
  • postgresql.listen et postgresql.connect_address : vérifier que les adresses IP sont correctes ;

Une fois ce modèle complété, la section dédiée au DCS doit encore être ajoutée. Collecter les adresses IP des nœuds etcd et ajouter à la configuration la section etcd3 sur le modèle suivant :

etcd3:
  hosts:
  - 10.0.0.11:2379
  - 10.0.0.12:2379
  - 10.0.0.13:2379

Cette configuration est minimale. Libre à vous de modifier la façon dont les instances sont créées (activation des checksums, collation par défaut, etc), ajouter des règles, activer l’authentification etcd, …

Pour ce TP, comme nous plaçons les journaux de Patroni dans des fichiers, nous recommandons de faire de même pour PostgreSQL en ajoutant ces paramètres :

postgresql:
  parameters:
    logging_collector: on
    log_destination: stderr

Ce paramétrage a pour seul but de faciliter ce TP. Ce n’est pas une recommandation pour un serveur en production. Aucune gestion de rotation, rétention ou externalisation n’est ici en place. Il est tout aussi possible de se reposer sur journald.

Voici un exemple de configuration obtenue sur le nœud p1 :

scope: 'acme'
name: p1.hapat.vm

etcd3:
  hosts:
  - 10.0.0.11:2379
  - 10.0.0.12:2379
  - 10.0.0.13:2379

log:
  format: '%(asctime)s %(levelname)s: %(message)s'
  level: INFO
  max_queue_size: 1000
  traceback_level: ERROR
  dir: '/var/log/patroni/acme'

restapi:
  connect_address: 10.0.0.21:8008
  listen: 10.0.0.21:8008

# The bootstrap configuration. Works only when the cluster is not yet initialized.
# If the cluster is already initialized, all changes in the `bootstrap` section are ignored!
bootstrap:
  # This section will be written into <dcs>:/<namespace>/<scope>/config after initializing
  # new cluster and all other cluster members will use it as a `global configuration`.
  # WARNING! If you want to change any of the parameters that were set up
  # via `bootstrap.dcs` section, please use `patronictl edit-config`!
  dcs:
    loop_wait: 10
    retry_timeout: 10
    ttl: 30
    postgresql:
      parameters:
        hot_standby: 'on'
        max_connections: 100
        max_locks_per_transaction: 64
        max_prepared_transactions: 0
        max_replication_slots: 10
        max_wal_senders: 10
        max_worker_processes: 8
        track_commit_timestamp: 'off'
        wal_keep_size: 128MB
        wal_level: replica
        wal_log_hints: 'on'
      use_pg_rewind: false
      use_slots: true

postgresql:
  authentication:
    replication:
      password: 'pass'
      username: replicator
    superuser:
      password: 'pass'
      username: postgres
  bin_dir: '/usr/pgsql-16/bin'
  connect_address: 10.0.0.21:5432
  data_dir: '/var/lib/pgsql/16/data'
  listen: 10.0.0.21:5432
  parameters:
    password_encryption: scram-sha-256
    logging_collector: on
    log_destination: stderr
  pg_hba:
  - local all all trust
  - host all all all trust
  - host replication replicator all scram-sha-256

tags:
  clonefrom: true
  failover_priority: 1
  noloadbalance: false
  nosync: false

Assurez-vous que ce fichier de configuration est bien accessible à l’utilisateur postgres et créer le répertoire nécessaire aux journaux applicatifs :

chmod 0644 /etc/patroni/patroni.yml
install -o postgres -g postgres -d /var/log/patroni/acme

Nous pouvons désormais valider la configuration :

patroni --validate /etc/patroni/patroni.yml

Le code retour de la commande est 0 si tout est valide. Sinon, la commande affiche les avertissements appropriés.

Il est maintenant possible de démarrer le service Patroni. Nous commençons d’abord sur p1 pour créer l’instance :

# systemctl start patroni

Puis nous démarrons le service sur p2, qui va donc créer le secondaire :

# systemctl start patroni
  • observer les traces de chaque nœud Patroni

Sur p1, nous trouvons les messages suivants dans le journal /var/log/patroni/acme/patroni.log :

INFO: Selected new etcd server http://10.0.0.11:2379
INFO: No PostgreSQL configuration items changed, nothing to reload.
INFO: Lock owner: None; I am p1.hapat.vm
INFO: trying to bootstrap a new cluster
INFO: postmaster pid=18415
INFO: establishing a new patroni heartbeat connection to postgres
INFO: running post_bootstrap
WARNING: Could not activate Linux watchdog device: Can't open watchdog device: [Errno 13] Permission denied: '/dev/watchdog'
INFO: initialized a new cluster
INFO: no action. I am (p1.hapat.vm), the leader with the lock

Le démon Patroni démarre, choisi un serveur etcd, se saisit du leader lock et initialise l’instance PostgreSQL locale. Les sorties standard et d’erreur des commandes exécutées par Patroni n’est pas capturée vers le journal de ce dernier. Ces commandes sont donc capturées par journald et associées au service patroni. Nous y retrouvons par exemple la sortie de initdb :

# journalctl -u patroni
[…]
patroni: The files belonging to this database system will be owned by user "postgres".
patroni: This user must also own the server process.
patroni: The database cluster will be initialized with locale "C.UTF-8".
[…]
patroni: Success. You can now start the database server using:
patroni:     /usr/pgsql-16/bin/pg_ctl -D /var/lib/pgsql/16/data -l logfile start

Sur p2, nous trouvons dans le journal correspondant :

INFO: Selected new etcd server http://10.0.0.13:2379
INFO: No PostgreSQL configuration items changed, nothing to reload.
INFO: Lock owner: p1.hapat.vm; I am p2.hapat.vm
INFO: trying to bootstrap from leader 'p1.hapat.vm'
INFO: replica has been created using basebackup
INFO: bootstrapped from leader 'p1.hapat.vm'
INFO: postmaster pid=18195
INFO: Lock owner: p1.hapat.vm; I am p2.hapat.vm
INFO: establishing a new patroni heartbeat connection to postgres
INFO: no action. I am (p2.hapat.vm), a secondary, and following a leader (p1.hapat.vm)

Comme sur p1, le démon Patroni démarre et choisi un serveur etcd, mais il découvre le leader lock appartient déjà à p1. L’instance PostgreSQL locale n’existant pas, Patroni décide de la créer depuis celle de p1.

Dans les deux cas, la configuration par défaut des journaux applicatifs de PostgreSQL les places dans le répertoire PGDATA/log, donc ici /var/lib/pgsql/16/data/log/, équivalent à ~postgres/16/data/log.

  • observer la topologie de l’agrégat avec patronictl.

L’utilisation de patronictl nécessite un fichier de configuration permettant de déterminer le nom du cluster et idéalement les nœuds du DCS. Sur les machines p1 et p2, nous pouvons utiliser directement le fichier de configuration de Patroni /etc/patroni/patroni.yml. La commande devient :

$ patronictl -c /etc/patroni/patroni.yml topology
+ Cluster: acme (7350009258581743592) +-----------+----+-----------+------+
| Member        | Host      | Role    | State     | TL | Lag in MB | Tags |
+---------------+-----------+---------+-----------+----+-----------+------+
| p1.hapat.vm   | 10.0.0.21 | Leader  | running   |  1 |           | […]  |
| + p2.hapat.vm | 10.0.0.22 | Replica | streaming |  1 |         0 | […]  |
+---------------+-----------+---------+-----------+----+-----------+------+

Nous constatons que p1 héberge bien l’instance primaire et p2 la secondaire.

Pour simplifier cette commande, il est possible de positionner dans votre environnement le variable PATRONICTL_CONFIG_FILE. Par exemple :

$ export PATRONICTL_CONFIG_FILE=/etc/patroni/patroni.yml
$ patronictl topology
[…]

Pour la positionner automatiquement, vous pouvez par exemple créer le fichier /etc/profile.d/99-patroni.sh avec le contenu suivant :

cat <<EOF > /etc/profile.d/99-patroni.sh
PATRONICTL_CONFIG_FILE=/etc/patroni/patroni.yml
export PATRONICTL_CONFIG_FILE
EOF
chmod +x /etc/profile.d/99-patroni.sh
  • Ajouter le 3ème nœud à l’agrégat.

Répéter les étapes précédentes sur le serveur p3 :

  • installation des dépôts PGDG ;
  • installation de PostgreSQL et Patroni ;
  • création du fichier de configuration de Patroni ;
  • démarrage de l’instance sur p3.

Après l’ajout du troisième nœud, la topologie est la suivante :

$ patronictl topology
+ Cluster: acme (7350009258581743592) +-----------+----+-----------+------+
| Member        | Host      | Role    | State     | TL | Lag in MB | Tags |
+---------------+-----------+---------+-----------+----+-----------+------+
| p1.hapat.vm   | 10.0.0.21 | Leader  | running   |  1 |           | […]  |
| + p2.hapat.vm | 10.0.0.22 | Replica | streaming |  1 |         0 | […]  |
| + p3.hapat.vm | 10.0.0.23 | Replica | streaming |  1 |         0 | […]  |
+---------------+-----------+---------+-----------+----+-----------+------+

Il est conseillé lors des tests de garder une fenêtre répétant l’ordre régulièrement :

$ watch -n1 patronictl topology
  • Déterminer le primaire via l’API Patroni en direct avec curl.

La configuration est visible de l’extérieur :

$ curl -s http://p1:8008/patroni | jq
{
  "state": "running",
  "postmaster_start_time": "[…]",
  "role": "master",
  "server_version": 160002,
  "xlog": {
    "location": 50651960
  },
  "timeline": 1,
  "replication": [
    {
      "usename": "replicator",
      "application_name": "p2.hapat.vm",
      "client_addr": "10.0.0.22",
      "state": "streaming",
      "sync_state": "async",
      "sync_priority": 0
    },
    {
      "usename": "replicator",
      "application_name": "p3.hapat.vm",
      "client_addr": "10.0.0.23",
      "state": "streaming",
      "sync_state": "async",
      "sync_priority": 0
    }
  ],
  "dcs_last_seen": 1711308128,
  "tags": {
    "clonefrom": true,
    "failover_priority": 1
  },
  "database_system_identifier": "7350009258581743592",
  "patroni": {
    "version": "3.2.2",
    "scope": "acme",
    "name": "p1.hapat.vm"
  }
}

De manière plus précise :

$ curl -s http://p1:8008/cluster |\
jq '.members[] | select ((.role == "leader" ) and ( .state == "running")) | { name }'
{
  "name": "p1.hapat.vm"
}
  • Quels sont les slots de réplication sur le leader ?

Par défaut, les slots de réplications portent le nom des nœuds réplicas.

$ curl -s http://p1:8008/cluster | jq '.members[] | select (.role == "replica") | { name }'
{
  "name": "p2.hapat.vm"
}
{
  "name": "p3.hapat.vm"
}

On peut le vérifier en se connectant à PostgreSQL sur le serveur primaire (ici p1) :

$ sudo -iu postgres
$ psql -c "SELECT slot_name FROM pg_replication_slots"
 slot_name
-----------
 p2_hapat_vm
 p3_hapat_vm
  • Forcer l’utilisation du watchdog dans la configuration de Patroni. Que se passe-t-il ?

Ajoutez la section watchdog dans le fichier de configuration de Patroni /etc/patroni/patroni.yml en positionnant mode: required :

watchdog:
  mode: required
  # device: /dev/watchdog
  safety_margin: 5

Il nous faut recharger la configuration de Patroni :

$ patronictl reload acme
+ Cluster: acme (7350009258581743592) -------------+----+-----------+
| Member      | Host      | Role    | State        | TL | Lag in MB |
+-------------+-----------+---------+--------------+----+-----------+
| p1.hapat.vm | 10.0.0.21 | Replica | start failed |    |   unknown |
| p2.hapat.vm | 10.0.0.22 | Replica | start failed |    |   unknown |
| p3.hapat.vm | 10.0.0.23 | Leader  | running      |  6 |           |
+-------------+-----------+---------+--------------+----+-----------+
Are you sure you want to reload members p1.hapat.vm, p2.hapat.vm, p3.hapat.vm? [y/N]: y
Reload request received for member p1.hapat.vm and will be processed within 10 seconds
Reload request received for member p2.hapat.vm and will be processed within 10 seconds
Reload request received for member p3.hapat.vm and will be processed within 10 seconds

Suite à cette modification, le cluster Patroni se retrouve sans primaire. Dans les journaux applicatif de p3, ici précédemment marqué Leader, nous trouvons :

INFO: Reloading PostgreSQL configuration.
INFO: Lock owner: p3.hapat.vm; I am p3.hapat.vm
ERROR: Configuration requires watchdog, but watchdog could not be configured.
INFO: Demoting self (immediate)
INFO: Leader key released
INFO: Demoting self because watchdog could not be activated
INFO: Lock owner: None; I am p3.hapat.vm
INFO: not healthy enough for leader race
INFO: starting after demotion in progress
INFO: closed patroni connections to postgres
INFO: postmaster pid=561
INFO: establishing a new patroni heartbeat connection to postgres
WARNING: Watchdog device is not usable
INFO: Dropped unknown replication slot 'p1_hapat_vm'
INFO: Dropped unknown replication slot 'p2_hapat_vm'
INFO: following a different leader because i am not the healthiest node
  • Donner les droits à l’utilisateur postgres sur le fichier /dev/watchdog. Après quelques secondes, que se passe-t-il ?

Un simple chown sur le device /dev/watchdog de chaque VM peut suffire, au moins le temps de ce TP. Néanmoins, il ne survivrait pas au redémarrage de la machine.

Le fichier de service de Patroni propose de systématiquement modifier les droits sur ce device décommentant la ligne suivante :

#ExecStartPre=-/usr/bin/sudo /bin/chown postgres /dev/watchdog

Une autre solution est de configurer udev afin qu’il modifie les droits sur ce fichier automatiquement à chaque démarrage :

cat <<'EOF' > /etc/udev/rules.d/99-watchdog.rules
# give writes on watchdog device to postgres
SUBSYSTEM=="misc", KERNEL=="watchdog", ACTION=="add", RUN+="/bin/chown postgres /dev/watchdog"

# Or a better solution using ACL:
#SUBSYSTEM=="misc", KERNEL=="watchdog", ACTION=="add", RUN+="/bin/setfacl -m u:postgres:rw- /dev/watchdog"
EOF

Une fois les droits positionnés, l’un des nœuds Patroni devrait finalement réussir à activer le watchdog et ainsi devenir leader et promouvoir son instance PostgreSQL :

INFO: i6300ESB timer activated with 25 second timeout, timing slack 15 seconds
INFO: promoted self to leader by acquiring session lock
INFO: Lock owner: p2.hapat.vm; I am p2.hapat.vm
INFO: updated leader lock during promote
INFO: Lock owner: p2.hapat.vm; I am p2.hapat.vm
INFO: no action. I am (p2.hapat.vm), the leader with the lock

Patroni : utilisation

  • Se connecter depuis l’extérieur à la base postgres, d’un des nœuds.

La connexion extérieure peut se faire depuis la machine hôte des VM. Il est nécessaire d’y installer un client PostgreSQL, par exemple postgresql-client. Pour se connecter à l’instance p1, utiliser l’une des commandes suivantes :

$ psql -h p1 -p 5432 -U postgres -d postgres 
$ psql -h 10.0.0.21 postgres postgres

En cas de problème, il faut regarder :

  • les règles de firewall ;
  • les traces du nœud PostgreSQL concerné, plus instructives que le simple message d’erreur du client ;
  • le fichier de configuration pg_hba.conf.
  • Comment obtenir une connexion en lecture et écrire ?

Il est nécessaire de se connecter à l’instance primaire pour réaliser des écritures en base. La chaîne de connexion permettant d’indiquer plusieurs nœuds, nous pouvons y préciser tous les nœuds du cluster. Afin de sélectionner le nœud primaire, il suffit d’ajouter le paramètre target_session_attrs=primary ou target_session_attrs=read-write.

Par exemple :

psql "host=p1,p2,p3 user=postgres target_session_attrs=primary"
psql "postgresql://postgres@p1,p2,p3/postgres?target_session_attrs=read-write"

Pour une connexion en lecture seule, nous utilisons par exemple :

psql "postgresql://postgres@p1,p2,p3:5432/postgres?target_session_attrs=prefer-standby"
  • Créer une table :

     CREATE TABLE insertions (
       id     int         GENERATED ALWAYS AS IDENTITY,
       d      timestamptz DEFAULT now(),
       source text        DEFAULT inet_server_addr()
     );
  • Insérer une ligne toutes les secondes, à chaque fois dans une nouvelle connexion au primaire.

  • Dans une autre fenêtre, afficher les 20 dernières lignes de cette table.

Dans la colonne source de la table créée, la valeur par défaut fait appel à la fonction inet_server_addr() qui retourne l’adresse IP du serveur PostgreSQL sur lequel nous sommes connectés.

Lancer une insertion toutes les secondes :

watch -n1 'psql -X -d "host=p1,p2,p3 user=postgres target_session_attrs=primary" \
                -c "INSERT INTO insertions SELECT;"'

Lire les vingt dernières lignes de la table :

watch -n1 'psql -X -d "host=p1,p2,p3 port=5432 user=postgres" \
                -c "SELECT * FROM insertions ORDER BY d DESC LIMIT 20"'

Bien entendu, nous obtenons toujours le même nœud dans la colonne source, ici p1 :

 id  |               d               |    source
-----+-------------------------------+---------------

 313 | 2024-03-05 15:40:03.118307+00 | 10.0.0.21/32
 312 | 2024-03-05 15:40:02.105116+00 | 10.0.0.21/32
 311 | 2024-03-05 15:40:01.090775+00 | 10.0.0.21/32
 310 | 2024-03-05 15:40:00.075847+00 | 10.0.0.21/32
 309 | 2024-03-05 15:39:59.061759+00 | 10.0.0.21/32
 308 | 2024-03-05 15:39:58.048074+00 | 10.0.0.21/32

(20 lignes)
  • Stopper le nœud leader Patroni.
  • Que se passe-t-il dans la topologie, et dans les requêtes ci-dessus ?

Sur p1 :

# systemctl stop patroni

Après l’arrêt de p1, p3 prend le rôle de leader :

$ patronictl -c /etc/patroni/patroni.yml topology
+ Cluster: acme (7350009258581743592) +---------+----+-----------+------+
| Member        | Host      | Role    | State   | TL | Lag in MB | Tags |
+---------------+-----------+---------+---------+----+-----------+------+
| p3.hapat.vm   | 10.0.0.23 | Leader  | running |  2 |           | […]  |
| + p1.hapat.vm | 10.0.0.21 | Replica | stopped |    |   unknown | […]  |
| + p2.hapat.vm | 10.0.0.22 | Replica | running |  1 |         0 | […]  |
+---------------+-----------+---------+---------+----+-----------+------+

Les insertions échouent le temps de la bascule, ici pendant environ 2 secondes, puis continuent depuis l’autre nœud :

 id  |               d               |    source
-----+-------------------------------+---------------

 445 | 2024-03-24 15:50:17.840394+00 | 10.0.0.23/32
 444 | 2024-03-24 15:50:16.823004+00 | 10.0.0.23/32
 431 | 2024-03-24 15:50:14.755045+00 | 10.0.0.21/32
 430 | 2024-03-24 15:50:13.740541+00 | 10.0.0.21/32
  • Arrêter les processus du nouveau primaire. Il ne reste qu’un nœud actif. Que se passe-t-il ?

Le leader Patroni a changé à nouveau :

$ patronictl -c /etc/patroni/patroni.yml topology
+ Cluster: acme (7350009258581743592) -------+----+-----------+------+
| Member      | Host      | Role   | State   | TL | Lag in MB | Tags |
+-------------+-----------+--------+---------+----+-----------+------+
| p2.hapat.vm | 10.0.0.22 | Leader | running |  6 |           | […]  |
+-------------+-----------+--------+---------+----+-----------+------+
  • Arrêter deux nœuds du cluster etcd. Que se passe-t-il ?

Sur e1 et e2 :

# systemctl stop etcd

Il n’y a plus de quorum etcd garantissant une référence. Le cluster Patroni se met en lecture seule et les insertions tombent en échec puisqu’elles exigent une connexion ouverte en écriture :

psql: error: connection to server at "p1" (10.0.0.21), port 5432 failed: Connection refused
        Is the server running on that host and accepting TCP/IP connections?
connection to server at "p2" (10.0.0.22), port 5432 failed: server is in hot standby mode
connection to server at "p3" (10.0.0.23), port 5432 failed: Connection refused
        Is the server running on that host and accepting TCP/IP connections?
  • Redémarrer les nœuds etcd.

Les écritures reprennent.

  • Relancer un des nœuds Patroni.

Dans le cadre de cette correction p2 est l’actuel leader, nous redémarrons donc Patroni sur p1 :

# systemctl restart patroni

L’instance sur p1 se raccroche en secondaire :

$ patronictl -c /etc/patroni/patroni.yml topology
+ Cluster: acme (7350009258581743592) +-----------+----+-----------+------+
| Member        | Host      | Role    | State     | TL | Lag in MB | Tags |
+---------------+-----------+---------+-----------+----+-----------+------+
| p2.hapat.vm   | 10.0.0.22 | Leader  | running   |  7 |           | […]  |
| + p1.hapat.vm | 10.0.0.21 | Replica | streaming |  7 |         0 | […]  |
+---------------+-----------+---------+-----------+----+-----------+------+
  • Sur le troisième nœud (arrêté), détruire le PGDATA. Relancer Patroni.

Le PGDATA de nos instances se trouve dans /var/lib/pgsql/16/data. Supprimons ce PGDATA sur p3, le nœud restant :

rm -rf /var/lib/pgsql/16/data

Relançons Patroni sur ce nœud :

# systemctl start patroni

Nous observons dans les journaux de Patroni et PostgreSQL que l’instance est recrée et se raccroche à p2 :

LOG:  starting PostgreSQL 16.2 on x86_64-pc-linux-gnu, […]
LOG:  listening on IPv4 address "10.0.0.23", port 5432
LOG:  listening on Unix socket "/run/postgresql/.s.PGSQL.5432"
LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
LOG:  database system was interrupted while in recovery at log time […]
HINT:  If this has occurred more than once some data might be corrupted and you might need to choose an earlier recovery target.
LOG:  entering standby mode
LOG:  starting backup recovery with redo LSN 0/45405620, checkpoint LSN 0/4FD4D788, on timeline ID 7
LOG:  redo starts at 0/45405620
LOG:  completed backup recovery with redo LSN 0/45405620 and end LSN 0/509EA568
LOG:  consistent recovery state reached at 0/509EA568
LOG:  database system is ready to accept read-only connections
LOG:  started streaming WAL from primary at 0/50000000 on timeline 7

Le temps de la reconstruction de zéro, il est possible de voir l’évolution de l’état de p3 de creating replica à streaming :

$ export PATRONICTL_CONFIG_FILE=/etc/patroni/patroni.yml
$ patronictl topology
+ Cluster: acme (7350009258581743592) +------------------+----+-----------+------+
| Member        | Host      | Role    | State            | TL | Lag in MB | Tags |
+---------------+-----------+---------+------------------+----+-----------+------+
| p2.hapat.vm   | 10.0.0.22 | Leader  | running          |  7 |           | […]  |
| + p1.hapat.vm | 10.0.0.21 | Replica | streaming        |  7 |         0 | […]  |
| + p3.hapat.vm | 10.0.0.23 | Replica | creating replica |    |   unknown | […]  |
+---------------+-----------+---------+------------------+----+-----------+------+

/* Après un certain temps */

$ patronictl topology
+ Cluster: acme (7350009258581743592) +-----------+----+-----------+------+
| Member        | Host      | Role    | State     | TL | Lag in MB | Tags |
+---------------+-----------+---------+-----------+----+-----------+------+
| p2.hapat.vm   | 10.0.0.22 | Leader  | running   |  7 |           | […]  |
| + p1.hapat.vm | 10.0.0.21 | Replica | streaming |  7 |         0 | […]  |
| + p3.hapat.vm | 10.0.0.23 | Replica | streaming |  7 |         0 | […]  |
+---------------+-----------+---------+-----------+----+-----------+------+
  • Forcer un failover vers le nœud p1.
$ patronictl failover
Current cluster topology
+ Cluster: acme (7350009258581743592) ----------+----+-----------+------+
| Member      | Host      | Role    | State     | TL | Lag in MB | Tags |
+-------------+-----------+---------+-----------+----+-----------+------+
| p1.hapat.vm | 10.0.0.21 | Replica | streaming |  7 |         0 | […]  |
+-------------+-----------+---------+-----------+----+-----------+------+
| p2.hapat.vm | 10.0.0.22 | Leader  | running   |  7 |           | […]  |
+-------------+-----------+---------+-----------+----+-----------+------+
| p3.hapat.vm | 10.0.0.23 | Replica | streaming |  7 |         0 | […]  |
+-------------+-----------+---------+-----------+----+-----------+------+
Candidate ['p1', 'p3'] []: p1
Are you sure you want to failover cluster acme, demoting current leader p2? [y/N]: y
[…] Successfully failed over to "p1"
+ Cluster: acme (7350009258581743592) --------+----+-----------+------+
| Member      | Host      | Role    | State   | TL | Lag in MB | Tags |
+-------------+-----------+---------+---------+----+-----------+------+
| p1.hapat.vm | 10.0.0.21 | Leader  | running |  7 |           | […]  |
+-------------+-----------+---------+---------+----+-----------+------+
| p2.hapat.vm | 10.0.0.22 | Replica | stopped |    |   unknown | […]  |
+-------------+-----------+---------+---------+----+-----------+------+
| p3.hapat.vm | 10.0.0.23 | Replica | running |  7 |         0 | […]  |
+-------------+-----------+---------+---------+----+-----------+------+

$ patronictl topology
+ Cluster: acme (7350009258581743592) +-----------+----+-----------+------+
| Member        | Host      | Role    | State     | TL | Lag in MB | Tags |
+---------------+-----------+---------+-----------+----+-----------+------+
| p1.hapat.vm   | 10.0.0.21 | Leader  | running   |  8 |           | […]  |
| + p2.hapat.vm | 10.0.0.22 | Replica | streaming |  8 |         0 | […]  |
| + p3.hapat.vm | 10.0.0.23 | Replica | streaming |  8 |         0 | […]  |
+---------------+-----------+---------+-----------+----+-----------+------+
  • Modifier les paramètres shared_buffers et work_mem. Si besoin, redémarrer les nœuds.

Pour modifier la configuration, nous devons utiliser patronictl, plutôt qu’éditer directement les fichiers de configuration. Une alternative est de modifier la configuration statique, dans le fichier YAML /etc/patroni/patroni.yml. Cette méthode facilite leur maintenance, mais impose que le contenu soit identique sur tous les nœuds, ce qui est généralement le cas dans un déploiement industrialisé.

La commande patronictl edit-config appelle l’éditeur par défaut, souvent vi, vim ou nano. Vous pouvez modifier la variable d’environnement EDITOR pour pointer sur votre éditeur favori.

Éditons la configuration dynamique et ajoutons les deux paramètres :

$ export PATRONICTL_CONFIG_FILE=/etc/patroni/patroni.yml
$ patronictl edit-config
[…]
--- 
+++ 
@@ -12,6 +12,8 @@
     wal_keep_size: 128MB
     wal_level: replica
     wal_log_hints: 'on'
+    shared_buffers: 300MB
+    work_mem: 50MB
   use_pg_rewind: false
   use_slots: true
 retry_timeout: 10


Apply these changes? [y/N]: y
Configuration changed

Les nœuds sont à redémarrer à cause de la modification de shared_buffers :

$ patronictl topology
+ Cluster: acme (7350009258581743592) +-----------+----+------+-----------------+-
| Member        | Host      | Role    | State     | TL | Lag… | Pending restart | 
+---------------+-----------+---------+-----------+----+------+-----------------+-
| p1.hapat.vm   | 10.0.0.21 | Leader  | running   |  1 |      | *               | 
| + p2.hapat.vm | 10.0.0.22 | Replica | streaming |  1 |    0 | *               | 
| + p3.hapat.vm | 10.0.0.23 | Replica | streaming |  1 |    0 | *               | 
+---------------+-----------+---------+-----------+----+------+-----------------+-

Commandons le redémarrage de PostgreSQL sur les trois nœuds :

$ patronictl restart acme
+ Cluster: acme (7350009258581743592) ----------+----+------+-----------------+-
| Member      | Host      | Role    | State     | TL | Lag… | Pending restart |
+-------------+-----------+---------+-----------+----+------+-----------------+-
| p1.hapat.vm | 10.0.0.21 | Leader  | running   |  1 |      | *               | 
+-------------+-----------+---------+-----------+----+------+-----------------+-
| p2.hapat.vm | 10.0.0.22 | Replica | streaming |  1 |    0 | *               | 
+-------------+-----------+---------+-----------+----+------+-----------------+-
| p3.hapat.vm | 10.0.0.23 | Replica | streaming |  1 |    0 | *               | 
+-------------+-----------+---------+-----------+----+------+-----------------+-
When should the restart take place (e.g. […])  [now]:
Are you sure you want to restart members p1.hapat.vm, p2.hapat.vm, p3.hapat.vm? [y/N]: y
Restart if the PostgreSQL version is less than provided (e.g. 9.5.2)  []: 
Success: restart on member p1.hapat.vm
Success: restart on member p2.hapat.vm
Success: restart on member p3.hapat.vm

Vérifions que le paramétrage a bien été modifié :

$ for h in p1 p2 p3; do echo -ne $h:; psql -Xtd "host=$h user=postgres" -c "show shared_buffers"; done
p1: 300MB

p2: 300MB

p3: 300MB

Noter que le contenu des modifications est tracé dans un fichier patroni.dynamic.json dans le PGDATA :

$ jq . patroni.dynamic.json
{
  "loop_wait": 10,
  "postgresql": {
    "parameters": {
      "hot_standby": "on",
      "max_connections": 100,
      "max_locks_per_transaction": 64,
      "max_prepared_transactions": 0,
      "max_replication_slots": 10,
      "max_wal_senders": 10,
      "max_worker_processes": 8,
      "track_commit_timestamp": "off",
      "wal_keep_size": "128MB",
      "wal_level": "replica",
      "wal_log_hints": "on",
      "shared_buffers": "300MB",
      "work_mem": "50MB"
    },
    "use_pg_rewind": false,
    "use_slots": true
  },
  "retry_timeout": 10,
  "ttl": 30
}

  1. Distributed Control System↩︎

  2. ou parfois Distributed Configuration Store↩︎

  3. Network Time Protocole↩︎

  4. Le quorum se calcule ainsi : (3 sites × 3 nœuds / 2) +1↩︎