Migration d’une base de données PostgreSQL réalisée avec 11 secondes d’indisponibilité
(gds.blog.gov.uk)- GOV.UK Notify a réduit l’indisponibilité à environ 11 secondes en migrant une base PostgreSQL 11 de 400 Go vers une instance PostgreSQL 15 sur RDS dans son propre compte AWS, dans le cadre de la fin du PaaS
- Dans la base cible, seules les tables ont d’abord été créées, puis les données ont été copiées avec un full load DMS avant d’appliquer ensuite les index et les contraintes de clés, afin de réduire le temps de chargement à grande échelle
- La base source comptait environ 1,3 milliard de lignes, 85 tables, 185 index et 120 clés étrangères, avec en semaine environ 1 000 insertions ou mises à jour par seconde ainsi qu’un volume similaire de lectures
- Comme le redéploiement de l’application prenait environ 5 minutes, l’équipe a préparé à l’avance les mêmes identifiants et un basculement pondéré DNS Route53 pour réduire le temps réel de transition
- DMS a été choisi parce qu’il était facile à faire supporter côté PaaS et AWS, mais pour une migration entre bases PostgreSQL, une alternative comme pglogical aurait peut-être été plus simple
Migration de la base de données Notify dans le contexte de la fin du PaaS
- GOV.UK Notify était hébergé sur GOV.UK Platform as a Service et, avec l’arrêt du PaaS, toute l’infrastructure devait être déplacée vers son propre compte AWS
- La base de données de Notify était une AWS RDS PostgreSQL située dans le compte AWS du PaaS, et stockait aussi bien les données d’envoi de notifications que le contenu de centaines de milliers de modèles utilisés par les équipes de service
- L’ancienne base a été désignée comme source database et la nouvelle comme target database pour mener la migration
- Le défi principal n’était pas de créer une nouvelle base PostgreSQL, mais de minimiser l’indisponibilité pendant le transfert complet des données et le basculement de la cible de connexion des applications
Taille de la base source et contraintes de service
- La base source était une PostgreSQL 11 d’environ 400 Go
- environ 1,3 milliard de lignes
- 85 tables
- 185 index
- 120 clés étrangères
- Un jour ouvré classique, elle enregistrait environ 1 000 insertions ou mises à jour par seconde, avec un nombre similaire de lectures
- GOV.UK Notify envoie chaque jour des millions de notifications importantes et sensibles au facteur temps, comme des alertes d’inondation ou le suivi de demandes de passeport
- Comme chaque envoi de notification nécessite un accès à la base de données, il fallait maintenir une interruption de service très courte
Chargement initial et réplication continue avec DMS
- L’équipe PaaS proposait une méthode de migration de base de données reposant sur AWS Database Migration Service
- DMS sert à transférer les données de la base source vers la base cible, et peut être exécuté dans le compte AWS source ou cible
- Une tâche DMS se divise en deux étapes
- full load : copie, table par table, toutes les données existantes jusqu’à un instant donné
- réplication continue : rejoue dans la base cible les nouvelles transactions de la base source afin de garder les deux bases synchronisées
- Le basculement des applications pour qu’elles utilisent la base cible au lieu de la base source relevait directement de l’équipe Notify
Préparation de la base cible et full load
- L’instance DMS a été créée dans le compte AWS source
- l’équipe PaaS avait déjà configuré une instance DMS dans ce compte, ce qui a permis d’aller vite
- l’instance DMS avait besoin d’identifiants permettant d’accéder aux bases PostgreSQL source et cible
- Comme l’instance DMS et la base cible se trouvaient dans des VPC différents, un VPC peering a été configuré afin que le trafic DMS depuis le VPC du PaaS soit routé vers leur propre VPC sans passer par l’internet public
- L’instance RDS cible a été créée dans leur propre compte AWS et, comme la fin de support de PostgreSQL 11 approchait, la nouvelle base a été déployée en PostgreSQL 15
- Le schéma de la base source a été exporté avec
pg_dumppour produire un fichier SQL de recréation du schéma, et seules les déclarations de tables ont d’abord été appliquées à la base cible - Les clés étrangères n’ont pas été appliquées au moment du full load
- parce que le full load de DMS ne copie pas les données dans un ordre respectant les contraintes de clés étrangères
- Les clés primaires et les index n’ont pas non plus été créés avant le full load
- sinon, chaque insertion aurait nécessité une mise à jour d’index, ce qui aurait fortement allongé le temps total pour charger des milliards de lignes
- il a été plus rapide de copier d’abord toutes les données, puis d’ajouter les index ensuite
- La tâche de full load copiait les données présentes au moment où le bouton de démarrage était déclenché
- les nouvelles données ou mises à jour survenues après ce point n’étaient pas incluses dans le full load
- l’achèvement du full load a pris environ 6 heures
- Après le full load, le reste du fichier de schéma a été appliqué pour ajouter les index et les contraintes de clés, ce qui a pris environ 3 heures
Dix jours de réplication avant le basculement du trafic
- Une fois le full load terminé, la base cible correspondait aux données de la base source au moment du démarrage du full load, mais la source continuait ensuite à recevoir des insertions, modifications et suppressions
- La réplication continue de DMS, c’est-à-dire la tâche de change data capture, a alors été lancée pour envoyer vers la base cible les transactions du journal de transactions de la base source depuis le début du full load
- Il a fallu plusieurs heures pour que le processus de réplication rattrape son retard, puis l’équipe a surveillé la latence de réplication DMS pour vérifier l’état de synchronisation
- La réplication DMS a tourné en arrière-plan pendant environ 10 jours, maintenant les deux bases synchronisées jusqu’à la fenêtre de migration annoncée aux utilisateurs
- La procédure de basculement du trafic a été écrite à l’avance sous forme de script Python
- arrêter le trafic applicatif vers la base source
- vérifier que la réplication avait totalement rattrapé son retard
- autoriser les applications à se connecter à la base cible
- Il fallait éviter une situation où une partie des applications utiliserait la base source tandis que le reste utiliserait la base cible
- les changements écrits dans la base cible n’étant pas répercutés vers la source, les utilisateurs auraient pu voir des données incohérentes
- Le script a été conçu pour être plus explicite, répétable et rapide qu’une procédure manuelle, et il a été utilisé au moins 40 fois lors des tests et répétitions préalables
- L’objectif d’indisponibilité était de moins de 5 minutes, et la migration a été planifiée un samedi soir relativement calme, tout en évitant le milieu de la nuit
Un basculement DNS qui a permis 11 secondes d’indisponibilité
- L’arrêt du trafic vers la base source a été géré en appelant
pg_terminate_backendsur les connexions applicatives, ce qui a pris moins d’une seconde - Le mot de passe de l’utilisateur PostgreSQL a aussi été changé afin d’empêcher les applications de se reconnecter à la base source, toute tentative entraînant alors une erreur d’authentification
- DMS créait dans la base cible une table d’état de réplication mise à jour chaque minute, et le script de migration s’appuyait sur cette table pour vérifier le décalage entre source et cible
- Comme sécurité supplémentaire, après l’arrêt des accès applicatifs à la base source, le script écrivait un enregistrement unique dans la base source puis attendait son arrivée dans la base cible
- Les informations de connexion à la base étaient fournies aux applications via la variable d’environnement
SQLALCHEMY_DATABASE_URI- le format existant était de type
postgresql://...@random-identifier.eu-west-1.rds.amazonaws.com:5432, avec nom d’utilisateur, mot de passe et emplacement RDS - changer l’emplacement de la base ou les identifiants nécessitait un redéploiement de l’application, qui prenait environ 5 minutes
- le format existant était de type
- Pour éviter cette indisponibilité supplémentaire liée au redéploiement, deux préparatifs ont été faits avant la migration
- créer sur les bases source et cible un utilisateur avec le même nom et le même mot de passe
- créer dans AWS Route53 un enregistrement DNS
database.notifications.service.gov.ukavec un TTL de 1 seconde
- L’enregistrement DNS utilisait au départ une pondération de 100 % vers la source et 0 % vers la cible
- L’URI de l’application a été modifié à l’avance pour utiliser les identifiants communs et le nouveau nom de domaine
- Au moment du basculement réel, le script changeait la pondération DNS AWS à 100 % vers la cible, puis attendait l’expiration du TTL d’une seconde
- Lors du basculement du samedi soir 4 novembre 2023, le décalage entre la base cible et la base source n’était plus que de quelques secondes
- À l’exécution du script de migration, les applications ont cessé d’accéder à la base source et ont commencé à utiliser la nouvelle base cible, avec une indisponibilité d’environ 11 secondes
Évaluation du choix de DMS et suite des opérations
- DMS a été choisi parce qu’il était bien pris en charge sur GOV.UK PaaS et qu’il était possible d’obtenir du support AWS
- Pour une future migration entre bases PostgreSQL, l’équipe prévoit d’examiner davantage des outils alternatifs comme pglogical
- DMS a peut-être ajouté plus de complexité et un processus de réplication moins familier que d’autres outils
- Après la migration de la base de données, l’étape suivante est celle de l’application
- L’application GOV.UK Notify doit être migrée vers AWS Elastic Container Service, et le déroulement sera partagé ultérieurement
1 commentaires
Avis sur Hacker News
Nous avons fait une migration similaire avec les AWS RDS Blue-Green Deployments ; la base de données était un peu plus grosse, mais le downtime a été d’environ 20 secondes et la charge de travail bien moindre. Étonnant que cela n’ait pas encore été mentionné dans ce fil
En gros, on lance un nouveau déploiement Blue/Green avec les changements souhaités, puis AWS synchronise le déploiement green via réplication logique pendant que la configuration blue existante continue de servir le trafic
Tant qu’on n’écrit pas dessus, on peut aussi modifier, tester et faire des tests de charge sur green ; les écritures continuent d’arriver sur blue, qui reste en production, puis sont répliquées vers green
Quand tout est prêt, on exécute la commande switch, et AWS se charge de vérifier la synchronisation, d’interrompre les écritures et les connexions, d’attendre que la réplication rattrape son retard, de renommer la base de données, puis de rétablir les connexions et les écritures
Dans notre cas, le downtime a été inférieur à 20 secondes, et l’ensemble de la configuration — y compris la primaire et plusieurs réplicas de lecture — a basculé sans problème. AWS met aussi à jour l’URL de la base de données, donc aucun besoin de modifier la configuration ; green devient blue, l’ancien blue devient old blue, et il suffit ensuite de le supprimer
Je recommande vivement, même s’il y a des limites, notamment pour des cas comme les déplacements entre comptes : https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-...
Il faut lire et relire la documentation, surtout les limitations. Le mieux est de tester en environnement de dev avec de la charge, puis de recommencer en staging
Ou sinon, vous pouvez toujours le pousser en production en mode YOLO, ça passera peut-être très bien
En revanche, j’ai appris à mes dépens que RDS Blue/Green ne peut pas servir à n’importe quel changement. Dans notre cas, on a découvert qu’on ne pouvait l’utiliser que pour monter la version du moteur, pas pour revenir en arrière
Une procédure stockée échouait très rarement sur MySQL 8.0, et nous avons envisagé un retour vers 5.7, mais c’était impossible
Le compromis, c’est que certaines requêtes ont été plus lentes pendant quelques secondes
La combinaison DNS Groups + retries est un mécanisme assez efficace pour ce type d’opération
Outil utilisé : https://github.com/shayonj/pg_easy_replicate
Il existe plusieurs façons de « mettre en pause » les requêtes Postgres entrantes ; par exemple, avec pgbouncer, on peut les retarder sans les faire échouer jusqu’à ce que la réplication ait rattrapé son retard, puis les laisser continuer sur la nouvelle base de données
Si quelque chose se passe mal et que la réplication ne rattrape pas son retard, il suffit de lever la pause et de laisser ces requêtes s’exécuter sur l’ancienne base
Ainsi, 11 secondes de downtime deviennent 0 à 11 secondes de temps de chargement supplémentaire. Plus important encore, cela réduit fortement les dommages collatéraux dans les cas où, parmi des milliers d’utilisateurs de la base qui n’ont jamais vu une requête échouer jusque-là, il existe un chemin de gestion d’erreur bogué ou un batch entier qui peut être ruiné par une seule requête en échec
Intéressant à comparer avec https://knock.app/blog/zero-downtime-postgres-upgrades. La discussion associée avait eu lieu ici : https://news.ycombinator.com/item?id=38616181
À l’époque, une bonne partie des échanges se résumaient à : « n’est-ce pas trop complexe juste pour éviter quelques minutes de downtime ? » Ce cas semble en apporter la démonstration, et donne l’impression qu’il suffit d’accepter 11 secondes de downtime après être passé en production avec AWS Data Migration Service et en changeant l’entrée DNS
Certains types de données spéciaux peuvent même ne pas être pris en charge du tout. Après la migration, il faut aussi mettre à jour les séquences, sinon on risque des erreurs de clé primaire dupliquée
En l’absence de clé primaire appropriée, cela peut aussi poser problème, car toute la ligne n’est pas toujours copiée d’un seul coup
Si les bases de données sont dans le même compte AWS et qu’on peut tolérer 4 à 5 minutes de downtime, une réplication au niveau matériel via une base de données globale ou des snapshots sera probablement plus simple
Récemment, nous avons déplacé une base de données PostgreSQL de 3 To auto-hébergée de la version 12 à la 16, tout en passant d’Ubuntu 18 à Ubuntu 22. Il a aussi fallu mettre à niveau plusieurs extensions en même temps, et en particulier Timescale, pour laquelle il n’existait pas de version compatible avec toutes les combinaisons
Nous avons procédé en mettant à niveau un réplica en lecture seule : au départ, nous étions sur PG12, Ubuntu 18 et TS2.9, et nous avons d’abord créé un réplica en lecture seule sous Ubuntu 22 en conservant PG12 et TS2.9
Ensuite, nous sommes passés en mode maintenance, avons arrêté tous les services, détaché le réplica en lecture seule, puis mis à niveau PG12 vers PG15 sous Ubuntu 22 tout en gardant TS2.9
Puis nous avons fait passer PG15 + TS2.9 à TS2.13, et enfin PG15 à PG16 sous Ubuntu 22 en conservant TS2.13
Pour finir, nous avons reconnecté les services au nouveau serveur de base de données, redémarré tous les services, puis quitté le mode maintenance
Toutes les étapes de mise à niveau de la base avaient été largement testées et automatisées avec Ansible, mais un problème qui n’était jamais apparu pendant les tests s’est produit et a porté le downtime à environ 30 minutes. C’était tout à fait acceptable pour notre usage
Si nous avions utilisé la réplication logique, nous aurions peut-être réduit les imprévus de dernière minute, et nous comptons étudier cette approche lors du prochain cycle de mise à niveau
Nous avons aussi étudié la réplication logique pour réduire le downtime de mise à niveau, mais comme le schéma de base de données et les commandes DDL ne sont pas répliqués, cela ne semblait pas recommandé dès qu’il y avait Timescale dans l’équation
Les changements de schéma sous-jacents que Timescale doit effectuer en interne dépendent probablement surtout de la taille des chunks d’hypertables et du profil des écritures entrantes ; on pourrait donc les planifier ou les synchroniser, mais nous avons estimé que la complexité et le risque potentiels étaient trop élevés par rapport à une courte fenêtre de maintenance pendant l’exécution de
pg_upgradeL’ennemi de ces migrations à faible downtime ou sans interruption, ce sont les requêtes longues
Par exemple, si vous avez une unique requête
updatequi dure 30 minutes, il faut soit la tuer et la rollback, soit accepter 30 minutes de perte de disponibilitéÀ ma connaissance, il n’existe actuellement aucun moyen de migrer une requête en cours d’exécution
statement_timeoutest votre amiS’il existe des transactions extrêmement longues, il y a de fortes chances qu’on puisse effectuer la bascule en évitant leur fenêtre d’exécution. Espérons qu’il s’agisse d’un job planifié plutôt que d’un phénomène aléatoire
En combinant une limite sur la durée des transactions avec une configuration de failover — par exemple en faisant échouer l’ancienne primary — ainsi que quelque chose comme pgbouncer, on peut contrôler assez finement un ralentissement temporaire au lieu d’un downtime
Franchement, ce qui m’inquiète davantage, c’est de savoir si l’ensemble de la stack et les serveurs DNS de cache externes dont elle dépend respectent correctement le DNS TTL
Mais en général, éviter une dizaine de secondes de downtime n’est pas critique pour une application, donc autant choisir la solution la plus simple pour son cas
updatede 30 minutes ne serait pas soit écrite de manière extrêmement inefficace, soit une migration ponctuelle de données à grande échelleCela dit, le premier cas existe bel et bien très souvent. J’ai souvent eu le plaisir de transformer des traitements qui prenaient des minutes ou des heures en opérations qui s’exécutent en millisecondes
Je me demande quel type de données, et quelles équipes, gèrent des écritures en base de données de cette nature au point de devoir compter sur le moteur de base de données pour travailler aussi longtemps, au lieu de découper cela plus finement en file d’attente au niveau supérieur
Le passage indiquant qu’ils ont créé un enregistrement DNS AWS Route53 pour
database.notifications.service.gov.ukavec un TTL d’1 seconde, puis modifié le script de migration pour que le poids DNS d’AWS envoie 100 % vers l’emplacement de la base cible avant d’attendre 1 seconde que le TTL expire, paraît étrangeDans ce cas, ils disent que la prochaine fois que l’application essaiera d’interroger la base de données, elle interrogera la base cible. Cela veut-il dire que leur ORM ou le comportement par défaut de Python bloque en faisant une résolution DNS à chaque requête ?
Cela signifie qu’il n’y a ni cache de l’adresse résolue pendant un certain temps, ni pooling ou réutilisation des connexions ?
getaddrinfoougethostname. Python réimplémente rarement les appels de bas niveau du système, donc cela dépend des réglages systèmeSi le TTL d’1 seconde a bien été respecté, l’adresse a sans doute été mise en cache pendant 1 seconde, mais il n’est pas rare que les bibliothèques de résolution DNS, et surtout les serveurs DNS de cache, ne respectent pas strictement le TTL. Franchement, cela pourrait aussi expliquer une partie de l’indisponibilité observée
Bien. Nous venons justement de migrer depuis RDS 3 clusters Postgres contenant environ 2 To de données et 8 bases de données, de Postgres 14 à 16. Nous avons eu une interruption de 00:00 à 04:00
Nous avons d’abord activé un site de remplacement ultra-léger, un « mode maintenance », exécuté sur Cloudflare Workers, puis réduit à 0 via Terraform toutes les applications qui utilisaient la BD
Ensuite, dans l’interface web AWS, nous avons cliqué sur le bouton de mise à niveau pour lancer
pg_upgradede 14 vers 15 et attendu la fin, puis recommencé pour passer de 15 à 16Nous avons attendu que la base de données recommence à accepter des connexions ; elle semblait en accepter avant même d’être marquée ready, et AWS paraissait faire autre chose en plus de
pg_upgradeAprès cela, nous avons lancé
VACUUM ANALYZE; REINDEX DATABASE CONCURRENTLY. L’objectif était d’éviter les problèmes de performance entre versions et de profiter des gains de performance de la nouvelle versionNous avons commencé à relancer les applications, attendu que chaque application ait quelques conteneurs démarrés, puis rouvert le trafic, désactivé le site de maintenance et sommes allés nous coucher
REINDEX CONCURRENTLYa continué à tourner pendant 18 heures supplémentaires sur la plus grosse base, mais sans rien bloquerLa prochaine fois, nous utiliserons AWS Blue/Green Deployments pour éviter l’interruption. Cette fois, nous ne pouvions pas, car nous n’étions pas sur 14.9, la version mineure minimale de la 14 prise en charge par Blue/Green
Si je devais le faire moi-même, je construirais mon propre Blue/Green avec réplication logique et load balancer, plutôt que de payer le surcoût AWS
pg_upgrade --hardlinkspour une mise à niveau sur placeJe l’ai déjà fait en moins d’une minute sur une instance Postgres on-prem avec une BD de 2 To
GOV.UK Notify fait partie de l’ensemble de services que le GDS fournit aux organismes publics britanniques. On y trouve aussi GOV.UK Pay et GOV.UK PaaS, et l’ensemble était à l’origine connu sous le nom de Government As A Platform
DMS est un outil de migration lamentable. J’ai fini par abandonner après avoir passé presque un mois à me battre avec divers problèmes de migration
Il n’arrivait pas à migrer les types text et json, et même le support AWS n’a pas pu proposer de solution
Nous avons utilisé AWS Blue/Green dès les premières phases de test, et c’est ce qui a rendu une mise à niveau presque sans interruption réaliste
C’est complètement cassé
Intéressant, mais je ne comprends pas pourquoi l’État utilise AWS au départ. Ce n’est pas une startup qui bricole pour trouver son adéquation produit-marché, ni une situation où il faut réagir à une hausse soudaine de trafic imprévisible due au marketing.
On sait que ce type de service sera nécessaire sur le long terme, et les schémas d’usage sont assez bien prévisibles.
On pourrait créer un cloud public ou adopter une approche on-premise raisonnable. Cela demande des financements, de la coordination et du leadership technique, mais à long terme cela ferait économiser énormément d’argent aux contribuables.
L’IT du secteur public est généralement en mauvais état, mais je sais aussi qu’il y a de bons ingénieurs qui y travaillent.
Une heure passée à courir après des disques durs et des pilotes, ou leurs équivalents cloud comme le stockage et des outils du type Ansible, c’est une heure qui n’est pas consacrée à construire ce dont les clients ont besoin.
Pourquoi l’État serait-il différent ?
Personne ne s’attend à ce que l’État fabrique lui-même des voitures ; on s’attend à ce qu’il les achète à Volkswagen ou Renault. Et ce, même s’il a clairement des besoins de transport. Alors pourquoi s’obstiner à vouloir construire soi-même l’infrastructure IT ?
Et puis il y a aussi des événements totalement imprévus, comme une pandémie. Le fait d’avoir pu monter en charge pour répondre à la demande pendant la pandémie a été l’une des démonstrations les plus parlantes de la nécessité, pour le secteur public, d’utiliser un cloud public commercial.
Je recommande de regarder cette présentation de septembre sur les différentes itérations de Gov.UK et les migrations entre clouds : https://youtube.com/watch?v=mpY1lxkikqM&pp=ygUOUmljaGFyZCB0b....
Au moins au Royaume-Uni, les exigences de la commande publique imposent de remettre tous les quelques années les estimations de consommation sur le marché.
Par exemple, si Oracle Cloud est dix fois moins cher, il y a de fortes chances qu’il remporte le contrat ; il faut alors migrer vers Oracle pour la durée du contrat, puis peut-être repartir ailleurs plus tard si un autre cloud encore moins cher apparaît.
C’était de loin la pire chose que j’aie vue de ma vie. Et pourtant j’ai travaillé sur beaucoup de legacy : VB.NET, Web Forms, de vieux SharePoint, Basic, et même des applis entières qui n’étaient qu’un énorme bloc de procédures stockées.
AWS, Azure et Google Cloud ont au moins été conçus en pensant à l’utilisateur final, c’est-à-dire au développeur. À l’inverse, les clouds gouvernementaux ont été conçus et construits par le moins-disant dont l’objectif premier était de rogner les coûts partout où c’était possible.
À l’inverse, j’ai aussi rencontré d’excellentes équipes infra/ops qui géraient le datacenter interne d’un organisme public de santé allemand. Le problème là-bas n’était ni la technique ni les gens, mais à 100 % le management et les processus qui cherchaient à devenir un goulot d’étranglement dans chaque interaction entre l’équipe infrastructure et l’équipe d’ingénierie.