- PagerDuty /
- Blog /
- Nicht kategorisiert /
- Upgrade Ihrer Engine: So migrieren Sie Ihren IOPS-intensiven MySQL/Rails-Stack ohne Ausfallzeiten zu Unicode
Blog
Upgrade Ihrer Engine: So migrieren Sie Ihren IOPS-intensiven MySQL/Rails-Stack ohne Ausfallzeiten zu Unicode

Du bist ein IT-Experte und arbeitest für eines der unzähligen Startups, die überstürzt auf den Markt gedrängt wurden und deren Gründer hastig ein System zusammengeklebt haben. Schienen Die App wurde zusammen mit Schokoriegelpapier und Alufolie verpackt. Als klar wurde, dass Begeisterung kein Ersatz für Programmierkenntnisse war, wurden Entwickler eingestellt, um die Lücken in der Softwarearchitektur zu kaschieren. Schließlich, als diese Den Entwicklern wurde klar, welch ein unbändiges Biest die App war, und sie stellten ein... Du um das Chaos zu beseitigen und alles wieder schön zu machen.
Sie kennen Ihren Stack. Sie verwenden eine ältere MySQL-Datenbank, wahrscheinlich Version 5.0 oder 5.1. Sie wurde von Anfang an mit Standardeinstellungen eingerichtet (sprich: Englisch wird unterstützt), und die einzige wirkliche Änderung („Fortschritt“), die seitdem vorgenommen wurde, ist vermutlich die Einrichtung eines Read-Slaves und asynchroner Replikation. Nach jahrelangem Betrieb in diesem Modus haben Ihre Entwickler unzählige schwer wartbare und umständliche Workarounds entwickelt, um die Speicherung einiger Nicht-ASCII-Zeichen in BLOB-Feldern zu ermöglichen. Gleichzeitig beschwert sich Ihr Support-Team, dass fast jeder Benutzer weltweit Fehler bei der Verwendung Ihrer Anwendung aufgrund nicht-romanisierter Namen erhält, und die Geschäftsleitung ist genervt von der Vielzahl an leicht unterschiedlichen Transliterationsfunktionen im Code.
Dies war die Situation bei PagerDuty vor einigen Monaten, und dieser Artikel beschreibt, wie wir das Problem gelöst haben – wie wir den Übergang von MySQL 5.1 Speicherung latin1 (ISO-8859-1)-Zeichen auf das glänzende MySQL 5.5 mit Unicode ( UTF-8 ) Charaktere… und wie du es nie bemerkt hast.
Das Problem mit MySQL in Kürze
Die von MySQL beim Schreiben von Daten auf die Festplatte verwendeten Zeichensätze schränken Ihre Anwendung ein. Ein unerfahrener Benutzer könnte behaupten, MySQL müsse nichts über Zeichensätze wissen, was Sinn ergeben würde, wenn Sie beim Sortieren Ihrer Zeichenketten (CHARs und VARCHARs) eine schlechte Performance in Kauf nehmen wollten. Da Sie wollen Um die Datenbankindizierung für implizite serverseitige Sortierungen zu nutzen (clientseitige Sortierung im Prozess ist nicht empfehlenswert), muss MySQL die verwendeten Zeichen verstehen, um einen Kontext für die Sortierung zu haben, der über den reinen Ordinalwert hinausgeht. Leider unterstützt MySQL standardmäßig den Zeichensatz Latin-1, der Symbole ausschließt, die in etwa 90 % der Welt verwendet werden. Ein Unicode-Zeichensatz wie UTF-8 ist deutlich besser geeignet, wenn Sie Zeichenketten mit internationalen Symbolen speichern möchten, ohne auf BLOBs zurückgreifen zu müssen.
MySQL legt die Zeichensätze fest, die beim Erstellen einer Spalte in diese eingebettet werden. Zwar ermöglicht MySQL schon lange die Verwendung von `ALTER TABLE`, um diese Eigenschaft zu ändern und so den Wechsel zwischen verschiedenen Zeichensätzen zu vereinfachen, doch sperrt `ALTER TABLE` die gesamte Tabelle während der Ausführung. Dies ist ungeeignet für produktive Anwendungen mit hohem Schreibaufkommen, bei denen die Benutzer eine kontinuierliche Reaktionsfähigkeit erwarten. Eine etwas komplexere Lösung ist erforderlich. Dies ist die Geschichte dieser Lösung.
Bevor Sie beginnen, lesen Sie die Anforderungen.
Wir bei PagerDuty sahen diese Herausforderung als ein überwindbares technisches Hindernis, das unser Geschäft nicht beeinträchtigen sollte. Genauer gesagt:
- Solange wir MySQL verwenden, wollen wir nie wieder Datenspeicher aufgrund von symbolbezogenen Speicher-/Eingabeproblemen migrieren müssen (wir wollen einen universellen Symbolsatz akzeptieren).
- Dieser Wechsel durfte die laufende Leistung der PagerDuty Anwendung höchstens geringfügig beeinträchtigen (es durfte keine neue Cloud-Infrastruktur für den Ereignisdurchsatz aufgebaut werden).
- Folgerung: Es sollten keine nennenswerten neuen Speicherressourcen für die Unterbringung von UTF-8-kodierten MySQL-Zeichen zugewiesen werden (wir erlauben höchstens das Doppelte des alten Speicherbedarfs; dies ist nicht unvernünftig, da die meisten unserer Benutzer einfach romanisierte Zeichen verwenden und erwarten, dass alles andere fehlschlägt).
- Der gesamte Prozess, der zur Veröffentlichung dieses Produkts führt, sollte für unsere Benutzer eine vernachlässigbare Ausfallzeit (< 1 Minute) verursachen.
Klingt ambitioniert? Dies sind die Mindestbedingungen, die uns gestellt wurden, und wir freuen uns, sagen zu können, dass wir alle erfüllt haben.
MySQL – Entwirrung eines wahren Chaos
MySQL gestaltet die Konvertierung nach UTF-8 unglaublich umständlich, um die Einschränkungen von … zu verschleiern. InnoDB Wir beginnen mit der Diskussion von Problemen mit Indizes für CHAR/VARCHAR-Daten, wobei wir davon ausgehen, dass Sie InnoDB verwenden (was wir auch getan haben, da zumindest unser Server nicht von der Engine stammte). Steinzeit ).
Wussten Sie, dass InnoDB die Größe von Einzelspaltenindizes stark beschränkt? Wir wussten es auch nicht, aber wir haben herausgefunden, wie weit die gerissenen MySQL-Entwickler gegangen sind, um Sie, den ahnungslosen Benutzer, vor diesem Problem zu schützen. Die „utf8“-Kodierung von MySQL 5.1 ist nämlich kein echtes UTF-8. UTF-8 unterstützt Symbole mit einer Länge von 1 bis 4 Byte. Unterstützt nur Symbole mit einer Größe zwischen 1 und 3 Byte. Dies widerspricht unserem ersten Ziel – alle Charaktere zu unterstützen. Um das Problem zu lösen, … Das wenig Aufsicht, wir haben die in MySQL 5.5 bereitgestellte „utf8mb4“-Kodierung [1] … nur dass wir noch nicht Version 5.5 verwendeten. Unsere Lösung für Das Das Problem erforderte einen Servertausch (ich hatte ja gesagt, dass es zu kurzen Ausfallzeiten kommen würde!) – aber dazu kommen wir noch.
Die ersten Tests von MySQL 5.5 verliefen positiv, bis wir versuchten, unsere Produktionstabellen mit mysqldump nachzubilden. [2] mit UTF-8-Kodierung anstelle von latin1:
mysqldump -d the_database | sed -e 's/(.*DEFAULT CHARSET=)latin1/1utf8mb4/' | mysql the_database_utf8
Bitte schlagen Sie uns nicht. Wir waren von diesem seltsamen Fehler völlig überrascht:
FEHLER 1071 (42000): Der angegebene Schlüssel war zu lang; die maximale Schlüssellänge beträgt 767 Bytes.
Wehe MySQL! Was es schon alles gesehen hat, was sieht es erst! Wahrlich, wenn man nur einen Blick ins Kleingedruckte wirft … InnoDB unterstützt nur einspaltige Indizes mit einer maximalen Größe von 767 Byte. Vielleicht unterstützt die „utf8“-Kodierung deshalb nur maximal 3 Byte pro Zeichen: Konvertierungen von anderen Zeichensätzen nach utf8 funktionieren nur mit Spaltenindizes. Für einen schnellen Indexvergleich müssen alle Einträge die gleiche Größe haben: die maximale Gesamtgröße der Spalten, über die sie verteilt sind. Bei einem VARCHAR(255), einem gängigen Zelltyp, und der eingeschränkten utf8-Kodierung von MySQL ergibt max_length_of_string * max_size_of_char 255 * 3 = 765. Bei utf8mb4 sind es 255 * 4 = 1020. Ups! Was für eine Zwickmühle!
Glücklicherweise beschreibt der Link, der diese Einschränkung erläutert, auch die Umgehungslösung (die es ermöglicht, die Indexgröße für eine einzelne Spalte auf maximal 3072 Bytes zu erhöhen), was zu folgenden Zeilen in unserer Datei /etc/my.cnf führte:
[client] default-character-set = utf8mb4 [mysqld] default-storage-engine = INNODB sql-mode='NO_ENGINE_SUBSTITUTION' # file_per_table ist für large_prefix erforderlich innodb_file_per_table # file_format = Barracuda ist für large_prefix erforderlich innodb_file_format = Barracuda # large_prefix ermöglicht maximale Einzelspaltenindizes von 3072 Bytes = Vorteil! # Wir müssen jedoch auch ROW_FORMAT=DYNAMIC für jede Tabelle setzen. 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
Wir sind überzeugt, dass es einen prägnanteren Weg gibt, Ihre Ziele mit MySQL zu erreichen, aber wie das alte Sprichwort in diesem Zusammenhang besagt: „Starten Sie ab, sprengen Sie die Website aus dem Orbit … nur so können Sie sichergehen.“ Wenn Sie einen Server mit dieser my.cnf-Datei und mysql_install_db starten, geben CREATE TABLE-Anweisungen Folgendes an: ROW_FORMAT=DYNAMIC Wille Das Richtige tun und liefern Ihnen CHAR/VARCHAR-Zeichenketten, die indiziert werden können, und unterstützen gleichzeitig alle Symbole, die Sie sich jemals wünschen könnten.
Hier besteht ein ähnliches Problem: Mehrspaltige Indizes sind ebenfalls auf maximal 3072 Byte beschränkt. Dieses Problem könnte schwieriger zu lösen sein. Wir hatten keine elegante Lösung dafür – nur ein zusammengesetzter Index war betroffen, und dieser Index erstreckte sich zufällig über eine Tabelle mit wenigen Zeilen (die somit per ALTER TABLE geändert werden konnte). Der Index erstreckte sich über die Spalte `phone_number`, die unnötigerweise vom Typ `VARCHAR(255)` war. Ein kurzer ALTER TABLE-Befehl (bzw. dessen abstrahiertes Pendant, die Rails-„Migration“) erledigte dies für uns und reduzierte die Größe des Index.
Sammlungen: Lernen Sie, sich keine Sorgen mehr zu machen und Unicode zu lieben
Wir gingen davon aus, dass die plötzliche Vergrößerung der Indizes unseren schnellen MySQL-Server in ein schwerfälliges Monstrum verwandeln würde. Dies erwies sich jedoch als Irrtum – das Endergebnis unserer Migration war sehr neutral, oder bewegte sich gerade noch im Bereich eines leichten Unterschieds. Geschwindigkeitszuwachs Wenn Sie eine halbwegs moderne Rails-Anwendung betreiben, trifft dies höchstwahrscheinlich auch auf Sie zu. Der Grund? Sortierungen.
Sortierungen geben MySQL vor, wie Zeichenketten sinnvoll sortiert werden sollen; intuitiv erscheint die Zeichenkette „abc“ in einer aufsteigenden Sortierung vor „bbc“, da das führende „a“ alphabetisch vor dem „b“ steht. Komplizierte Zeichen erfordern jedoch komplexere Regeln. Zum Beispiel… Deutsches Institut für Normung DIN definiert zwei mögliche Latin-1-Sortierungen: DIN-1 (deutsches Wörterbuch) definiert das Symbol „ß“ als äquivalent zu „s“, DIN-2 (deutsche Telefonbücher) definiert ß = „ss“ (neben anderen Unterschieden). Nach der Reduktion wird eine standardmäßige (englische) lexikalische Sortierung verwendet.
Dies ist wichtig, wenn Clientverbindungen eine nach einem Zeichenkettenfeld sortierte Abfrage wünschen, und daher benötigen Sie manche Eine Möglichkeit, Zeichenketten zu sortieren. MySQL-Sortierungen bieten diese Sortierung bei korrekter Anwendung praktisch kostenlos (vorausgesetzt, es existiert ein Index für die Zeichenkettenfelder). Eine oft übersehene Voraussetzung für diesen Vorteil ist, dass Client und Server denselben Zeichensatz und dieselbe Sortierung verwenden müssen. Wie sich herausstellte, war dies bei PagerDuty bis zu unserer UTF-8-Datenbankmigration nicht der Fall.
Betrachten Sie Ihre Rails-Anwendung. Wahrscheinlich verwenden Sie entweder die MySQL/Ruby oder die mysql2 Ein Gem, das ActiveRecord unterstützt. Es liest aus einer database.yml-Datei, die Folgendes angibt: Was Als Kodierungstyp? Oh, UTF-8? Wenn man sich eine Weile den Gem-Code ansieht (was wir schließlich getan haben), müssen Sie werden feststellen, dass diese Kodierung an die MySQL-Verbindungseinstellungen übergeben wird; sie wird zum Zeichensatz (und definiert die Sortierung), der für die Kommunikation mit MySQL verwendet wird. Die Tatsache, dass Sie diese auf UTF-8 eingestellt haben, während Sie mit einer Datenbank kommunizieren, die auf Latin-1 basiert, ist der Kern der Geschwindigkeitssteigerung, die Sie gleich erzielen werden.
Fakt ist: Sie haben die ganze Zeit CPU-Zyklen mit dem Sortieren verschwendet. MySQL hat dies abstrahiert und Ihnen Strings in einer Reihenfolge bereitgestellt, die Ihre Client-Anwendung (Rails) versteht. Dabei muss die Zuordnung zwischen einer UTF-8-Sortierung (wahrscheinlich utf8_general_ci) und der Sortierung Ihrer Tabellen verwaltet werden. Sie glauben mir nicht? Beobachten Sie, was passiert, wenn Sie Client und Server auf utf8mb4 mit derselben Sortierung einstellen (wir haben utf8mb4_unicode_ci gewählt; siehe [Link einfügen]). Hier (Hier folgt eine Diskussion über die Unterschiede in der Unicode-Sortierung in MySQL). Viel Spaß mit der Geschwindigkeitssteigerung. Danke mir später.
Sorgen Sie dafür, dass Ihre Daten reibungslos laufen: Migrieren + Replizieren + Aktualisieren
Nach so viel Text kommen wir endlich zum kniffligen Teil: Wie migrieren Sie Ihren alten, kohlebasierten Datenspeicher? Sie haben bereits einen cleveren Trick mit mysqldump und sed kennengelernt, um Daten auf einen neuen Server zu laden. Ihre alte Datenbank wird aber noch beschrieben – was nun? Die Lösung: MySQL erweist Ihnen einen Gefallen, indem es darauf besteht, die Zeichensätze von Client und Server zu kennen und zu trennen.
Wir hätten gerne die Funktionsweise im Detail kennengelernt, aber leider hatte ich keine Zeit, den MySQL-Quellcode zu lesen oder verlässliche Informationen dazu zu finden. Mit der oben genannten Konfigurationsdatei für unseren neuen Server funktionierte die Einrichtung der Master/Slave-Replikation zwischen unserer alten Datenbank und der neuen UTF-8-Datenbank (Version 5.5) einwandfrei. Wir testeten alle möglichen Latin-1-Zeichen, die in die alte Datenbank eingefügt wurden, und sie erschienen problemlos in der replizierten Kopie. MySQL führte alle korrekten Übersetzungen durch, und wir konnten uns entspannt zurücklehnen. Nachdem der anfängliche Zauber verflogen war, ging es an die Arbeit – genauer gesagt, mussten alle unsere Webserver entsprechend konfiguriert werden. mysql-client Die Pakete wurden aktualisiert. Wie Sie sehen, unterstützt mysql-client 5.1 kein utf8mb4 und wird daher auch Probleme bei der Kommunikation mit Ihrem 5.5-Server haben.
Um dies zu erreichen, haben wir Folgendes verwendet: Küchenchef Um schnell neue App-Backend-Server – Klone unserer bestehenden Server – mit der neuen MySQL-Client-Version zu erstellen und die Hintergrundprozesse (die gesamte Warteschlangenverarbeitung und asynchronen Aufgaben von PagerDuty) zu deaktivieren, haben wir diese Server entsprechend konfiguriert. Die wunderbaren Vorteile der Cloud – manchmal sogar nützlich! Diese Server griffen auf die Slave-Datenbank zu und waren bereits (über die Chef-Umgebungseinstellungen, die für Chef-Nutzer deutlich verständlicher sind) so konfiguriert, dass sie UTF-8MB4 über MySQL2 vollständig in Rails unterstützen. Nachdem diese Server einsatzbereit waren (und wir sie getestet hatten, um sicherzustellen, dass sie wie in unseren Testumgebungen funktionierten), konnten wir unsere Datenbank umstellen.
Mach schon, mach schon jetzt! Los, dreh mich um!
Der Umstiegsprozess ist dieser unglaublich riskante Moment, in dem man sich einfach nicht sicher ist, ob alles funktionieren wird oder ob man ein entscheidendes Detail übersehen hat, und die Kunden kurz davor stehen, sehr unzufrieden zu sein. Nervös geht man die Checkliste durch und stellt sicher, dass man den kritischen Moment, in dem es kein Zurück mehr gibt, nicht vergisst. Bei unserem Umstieg hatten wir die folgenden Komponenten:
- Die aktuellen Hintergrundprozesse auf den alten App-Backends wurden abgeschaltet.
- (An diesem Punkt werden keine Benachrichtigungen mehr versendet, Anfragen werden aber weiterhin in die Warteschlange gestellt.)
- Sperre die Masterdatenbank
- (An diesem Punkt stehen wir komplett still – dies ist die Ausfallzeit, vor der Sie gewarnt wurden! Neue Anfragen werden derzeit nicht angenommen.)
- Stoppen und zurücksetzen Sie den Slave
- Wir werden Chef auf unseren kundenseitigen Load-Balancern ausführen, diese in die neue Chef-Umgebung integrieren und sie so umstellen, dass sie unsere neu eingerichteten Maschinen als App-Backends nutzen.
- (An dieser Stelle nehmen wir wieder Anfragen entgegen, Anfragen vor dem Verkaufsstart werden nach Ablauf einer bestimmten Zeit abgebrochen.)
- Hintergrundprozesse auf den neuen App-Backends starten
- (An diesem Punkt sind wir voll funktionsfähig)
- Die alten App-Backends beenden
Wie Sie sich vorstellen können, da ich ja jetzt mit Ihnen darüber spreche, wurden all diese Schritte fehlerfrei ausgeführt. Sie haben gelesen in einem anderen Beitrag wie unsere Hintergrundprozesse ablaufen und wie einfach sich skripten lässt. Monitor insbesondere in Verbindung mit chef, um unsere Hintergrundprozesse herunterzufahren und wieder zu starten. Chef-Kunde Die Ausführung von Prozessen auf unseren Load Balancern ist dank des unermüdlichen Einsatzes unseres Betriebsteams in der Regel innerhalb von 20 Sekunden abgeschlossen. Daher war uns klar, dass dies die maximale Ausfallzeit darstellen würde. Die einzigen SQL-Befehle, die wir ausführen mussten, um die Prozesse in Gang zu setzen, waren:
(auf dem Master)
BEGIN; FLUSH TABLES WITH READ LOCK;
(auf dem Slave-Server, sobald Sie überprüft haben, dass er mit dem Master-Server synchronisiert ist)
Sklave stoppen; Sklave zurücksetzen;
Das war's. Der Stress war weg, und Sie als Kunde haben kaum bemerkt, dass wir Ihre Anfragen vorübergehend ignoriert haben. Wir mussten lediglich unsere Slave-Server und Backups in Ruhe neu konfigurieren, um einen neuen MySQL-Server zu verwenden. Ach ja, und Rails brauchte etwas Zuwendung; hier ist, was wir in der Datei `config/initializers/activerecord_ext.rb` unserer Anwendung hinzugefügt haben:
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
Post-Mortem
Wenn Sie es bis hierher geschafft haben, herzlichen Glückwunsch, lieber Leser – Sie sind engagiert. Hoffentlich inspiriert Sie dieser Artikel dazu, Gutes zu tun und die Spuren jahrelanger, überwiegend auf Latin-1 basierender Anwendungen zu beseitigen. dein Firma. Verpassen Sie dem Motor eine gründliche Überholung. Aber Vorsicht … in der technologisch fehlerhaft implementierten Welt von Unicode hört die Aufregung nie auf. Es gibt immer weitere Komponenten, die in das 21. Jahrhundert integriert werden müssen. st -jahrhundert-Sprachmix.
[1] Wenn Sie sich dagegen aussprechen Han-Vereinigung Dann müssen wir uns leider nach all den Bemühungen hier mit Ihren vielseitigen Charakteren anfreunden.
[2] Ihr mysqldump sollte so eingestellt sein, dass er den Zeichensatz utf8 (eine strikte Obermenge von latin1) verwendet, um Ihre Textdateien zu generieren, andernfalls kann es passieren, dass eine Unmenge an unverständlichem Text in Ihre neue Datenbank eingefügt wird.