Comment rattraper des millions de lignes dans une base Rails sans interruption de service
Rattraper une table de quarante millions de lignes n'est pas le même problème que d'en rattraper une de quarante mille. Ce qui change, et comment nous le régulons en toute sécurité.
Publié le 18 juin 2023 · 4 min de lecture
Remplir une nouvelle colonne sur une table de quarante millions de lignes n'est pas le même problème que le faire sur une table de quarante mille. En dessous d'une certaine taille, une mise à jour en masse unique fonctionne très bien. Au-dessus, la même ligne de code peut verrouiller la table, dépasser le statement timeout ou gonfler le journal de transactions au point de remplir le disque.
Toujours par lots
Invoice.where(currency: nil).in_batches(of: 2_000) do |batch|
batch.update_all(currency: "EUR")
sleep 0.05 # laisser les réplicas et l'autovacuum suivre
end
Chaque lot est sa propre transaction : en cas d'échec en cours de route, vous disposez d'un point de reprise au lieu d'une opération tout ou rien couvrant toute la table. Le petit sleep entre les lots n'est pas de la paresse : il laisse au retard de réplication et à l'autovacuum le temps de suivre le volume d'écriture au lieu de prendre un retard définitif.
À exécuter en dehors du cycle de requêtes et de déploiement
Une tâche rake ou un job en arrière-plan, jamais une migration exécutée pendant le déploiement. Une migration qui bloque un déploiement pendant vingt minutes le temps du rattrapage est une panne que l'on s'inflige soi-même ; un job qui exécute la même logique en arrière-plan, supervisé et reprenable en cas d'échec, n'en est pas une.
Surveillez la base de données, pas seulement le job
Le retard de réplication, l'activité de l'autovacuum et les I/O disque sont les métriques qui disent réellement si un backfill peut continuer à son rythme actuel, pas le journal de progression du job lui-même. Sur toute table qui reçoit du trafic de production actif, nous régulons dynamiquement la taille des lots et l'intervalle de sleep en fonction du retard de réplication, en ralentissant automatiquement dès que la base montre des signes de décrochage.
Les backfills qui alimentent un index demandent une attention particulière
Remplir une nouvelle colonne qui sera immédiatement indexée double le coût d'écriture de chaque lot : la mise à jour de la ligne, puis la maintenance de l'index qui en découle. Quand c'est possible, nous remplissons d'abord la colonne, puis nous construisons l'index de manière concurrente ensuite, plutôt que de faire payer le coût de l'index à chaque lot dès le départ.
Articles liés
Migrations de base de données sans interruption avec Rails : un guide pratique
Pourquoi les migrations qui paraissent anodines sont celles qui cassent la production, et la séquence que nous suivons pour modifier un schéma sans verrouiller une table très sollicitée.
Les transactions de base de données dans Rails : ce que signifie vraiment ACID pour votre code
ActiveRecord rend les transactions très faciles à utiliser. Ce que promet réellement chacune des quatre garanties ACID, et où les développeurs Rails se trompent sur l'isolation.