- Knock a mis en place une procédure pour migrer Postgres, le stockage central de son moteur de workflows de notifications, d’AWS RDS Aurora 11.9 vers 15.3, sans impact pour les clients
- Sans action avant la date de retrait de Postgres 11.9 sur Amazon RDS, le 29 février 2024, l’entreprise s’exposait à une mise à niveau forcée et à du downtime
- Les mises à niveau in-place et
pg_dump/pg_restorenécessitant de longues interruptions, elles ont été écartées au profit d’une réplication logique basée surPUBLICATION/SUBSCRIPTIONvers une nouvelle base - La stratégie de réplication a été adaptée à la taille des tables et aux schémas d’écriture : réplication directe pour les petites tables, et combinaison de
copy_data = falseavec un backfill depuis snapshot pour les grandes tables append-only - Le basculement final s’est terminé en quelques secondes en conservant les connexions aux deux bases, en modifiant un flag, en laissant 500 ms aux requêtes en cours, puis en suspendant pendant 1 seconde les requêtes vers la nouvelle base afin de réduire le risque de stale reads
Objectifs et contraintes de la mise à niveau
- Knock dépend de Postgres pour son moteur de workflows de notifications, et l’utilise pour la configuration des workflows, les modèles de messages, la collecte de millions de logs et la mise en file de travaux en arrière-plan
- En tant que base de données relationnelle, Postgres nécessite au minimum un redémarrage lors d’une mise à niveau ; une mise à niveau de version majeure peut exiger un arrêt complet de plusieurs minutes ou plus en raison de changements dans le format de stockage des données et des index sur disque
- Postgres 11.9, utilisé depuis les débuts de l’entreprise, devait être retiré d’Amazon RDS ; sans action spécifique, il existait un risque de mise à niveau forcée et de downtime imposé
- Les conditions de mise à niveau ont été définies pour réduire le risque opérationnel
- Passer directement à la version la plus récente possible, Postgres 15.3 pour Aurora
- Ne pas accepter plus de 60 secondes de downtime, et idéalement viser zéro downtime système
- Terminer avant l’échéance d’Amazon en février 2024
- Minimiser l’impact client, par exemple avec 0 réponse d’erreur API
- Transformer la procédure en runbook réutilisable pour les prochaines mises à niveau
- Passer de 11.9 à 15.3 correspondait à une mise à niveau de 4 versions majeures ; répéter 4 fois une mise à niveau in-place a donc été écarté
Préparation : réduction des risques et observabilité
- La mise à niveau de Postgres a d’abord été abordée en dressant une liste de risques, puis en réduisant en priorité ceux qui avaient un impact fort tout en étant faciles à éliminer à l’avance
- Long downtime
- Perte de données
- Changements de performance de la base pour les workloads applicatifs
- Changements de fréquence ou de comportement de
VACUUM - Nécessité de migrer les slots de réplication
- Les notes de version de Postgres ont servi à vérifier les changements entre versions et à identifier des risques comme des modifications de comportement de
VACUUMou la nécessité de réindexer lors de certaines mises à niveau - Pendant la mise à niveau, il faut suivre en continu les métriques système et base de données
- Max TXN ID pour prévenir le transaction wraparound
- Utilisation CPU de la base
- Sessions en attente sur l’instance writer
- Latence des requêtes
- Latence des réponses API de l’application
- Knock surveillait aussi des métriques propres à l’application, comme le temps nécessaire pour qu’une requête API se transforme en notification
- Sans métriques consultables à temps, on avance les yeux bandés pendant la mise à niveau
Approches écartées : mise à niveau in-place et dump/restore
- La mise à niveau in-place d’AWS RDS s’exécute depuis la console AWS : AWS arrête la base, lance les scripts de mise à niveau, puis la remet en ligne
- Ce processus peut prendre de quelques minutes à plusieurs heures ou plus, selon le volume de données et l’ampleur des changements entre versions
- Même une fois la base revenue en ligne, des opérations de maintenance comme
VACUUMouREINDEXpeuvent être nécessaires, de sorte qu’elle n’est pas forcément immédiatement pleinement exploitable - L’approche
pg_dumpetpg_restoreimpose de déconnecter toutes les applications de l’ancienne base afin d’obtenir une sauvegarde fiable, et sur une grosse base le dump comme le restore prennent eux-mêmes longtemps - Les deux approches ont été écartées, car elles risquaient fortement de dépasser largement la limite de downtime de Knock
Approche retenue : mise à niveau par réplication logique
- Le choix final a été d’utiliser la réplication logique de Postgres avec
PUBLICATIONetSUBSCRIPTION - Le flux général était le suivant
- Créer une nouvelle base avec la version cible de Postgres
- Migrer la configuration, les extensions, la structure des tables, les utilisateurs, etc.
- Créer une publication sur l’ancienne base et configurer une subscription sur la nouvelle
- Ajouter les tables à la publication
- Une fois la réplication terminée, exécuter des tests pour vérifier les risques restants
- Quand la configuration de la nouvelle base est suffisamment validée, basculer l’application vers la nouvelle base
- Supprimer l’ancienne base
- Cela permettait d’avancer par étapes progressives plutôt que d’exécuter une grosse mise à niveau d’un seul coup, et de tester la nouvelle base avec les données et workloads réels
- Une fois la nouvelle base prête, le basculement lui-même s’est terminé en quelques secondes, offrant un meilleur contrôle sur le moment et la méthode de transition
Points clés de la configuration de la réplication
- La réplication logique de Postgres utilise des paramètres nécessaires à la configuration des slots de réplication ; dans une application simple, le principal changement peut être de définir
wal_levelsurlogical - Si vous utilisez déjà des slots de réplication pour des read replicas, du failover de base de données ou la synchronisation d’un data warehouse, il faut ajuster les paramètres associés, comme
max_replication_slots, conformément à la documentation - La structure des tables de la nouvelle base doit être identique, mais vide
- Un snapshot de schéma peut être généré avec
pg_dumpallen utilisant les options--schema-onlyet--no-role-passwords, puis comparé au SQL destiné à la nouvelle base pour corriger les écarts - Lors de la création de la publication sur l’ancienne base et de la subscription sur la nouvelle, les principales options sont configurées
enabled = false: évite de lancer la synchronisation dès le départcreate_slot = true: laisse Postgres gérer le slot de réplicationcopy_data = true: copie par défaut le contenu des tablesdisable_on_error = true: arrête la subscription en cas d’erreur inattendue pour permettre de corriger le problème puis de reprendre
- Ajouter toutes les tables d’un coup à une publication avec
FOR ALL TABLESpeut poser des problèmes de performance sur une grosse base ; Knock a donc ajouté les tables une par une avecALTER PUBLICATION ... ADD TABLE
Classification des tables et stratégie de réplication
- Knock a classé les tables selon leur taille disque et leur nombre de tuples
- Petites tables synchronisables en quelques minutes
- Tables volumineuses mais proches de l’append-only
- Grandes tables dont la plupart des rows sont fréquemment mises à jour
- Pour Knock, une “petite” table faisait moins de 50 Go et comptait moins de 10 millions de tuples
- Dans Postgres, un tuple est l’unité de stockage d’un insert ou d’un update ; même avec peu de rows, un grand nombre de tuples non nettoyés peut allonger le temps de réplication
- Exécuter
VACUUMavant la réplication peut aider à réduire le nombre de tuples que la base source doit copier vers la base cible - Le temps de synchronisation d’une table est directement lié à sa taille disque et à son nombre de tuples ; une synchronisation longue peut empêcher
VACUUMsur la base primary, entraînant une dégradation des performances et un risque de transaction wraparound
Réplication des petites tables
- Les petites tables ont été traitées en les ajoutant à la publication sur l’ancienne base, puis en rafraîchissant la subscription sur la nouvelle base
- Postgres prend en charge la copie des tables, la synchronisation et l’application des changements ultérieurs
- Les très petites tables peuvent être synchronisées en moins d’une seconde
Réplication des grandes tables append-only
- Les grandes tables sans updates, ou dont les updates ne concernent que les rows récentes, peuvent utiliser une publication/subscription séparée avec
copy_data = false - Knock a ajouté le suffixe
_nocopyaux noms pour les distinguer de la réplication standard - Seules les nouvelles modifications sont d’abord répliquées, tandis que les données historiques sont backfillées séparément depuis une sauvegarde ou un snapshot
- La procédure utilisée sur AWS RDS Aurora était la suivante
- Créer un snapshot de la base de production
- Restaurer le snapshot dans une nouvelle instance de base
- Ajouter un suffixe comme
_snapshotau nom des tables du snapshot à répliquer - Créer aussi, dans la base cible, des tables de snapshot avec le même schéma
- Configurer une publication/subscription de la base snapshot vers la base cible
- Surveiller l’avancement de la réplication
- Une fois la réplication rattrapée, fusionner dans les vraies tables cibles avec
INSERT ... ON CONFLICT DO NOTHING
- Pour les très grandes tables, ce processus peut prendre plusieurs jours, mais il se déroule en arrière-plan et ne doit donc pas affecter l’environnement de production
- Après la fusion, le nombre de rows est comparé pour vérifier la cohérence, puis les tables snapshot de la base cible, la subscription snapshot et l’instance de base snapshot sont supprimées
Tables volumineuses et fréquemment mises à jour
- Les tables volumineuses dont la plupart des rows sont fréquemment mises à jour sont les plus difficiles, et une réplication longue peut empêcher l’exécution d’
AUTOVACUUM - Les mesures possibles sont les suivantes
- Vérifier si du housekeeping peut réduire la taille de la table
- Vérifier si un
VACUUMrécent a été exécuté - Étudier si la table peut être partitionnée en fragments plus petits
- Vérifier si les updates de rows cessent après un certain temps afin de déterminer si la table peut être traitée comme append-only
- Si la base source est antérieure à PG 15, les options sont limitées ; il faut répliquer comme pour les petites tables et surveiller l’éventuelle dégradation du service
- Si nécessaire, il est possible de retirer la table de la publication puis de rafraîchir la subscription pour revenir en arrière
- Pour les tables trop volumineuses, la réplication peut être lancée pendant les périodes de faible trafic afin de réduire l’impact de la charge et de l’activité d’écriture
Réplication fractionnée de grandes tables possible avec PG 15+
- Si la base source est en PG 15 ou plus, il est possible de diviser la réplication entre plusieurs publications pour déplacer une grande table par petits fragments
- Cette approche fonctionne un peu comme le partitionnement ou le sharding, au prix d’un plus grand nombre de slots de réplication utilisés
- Comme Knock migrait de 11.9 à 15.3, cette méthode n’était pas disponible et n’a pas été testée directement
- L’exemple consiste à répartir les rows entre plusieurs publications avec un hash de primary key et une clause
WHERE - La taille de fragment jugée gérable par Knock était d’environ 100 Go de données hors index
Vérification et interruption de l’état de réplication
- Lorsqu’une table est ajoutée à une subscription, son état peut être consulté dans
pg_subscription_rel.srsubstatesur la base ciblei: initialisationd: copie du contenu de la tablef: copie terminée, en attente de synchronisation finales: finalisation de la synchronisation initialer: réplication normale en cours
- L’étape
ddoit conserver d’anciens IDs de transaction Postgres, ce qui peut effectivement bloquerVACUUMet entraîner des problèmes de performance ou un transaction ID wraparound - Si l’on approche du wraparound, il vaut mieux interrompre la migration et la découper en fragments plus petits
- Pour interrompre la réplication d’une table donnée, il faut retirer la table de la publication sur l’ancienne base, puis rafraîchir la subscription sur la nouvelle base
- Désactiver simplement la subscription peut laisser la base source retenir d’anciens IDs de transaction, sans résoudre le problème de performance
- En urgence, il est possible de supprimer entièrement publication et subscription et de recommencer depuis le début ; Postgres nettoiera les slots de réplication associés
Contraintes liées à la migration des slots de réplication
- Les slots de réplication Postgres conservent les logs d’activité de la base que d’autres bases ou applications peuvent consommer
- L’avancement d’un slot est suivi par le Log Sequence Number, ou LSN, qui est propre à la base Postgres primary
- Il n’est pas possible de copier tel quel le LSN d’un slot de réplication de l’ancienne base vers la nouvelle
- Les applications qui consomment des slots de réplication, comme les outils de data warehouse, doivent définir leur stratégie de migration selon la documentation de chaque outil
- Si votre propre application utilise des slots de réplication, un mécanisme d’idempotence capable d’éliminer les transactions en double entre l’ancienne et la nouvelle base peut aider
Validation finale
- Une fois toutes les tables ajoutées à la publication et la subscription rattrapée, il faut vérifier que les tables correspondent entre elles
- En raison du délai de la réplication logique, l’ancienne et la nouvelle base peuvent difficilement être parfaitement identiques au même instant, mais une comparaison du nombre de rows permet de vérifier qu’elles sont suffisamment proches
- Knock a écrit un script qui comptait le nombre de rows dans chaque table sur l’ancienne et la nouvelle base
- Pour les tables ayant une colonne
inserted_at, seules les rows plus anciennes que 10 secondes étaient comparées, en supposant que les 10 secondes les plus récentes seraient bientôt répliquées - Certaines tables ont aussi été vérifiées par comparaison d’un échantillon aléatoire de rows pour confirmer que leur contenu correspondait
Méthode de basculement applicatif
- Pour le cutover final, l’application peut être modifiée afin de se connecter aux deux bases
- Les bases à faible trafic ont été migrées de manière simple, en changeant la configuration vers la nouvelle base puis en redémarrant l’application
- Pour les applications avec beaucoup d’activité concurrente, il fallait éviter les écritures conflictuelles entre l’ancienne et la nouvelle base
- Le script de cutover de Knock suivait cette séquence
- Ordonner à toutes les instances applicatives d’envoyer les nouvelles requêtes vers la nouvelle base
- Laisser 500 ms aux requêtes DB en cours pour se terminer, puis les annuler de force
- Après le changement de flag, mettre artificiellement en pause les requêtes vers la nouvelle base pendant la première seconde afin de laisser le temps aux pending transactions d’être répliquées vers la nouvelle base
- Reprendre ensuite l’activité DB normale, mais en pointant vers la nouvelle base
- Arrêter certains workloads DB spécifiques puis les redémarrer pour qu’ils se reconnectent à la nouvelle base
- Knock a confirmé que 500 ms étaient largement supérieurs à la durée de la plupart des requêtes DB, et qu’aucune erreur due à une déconnexion forcée n’avait été observée
Gestion des séquences
- La réplication logique Postgres ne synchronise pas les sequences
- Même si des valeurs de sequence sont consommées sur l’ancienne base, les valeurs de sequence de la nouvelle base n’augmentent pas
- Juste avant le changement de feature flag, Knock a exécuté un script connecté aux deux bases
- Pour chaque sequence de l’ancienne base, récupérer la valeur suivante avec
SELECT nextval('sequence_name') - Sur la nouvelle base, avancer la sequence avec
SELECT setval('sequence_name', value::int4 + 100000)
- Pour chaque sequence de l’ancienne base, récupérer la valeur suivante avec
- Cette méthode crée un gap dans les sequences, mais les sequences de Knock étant en bigint, sauter 100 000 valeurs représentait en pratique presque 0 % de l’espace de sequence disponible
- La taille du gap doit être ajustée au volume de valeurs de sequence susceptibles d’être utilisées pendant le cutover réel
Points à vérifier avant le cutover
- La checklist avant le basculement final couvre largement l’état de préparation opérationnelle
- Le nombre de rows de toutes les tables correspond-il aux attentes ?
- Toutes les subscriptions sont-elles activées et en cours d’exécution sans erreur ?
- Le schéma correspond-il, et peut-on geler les releases de migrations ?
- La nouvelle base est-elle dimensionnée pour le workload ?
- Faut-il des read replicas pour aligner la topologie du cluster de l’ancienne et de la nouvelle base ?
REINDEXet la maintenanceVACUUMde base ont-ils été exécutés sur la nouvelle base ?- Les notes de version de Postgres ont-elles été revérifiées pour d’éventuelles régressions applicatives ?
- Des tests automatiques et manuels ont-ils été menés sur la base de staging avec la nouvelle version ?
- Les requêtes les plus lourdes ont-elles été testées en charge avec
pg_bench? - Reste-t-il des risques qui peuvent encore être réduits ?
- La procédure de cutover a-t-elle été répétée plusieurs fois en staging ou en test ?
- Une sauvegarde de la base a-t-elle été créée juste avant le cutover ?
Résultat du basculement réel
- Knock a répliqué les tables une par une pendant plusieurs semaines, principalement après les heures de bureau et pendant les périodes de trafic le plus faible
- La procédure a été affinée en répétant plusieurs fois le cutover en staging, afin qu’elle fonctionne avec peu d’intervention des opérateurs
- Une fois le replica PG 15 et le code applicatif de basculement prêts, les derniers contrôles ont été effectués et le flag a été changé
- Le cutover réel s’est terminé en quelques secondes, et l’application a continué à fonctionner hormis un bref pic de latence intentionnel destiné à attendre la réplication
- Ensuite, les changements applicatifs temporaires ont été annulés, toutes les connexions ont été définitivement basculées vers la nouvelle base, puis les subscriptions de la nouvelle base et l’ancienne base ont été supprimées
- Knock a terminé une migration sans interruption de Postgres 11.9 vers 15.3
Conclusion
- Sauter quatre versions majeures de Postgres d’un seul coup est difficile, mais possible
- L’approche par réplication logique peut être plus sûre qu’un downtime planifié, car elle permet de répéter, tester et retravailler la procédure plusieurs fois avant le cutover réel
- En cas de problème en cours de route, il était possible de supprimer la publication sur l’ancienne base et de recommencer, ce qui permettait de revenir en arrière sans dégradation du service
- Une disponibilité parfaite à 100 % n’est pas techniquement possible, mais une migration sans interruption aide à maintenir le système en fonctionnement sans grosse coupure de service
1 commentaires
Avis sur Hacker News
La méthode qui consiste à copier intégralement le contenu des tables une par une impose une charge d’E/S beaucoup trop élevée, et ne fonctionne pas avec de très grosses tables
Une meilleure approche consiste à créer un slot de réplication, prendre un snapshot, le restaurer sur une nouvelle instance, faire avancer le LSN, puis répliquer à partir de là. On obtient ainsi un réplica logique contenant toutes les données, qu’il suffit ensuite de mettre à niveau
La méthode est décrite dans l’article d’Instacart : https://archive.ph/K5ZuJ
Si je me souviens bien, l’article contenait quelques petites erreurs, mais la procédure générale fonctionnait, et j’ai déjà effectué plusieurs upgrades d’instances de plusieurs To de cette manière
Si on lance d’abord la réplication logique puis qu’on exécute pg_upgrade, il y a un risque de corruption. La discussion associée se trouve sur pgsql-hackers : https://www.postgresql.org/message-id/flat/20230217075433.u5...
Pour résoudre cela, il faut d’abord créer le slot logique, faire avancer le nouveau cluster jusqu’à la position LSN du slot sans encore démarrer la réplication logique, puis exécuter pg_upgrade, et ne démarrer la réplication logique qu’après le lancement du cluster sur la nouvelle version de PostgreSQL
Postgres.ai a récemment utilisé exactement cette approche pour mettre à niveau sans interruption plusieurs clusters multi-TiB de GitLab sous forte charge, en utilisant aussi PAUSE/RESUME de PgBouncer. Une présentation d’Alexander Sosna est prévue plus tard cette semaine : https://www.postgresql.eu/events/pgconfeu2023/schedule/sessi...
La progression table par table était bien plus fastidieuse, mais elle me semblait plus fiable
Cette méthode a permis de mettre à niveau avec succès de très grandes bases de données très actives à de nombreuses reprises
https://www.instacart.com/company/how-its-made/zero-downtime...
L’approche est intéressante et bien documentée, mais la phrase « les clients modernes attendent une disponibilité à 100 % » me gêne
Ce n’est ni ma préférence en tant que client, ni ce que j’observe en tant que fournisseur. Pour beaucoup de workloads, la cohérence est bien plus importante que la disponibilité
Lorsqu’un fournisseur annonce une fenêtre de downtime, cela me rassure souvent davantage, car j’y vois le signe qu’il traite mes données avec prudence
Je voulais inspirer confiance à la fois dans la fiabilité du produit et dans la cohérence du workload. Bien sûr, il vaut largement mieux gérer les attentes des clients et prévoir volontairement un downtime pour améliorer le temps de disponibilité sur le long terme, plutôt que de prétendre garantir la cohérence tout en restant fragile
Le fait d’anticiper des fenêtres de maintenance régulières peut même conduire globalement à une architecture plus robuste. Si les clients mettent en place des garde-fous pour tolérer le downtime, leur résilience augmente, et l’équipe gagne aussi du temps pour investir dans un meilleur produit quand elle peut faire ce pari de confiance avec eux
Peut-être qu’après le prochain upgrade de version majeure, j’écrirai un article intitulé « définir des attentes réalistes autour du downtime est la voie vers un très haut niveau de disponibilité »
En tant que client d’AWS, j’attends une disponibilité à 100 %. Mes propres clients sont répartis dans le monde entier et je n’ai aucun créneau possible pour du downtime
AWS prend désormais en charge les déploiements blue/green : https://aws.amazon.com/about-aws/whats-new/2023/10/amazon-rd...
Après plusieurs échanges avec AWS, le test est resté bloqué pendant des heures, et ce n’est que plus tard que l’interface AWS a reconnu que le basculement n’avait pas été appliqué. Heureusement, l’échec a été sûr, mais je n’ai aucune confiance dans sa capacité à caler le moment exact du basculement sur des jeux de données de plusieurs Go ou plus
Peut-être que ce sera possible la prochaine fois
C’est excellent
J’ai créé un outil qui automatise l’essentiel de ce processus, et je serais ravi s’il peut être utile ou si certains veulent l’enrichir avec des retours ou des idées : https://github.com/shayonj/pg_easy_replicate
Les observations tirées des grosses tables pourraient être intéressantes pour un outil comme celui-ci. S’il facilite l’application de la bonne stratégie selon les tables, il pourrait devenir un outil indispensable pour les équipes qui devront mener ce type de migration à l’avenir
Il est douteux d’affirmer que « pour un service comme Knock, aucune indisponibilité n’est acceptable, qu’elle soit planifiée ou non »
Dans un système complexe, il y a des incidents et des temps d’arrêt. Une indisponibilité planifiée de 15 minutes est acceptable pour presque toutes les activités SaaS. Ce n’est ni un hôpital ni une centrale électrique
Beaucoup de faux travail naît du fait de considérer le service comme plus important qu’il ne l’est réellement. Si le temps d’ingénierie investi ici avait été consacré au produit ou à l’amélioration de la productivité de l’équipe de développement, il est fort probable que les utilisateurs en auraient été plus satisfaits. C’est d’autant plus vrai si l’on peut mettre les notifications en file d’attente puis rattraper après l’indisponibilité
S’il existe un SLA enterprise prévoyant des compensations pour 15 minutes d’indisponibilité, cela peut se justifier, mais dans la plupart des cas ce n’est pas le cas. En réalité, il est aussi fort probable qu’il y ait déjà eu quelques incidents comparables ou plus longs
Dans les migrations de base de données, la différence de charge de travail entre une « courte indisponibilité » et le « zéro interruption » est généralement importante, ce qui rend la question encore plus cruciale. Dans un cas ponctuel comme celui-ci, avec une version récente de PostgreSQL sur RDS déjà supportée nativement, cela me paraît particulièrement difficile à justifier
Nous avons aussi envisagé une fenêtre d’intervention, mais ce qui nous préoccupait en continu, c’était : comment répéter l’upgrade avec les données de production ? Un réplicat PG 15 synchronisé avec les données de production a été crucial pour valider que la charge se comporterait comme prévu
Avec un réplicat en temps réel, on peut faire des répétitions en minimisant l’impact sur l’environnement de production
La grande leçon de cette migration a été de voir à quel point il est utile, dans ce genre de projet, de suivre et d’atténuer tous les risques imaginables. Au final, le risque d’un upgrade sur place nous a semblé plus élevé que celui de l’approche retenue, et c’était un jugement distinct de la question d’une éventuelle fenêtre d’indisponibilité
En bonus, si nous devons réutiliser cette approche plus tard, ce billet de blog servira de point de départ et fera gagner plusieurs semaines. J’espère aussi qu’il aidera d’autres équipes dans une situation similaire
Même Epic, l’un des plus grands fournisseurs de dossiers médicaux électroniques aux États-Unis, a au moins une indisponibilité planifiée par mois pour ses mises à niveau, d’environ 30 à 60 minutes à chaque fois
On ne peut pas contrôler le moment du redémarrage. Une fois le processus lancé, le basculement peut commencer une heure, deux heures ou trois heures plus tard, et on ne peut ni savoir ni contrôler quand le redémarrage se produira
S’il existe des réplicats, ils sont mis à niveau en parallèle et redémarrent à des moments aléatoires, ce qui complique encore plus la situation
Donc, si vous ne pouvez pas tolérer une indisponibilité aléatoire sur une plage pouvant durer plusieurs heures selon la taille de la base, alors pour les upgrades RDS, la réplication logique est pratiquement la seule solution
Plus l’instance est grosse, plus le problème devient difficile
Si Jira est indisponible 15 minutes par jour, en général l’impact reste limité. Il y a d’autres tâches dans la file, et au pire, même si plusieurs incidents se cumulent, il reste de la documentation promise à quelqu’un
Mais si toute la suite Atlassian tombe simultanément, il devient bien plus difficile de conserver un travail tampon permettant de continuer à avancer. Si l’on fait en sorte que toutes les applications de l’entreprise reposent sur la même baie de stockage, la perte de productivité peut passer de 5 % à 95 %
Un tel concurrent fait donc passer mes besoins avant sa propre commodité
Votre incident devient aussi mon incident
Nous sommes en train de traverser ce processus en ce moment chez hava.io
Nous passons de AWS RDS PostgreSQL 11.13 à 15.5
Au final, nous avons choisi une approche relativement simple : une réplication unidirectionnelle avec pglogical. Comme nous avions déjà réalisé de la même manière une migration sans interruption de Google Cloud SQL vers AWS RDS, nous étions confiants sur le fait que cela fonctionnerait sans impact visible pour les clients
pglogical rend ce type de migration assez simple. Ce n’est pas toujours rapide, mais si l’on peut attendre quelques jours pendant que toute la base est répliquée progressivement vers la nouvelle instance, cela fonctionne bien
Cette méthode nous a aussi donné plus de liberté pour changer le type et la taille du stockage. Nous avions surdimensionné le stockage pour obtenir des IOPS, et nous voulions donc changer le type de stockage et réduire aussi la taille. Un simple restore de snapshot ne suffisait donc pas
Je me demande si cela désigne la fonctionnalité qu’AWS avait promise pendant la phase de « sales engineering »
En pratique, ils n’ont pas été capables de la fournir lorsqu’il a fallu imposer un upgrade de version majeure
Il est surprenant de ne pas pouvoir initialiser une réplique à partir d’une sauvegarde
Si c’était possible, cela aurait évité une bonne partie des efforts nécessaires pour streamer le contenu stable de l’ancienne base de données vers le nouveau serveur
Et puis ce n’est pas vraiment du « sans interruption » : il y a bien quelques secondes d’indisponibilité au moment du basculement vers le nouveau serveur
L’article ne disait pas comment la cohérence avait été préservée. Par exemple, on ne peut pas simplement laisser l’application pointer vers les deux serveurs pendant un certain temps. Les lectures pourraient éventuellement être servies par les deux, mais même cela ne serait pas parfait, et les écritures doivent impérativement aller vers un seul serveur
Enfin, il n’y a pas non plus d’option de rollback. Quand on déplace d’un coup un volume de données aussi important, c’est souvent là que les choses tournent mal tard dans la nuit. Il faut donc toujours avoir un plan qui permette de revenir à l’étape précédente et d’aller se coucher en sachant que le service sera encore vivant le matin
En particulier, une fois que des transactions d’écriture ont déjà été envoyées au nouveau serveur, il devient difficile de revenir à l’ancien pour quelque raison que ce soit, car les données sont déjà désynchronisées
Sans mécanisme de réplication, ou sans remonter cela au niveau applicatif, le système restauré aura des écritures manquantes
Par exemple, on peut modifier l’application pour mettre en place une double écriture. À ma connaissance, des équipes qui ont replatformé toute leur application depuis un SGBDR vers une base de données complètement différente comme Apache Cassandra ont procédé ainsi
Dans notre cas, la double écriture nous semblait plus risquée que de configurer la réplication en streaming avec les fonctionnalités natives de PostgreSQL. Mais pour certaines équipes, cela peut être un meilleur choix
Concernant le fait que « ce n’est pas sans interruption » et que « les détails de préservation de la cohérence manquaient », l’article expliquait en détail comment nous avons maintenu la cohérence et évité une interruption de l’API. En résumé, l’application était connectée aux deux bases de données, mais n’utilisait pas la nouvelle comme base principale
Ensuite, nous avons envoyé avec LaunchDarkly un signal de basculement à toutes les instances de l’application, et LaunchDarkly maintenait une connexion à faible latence avec chacune d’elles
Pendant la première seconde après le signal, les serveurs ont mis les requêtes base de données en file d’attente pour laisser le temps à la réplication de rattraper son retard. Cela a provoqué un bref pic de latence, mais il restait délibérément dans les marges acceptables que nous avions calculées. Après cette pause momentanée, les requêtes ont repris leur flux habituel, mais vers la nouvelle base de données, et le basculement était terminé
Pour le trafic qui restait encore sur l’ancienne base de données, nous avons aussi mis en place une déconnexion forcée avec un timeout de 500 ms. Cette valeur était très supérieure au temps de requête p99, donc aucune requête en cours n’a été brutalement interrompue. Cela a permis d’arrêter le trafic vers l’ancienne base et de laisser suffisamment de temps à la réplication pour rattraper son retard
L’option de rollback n’était pas dans l’article de blog, mais nous avions aussi étudié la création d’une base de données alternative en PG 11.9, puis la réplication de la base 15.3 vers cette troisième base. Si nous avions dû interrompre, nous aurions pu faire un roll-forward vers cette base de même version
Après avoir répété plusieurs fois la procédure de montée de version en staging pour confirmer les chances de réussite, nous avons décidé de ne pas utiliser cette option. Comme nous avions fait plusieurs répétitions, nous étions confiants au moment du basculement réel. En production aussi, nous avions validé sur l’instance 15.3 une partie des charges de travail en lecture seule via un déploiement canari, en la traitant comme une réplique en lecture
Pour éviter les problèmes tard dans la nuit, nous avons volontairement procédé en début de soirée le week-end. Le basculement avait été minutieusement scripté et répété afin de réduire le risque d’erreur humaine
En cas d’échec catastrophique, le système était aussi prêt à revenir à l’ancienne base de données. Dans ce scénario, il y aurait eu une certaine perte des données entrées dans la nouvelle base, et nous étions préparés à réconcilier les parties critiques. Pour réduire le risque de perte de données, nous avons temporairement suspendu certains jobs en arrière-plan pendant le basculement afin de diminuer le nombre d’écritures
Ces détails n’ont pas été inclus dans le blog parce que nous voulions nous concentrer sur les aspects PostgreSQL plutôt que sur les considérations spécifiques à Knock. Les équipes qui voudraient reprendre ce playbook doivent toujours établir et atténuer leur propre liste de risques dans leur contexte
La partie sur les séquences est clairement intéressante
Depuis un certain temps, j’utilise très peu les séquences et je privilégie surtout des UUID séquentiels, des UUID v7 ou encore des approches comme HiLo
https://en.wikipedia.org/wiki/Hi/Lo_algorithm
L’idée est de créer une séquence sur 12 bits à partir du projet de spécification de l’IETF, puis de construire l’UUID en combinant les millisecondes de l’epoch UNIX courante avec 62 bits aléatoires
L’essentiel consiste à avoir
uuidv7_seqet à faire en sorte que la fonctiongenerate_uuidv7()utiliseclock_timestamp(),NEXTVALetRANDOM()pour renvoyer une valeur au format UUID v7À plusieurs endroits, nous utilisons des KSUID et des UUID v4. Ce « piège » s’applique à toutes les séquences, donc cela vaut la peine d’en parler comme conseil général lorsqu’on effectue ce type de migration
[1]: https://segment.com/blog/a-brief-history-of-the-uuid/
Sans vouloir minimiser l’énorme travail qu’a représenté cette réussite, je me demande pourquoi ils n’ont pas fait de petites montées de version à chaque nouvelle version
C’est un excellent sujet de lecture, mais on a l’impression de lire l’histoire de marins qui ont choisi de traverser droit dans la tempête alors qu’ils savaient qu’en ne la contournant pas, cela pouvait finir en tragédie
Dans ce cas, les petites montées de version étaient-elles hors de portée ? Je me demande si c’était plutôt quelque chose comme : « même une petite montée de version coûtait autant en indisponibilité qu’une grosse, donc on a repoussé au maximum ». On en voit un indice dans l’introduction, mais je surinterprète peut-être
Plus que « on a repoussé jusqu’à être acculés », c’était plutôt une approche du type « si ce n’est pas cassé, ne le répare pas », tout en sachant qu’il faudrait sauter le pas un jour
Même si, en pratique, la vraie réponse est moins de 60 secondes, il aurait quand même fallu subir cette indisponibilité plusieurs fois pour arriver jusqu’à la version 15