• PagerDuty
    /
  • Blog
    /
  • Non classé
    /
  • Changez votre moteur : comment migrer votre pile MySQL/Rails, riche en IOPS, vers Unicode sans interruption de service

Blog

Changez votre moteur : comment migrer votre pile MySQL/Rails, riche en IOPS, vers Unicode sans interruption de service

par PagerDuty 30 octobre 2012 | 13 minutes de lecture

Out with the old, in with the New

Vous êtes un technicien travaillant pour l'une des nombreuses startups qui se sont précipitées sur le marché, où les fondateurs ont collé à la hâte un Rails application avec des emballages de barres chocolatées et du papier aluminium. Lorsqu'il est devenu évident que l'enthousiasme ne pouvait remplacer la puissance brute du codage, des développeurs ont été embauchés pour masquer les failles de l'architecture logicielle. Finalement, lorsque ceux Les développeurs ont réalisé à quel point l'application était une bête sauvage et ont embauché toi pour nettoyer le désordre et rendre les choses jolies.

Vous connaissez votre pile. Vous disposez d'une ancienne base de données MySQL ; probablement MySQL 5.0 ou 5.1. Elle a été configurée avec les paramètres par défaut (comprendre : nous prenons en charge l'anglais) dès le départ, et la seule véritable modification (« avancée ») ajoutée depuis est probablement une réplication asynchrone en mode esclave de lecture. Après des années de fonctionnement continu dans ce mode, vos développeurs ont mis au point des milliers de correctifs ingérables et catastrophiques pour permettre le stockage de certains caractères non ASCII dans les champs BLOB. Pendant ce temps, votre support se plaint que la plupart des utilisateurs rencontrent des erreurs en utilisant votre application avec leurs noms non romanisés, et la direction est agacée par le nombre impressionnant de fonctions de translittération subtilement différentes dans le code.

C'était la situation à PagerDuty il y a plusieurs mois, et cet article explique comment nous avons résolu le problème – comment nous sommes passés de MySQL 5.1 stockage latin1 (ISO-8859-1) caractères vers le brillant MySQL 5.5 avec Unicode ( UTF-8 ) personnages… et comment vous ne l’avez jamais remarqué.

Le problème avec MySQL en un mot

Les jeux de caractères utilisés par MySQL lors de l'écriture des données sur le disque imposent certaines limitations à votre application. Un utilisateur naïf pourrait prétendre que MySQL n'a pas besoin de connaître les jeux de caractères, ce qui serait logique si vous souhaitiez de faibles performances lors du tri de vos chaînes (char et varchar). vouloir Pour exploiter l'indexation de la base de données et effectuer des tris implicites côté serveur (tri côté client en cours de traitement : veuillez mourir), MySQL doit comprendre les caractères utilisés afin de disposer d'un contexte de tri autre que des valeurs ordinales. Malheureusement, le jeu de caractères par défaut reconnu par MySQL est le latin1, qui exclut les symboles utilisés dans environ 90 % du monde. Un jeu de caractères Unicode comme UTF-8 est bien plus approprié pour stocker des chaînes de symboles multinationaux sans recourir aux BLOB.

Les jeux de caractères MySQL sont intégrés à une colonne lors de sa création. Bien sûr, MySQL permet depuis longtemps d'utiliser ALTER TABLE et de modifier cette propriété, ce qui facilite le passage d'un jeu de caractères à un autre. Cependant, ALTER TABLE verrouille l'intégralité de la table lors de son fonctionnement, ce qui est néfaste pour les applications en production soumises à des écritures intensives, où les utilisateurs attendent une réactivité constante. Il est donc nécessaire d'avoir recours à une méthode plus complexe. Voici l'histoire de cette méthode.

Avant de commencer, lisez les exigences

Chez PagerDuty, nous avons perçu ce défi comme un obstacle technique surmontable qui ne devrait pas impacter notre activité. À savoir :

  • Tant que nous continuerons à utiliser MySQL, nous ne souhaitons plus jamais migrer les banques de données en raison de problèmes de stockage/d'entrée liés aux symboles (nous souhaitons accepter un ensemble de symboles universel).
  • Ce changement devait avoir au plus un impact négligeable sur les performances continues de l’application PagerDuty (aucune nouvelle infrastructure cloud ne pouvait être mise en place aux fins du débit des événements).
  • Corollaire : aucune ressource de stockage significative ne doit être nouvellement allouée pour accueillir les caractères MySQL codés en UTF-8 (nous autorisons au maximum 2 fois les anciennes exigences de stockage ; ce n'est pas déraisonnable étant donné que la plupart de nos utilisateurs utiliseront simplement des caractères romanisés, s'attendant à ce que tout autre échec).
  • L’ensemble de la procédure qui permet de mettre en œuvre ce projet devrait entraîner un temps d’arrêt négligeable (< 1 minute) pour nos utilisateurs.

Cela vous paraît ambitieux ? C'est le minimum de conditions qui nous a été imposé, et nous sommes heureux de pouvoir dire que nous les avons toutes remplies.

MySQL – Démêler une pléthore de folies

MySQL rend la conversion vers UTF-8 incroyablement pénible, afin d'essayer de masquer les limitations du InnoDB moteur. Nous commençons par discuter des problèmes liés aux index sur les données CHAR/VARCHAR, en supposant que vous utilisiez InnoDB (ce que nous avons utilisé, car au moins notre serveur ne provenait pas du l'âge de pierre ).

Saviez-vous qu'InnoDB impose des limites basses à la taille des index mono-colonne ? Nous non plus, mais nous avons découvert jusqu'où ces développeurs MySQL ont mis les bouchées doubles pour éviter que cela ne vous nuise, à vous, utilisateur imprudent. En effet, l'encodage « utf8 » de MySQL 5.1 n'est pas du véritable UTF-8. UTF-8 prend en charge les symboles de 1 à 4 octets. L'utf8 de MySQL prend en charge uniquement les symboles d'une taille comprise entre 1 et 3 octets Cela va à l'encontre de notre premier objectif : prendre en charge tous les personnages. Pour résoudre ce problème, que peu de surveillance, nous avons utilisé l'encodage « utf8mb4 » fourni dans MySQL 5.5 [1] … sauf que nous n'utilisions pas encore la version 5.5. Notre solution ce Le problème nécessitait de retourner les serveurs de base de données (j'avais dit qu'il y aurait un peu de temps d'arrêt !) – mais nous y reviendrons.

Les tests initiaux de MySQL 5.5 ont été positifs jusqu'à ce que nous essayions de recréer nos tables de production via mysqldump [2] avec l'encodage UTF-8 à la place de latin1 :

 mysqldump -d la_base_de_données | sed -e 's/(.*DEFAULT CHARSET=)latin1/1utf8mb4/' | mysql la_base_de_données_utf8 

Merci de ne pas nous frapper. Nous avons été surpris par cette étrange erreur :

 ERREUR 1071 (42000) : la clé spécifiée était trop longue ; la longueur maximale de la clé est de 767 octets 

Oh, malheur à MySQL ! Après avoir vu ce qu'il a vu, voyez ce qu'il voit ! En effet, si vous jetez un coup d'œil aux petits caractères, InnoDB ne prend en charge que les index à colonne unique d'une taille maximale de 767 octets C'est peut-être pour cette raison que l'encodage « utf8 » ne prend en charge qu'un maximum de 3 octets par caractère : cela signifie que les conversions vers l'utf8 à partir d'autres jeux de caractères fonctionnent lorsque des index de colonnes sont impliqués. Considérez ceci : pour que le comparateur d'index soit rapide, toutes les entrées doivent avoir la même taille : la taille combinée maximale de la ou des colonnes sur lesquelles elles sont réparties. Avec un VARCHAR(255), un type de cellule assez standard, et l'encodage utf8 paralysé de MySQL, max_length_of_string * max_size_of_char s'étend à 255 * 3 => 765. Avec utf8mb4, 255 * 4 => 1020. Oups. Quel problème.

Heureusement, le lien qui décrit cette limitation décrit également la solution de contournement (permettant à la taille de l'index d'atteindre un maximum de 3072 octets pour une seule colonne), ce qui conduit à certaines des lignes suivantes dans notre fichier /etc/my.cnf :

 [client] default-character-set = utf8mb4 [mysqld] default-storage-engine = INNODB sql-mode='NO_ENGINE_SUBSTITUTION' # file_per_table est requis pour large_prefix innodb_file_per_table # file_format = Barracuda est requis pour large_prefix innodb_file_format = Barracuda # large_prefix donne un maximum d'index à colonne unique de 3072 octets = win! # nous allons également définir ROW_FORMAT=DYNAMIC sur chaque table, cependant. innodb_large_prefix character-set-client-handshake = FALSE collation-server = utf8mb4_unicode_ci init-connect='SET collation_connection = utf8mb4_unicode_ci' init-connect='SET NAMES utf8mb4' character-set-server = utf8mb4 [mysqldump] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 

Nous sommes convaincus qu'il existe une méthode plus concise pour obtenir ce que vous souhaitez de MySQL, mais comme le dit le vieil adage : « Décoller, détruire le site depuis l'orbite… c'est la seule façon d'être sûr. » Si vous démarrez un serveur avec ce fichier my.cnf et des instructions mysql_install_db, CREATE TABLE spécifiant ROW_FORMAT=DYNAMIQUE volonté faire la bonne chose , et vous fournit des chaînes CHAR/VARCHAR qui peuvent être indexées, tout en prenant en charge tous les symboles dont vous pourriez avoir besoin.

Il existe un problème similaire : les index multicolonnes sont également limités à 3 072 octets. Ce problème pourrait être plus complexe à résoudre. Nous n'avions pas de solution miracle : un seul index composite était concerné, et cet index s'exécutait sur une table contenant peu de lignes (et donc modifiable). L'index s'exécutait sur la colonne phone_number, qui était inutilement de type VARCHAR(255). Une simple commande ALTER TABLE (ou plutôt sa cousine abstraite, la « migration » Rails) a donc réglé le problème et réduit sa taille.

Classements : Apprenez à ne plus vous inquiéter et à aimer Unicode

Nous pensions qu'une augmentation soudaine des index transformerait notre serveur MySQL, pourtant performant, en un mastodonte encombrant. Il s'est avéré que ce ne fut pas le cas : le résultat final de notre migration fut très neutre, voire hésitant. gain de vitesse Si vous utilisez une application Rails semi-moderne, il y a de fortes chances que ce soit également le cas. La raison ? Les classements.

Les classements indiquent à MySQL comment trier les chaînes de caractères pour qu'elles soient cohérentes ; intuitivement, la chaîne « abc » précède « bbc » dans un tri croissant, car le « a » initial précède alphabétiquement le « b ». Cependant, des caractères complexes nécessitent des règles encore plus complexes. Par exemple, Institut allemand de normalisation (DIN) définit deux classements latin1 possibles : le DIN-1 (dictionnaire allemand) définit le symbole « ß » comme équivalent à « s », et le DIN-2 (annuaires téléphoniques allemands) définit ß comme « ss » (entre autres différences). Une fois la réduction effectuée, un tri lexical standard (anglais) est utilisé.

Ceci est important lorsque les connexions client veulent une requête ordonnée par un champ de chaîne, et vous avez donc besoin quelques Un moyen de trier les chaînes. Les classements MySQL, utilisés correctement, permettent ce tri quasiment gratuitement (à condition de disposer d'un index sur les champs de chaîne). Une condition souvent négligée pour bénéficier de cet avantage est que le client et le serveur doivent utiliser le même jeu de caractères et le même classement. Il s'avère que, jusqu'à notre migration de base de données UTF-8, ce n'était pas le cas chez PagerDuty.

Pensez à votre application Rails. Il y a de fortes chances que vous utilisiez l'un des deux MySQL/Ruby ou le mysql2 gem pour alimenter ActiveRecord. Ils lisent depuis un fichier database.yml qui spécifie   quoi Comme type d'encodage ? Ah bon, utf8 ? Si vous fouillez un peu dans le code de gem (ce que nous avons fini par faire), devoir faire ), vous remarquerez que cet encodage est transmis aux paramètres de connexion MySQL ; il devient le jeu de caractères (et définit le classement) utilisé pour communiquer avec MySQL. Le fait que vous le définissiez sur UTF-8 tout en communiquant avec une base de données basée sur Latin1 est au cœur de l'accélération que vous allez réaliser.

Fait : pendant tout ce temps, vous avez gaspillé des cycles CPU à trier. MySQL a fait abstraction de cela et vous a fourni des chaînes triées dans un ordre compréhensible par votre application cliente (Rails), tout en devant maintenir un mappage entre une collation UTF-8 (probablement utf8_general_ci) et la collation associée à vos tables. Vous ne me croyez pas ? Observez ce qui se passe lorsque vous configurez le client et le serveur pour qu'ils s'exécutent tous deux en utf8mb4 avec la même collation (nous avons choisi utf8mb4_unicode_ci ; voir ici (pour une discussion sur les différences de classement Unicode dans MySQL). Profitez de l'accélération. Remerciez-moi plus tard.

Maintenez vos données en vie : migrez, répliquez et mettez à niveau

Après tout ce texte, nous arrivons enfin au point le plus délicat : comment migrer votre ancienne base de données alimentée au charbon ? Vous avez déjà vu une astuce astucieuse utilisant mysqldump + sed pour charger des données sur un nouveau serveur. Mais votre ancienne base de données est toujours en cours d'écriture – et maintenant ? La solution consiste pour MySQL à vous mettre la puce à l'oreille en insistant sur la connaissance et la séparation des jeux de caractères client/serveur.

Nous aimerions bien comprendre le fonctionnement interne de ce système, mais je n'ai malheureusement pas pu prendre le temps de lire le code source de MySQL ni trouver d'informations fiables à ce sujet. En utilisant le fichier de configuration ci-dessus pour notre nouveau serveur, la simple configuration de la réplication maître/esclave entre notre ancienne base de données et la nouvelle version UTF-8 5.5 a fonctionné parfaitement. Nous avons testé toutes sortes de caractères latin1 insérés dans l'ancienne base de données, et ils sont revenus sans problème dans la copie répliquée. MySQL effectuait toutes les traductions correctement, et nous n'avions plus qu'à observer. Une fois l'effet hypnotique passé, il était temps de passer aux choses sérieuses : tous nos serveurs web devaient avoir leur propre client mysql Paquets mis à jour. En effet, mysql-client 5.1 ne prend pas en charge l'utf8mb4 et rencontrera également des difficultés à communiquer avec votre serveur 5.5.

Pour ce faire, nous avons utilisé cuisinier Pour lancer rapidement de nouveaux serveurs back-end d'applications – des clones de nos serveurs existants –, mais avec la nouvelle version du client MySQL et configurés de manière à désactiver les tâches d'arrière-plan (toutes les tâches de traitement des files d'attente et asynchrones de PagerDuty). Le fruit du cloud, parfois utile ! Ces serveurs pointaient vers la base de données esclave et étaient déjà configurés (via les paramètres d'environnement Chef, bien plus pertinents pour les utilisateurs de Chef) pour prendre en charge pleinement l'utf8mb4 jusqu'à Rails via MySQL2. Une fois ces serveurs prêts (et quelques tests pour vérifier leur bon fonctionnement), nous étions prêts à transformer notre base de données.

Fais-le, fais-le maintenant ! Allez, retourne-moi !

Le processus de retournement est ce moment extrêmement risqué où vous n'êtes pas sûr que tout fonctionnera ou que vous avez oublié un détail crucial, et où vos clients sont sur le point d'être très mécontents. Vous parcourez nerveusement votre liste de contrôle, en veillant à souligner le moment critique de non-retour. Dans notre retournement, nous avions les éléments suivants :

  • fermer les travailleurs en arrière-plan actuels sur les anciens backends d'application
  • (à ce stade, nous ne traitons plus l'envoi de notifications, mais nous mettons toujours les demandes en file d'attente)
  • verrouiller la base de données principale
  • (à ce stade, nous sommes complètement arrêtés – c'est le temps d'arrêt dont vous avez été averti ! Les nouvelles demandes sont gelées)
  • arrêter et réinitialiser l'esclave
  • exécuter Chef sur nos équilibreurs de charge orientés client, en les intégrant au nouvel environnement Chef et en les modifiant pour utiliser nos machines nouvellement lancées comme backends d'application
  • (à ce stade, nous acceptons à nouveau les demandes, les demandes de pré-retournement expireront)
  • faire tourner les travailleurs en arrière-plan sur les nouveaux backends d'application
  • (à ce stade, nous sommes pleinement fonctionnels)
  • mettre fin aux anciens backends d'application

Comme vous pouvez l'imaginer puisque je vous en parle maintenant, tout cela s'est déroulé sans erreur. Vous avez lu dans un autre post comment fonctionnent nos processus d'arrière-plan et à quel point il est facile de créer des scripts surveillance , notamment en collaboration avec le chef, pour arrêter et relancer nos tâches en arrière-plan. Chef-client Grâce au travail acharné de notre équipe opérationnelle, les exécutions sur nos équilibreurs de charge se terminent généralement en 20 secondes. Nous savions donc que ce serait la limite maximale de notre temps d'arrêt. Les seules commandes SQL que nous avons dû exécuter pour activer le système étaient :

(sur le maître)

 DÉBUT; RINCER LES TABLES AVEC VERROUILLAGE EN LECTURE ; 

(sur l'esclave, une fois que vous avez vérifié qu'il a rattrapé le maître)

 ARRÊTER L'ESCLAVE ; RÉINITIALISER L'ESCLAVE ; 

Et voilà. Le stress était parti, et vous, le client, avez à peine remarqué que nous ignorions temporairement vos événements. Il ne restait plus qu'à reconfigurer tranquillement nos esclaves et nos sauvegardes pour cibler un nouveau serveur MySQL. Oh, et Rails avait besoin d'un peu d'amour ; voici ce que nous avons ajouté au fichier config/initializers/activerecord_ext.rb de notre application :

 module ActiveRecord module ConnectionAdapters module SchemaStatements def create_table_with_dynamic_row_format(table_name, options = {}, &block) new_options = options.dup new_options[:options] ||= '' new_options[:options] << ' DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC' create_table_without_dynamic_row_format(table_name, new_options, &block) end alias_method_chain :create_table, :dynamic_row_format end end end 

Autopsie

Si vous êtes arrivé jusqu'ici, félicitations, cher lecteur ! Vous êtes dévoué. J'espère que cet article vous inspirera à faire le bien et à effacer les traces d'années de candidatures majoritairement en latin1. ton Entreprise. Offrez une bonne refonte à ce moteur. Attention cependant… dans le monde technologiquement mal implémenté d'Unicode, l'excitation est sans fin. Il y a toujours de nouveaux composants à intégrer au 21 St Mélange de langues du 19e siècle.

[1] Si vous vous opposez Unification des Han , alors après tous les efforts ici, nous ne soutenons malheureusement toujours pas vos personnages éclectiques.

[2] Votre mysqldump doit être configuré pour utiliser le jeu de caractères utf8 (un sur-ensemble strict de latin1) pour générer vos fichiers texte, sinon vous risquez de voir un tas de charabia inséré dans votre nouvelle base de données.