Der Blog

Upgrade Ihrer Engine: So verschieben Sie Ihren IOPS-intensiven MySQL/Rails-Stack ohne Ausfallzeiten auf Unicode

von PagerDuty 30. Oktober 2012 | 13 Minuten Lesezeit

Out with the old, in with the New

Sie sind ein Technikfreak und arbeiten für eines der vielen Startups, die schnell auf den Markt kamen, wo die Gründer hastig ein Schienen App zusammen mit Schokoriegelverpackungen und Alufolie. Als klar wurde, dass Begeisterung kein Ersatz für reine Programmierkenntnisse war, wurden Entwickler angeheuert, um Löcher in der Softwarearchitektur zu überdecken. Schließlich, als diese Als die Entwickler erkannten, was für ein ungezähmtes Biest die App war, stellten sie Du um das Chaos zu beseitigen und alles hübsch zu machen.

Sie kennen Ihren Stack. Sie haben eine alte MySQL-Datenbank, wahrscheinlich MySQL 5.0 oder 5.1. Sie wurde vom ersten Tag an mit den Standardeinstellungen eingerichtet (sprich: wir unterstützen Englisch), und die einzige wirkliche Änderung („Erweiterung“), die seitdem hinzugefügt wurde, ist wahrscheinlich ein Lese-Slave und eine asynchrone Replikation. Nach Jahren des kontinuierlichen Betriebs in diesem Modus haben Ihre Entwickler tausende nicht zu wartende, schreckliche Fixes entwickelt, um die Speicherung einiger nicht-ASCII-Zeichen in BLOB-Feldern zu ermöglichen. In der Zwischenzeit beschweren sich Ihre Support-Mitarbeiter, dass der Großteil der Welt Fehler bekommt, wenn sie Ihre App mit ihren nicht-romanisierten Namen verwenden, und das Management ist verärgert über die schiere Anzahl der geringfügig unterschiedlichen Transliterationsfunktionen im Code.

Dies war die Situation bei PagerDuty vor einigen Monaten, und dieser Artikel beschreibt, wie wir es behoben haben – wie wir von MySQL 5.1 Speicherung Latein1 (ISO-8859-1) Zeichen in das glänzende MySQL 5.5 mit Unicode ( UTF-8 ) Zeichen … und wie Sie es nie bemerkt haben.

Das Problem mit MySQL auf den Punkt gebracht

Die von MySQL beim Schreiben von Daten auf die Festplatte verwendeten Zeichensätze setzen Ihrer Anwendung einige Einschränkungen auf. Ein unbedarfter Benutzer könnte behaupten, dass MySQL nichts über Zeichensätze wissen muss, was Sinn machen würde, wenn Sie eine schlechte Leistung beim Sortieren Ihrer Strings, Ihrer CHARs und VARCHARs, wünschen. Da Sie wollen Um die Vorteile der Datenbankindizierung für implizite serverseitige Sortierungen nutzen zu können (clientseitige Sortierung im Prozess: bitte sterben), muss MySQL die von Ihnen verwendeten Zeichen verstehen, damit es einen Kontext zum Sortieren hat, der nicht nur aus Ordinalwerten besteht. Leider ist der von MySQL verstandene Standardzeichensatz Latin1, der Symbole ausschließt, die in etwa 90 % der Welt verwendet werden. Ein Unicode-Zeichensatz wie UTF-8 ist viel geeigneter, wenn Sie Zeichenfolgen mit multinationalen Symbolen speichern möchten, ohne auf BLOBs zurückzugreifen.

MySQL-Zeichensätze werden beim Erstellen einer Spalte in eine Spalte integriert. Natürlich können Sie mit MySQL schon seit langem ALTER TABLE verwenden, um diese Eigenschaft zu ändern. Dadurch wird der Wechsel von einem Zeichensatz zu einem anderen einfacher. Allerdings sperrt ALTER TABLE die gesamte Tabelle, wenn die Tabelle ausgeführt wird. Dies ist nicht gut für Live-Anwendungen mit hohem Schreibaufkommen, bei denen Ihre Benutzer kontinuierliche Reaktionsfähigkeit erwarten. Es ist etwas komplizierteres erforderlich. Dies ist die Geschichte dieses Etwas.

Bevor Sie beginnen, lesen Sie die Anforderungen

Bei PagerDuty betrachteten wir diese Herausforderung als überwindbares technisches Hindernis, das unser Geschäft nicht beeinträchtigen sollte. Nämlich:

  • Solange wir MySQL weiterhin verwenden, möchten wir nie wieder Datenspeicher aufgrund symbolbezogener Speicher-/Eingabeprobleme migrieren (wir möchten einen universellen Symbolsatz akzeptieren).
  • Dieser Wechsel durfte nur einen vernachlässigbaren Einfluss auf die laufende Leistung der PagerDuty Anwendung haben (zum Zwecke des Ereignisdurchsatzes durfte keine neue Cloud-Infrastruktur bereitgestellt werden).
  • Schlussfolgerung: Für die Unterbringung von UTF-8-kodierten MySQL-Zeichen sollten keine signifikanten neuen Speicherressourcen zugewiesen werden (wir rechnen höchstens mit dem Doppelten des alten Speicherbedarfs; dies ist nicht unangemessen, wenn man bedenkt, dass die meisten unserer Benutzer einfach romanisierte Zeichen verwenden und davon ausgehen, dass alles andere fehlschlägt).
  • Das gesamte Verfahren zur Veröffentlichung dieser Informationen sollte für unsere Benutzer nur vernachlässigbare (< 1 Minute) Ausfallzeiten verursachen.

Klingt das ehrgeizig? Das sind die Mindestanforderungen, die uns gestellt wurden, und wir freuen uns, sagen zu können, dass es uns gelungen ist, sie alle zu erfüllen.

MySQL – Eine Fülle von Wahnsinn aufdecken

MySQL macht die Konvertierung nach UTF-8 unglaublich mühsam, um die Einschränkungen der InnoDB Engine. Wir beginnen mit der Diskussion von Problemen mit Indizes über CHAR/VARCHAR-Daten, vorausgesetzt, Sie verwenden InnoDB (was wir verwendet haben, da zumindest unser Server nicht vom Steinzeit ).

Wussten Sie, dass InnoDB niedrige Grenzen für die Größe von einspaltigen Indizes hat? Wir wussten es auch nicht, aber wir haben herausgefunden, wie weit diese hinterhältigen MySQL-Entwickler gegangen sind, um zu verhindern, dass Ihnen, dem unvorsichtigen Benutzer, dies schadet. Sie sehen, die „utf8“-Kodierung von MySQL 5.1 ist kein echtes UTF-8. UTF-8 unterstützt Symbole zwischen 1 und 4 Byte Länge. MySQLs utf8 unterstützt nur Symbole mit einer Größe zwischen 1-3 Byte . Dies widerspricht unserem ersten Ziel – alle Charaktere zu unterstützen. Um das zu lösen Das wenig Versehen, wir verwendeten die in MySQL 5.5 bereitgestellte Kodierung „utf8mb4“ [1] … allerdings hatten wir noch nicht Version 5.5 im Einsatz. Unsere Lösung für Das Das Problem erforderte das Umschalten der Datenbankserver (ich sagte ja, es würde zu leichten Ausfallzeiten kommen!) – aber dazu kommen wir noch.

Die ersten Tests von MySQL 5.5 verliefen positiv, bis wir versuchten, unsere Produktionstabellen über mysqldump neu zu erstellen [2] mit UTF-8-Kodierung anstelle von latin1:

 mysqldump -d die_Datenbank | sed -e 's/(.*DEFAULT CHARSET=)latin1/1utf8mb4/' | mysql die_Datenbank_utf8 

Bitte schlagen Sie uns nicht. Wir wurden von diesem seltsamen Fehler überrascht:

 FEHLER 1071 (42000): Der angegebene Schlüssel war zu lang; die maximale Schlüssellänge beträgt 767 Bytes 

Oh wehe MySQL! Nachdem man gesehen hat, was es gesehen hat, muss man sehen, was es sieht! Wenn man sich das Kleingedruckte ansieht, InnoDB unterstützt nur einspaltige Indizes mit einer Größe von maximal 767 Bytes . Vielleicht ist das der Grund, warum die „utf8“-Kodierung nur maximal 3 Bytes pro Zeichen unterstützt: Das bedeutet, dass Konvertierungen von anderen Zeichensätzen in UTF8 funktionieren, wenn Spaltenindizes beteiligt sind. Bedenken Sie: Damit der Indexkomparator schnell ist, müssen alle Einträge dieselbe Größe haben: die maximale Gesamtgröße der Spalten, über die sie verteilt werden. Mit VARCHAR(255), einem ziemlich standardmäßigen Zellentyp, und MySQLs eingeschränkter UTF8-Kodierung wird max_length_of_string * max_size_of_char auf 255 * 3 => 765 erweitert. Mit utf8mb4 ist es 255 * 4 => 1020. Ups. Was für eine Zwickmühle.

Glücklicherweise wird in dem Link, der diese Einschränkung beschreibt, auch die Problemumgehung beschrieben (wodurch eine Vergrößerung der Indexgröße auf maximal 3072 Byte für eine einzelne Spalte möglich ist), was zu einigen der 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 wird für large_prefix benötigt innodb_file_per_table # file_format = Barracuda wird für large_prefix benötigt innodb_file_format = Barracuda # large_prefix ergibt max. einspaltige Indizes von 3072 Bytes = Gewinn! # wir müssen allerdings auch ROW_FORMAT=DYNAMIC für jede Tabelle festlegen. innodb_large_prefix Zeichensatz-Client-Handshake = FALSE Sortierungsserver = utf8mb4_unicode_ci init-connect='SET Sortierungsverbindung = utf8mb4_unicode_ci' init-connect='SET NAMES utf8mb4' Zeichensatzserver = utf8mb4 [mysqldump] Standardzeichensatz = utf8mb4 [mysql] Standardzeichensatz = utf8mb4 

Wir sind davon überzeugt, dass es einen präziseren Weg gibt, um aus MySQL das zu bekommen, was Sie wollen, aber wie das alte Sprichwort uns zu dieser Situation sagt: „Heben Sie ab, sprengen Sie die Site aus dem Orbit … das ist der einzige Weg, um sicherzugehen.“ Wenn Sie einen Server mit dieser my.cnf-Datei und mysql_install_db starten, CREATE TABLE-Anweisungen, die angeben ROW_FORMAT=DYNAMISCH Wille tue das Richtige , und bietet Ihnen indizierbare CHAR/VARCHAR-Zeichenfolgen, während gleichzeitig alle erdenklichen Symbole unterstützt werden.

Hier gibt es ein etwas verwandtes Problem, nämlich dass mehrspaltige Indizes ebenfalls auf maximal 3072 Bytes beschränkt sind. Dieses Problem ist möglicherweise schwieriger zu lösen. Wir hatten keine clevere Lösung für das Problem – nur ein zusammengesetzter Index war davon betroffen, und dieser Index lief zufällig über eine Tabelle mit wenigen Zeilen (die daher ALTER-fähig war). Der Index lief über die Spalte „phone_number“, die unnötigerweise ein VARCHAR(255) war, also kümmerte sich ein schnelles ALTER TABLE (also sein abstrahierter Cousin, die Rails-„Migration“) für uns darum und verkleinerte sie.

Sortierungen: Machen Sie sich keine Sorgen mehr und lieben Sie Unicode

Wir dachten, dass die plötzliche Vergrößerung der Indizes unseren schnellen MySQL-Server in einen schwerfälligen Giganten verwandeln würde. Dies stellte sich als falsch heraus – das Nettoergebnis unserer Migration war sehr neutral oder schwankte auf einem kleinen Geschwindigkeitsgewinn ! Wenn Sie eine halbwegs moderne Rails-Anwendung ausführen, ist die Wahrscheinlichkeit groß, dass dies auch für Sie zutrifft. Der Grund? Sortierungen.

Sortierungen teilen MySQL mit, wie Zeichenfolgen sortiert werden sollen, damit sie Sinn ergeben. Intuitiv kommt die Zeichenfolge „abc“ bei einer aufsteigenden Sortierung vor „bbc“, da das führende „a“ alphabetisch vor „b“ steht. Komplizierte Zeichen erfordern jedoch kompliziertere Regeln. Beispielsweise ist die Deutsches Institut für Normung (DIN) definiert zwei mögliche Latin1-Sortierungen; DIN-1 (Deutsches Wörterbuch) definiert das Symbol „ß“ als Äquivalent zu „s“, und DIN-2 (Deutsche Telefonbücher) definiert ß = „ss“ (neben anderen Unterschieden). Nachdem die Reduktion durchgeführt wurde, wird eine standardmäßige (englische) lexikalische Sortierung verwendet.

Dies ist wichtig, wenn Clientverbindungen eine Abfrage nach einem Zeichenfolgenfeld wünschen. Daher benötigen Sie manche Möglichkeit, die Zeichenfolgen zu sortieren. MySQL-Sortierungen bieten Ihnen diese Sortierung bei richtiger Verwendung im Grunde kostenlos (solange Sie einen Index über die Zeichenfolgenfelder haben). Eine oft missachtete Voraussetzung für diesen Vorteil ist, dass sowohl Client als auch Server denselben Zeichensatz und dieselbe Sortierung verwenden müssen. Es stellte sich heraus, dass dies bis zu unserer UTF-8-Datenbankmigration bei PagerDuty nicht der Fall war.

Betrachten Sie Ihre Rails-Anwendung. Wahrscheinlich verwenden Sie entweder MySQL/Ruby oder der MySQL2 gem, um ActiveRecord zu betreiben. Sie lesen aus einer database.yml-Datei, die angibt   Was als Kodierungstyp? Oh, utf8? Wenn Sie sich eine Weile durch den Gem-Code wühlen (was wir schließlich gefunden haben tun müssen ), werden Sie feststellen, dass diese Kodierung in die MySQL-Verbindungseinstellungen übernommen wird; sie wird zum Zeichensatz (und definiert die Sortierung), der für die Kommunikation mit MySQL verwendet wird. Die Tatsache, dass Sie dies auf utf8 setzen, während Sie mit einer auf Latin1 basierenden Datenbank kommunizieren, ist der Kern der Beschleunigung, die Sie gleich erreichen werden.

Fakt: Sie haben die ganze Zeit CPU-Zyklen mit dem Sortieren verschwendet. MySQL hat dies abstrahiert und Ihnen Zeichenfolgen in einer Reihenfolge geliefert, die Ihre Client-App (Rails) versteht, während gleichzeitig eine Zuordnung zwischen einer UTF-8-Sortierung (wahrscheinlich utf8_general_ci) und der Sortierung, an die Ihre Tabellen gebunden sind, aufrechterhalten werden muss. Sie glauben mir nicht? Beobachten Sie, was passiert, wenn Sie Client und Server so einstellen, dass sie beide mit utf8mb4 und derselben Sortierung laufen (wir haben utf8mb4_unicode_ci gewählt; siehe Hier für eine Diskussion über Unicode-Sortierungsunterschiede in MySQL). Viel Spaß mit der Beschleunigung. Bedanken Sie sich später bei mir.

Sorgen Sie dafür, dass Ihre Daten weiterlaufen: Migrieren + Replizieren + Aktualisieren

Wir haben so viel Text gebraucht, aber jetzt kommen wir endlich zum kniffligen Teil: Wie migrieren Sie Ihren alten, mit Kohle betriebenen Datenspeicher? Sie haben bereits einen cleveren Hack gesehen, der mysqldump + sed verwendet, um Daten auf einen neuen Server zu laden. Aber Ihre alte Datenbank ist noch im Schreibmodus – was nun? Die Lösung besteht darin, dass MySQL Ihnen einen Knochen hinlegt, indem es darauf besteht, Client-/Server-Zeichensätze zu kennen und zu trennen.

Wir würden gerne sehen, wie das im Detail funktioniert, aber leider konnte ich mir nicht die Zeit nehmen, den MySQL-Quellcode zu lesen oder glaubwürdige Informationen dazu zu finden. Mit der obigen Konfigurationsdatei für unseren neuen Server funktionierte die Einrichtung einer Master/Slave-Replikation zwischen unserer alten Datenbank und der neuen 5.5 UTF-8-Datenbank einwandfrei. Wir testeten alle möglichen Latin1-Zeichen, die in die alte Datenbank eingefügt wurden, und sie kamen ohne Probleme in der replizierten Kopie zurück. MySQL führte alle korrekten Übersetzungen durch und wir mussten uns einfach zurücklehnen und zusehen. Als die hypnotisierende Wirkung nachließ, war es Zeit, etwas Arbeit zu erledigen – nämlich alle unsere Webserver mussten ihre MySQL-Client Pakete aktualisiert. Wie Sie sehen, spricht MySQL-Client 5.1 kein UTF8MB4 und wird auch bei der Kommunikation mit Ihrem 5.5-Server einige Probleme haben.

Dazu verwendeten wir Koch um schnell neue App-Backend-Server hochzufahren – Klone unserer bestehenden Server – aber mit der neuen MySQL-Client-Version und so konfiguriert, dass die Hintergrund-Worker (alle Warteschlangenverarbeitungs- und asynchronen Aufgaben von PagerDuty) deaktiviert waren. Die wunderbaren Früchte der Cloud – gelegentlich nützlich! Diese Server waren auf die Slave-Datenbank ausgerichtet und bereits so konfiguriert (über Chef-Umgebungseinstellungen, die für Chef-Benutzer viel sinnvoller sind), dass sie utf8mb4 vollständig über Rails via MySQL2 unterstützen. Nachdem diese bösen Jungs bereit waren (und einige Tests durchgeführt wurden, um sicherzustellen, dass sie so funktionierten, wie es unsere Testumgebungen taten), waren wir bereit, unsere Datenbank umzustellen.

Tu es, tu es jetzt! Komm schon, dreh mich um!

Der Flip-Prozess ist dieser unglaublich riskante Moment, in dem Sie einfach nicht sicher sind, ob alles funktioniert oder ob Sie ein entscheidendes Detail übersehen haben und Ihre Kunden kurz davor sind, sehr unzufrieden zu sein. Sie gehen nervös Ihre Checkliste durch und achten darauf, den kritischen Moment ohne Umkehr hervorzuheben. In unserem Flip hatten wir die folgenden Komponenten:

  • aktuelle Hintergrundarbeiter auf den alten App-Backends herunterfahren
  • (zu diesem Zeitpunkt verarbeiten wir das Senden von Benachrichtigungen nicht mehr, stellen aber weiterhin Anfragen in die Warteschlange)
  • Sperren der Masterdatenbank
  • (an diesem Punkt sind wir vollständig gestoppt – das ist die Ausfallzeit, vor der Sie gewarnt wurden! Neue Anfragen werden eingefroren)
  • Stoppen und Zurücksetzen des Slaves
  • Führen Sie Chef auf unseren kundenorientierten Load Balancern aus, bringen Sie sie in die neue Chef-Umgebung und ändern Sie sie so, dass sie unsere neu gestarteten Maschinen als App-Backends verwenden.
  • (an diesem Punkt nehmen wir wieder Anfragen entgegen, bei Anfragen vor dem Umdrehen kommt es zu einer Zeitüberschreitung)
  • Hintergrund-Worker auf den neuen App-Backends hochfahren
  • (an diesem Punkt sind wir voll funktionsfähig)
  • Beenden Sie die alten App-Backends

Wie Sie sich vorstellen können, da ich jetzt mit Ihnen darüber spreche, wurden alle diese ohne Fehler ausgeführt. Sie haben gelesen in einem anderen Beitrag wie unsere Hintergrundprozesse ablaufen und wie einfach es ist, Skripte zu erstellen überwachen , insbesondere in Verbindung mit Chef, um unsere Hintergrundaufgaben herunterzufahren und zu starten. Chef-Kunde Dank der unermüdlichen Arbeit unseres Betriebsteams sind Ausführungen auf unseren Load Balancern normalerweise innerhalb von 20 Sekunden abgeschlossen. Daher wussten wir, dass dies die Obergrenze unserer Ausfallzeit sein würde. Die einzigen SQL-Befehle, die wir ausführen mussten, um die Dinge in Gang zu bringen, waren:

(auf dem Master)

 BEGINNEN; TABELLEN MIT LESESPERRUNG LESEN; 

(auf dem Slave, sobald Sie überprüft haben, dass er den Master eingeholt hat)

 SLAVE STOPPEN; SLAVE ZURÜCKSETZEN; 

Das war es. Der Stress war weg und Sie, der Kunde, haben kaum bemerkt, dass wir Ihre Ereignisse vorübergehend ignoriert haben. Alles, was übrig blieb, war die gemächliche Neukonfiguration unserer Slaves und Backups, um einen neuen MySQL-Server anzusprechen. Oh, und Rails brauchte etwas Liebe; Folgendes haben wir zu config/initializers/activerecord_ext.rb unserer Anwendung hinzugefügt:

 Modul ActiveRecord Modul ConnectionAdapters Modul SchemaStatements def create_table_with_dynamic_row_format(Tabellenname, Optionen = {}, &Block) neue_Optionen = Optionen.dup neue_Optionen[:Optionen] ||= '' neue_Optionen[:Optionen] << ' DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC' create_table_without_dynamic_row_format(Tabellenname, neue_Optionen, &Block) Ende Alias-Methodenkette :create_table, :dynamic_row_format Ende Ende Ende 

Obduktion

Wenn Sie es bis hierhin geschafft haben, herzlichen Glückwunsch, lieber Leser – Sie sind engagiert. Hoffentlich inspiriert Sie dieser Artikel, Gutes zu tun und die Flecken jahrelanger überwiegend lateinamerikanischer Bewerbungen in dein Unternehmen. Überholen Sie diese Engine gründlich. Aber seien Sie gewarnt … in der technologisch falsch implementierten Welt von Unicode hört die Aufregung nie auf. Es gibt immer mehr Komponenten, die in die 21. st Sprachenmix des 20. Jahrhunderts.

[1] Wenn Sie dagegen sind Han-Vereinigung , dann unterstützen wir Ihre vielseitigen Charaktere nach all den Bemühungen hier leider immer noch nicht.

[2] Ihr mysqldump sollte so eingestellt sein, dass er zum Generieren Ihrer Textdateien den UTF-8-Zeichensatz (eine strikte Obermenge von Latin1) verwendet. Andernfalls wird in Ihrer neuen Datenbank möglicherweise eine Menge Kauderwelsch eingefügt.