he sc e e ut ab ag De sg ufl Au 2. A
r
de
Fortgeschrittene Techniken für MySQL-Administratoren
High Performance MySQL Optimierung, Datensicherung, Replikation & Lastverteilung
O’Reilly
Baron Schwartz, Peter Zaitsev, Vadim Tkachenko, Jeremy D. Zawodny, Arjen Lentz & Derek J. Balling Deutsche Übersetzung von Kathrin Lichtenberg
High Performance MySQL Optimierung, Backups, Replikation und Lastverteilung 2. Auflage
Baron Schwartz, Peter Zaitsev, Vadim Tkachenko, Jeremy D. Zawodny, Arjen Lentz & Derek J. Balling
Deutsche Übersetzung von Kathrin Lichtenberg
Beijing · Cambridge · Farnham · Köln · Sebastopol · Taipei · Tokyo
Die Informationen in diesem Buch wurden mit größter Sorgfalt erarbeitet. Dennoch können Fehler nicht vollständig ausgeschlossen werden. Verlag, Autoren und Übersetzer übernehmen keine juristische Verantwortung oder irgendeine Haftung für eventuell verbliebene Fehler und deren Folgen. Alle Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt und sind möglicherweise eingetragene Warenzeichen. Der Verlag richtet sich im Wesentlichen nach den Schreibweisen der Hersteller. Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen. Kommentare und Fragen können Sie gerne an uns richten: O’Reilly Verlag Balthasarstr. 81 50670 Köln E-Mail:
[email protected]
Copyright der deutschen Ausgabe: © 2009 by O’Reilly Verlag GmbH & Co. KG 1. Auflage 2005 2. Auflage 2009 Die Originalausgabe erschien 2008 unter dem Titel High Performance MySQL: Optimization, Backups, Replication and more, 2nd Edition bei O’Reilly Media, Inc. Die Darstellung eines Sperbers im Zusammenhang mit dem Thema MySQL ist ein Warenzeichen von O’Reilly Media, Inc.
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar. Übersetzung und deutsche Bearbeitung: Kathrin Lichtenberg, Ilmenau Lektorat: Christine Haite, Köln Korrektorat: Friederike Daenecke, Zülpich Satz: Tim Mergemeier, Reemers Publishing Services GmbH, Krefeld; www.reemers.de Umschlaggestaltung: Karen Montgomery, Boston & Michael Oreal, Köln Produktion: Andrea Miß, Köln Belichtung, Druck und buchbinderische Verarbeitung: Druckerei Kösel, Krugzell; www.koeselbuch.de
ISBN 978-3-89721-889-5 Dieses Buch ist auf 100% chlorfrei gebleichtem Papier gedruckt.
Inhalt
Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XI .............................................. 1 Die logische Architektur von MySQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Nebenläufigkeitskontrolle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Multi-Version Concurrency Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Die Storage-Engines von MySQL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1 Die MySQL-Architektur
2 Engpässe finden: Benchmarking und Profiling. . . . . . . . . . . . . . . . . . . . . . . . . . . 35 Wozu Benchmarks? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benchmarking-Strategien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benchmarking-Taktiken. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benchmarking-Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benchmarking-Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Profiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Profiling des Betriebssystems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36 36 41 46 48 59 82
3 Schema-Optimierung und Indizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 Optimale Datentypen auswählen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Grundlagen der Indizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 Indizierungsstrategien für High Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 Indizierung – eine Fallstudie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 Index- und Tabellenpflege . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 Normalisierung und Denormalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 ALTER TABLE beschleunigen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 Hinweise zu Storage-Engines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
|
V
4 Optimierung der Abfrageleistung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 Grundlagen langsamer Abfragen: Datenzugriff optimieren . . . . . . . . . . . . . . . . . Methoden zum Umstrukturieren von Abfragen. . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen der Abfrageverarbeitung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grenzen des MySQL-Abfrageoptimierers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bestimmte Arten von Abfragen optimieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hinweise für den Abfrageoptimierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benutzerdefinierte Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5 Erweiterte MySQL-Funktionen
...................................... Der MySQL-Abfrage-Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Code in MySQL speichern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Cursor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vorbereitete Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benutzerdefinierte Funktionen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sichten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichensätze und Sortierreihenfolgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Volltextsuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fremdschlüsselbeschränkungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Merge-Tabellen und Partitionierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verteilte (XA-) Transaktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
163 169 172 192 202 210 213
220 220 234 242 243 248 250 255 263 272 273 283
6 Die Servereinstellungen optimieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286 Grundlagen der Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Allgemeines Tuning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Ein-/Ausgabeverhalten von MySQL anpassen . . . . . . . . . . . . . . . . . . . . . . . Die MySQL-Nebenläufigkeit anpassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lastbasierte Anpassungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verbindungsbezogene Werte anpassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7 Betriebssystem- und Hardwareoptimierung
........................... Was beschränkt die Leistung von MySQL? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wie Sie CPUs für MySQL auswählen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Speicher- und Festplattenressourcen abwägen. . . . . . . . . . . . . . . . . . . . . . . . . . . Hardware für einen Slave wählen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . RAID-Leistungsoptimierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Storage Area Networks und Network-Attached Storage . . . . . . . . . . . . . . . . . . . Mehrere Festplatten-Volumes benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Netzwerkkonfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
VI
| Inhalt
287 293 304 320 323 330
331 332 332 336 345 345 354 355 358
Ein Betriebssystem wählen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Dateisystem wählen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threading. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Swapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Betriebssystemstatus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8 Replikation
..................................................... Replikation im Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Replikation einrichten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Replikation näher betrachtet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Replikationstopologien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Replikation und Kapazitätsplanung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Replikationsadministration und -wartung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Replikationsprobleme und Lösungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wie schnell ist die Replikation? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Zukunft der MySQL-Replikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
360 361 363 364 366
373 373 377 386 393 408 410 421 441 443
9 Skalierung und Hochverfügbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MySQL skalieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Lastausgleich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hochverfügbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
446 448 475 487
10 Optimierung auf Anwendungsebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 498 Überblick über die Anwendungsleistung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Webserverprobleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MySQL erweitern. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Alternativen zu MySQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
498 502 505 512 514
11 Backup und Wiederherstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 515 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überlegungen und Kompromisse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Binärlogs organisieren und sichern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Daten in einem Backup sichern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wiederherstellung aus einem Backup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Backup- und Wiederherstellungsgeschwindigkeit . . . . . . . . . . . . . . . . . . . . . . . . Backup-Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Backups mit Skripten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
516 521 531 534 546 558 558 566
Inhalt | VII
12 Sicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 570 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Account-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Betriebssystemsicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Netzwerksicherheit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenverschlüsselung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MySQL in einer chroot-Umgebung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13 Der MySQL-Serverstatus
........................................... Systemvariablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . SHOW STATUS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . SHOW INNODB STATUS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . SHOW PROCESSLIST. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . SHOW MUTEX STATUS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Status der Replikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . INFORMATION_SCHEMA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
570 571 592 593 601 606
608 608 609 616 631 631 633 634
14 Werkzeuge für High Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 636 Schnittstellenwerkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Überwachungswerkzeuge. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Analysewerkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . MySQL-Dienstprogramme. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Weitere Informationsquellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
636 638 649 652 655
A Große Dateien übertragen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656 B EXPLAIN benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 661 C Sphinx mit MySQL benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 677 D Sperren debuggen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 706 Index
...............................................................
VIII | Inhalt
717
Vorwort
Wir hatten für dieses Buch verschiedene Ziele im Sinn. Viele von ihnen rührten daher, dass wir über das sagenumwobene perfekte MySQL-Buch nachdachten, das noch niemand von uns zu Gesicht bekommen hatte, nach dem wir jedoch in den Regalen der Buchläden Ausschau hielten. Andere Ideen stammten aus unseren umfassenden Erfahrungen, die wir sammelten, wenn wir anderen Benutzern halfen, MySQL in ihren Umgebungen zum Laufen zu bringen. Wir wollten ein Buch, das nicht nur ein SQL-Einstieg ist. Wir wollten ein Buch mit einem Titel, der nicht in einem beliebigen Zeitrahmen begann oder endete (»…in 30 Tagen«, »In sieben Tagen zu besseren…«) und nicht von oben herab auf den Leser »einredete«. Vor allem wollten wir ein Buch, das Ihnen helfen würde, Ihre Fähigkeiten auf die nächste Stufe zu bringen und schnelle, zuverlässige Systeme mit MySQL aufzubauen – eines, das Fragen beantworten würde wie: »Wie kann ich einen Cluster aus MySQL-Servern einrichten, die in der Lage sind, Millionen und Abermillionen von Abfragen zu verarbeiten, und sicherstellen, dass alles weiter funktioniert, auch wenn einige der Server sterben?« Wir beschlossen, ein Buch zu schreiben, das sich nicht nur auf die Bedürfnisse der MySQL-Anwendungsentwickler konzentrierte, sondern auch auf die rigorosen Anforderungen des MySQL-Administrators, der das System am Laufen halten muss, was auch immer die Programmierer oder Benutzer dem Server zumuten. Infolgedessen gehen wir davon aus, dass Sie bereits relativ erfahren mit MySQL sind und idealerweise ein einführendes Buch darüber gelesen haben. Wir setzen außerdem eine gewisse Erfahrung mit der allgemeinen Systemadministration, dem Betrieb von Netzwerken und Unix-artigen Betriebssystemen voraus. Diese überarbeitete und erweiterte zweite Auflage enthält eine tiefergehende Behandlung all der Themen aus der ersten Auflage sowie viele neue Themen. Zum Teil ist dies eine Reaktion auf die Änderungen, die seit der ersten Veröffentlichung des Buches stattgefunden haben: MySQL ist jetzt eine viel größere und komplexere Software. Genauso wichtig: Seine Beliebtheit hat stark zugenommen. Die MySQL-Community ist sehr gewachsen, und große Unternehmen setzen MySQL inzwischen für ihre entscheidenden Anwendungen ein. Seit der ersten Auflage wird MySQL als vollwertige Software für den
| IX
Einsatz in Unternehmen angesehen.1 Es wird immer mehr auch für Anwendungen im Internet eingesetzt, wo Ausfallzeiten und andere Probleme nicht verschleiert oder toleriert werden können. Aus diesem Grund hat diese zweite Auflage einen etwas anderen Schwerpunkt als die erste Auflage. Wir betonen die Zuverlässigkeit und Korrektheit fast genauso stark wie die Performance – zum Teil, weil wir MySQL selbst für Anwendungen einsetzt haben, bei denen nicht unwesentliche Mengen Geldes vom Datenbankserver abhängen. Wir verfügen außerdem über umfassende Erfahrungen mit Webanwendungen, bei denen MySQL sehr populär geworden ist. Die zweite Auflage spricht die erweiterte Welt von MySQL an, die in dieser Form noch nicht existierte, als die erste Auflage erschienen ist.
Wie dieses Buch aufgebaut ist Wir haben in dieses Buch eine Menge komplizierter Themen aufgenommen. Hier wollen wir erklären, in welcher Reihenfolge wir sie zusammengestellt haben, damit sie leichter zu lernen sind.
Ein breiter Überblick Kapitel 1, Die MySQL-Architektur, widmet sich den Grundlagen – Dingen, mit denen Sie vertraut sein müssen, bevor Sie tiefer einsteigen. Sie müssen verstehen, wie MySQL organisiert ist, bevor Sie in der Lage sind, es effektiv zu benutzen. Dieses Kapitel erläutert die Architektur von MySQL und liefert wesentliche Fakten über seine Storage-Engines. Es hilft Ihnen beim Einstieg, falls Sie nicht mit den Grundlagen relationaler Datenbanken, inklusive Transaktionen, vertraut sind. Dieses Kapitel ist auch ganz nützlich, wenn dieses Buch Ihr Einstieg in MySQL ist, Sie sich aber schon mit einer anderen Datenbank, wie etwa Oracle, auskennen.
Eine solide Grundlage bauen Die nächsten vier Kapitel behandeln Material, auf das Sie immer wieder stoßen werden, wenn Sie MySQL benutzen. Kapitel 2, Engpässe finden: Benchmarking und Profiling, diskutiert die Grundlagen des Benchmarkings und Profilings – d.h., wie Sie feststellen, mit welcher Art von Arbeitsbelastung Ihr Server umgehen kann, wie schnell er bestimmte Aufgaben ausführen kann usw. Sie werden Ihre Anwendung sowohl vor als auch nach einer großen Änderung einem Benchmark-Test unterziehen, damit Sie beurteilen können, wie effektiv Ihre Änderungen sind. Was wie eine positive Änderung scheint, könnte sich unter realem Stress als negativ herausstellen, und Sie werden nie wissen, was wirklich eine schlechte Leistung verursacht, wenn Sie es nicht exakt messen. 1 Wir glauben, dass diese Phrase im Wesentlichen Marketing-Geblubber ist, sie scheint aber für viele Leute eine gewisse Wichtigkeit zu vermitteln.
X
| Vorwort
In Kapitel 3, Schema-Optimierung und Indizierung, behandeln wir die verschiedenen Nuancen der Datentypen, Tabellendesigns und Indizes. Ein gut gestaltetes Schema unterstützt MySQL bei der Arbeit. Viele der Dinge, die wir in späteren Kapiteln besprechen, sind davon abhängig, wie gut Ihre Anwendung die Indizes von MySQL zum Laufen bringt. Ein solides Verständnis der Indizes und ihrer Verwendung ist entscheidend für die effektive Benutzung von MySQL, so dass Sie wahrscheinlich öfter einmal zu diesem Kapitel zurückkehren werden. Kapitel 4, Optimierung der Abfrageleistung, erläutert, wie MySQL Abfragen ausführt und wie Sie die Stärken seines Abfrageoptimierers ausnutzen können. Ein sicheres Gespür dafür, wie der Abfrageoptimierer funktioniert, wirkt Wunder für Ihre Abfragen und hilft Ihnen dabei, die Indizes besser zu verstehen. (Indizierung und Abfrageoptimierung stellen eine Art Henne-Ei-Problem dar; möglicherweise hilft es, Kapitel 3 erneut zu lesen, wenn Sie mit Kapitel 4 fertig sind.) Dieses Kapitel präsentiert außerdem besondere Beispiele für praktisch alle gebräuchlichen Klassen von Abfragen, womit verdeutlicht wird, wo MySQL gute Arbeit leistet und wie man Abfragen in Formen umwandelt, die seine Stärken ausnutzen. Bis zu dieser Stelle haben wir die Grundlagen behandelt, die für jede Datenbank gelten: Tabellen, Indizes, Daten und Abfragen. Kapitel 5, Erweiterte MySQL-Funktionen, geht über die Grundlagen hinaus und zeigt Ihnen, wie die erweiterten Funktionen von MySQL arbeiten. Wir untersuchen den Abfrage-Cache, gespeicherte Prozeduren, Trigger, Zeichensätze und mehr. MySQLs Implementierung dieser Merkmale unterscheidet sich von der anderer Datenbanken, und ein gutes Verständnis dafür kann neue Möglichkeiten für Leistungsgewinne öffnen, die Sie möglicherweise sonst nicht erkannt hätten.
Ihre Anwendung verbessern In den nächsten beiden Kapiteln besprechen wir, wie man Änderungen vornimmt, um die Leistung Ihrer MySQL-basierten Anwendung zu verbessern. In Kapitel 6, Die Servereinstellungen optimieren, erläutern wir, wie Sie MySQL so einstellen, dass es das meiste aus Ihrer Hardware herausholt und so gut wie möglich für Ihre spezielle Anwendung funktioniert. Kapitel 7, Betriebssystem- und Hardwareoptimierung, zeigt, wie Sie das Betriebssystem und die Hardware am besten ausnutzen. Wir schlagen außerdem Hardware-Konfigurationen vor, die eine bessere Leistung für größere Anwendungen liefern.
Erweiterungen nach Änderungen Ein Server reicht nicht immer aus. In Kapitel 8, Replikation, besprechen wir die Replikation, d.h., wie Sie Ihre Daten automatisch auf mehrere Server kopiert bekommen. Kombiniert mit Skalierung, Lastausgleich und den Lektionen über Hochverfügbarkeit in Kapitel 9, Skalierung und Hochverfügbarkeit, bietet Ihnen das die Grundlage, um Ihre Anwendungen so zu skalieren, wie Sie sie brauchen.
Vorwort | XI
Eine Anwendung, die auf einem groß ausgebauten MySQL-Backend läuft, bietet oft wesentliche Möglichkeiten zur Optimierung in der Anwendung selbst. Es gibt bessere und schlechtere Methoden, um große Anwendungen zu gestalten. Sie sollen über den Tellerrand von MySQL hinaussehen. Kapitel 10, Optimierung auf Anwendungsebene, hilft Ihnen dabei, zu verbessernde Schwachstellen in Ihrer Gesamtarchitektur zu erkennen, vor allem, wenn es sich um eine Webanwendung handelt.
Ihre Anwendung zuverlässig machen Auch die am besten gestaltete, gut skalierbare Architektur nützt nichts, wenn sie Stromausfälle, bösartige Angriffe, Fehler in der Anwendung oder der Programmierung und andere Katastrophen nicht überstehen kann. In Kapitel 11, Backup und Wiederherstellung, diskutieren wir die verschiedenen Backupund Wiederherstellungsstrategien für Ihre MySQL-Datenbanken. Diese Strategien helfen Ihnen dabei, Ausfallzeiten zu minimieren, falls irgendwann (was unweigerlich geschehen wird) die Hardware kaputtgeht, und sie helfen Ihnen sicherzustellen, dass Ihre Daten solche Katastrophen überleben. Kapitel 12, Sicherheit, bietet Ihnen einen soliden Einblick in einige der Sicherheitsprobleme, die mit dem Betrieb eines MySQL-Servers verbunden sind. Noch wichtiger: Wir machen viele Vorschläge, mit deren Hilfe Sie verhindern können, dass Außenstehende die Server beschädigen, die Sie die ganze Zeit zu konfigurieren und zu optimieren versuchen. Wir erläutern einige der selten erkundeten Bereiche der Datenbanksicherheit, wobei wir Ihnen sowohl die Vorteile als auch die Auswirkungen auf die Performance der verschiedenen Praktiken zeigen. In Bezug auf die Leistung zahlt es sich normalerweise aus, einfache und klare Sicherheitsstrategien zu wählen.
Verschiedene nützliche Themen In den letzten Kapiteln und den Anhängen des Buches befassen wir uns mit verschiedenen Themen, die entweder in keines der früheren Kapitel »passen« oder auf die in anderen Kapiteln so oft verwiesen wird, dass sie ein wenig mehr Aufmerksamkeit verdienen. Kapitel 13, Der MySQL-Serverstatus, zeigt Ihnen, wie Sie Ihren MySQL-Server untersuchen. Es ist wichtig zu wissen, wie man Statusinformationen vom Server gewinnt. Noch wichtiger ist es, die Bedeutung dieser Informationen zu kennen. Besonders ausführlich behandeln wir SHOW INNODB STATUS, weil dies Ihnen einen tiefen Einblick in die Operationen der transaktionsfähigen Storage-Engine InnoDB bietet. Kapitel 14, Werkzeuge für High Performance, stellt Werkzeuge vor, mit denen Sie MySQL effizienter verwalten. Dazu gehören Überwachungs- und Analysewerkzeuge, Werkzeuge zum Schreiben von Abfragen usw. Dieses Kapitel behandelt die Maatkit-Werkzeuge von Baron, mit denen Sie die Funktionalität von MySQL erweitern und sich das Leben als Datenbankadministrator erleichtern können. Es zeigt außerdem ein Programm namens innotop, das Baron als leicht zu bedienende Schnittstelle zu dem, was Ihr MySQL-Server gerade tut, geschrieben hat. Es funktioniert fast wie das Unix-Programm top und kann in
XII | Vorwort
allen Phasen des Einstellungsprozesses gute Dienste leisten, indem es überwacht, was innerhalb von MySQL und seinen Storage-Engines geschieht. Anhang A, Große Dateien übertragen, zeigt Ihnen, wie Sie effizient sehr große Dateien von einer Stelle zur anderen kopieren – ein Muss, wenn Sie große Datenvolumen verwalten. In Anhang B, EXPLAIN benutzen, erfahren Sie, wie Sie den überaus wichtigen EXPLAIN-Befehl verwenden. Anhang C, Sphinx mit MySQL benutzen, ist eine Einführung in Sphinx, ein High-Performance-Volltextindizierungssystem, das die MySQL-eigenen Fähigkeiten ergänzt. Und Anhang D, Sperren debuggen, schließlich zeigt Ihnen, wie Sie entschlüsseln, was passiert, wenn Abfragen Locks anfordern, die einander stören.
Software-Versionen und Verfügbarkeit MySQL ist ständig in der Weiterentwicklung begriffen. Seit Jeremy den Entwurf für die erste Auflage dieses Buches geschrieben hat, sind zahlreiche Versionen von MySQL erschienen. MySQL 4.1 und 5.0 gab es nur als Alpha-Versionen, als die erste Auflage in den Druck ging, aber diese Versionen sind inzwischen seit Jahren im täglichen Einsatz und bilden heute das Rückgrat vieler großer Online-Anwendungen. Als wir diese zweite Auflage abgeschlossen haben, waren stattdessen MySQL 5.1 und 6.0 der neueste Schrei. (MySQL 5.1 ist ein Release-Kandidat, und 6.0 ist im Alpha-Stadium.) Wir haben uns für dieses Buch nicht auf eine einzige Version von MySQL versteift, sondern haben unser umfangreiches kollektives Wissen über MySQL in der echten Welt ins Rennen geworfen. Der Kern dieses Buches konzentriert sich auf MySQL 5.0, weil wir dies als die »aktuelle« Version ansehen. Die meisten unserer Beispiele gehen davon aus, dass Sie eine einigermaßen ausgereifte Version von MySQL 5.0 betreiben, wie etwa MySQL 5.0.40 oder neuer. Wir haben versucht, Eigenschaften oder Funktionalitäten anzugeben, die in älteren Ausgaben nicht existieren oder die es nur in der kommenden 5.1-Serie gibt. Allerdings ist die definitive Referenz zum Zuordnen von Funktionen zu bestimmten Versionen die MySQLDokumentation selbst. Wir erwarten, dass Sie von Zeit zu Zeit in der angegebenen OnlineDokumentation (http://dev.mysql.com/doc/) nachsehen, während Sie dieses Buch lesen. Ein weiterer großartiger Aspekt von MySQL ist, dass es auf allen heute verbreiteten Plattformen läuft: Mac OS X, Windows, GNU/Linux, Solaris, FreeBSD – was Sie wollen! Wir richten uns jedoch vorrangig an Nutzer von GNU/Linux2 und anderen Unix-artigen Betriebssystemen. Windows-Benutzer werden wahrscheinlich einige Unterschiede bemerken. Zum Beispiel sind die Dateipfade völlig anders. Wir verweisen darüber hinaus auf die normalen Unix-Kommandozeilenprogramme; wir gehen davon aus, dass Sie die entsprechenden Befehle unter Windows kennen.3
2 Um Verwirrung zu vermeiden, beziehen wir uns auf Linux, wenn wir über den Kernel schreiben, und auf GNU/Linux, wenn wir über die gesamte Betriebssysteminfrastruktur schreiben, die die Anwendungen unterstützt. 3 Sie erhalten Windows-kompatible Versionen der Unix-Dienstprogramme unter http://unxutils.sourceforge.net oder unter http://gnuwin32.sourceforge.net.
Vorwort
| XIII
Perl ist der andere Knackpunkt, wenn es um MySQL unter Windows geht. MySQL enthält eine Reihe nützlicher Dienstprogramme, die in Perl geschrieben sind, und verschiedene Kapitel in diesem Buch präsentieren beispielhaft Perl-Skripten, die die Grundlage der komplexeren Werkzeuge bilden, die Sie aufbauen. Maatkit ist ebenfalls in Perl geschrieben. Allerdings ist Perl nicht in Windows enthalten. Um diese Skripten zu benutzen, müssen Sie sich eine Windows-Version von Perl von ActiveState herunterladen und die notwendigen Zusatzmodule (DBI und DBD::mysql) für den MySQL-Zugriff installieren.
Konventionen in diesem Buch Folgende typografische Konventionen kommen in diesem Buch zum Einsatz: Kursiv Wird für neue Begriffe, URLs, E-Mail-Adressen, Benutzernamen, Hostnamen, Dateinamen, Dateierweiterungen, Pfadnamen, Verzeichnisse sowie Unix-Befehle und -Dienstprogramme verwendet. Nichtproportionalschrift
Kennzeichnet Codeelemente, Konfigurationsoptionen, Datenbank- und Tabellennamen, Variablen und deren Werte, Funktionen, Module, den Inhalt von Dateien oder die Ausgabe von Befehlen. Nichtproportionalschrift fett
Zeigt Befehle oder anderen Text, der so, wie er ist, vom Benutzer eingegeben werden soll. Wird auch zum Hervorheben in Befehlsausgaben benutzt. Nichtproportionalschrift kursiv
Zeigt Text, der durch Werte ersetzt werden soll, die der Benutzer angibt. Dieses Icon kennzeichnet einen Tipp, einen Vorschlag oder eine allgemeine Anmerkung.
Dieses Icon kennzeichnet eine Warnung oder einen Achtungshinweis.
Die Codebeispiele benutzen Dieses Buch soll Ihnen helfen, Ihre Arbeit zu erledigen. Im Allgemeinen dürfen Sie den Code aus diesem Buch in Ihren Programmen und Dokumentationen einsetzen. Sie müssen uns nicht um Erlaubnis fragen, es sei denn, Sie reproduzieren einen wesentlichen Teil des Codes. Es ist z.B. keine Erlaubnis erforderlich, wenn Sie ein Programm schreiben, das einige Teile des Codes aus diesem Buch benutzt. Wenn Sie dagegen eine CD-ROM mit Beispielen aus O’Reilly-Büchern verteilen oder verkaufen, ist eine Erlaubnis nötig. Das Beantworten einer Frage, indem dieses Buch sowie Beispielcode daraus zitiert werden, XIV | Vorwort
erfordert keine Erlaubnis. Fügen Sie dagegen einen wesentlichen Anteil Beispielcode aus diesem Buch in die Dokumentation Ihres Produkts ein, ist eine Erlaubnis nötig. Die Beispiele stehen auf der Site http://www.highperfmysql.com und werden gelegentlich aktualisiert. Wir können jedoch den Code nicht für jedes kleine Release von MySQL aktualisieren und testen. Wir freuen uns über eine Quellenangabe, bestehen aber nicht darauf. Eine Quellenangabe besteht normalerweise aus Titel, Autor, Verlag und ISBN. Zum Beispiel: »High Performance MySQL/Optimierung, Backups, Replikation und Lastverteilung, 2. Auflage, von Baron Schwartz et al. Copyright 2009, O’Reilly Verlag, ISBN 9783897218895.« Falls Sie das Gefühl haben, dass Ihre Benutzung des Codes durch die oben angegebenen Umstände nicht abgedeckt ist, dann schreiben Sie einfach an
[email protected].
Wie Sie mit uns in Verbindung treten Sie können mit den Autoren direkt Kontakt aufnehmen. Barons Weblog finden Sie unter http://www.xaprb.com. Peter und Vadim betreiben zwei Weblogs, das etablierte und beliebte http://www.mysqlperformanceblog.com und das neuere http://www.webscalingblog.com. Sie finden die Website ihrer Firma, Percona, unter http://www.percona.com. Arjens Unternehmen, OpenQuery, besitzt eine Website unter http://openquery.com.au. Arjen betreibt ebenfalls ein Weblog unter http://arjen-lentz.livejournal.com und eine persönliche Site unter http://lentz.com.au.
Danksagungen für die zweite Auflage Sphinx-Entwickler Andrew Aksyonoff schrieb Anhang C, Sphinx mit MySQL benutzen. Wir wollen ihm für seine ausführliche Beschreibung danken. Wir haben beim Schreiben dieses Buches von vielen Leuten unschätzbare Hilfe erhalten. Es ist unmöglich, hier alle aufzuzählen – wir schulden der gesamten MySQL-Gemeinde sowie den Leuten bei MySQL AB unseren Dank. Hier ist eine Liste der Leute, die direkt etwas beigetragen haben; wir entschuldigen uns gleich bei denen, die wir vergessen haben: Tobias Asplund, Igor Babaev, Pascal Borghino, Roland Bouman, Ronald Bradford, Mark Callaghan, Jeremy Cole, Britt Crawford und das HiveDB-Projekt, Vasil Dimov, Harrison Fisk, Florian Haas, Dmitri Joukovski und Zmanda (danke für das Diagramm, das LVM-Schnappschüsse erläutert), Alan Kasindorf, Sheeri Kritzer Cabral, Marko Makela, Giuseppe Maxia, Paul McCullagh, B. Keith Murphy, Dhiren Patel, Sergey Petrunia, Alexander Rubin, Paul Tuckfield, Heikki Tuuri und Michael »Monty« Widenius. Ein besonderer Dank geht an Andy Oram, Isabel Kunkle und Rachel Wheeler, das Lektorenteam bei O’Reilly. Danke auch an den Rest der O’Reilly-Mannschaft.
Vorwort |
XV
Von Baron Ich möchte meiner Frau Lynn Rainville und unserem Hund Carbon danken. Wenn Sie ein Buch geschrieben hätten, dann wüssten Sie sicher, wie dankbar ich ihnen bin. Ganz besonderen Dank schulde ich außerdem Alan Rimm-Kaufman und meinen Kollegen bei der Rimm-Kaufman-Gruppe für ihre Unterstützung und Ermutigung während dieses Projekts. Danke an Peter, Vadim und Arjen dafür, dass sie mir die Möglichkeit gaben, meinen Traum zu verwirklichen. Und danke an Jeremy und Derek dafür, dass sie den Weg für uns gebahnt haben.
Von Peter Seit Jahren führe ich MySQL-Leistungs- und Skalierungspräsentationen, Schulungen und Beratungen durch und wollte immer ein breiteres Publikum erreichen. Deshalb war ich sehr aufgeregt, als Andy Oram an mich herantrat und mich bat, bei diesem Buch mitzuarbeiten. Ich habe vorher noch kein Buch geschrieben, war also nicht darauf vorbereitet, wie viel Zeit und Aufwand so etwas erfordert. Zuerst sprachen wir nur darüber, die erste Auflage zu aktualisieren, um neuere Versionen von MySQL einzubeziehen. Wir wollten aber so viel Material aufnehmen, dass wir schließlich den größten Teil des Buches neu schrieben. Dieses Buch ist wirklich eine Teamleistung. Da ich sehr damit beschäftigt war, Percona, Vadims und meine Beratungsfirma, in Gang zu bringen, und Englisch nicht meine Muttersprache ist, spielten wir alle unterschiedliche Rollen. Ich lieferte den Entwurf und den technischen Inhalt, anschließend sichtete ich das Material, wobei ich es während des Schreibens überarbeitete und erweiterte. Als Arjen (früherer Kopf des MySQL-Dokumentationsteams) zum Projekt stieß, begannen wir damit, den Entwurf mit Leben zu füllen. Die Dinge kamen dann wirklich ins Rollen, als Baron hinzukam, der in wahnwitziger Geschwindigkeit qualitativ hochwertigen Inhalt schreiben kann. Vadim war eine große Hilfe bei den tiefgehenden MySQL-Quellcodetests und wenn wir unsere Behauptungen mit Benchmarks und anderen Untersuchungen untermauern mussten. Während der Arbeit an dem Buch fanden wir immer mehr Bereiche, die wir genauer erkunden wollten. Viele der Themen des Buches, wie etwa Replikation, Abfrageoptimierung, InnoDB, Architektur und Entwurf, könnten leicht eigene Bücher füllen, so dass wir irgendwo aufhören und Material für eine künftige Auflage oder für unsere Blogs, Präsentationen und Artikel lassen mussten. Umfangreiche Hilfe bekamen wir von unseren Gutachtern, die zu den echten MySQLExperten, sowohl innerhalb als auch außerhalb von MySQL AB, gehören. Das sind der MySQL-Begründer Michael Widenius, der InnoDB-Begründer Heikki Tuuri, Igor Babaev, der Chef des MySQL-Optimiererteams, und viele andere.
XVI |
Vorwort
Außerdem danke ich meiner Frau Katya Zaytseva und meinen Kindern Ivan und Nadezhda, dass sie mir erlaubt haben, Zeit, die eigentlich der Familie gewidmet sein sollte, mit dem Buch zu verbringen. Ich bin darüber hinaus den Angestellten von Percona dankbar, dass sie eingesprungen sind, wenn ich verschwand, um an dem Buch zu arbeiten, und natürlich Andy Oram und O’Reilly, dass sie das Buch ermöglicht haben.
Von Vadim Ich danke Peter, mit dem ich wunderbar an diesem Buch zusammengearbeitet habe und auch gern andere Projekte durchführen würde, Baron, der entscheidend dafür verantwortlich ist, dass dieses Buch zustande kam, und Arjen, mit dem zu arbeiten mir viel Spaß macht. Dank gebührt auch unserem Lektor Andy Oram, der viel Geduld mit uns hatte, dem MySQL-Team, das eine großartige Software geschaffen hat, und unseren Kunden, die mir die Möglichkeiten geboten haben, mein MySQL-Verständnis zu schärfen. Und schließlich geht ein besonderer Dank an meine Frau Valerie und unsere Söhne Myroslav und Timur, die mich immer unterstützen und mir helfen voranzukommen.
Von Arjen Ich danke Andy für seine Klugheit, Führung und Geduld. Mein Dank gilt Baron, der auf den Zug zur zweiten Auflage aufgesprungen ist, obwohl er sich schon in Bewegung gesetzt hatte, und Peter und Vadim für die soliden Hintergrundinformationen und Benchmarks. Ein Dank geht auch an Jeremy und Derek, die mit der ersten Auflage das Fundament geschaffen haben. Wie Du, Derek, in meine Kopie geschrieben hast: »Macht es ordentlich, mehr will ich nicht.« Ich danke ebenfalls meinen früheren Kollegen (und jetzigen Freunden) von MySQL AB, wo ich das meiste erfahren habe, was ich über das Thema weiß, und ich möchte in diesem Zusammenhang vor allem Monty erwähnen, den ich weiterhin als stolzen Vater von MySQL betrachte, auch wenn sein Unternehmen jetzt Teil von Sun Microsystems ist. Ich danke außerdem allen anderen in der globalen MySQL-Gemeinschaft. Und last but not least, danke ich meiner Tochter Phoebe, die sich in dieser Phase ihres Lebens noch nicht um das Ding namens »MySQL« kümmert und auch keine Ahnung hat, welcher der Wiggles (die australische Popband, nicht die Zwerge) damit gemeint sein könnte! Für manche ist Ignoranz ein wahrer Segen, und sie bieten uns eine erfrischende Sicht auf die Dinge, die wirklich wichtig sind im Leben. Für alle anderen ist dieses Buch hoffentlich eine sinnvolle Ergänzung im Bücherschrank. Und vergessen Sie nicht zu leben!
Vorwort | XVII
Danksagungen für die erste Auflage Ein Buch wie dieses würde ohne die Hilfe Dutzender Menschen nie Wirklichkeit werden. Ohne sie wäre das Buch, das Sie gerade in den Händen halten, nur ein Haufen an den Rändern unserer Monitore angebrachter Notizen. Dies ist der Teil des Buches, in dem wir über die Leute, die uns geholfen haben, sagen dürfen, was wir wollen. Und wir müssen uns keine Sorgen machen, dass im Hintergrund Musik ertönt, die uns auffordert, den Mund zu halten und zu verschwinden, wie man das von Preisverleihungen im Fernsehen kennt. Wir hätten dieses Projekt ohne das ständige Drängeln, Betteln, Fordern und die Unterstützung unseres Lektors Andy Oram niemals abschließen können. Wenn es eine Person gibt, die dafür verantwortlich ist, dass Sie dieses Buch in den Händen halten, dann ist es Andy. Wir wissen die wöchentlichen Meckerrunden wirklich zu schätzen. Aber natürlich ist Andy nicht allein. Bei O’Reilly hatte eine ganze Menge anderer Leute ihren Anteil daran, dass aus den am Monitor befestigten Notizen das Buch wurde, das Sie gerade lesen möchten, deshalb wollen wir an dieser Stelle auch den Produktions-, Grafikund Marketingleuten danken, die uns dabei behilflich waren, dieses Buch zu schaffen. Und natürlich geht unser Dank auch an Tim O’Reilly für sein fortwährendes Engagement, die beste Dokumentation zu populärer Open-Source-Software zu veröffentlichen. Abschließend geht unser Dank an die Leute, die sich bereit erklärt haben, die verschiedenen Rohfassungen dieses Buches anzuschauen und uns all die Dinge aufzuzeigen, die wir falsch gemacht haben: unsere Korrektoren. Sie haben einen Teil ihres 2003er-Urlaubs damit verbracht, sich grob formatierte Versionen dieses Textes anzuschauen, der voller Schreibfehler, irreführender Anweisungen und mathematischer Fehler war. Unser Dank geht (ohne bestimmte Reihenfolge) an Brian »Krow« Aker, Mark »JDBC« Matthews, Jeremy »the other Jeremy« Cole, Mike »VBMySQL.com« Hillyer, Raymond »Rainman« De Roo, Jeffrey »Regex Master« Friedl, Jason DeHaan, Dan Nelson, Steve »Unix Wiz« Friedl und Kasia »Unix Girl« Trapszo.
Von Jeremy Ich möchte noch einmal Andy dafür danken, dass er dieses Projekt übernommen und uns fortlaufend weitere Kapitel abgerungen hat. Dereks Hilfe war unerlässlich, um die letzten 20–30 % des Buches abschließen zu können, ohne einen weiteren Zieltermin zu verpassen. Dank dafür, dass er noch so spät in diesen Prozess eingestiegen ist, meine sporadischen Produktivitätsausbrüche verarbeitet hat und die XML-Arbeit, Kapitel 10, Anhang C und all die anderen Dinge übernommen hat, die ich auf ihn abgewälzt habe. Ich muss auch meinen Eltern danken, die mir vor so vielen Jahren den ersten Commodore 64 kauften. Sie haben nicht nur die ersten zehn Jahre alles toleriert, was nach einer lebenslangen Besessenheit für Elektronik und Computertechnik aussah, sondern unterstützten auch sehr schnell meinen nie endenden Wunsch, mehr zu lernen und zu tun.
XVIII | Vorwort
Als Nächstes möchte ich einer Gruppe von Leuten danken, mit denen ich in den vergangenen Jahren arbeiten durfte, während ich missionarisch die Lehre von MySQL bei Yahoo! verbreitete. Jeffrey Friedl und Ray Goldberger ermutigten mich seit den frühen Anfängen dieses Unternehmens und lieferten Feedback zurück. Gleichzeitig fanden sich Steve Morris, James Harvey und Sergey Kolychev mit meinen scheinbar pausenlosen Experimenten mit den Yahoo! Finance MySQL-Servern ab, auch wenn dies ihre wichtige Arbeit störte. Dank auch an die unzähligen anderen Yahoo!s, die mir dabei halfen, interessante MySQL-Probleme und -Lösungen zu finden. Und, was noch wichtiger ist, danke für das Vertrauen und den Glauben an mich, der notwendig war, um MySQL an einer der wichtigsten und sichtbarsten Stellen des Yahoo!-Geschäfts einsetzen zu dürfen. Adam Goodman, Herausgeber und Besitzer des Linux Magazine, erleichterte mir den Einstieg in die Welt des Schreibens für ein technisches Publikum, indem er meine ersten längeren MySQL-Artikel im Jahr 2001 veröffentlichte. Seither hat er mir mehr über redaktionelle Bearbeitung und Herausgabe beigebracht, als ihm bewusst war, und mich ermutigt, diesen Weg weiterzugehen, indem er mir eine eigene monatliche Kolumne in seinem Magazin gewährte. Danke, Adam. Ich danke Monty und David dafür, MySQL mit der Welt zu teilen. Und da wir gerade bei MySQL AB sind, möchte ich auch all den anderen danken, die mich ermutigt haben, dieses Buch zu schreiben: Kerry, Larry, Joe, Marten, Brian, Paul, Jeremy, Mark, Harrison, Matt und dem Rest des Teams. Ihr seid der Hammer. Schließlich möchte ich all meinen Weblog-Lesern danken, die mich ermutigt haben, jeden Tag informell über MySQL und andere technische Themen zu schreiben.
Von Derek Wie Jeremy möchte ich, aus den gleichen Gründen, meiner Familie danken. Ich möchte meinen Eltern danken, die mich fortwährend ermutigt haben, ein Buch zu schreiben, auch wenn das Ergebnis nicht dem entspricht, was sie im Sinn hatten. Meine Großeltern brachten mir zwei wertvolle Dinge bei: den Umgang mit Geld und die Liebe zu Computern (als sie mir das Geld für meinen ersten Commodore VIC-20 liehen). Ich kann Jeremy nicht genug danken, mich zu dieser Achterbahnfahrt des Buchschreibens eingeladen zu haben. Es war eine großartige Erfahrung, und ich hoffe, zukünftig wieder mit ihm arbeiten zu können. Besonderer Dank geht an Raymond De Roo, Brian Wohlgemuth, David Calafrancesco, Tera Doty, Jay Rubin, Bill Catlan, Anthony Howe, Mark O’Neal, George Montgomery, George Barber und die vielen anderen Menschen, die sich mein Meckern angehört haben, auf die ich meine Ideen loslassen durfte, um zu sehen, ob ein Außenstehender verstehen kann, was ich zu sagen versuche, oder die ein Lächeln auf meine Lippen zauberten, als ich es am meisten brauchte. Ohne euch wäre das Buch vielleicht auch geschrieben worden, aber ich wäre darüber verrückt geworden.
Vorwort | XIX
KAPITEL 1
Die MySQL-Architektur
Die MySQL-Architektur unterscheidet sich stark von der anderer Datenbankserver und ist sehr vielseitig einsetzbar. MySQL ist zwar nicht perfekt, aber dennoch so flexibel, dass es in anspruchsvollen Umgebungen, wie etwa Webanwendungen, gut funktioniert. Gleichzeitig kann MySQL eingebettete Anwendungen, Data-Warehouses, Content-Indexing- und -Delivery-Software, hochverfügbare redundante Systeme, Online Transaction Processing (OLTP) und vieles mehr versorgen. Um das meiste aus MySQL herauszuholen, müssen Sie seinen Aufbau verstehen, damit Sie mit ihm und nicht gegen es arbeiten. MySQL ist ausgesprochen flexibel. So können Sie es für den Gebrauch auf vielerlei Hardware konfigurieren, außerdem unterstützt es eine Vielzahl von Datentypen. Die ungewöhnlichste und wichtigste Eigenart von MySQL ist jedoch seine Storage-Engine-Architektur, deren Design die Verarbeitung von Abfragen sowie andere Serveraufgaben von der Datenspeicherung und -bereitstellung trennt. In MySQL 5.1 können Sie Storage-Engines sogar als Laufzeit-Plugins laden. Diese Trennung erlaubt es Ihnen, tabellenweise zu entscheiden, wie Ihre Daten gespeichert werden und welche Leistungen, Funktionen und andere Eigenschaften Sie haben wollen. Dieses Kapitel bietet einen Überblick über die MySQL-Serverarchitektur, zeigt die wesentlichen Unterschiede zwischen den Storage-Engines und erläutert, weshalb diese Unterschiede wichtig sind. Wir versuchen, MySQL zu erklären, indem wir Details vereinfachen und Beispiele zeigen. Diese Diskussion ist vor allem für Neulinge auf dem Gebiet der Datenbankserver sinnvoll sowie für Leser, die sich eher mit anderen Datenbankservern auskennen.
Die logische Architektur von MySQL Es wird Ihnen helfen, den Server zu verstehen, wenn Sie sich eine Vorstellung davon machen, wie die Komponenten von MySQL zusammenarbeiten. Abbildung 1-1 zeigt den Aufbau von MySQL aus logischer Sicht.
|
1
Clients
Verbindungs-/Thread-Verarbeitung
AbfrageCache
Parser
Optimierer
Storage-Engines
Abbildung 1-1: Logischer Aufbau der MySQL-Serverarchitektur
Die oberste Schicht enthält die Dienste, die nicht allein von MySQL verwendet werden. Diese Dienste werden von den meisten netzwerkbasierten Client/Server-Programmen oder -Servern benötigt: Behandlung von Verbindungen, Authentifizierung, Sicherheit usw. In der zweiten Schicht werden die Dinge interessant. Ein Großteil des in MySQL investierten Gehirnschmalzes liegt hier, einschließlich des Codes für Parsing, Analyse, Optimierung, Caching von Abfragen sowie alle fest eingebauten Funktionen (z.B. Datum, Zeit, Mathematik und Verschlüsselung). Jegliche Funktionalität, die über StorageEngines hinweg zur Verfügung gestellt wird, ist hier zusammengefasst: z.B. gespeicherte Prozeduren (Stored Procedures), Trigger und Sichten. Die dritte Schicht enthält die Storage-Engines (auch: Speicher-Engines). Sie sind für die Speicherung und den Abruf aller Daten verantwortlich, die »in« MySQL gespeichert werden. Wie die verschiedenen unter GNU/Linux zur Verfügung stehenden Dateisysteme besitzt jede Storage-Engine ihre Vor- und Nachteile. Der Server kommuniziert mit ihnen über die Storage-Engine-API. Diese Schnittstelle verbirgt die Unterschiede zwischen den Storage-Engines, so dass sie auf der Abfrageebene im Großen und Ganzen transparent sind. Die API enthält einige Dutzend Low-Level-Funktionen für solche Operationen wie »beginne eine Transaktion« oder »lies die Zeile mit dem Primärschlüssel«. Die StorageEngines verarbeiten weder SQL1 noch kommunizieren sie miteinander; sie reagieren einfach auf Anforderungen vom Server.
1 Eine Ausnahme bildet InnoDB, das fremde Schlüsseldefinitionen verarbeitet, weil der MySQL-Server diese nicht selbst implementiert.
2 | Kapitel 1: Die MySQL-Architektur
Verbindungsmanagement und Sicherheit Jede Clientverbindung erhält innerhalb des Serverprozesses ihren eigenen Thread. Die Abfragen der Verbindung werden in dem jeweiligen Thread ausgeführt, der sich wiederum auf einem Prozessorkern oder einer CPU befindet. Der Server speichert Threads im Cache, so dass sie bei einer neuen Verbindung nicht jedes Mal erzeugt und zerstört werden müssen.2 Wenn Clients (Anwendungen) eine Verbindung zum MySQL-Server herstellen, muss der Server sie authentifizieren. Die Authentifizierung erfolgt anhand des Benutzernamens, des Ursprungshosts und des Passworts. Über eine SSL-Verbindung (Secure Sockets Layer) können auch X.509-Zertifikate eingesetzt werden. Sobald die Verbindung zum Client besteht, verifiziert der Server, ob der Client die Berechtigungen für die jeweils ausgeführten Abfragen besitzt (z.B., ob es dem Client erlaubt ist, eine SELECT-Anweisung auszuführen, die auf die Country-Tabelle in der Datenbank world zugreift). Wir behandeln diese Themen detailliert in Kapitel 12.
Optimierung und Ausführung MySQL analysiert Abfragen, um eine interne Struktur zu erzeugen (den Parse- oder Syntaxbaum), und führt dann eine Reihe von Optimierungen durch. Dazu können das Umschreiben der Abfrage, das Feststellen der Reihenfolge, in der die Tabellen gelesen werden, das Auswählen der zu verwendenden Indizes usw. gehören. Über spezielle Schlüsselwörter in der Abfrage können Sie Hinweise an den Optimierer übergeben, die die Entscheidungsfindung beeinflussen. Darüber hinaus können Sie den Server auffordern, verschiedene Aspekte der Optimierung zu erklären. Auf diese Weise erfahren Sie, welche Entscheidungen der Server trifft. Sie erhalten also einen Anhaltspunkt zum Überarbeiten von Abfragen, Schemata und Einstellungen, damit Sie alles so effizient wie möglich gestalten können. Auf den Optimierer gehen wir in Kapitel 4 ausführlicher ein. Dem Optimierer ist es im Prinzip egal, welche Storage-Engine eine bestimmte Tabelle verwendet, die Storage-Engine hingegen beeinflusst, wie der Server die Abfrage optimiert. Der Optimierer fragt die Storage-Engine nach einigen ihrer Fähigkeiten und den Kosten für bestimmte Operationen sowie nach Statistiken über die Tabellendaten. Einige Storage-Engines unterstützen z.B. Indextypen, die für bestimmte Abfragen hilfreich sein können. Mehr Informationen über Indizierung und Schema-Optimierung erhalten Sie in Kapitel 3. Bevor allerdings die Abfrage geparst wird, schaut der Server im Abfrage-Cache nach, der nur SELECT-Anweisungen sowie deren Ergebnissätze speichern kann. Wenn jemand eine Abfrage ausführt, die identisch ist mit einer, die sich bereits im Cache befindet, dann muss der Server diese Abfrage nicht parsen, optimieren oder ausführen – er kann einfach den gespeicherten Ergebnissatz zurückliefern! Wir besprechen den Abfrage-Cache ausführlich in »Der MySQL-Abfrage-Cache« auf Seite 220. 2 MySQL AB plant, in künftigen Versionen des Servers Verbindungen von Threads zu trennen.
Die logische Architektur von MySQL | 3
Nebenläufigkeitskontrolle Immer wenn zu einem Zeitpunkt mehr als eine Abfrage Daten ändern muss, tritt das Problem der Nebenläufigkeitskontrolle (Concurrency Control) auf. Für unsere Zwecke in diesem Kapitel muss MySQL auf zwei Ebenen aktiv werden: auf der Serverebene und auf der Ebene der Storage-Engine. Dem großen Thema der Nebenläufigkeitskontrolle widmet sich ein Großteil der theoretischen Literatur. Dieses Buch allerdings befasst sich nicht mit der Theorie oder gar mit den Interna von MySQL. Daher geben wir Ihnen nur einen vereinfachten Überblick darüber, wie MySQL mit nebenläufigen Lese- und Schreibvorgängen umgeht, damit Sie die nötigen Informationen für den Rest dieses Kapitels haben. Wir benutzen eine Mailbox auf einem Unix-System als Beispiel. Das klassische mboxDateiformat ist sehr einfach. Alle Nachrichten in einer mbox-Mailbox werden eine nach der anderen aneinandergefügt. Das macht es sehr einfach, E-Mail-Nachrichten zu lesen und zu verarbeiten. Außerdem vereinfacht es die Auslieferung der E-Mails: Eine neue Nachricht wird einfach an das Ende der Datei angehängt. Was geschieht jedoch, wenn zwei Prozesse versuchen, etwas zur gleichen Zeit an die gleiche Mailbox auszuliefern? Natürlich kann das die Mailbox beschädigen und zwei ineinander vermischte Nachrichten am Ende der Mailbox-Datei hinterlassen. Anständige Systeme zur Mail-Auslieferung setzen Sperren (Locks) ein, um eine Beschädigung zu vermeiden. Versucht ein Client eine zweite Auslieferung, während die Mailbox gesperrt ist, muss er warten, bis er den Lock erhalten hat, bevor er die Nachricht zustellen kann. Dieses Schema funktioniert in der Praxis recht gut, bietet aber keine Unterstützung für Nebenläufigkeit. Da zu einem Zeitpunkt nur ein einziger Prozess die Mailbox ändern darf, wirft dieser Ansatz bei einer stark genutzten Mailbox Probleme auf.
Lese/Schreib-Locks Das Lesen aus der Mailbox ist nicht ganz so problematisch. Es gibt keine Schwierigkeiten, wenn mehrere Clients gleichzeitig die gleiche Mailbox auslesen. Da sie keine Änderungen vornehmen, kann eigentlich auch nichts schiefgehen. Was passiert aber, wenn jemand versucht, Nachricht Nummer 25 zu löschen, während andere Programme die Mailbox auslesen? Es kommt drauf an. Ein Leser könnte eine beschädigte oder inkonsistente Ansicht der Mailbox erhalten. Um sicherzugehen, verlangt sogar das Lesen aus einer Mailbox besondere Sorgfalt. Wenn Sie sich die Mailbox als Datenbanktabelle und jede Nachricht als eine Zeile vorstellen, dann werden Sie leicht merken, dass das Problem in diesem Kontext das gleiche ist. In vielerlei Hinsicht ist eine Mailbox nur eine einfache Datenbanktabelle. Das Modifizieren von Zeilen (bzw. Datensätzen) in einer Datenbanktabelle ist dem Löschen oder Ändern von Nachrichten in einer Mailbox-Datei sehr ähnlich. Die Lösung dieses klassischen Problems der Nebenläufigkeitskontrolle ist recht einfach. Systeme, die einen nebenläufigen, d.h. parallel erfolgenden Lese-/Schreibzugriff erlau-
4 | Kapitel 1: Die MySQL-Architektur
ben, implementieren typischerweise ein Locking-System, das zwei Arten von Sperren unterstützt. Diese Sperren oder Locks sind üblicherweise als Shared Locks und Exklusive Locks oder als Lese-Locks und Schreib-Locks bekannt. Ohne uns um die eigentliche Locking-Technik zu kümmern, können wir das Konzept folgendermaßen beschreiben. Die Lese-Locks (Read Locks) für eine Ressource werden gemeinsam genutzt (engl. Shared): Viele Clients können die Ressource gleichzeitig lesen und stören einander nicht. Schreib-Locks dagegen sind exklusiv – d.h., sie blockieren sowohl Lese-Locks als auch andere Schreib-Locks –, da die einzig sichere Methode darin besteht, zu einem Zeitpunkt nur einen einzigen Client auf die Ressource schreiben zu lassen und alle Lesevorgänge zu verhindern, wenn ein Client schreibt. In der Datenbankwelt kommt das Locking ständig vor: MySQL muss verhindern, dass ein Client einen Teil der Daten liest, während ihn ein anderer Client ändert. Es führt dieses Lock-Management intern auf eine Art und Weise aus, die größtenteils transparent ist.
Locking-Granularität Eine Möglichkeit, die Nebenläufigkeit einer gemeinsam genutzten Ressource zu verbessern, besteht darin, bei der Wahl dessen, was gesperrt wird, selektiver zu sein. Anstatt die gesamte Ressource zu sperren, könnten Sie nur den Teil sperren, der die zu ändernden Daten enthält. Oder noch besser: Sperren Sie genau den Teil der Daten, der geändert werden soll. Indem Sie die Menge der zu sperrenden Daten einschränken, können mehr Änderungen gleichzeitig erfolgen – solange sie sich untereinander nicht überschneiden. Problematisch ist allerdings, dass die Locks Ressourcen beanspruchen. Jede Lock-Operation – das Erstellen eines Locks, die Prüfung, ob ein Lock frei ist, das Freigeben eines Locks usw. – erfordert einen gewissen Aufwand. Wenn das System zu viel Zeit damit verbringt, die Locks zu verwalten, anstatt Daten zu speichern und abzurufen, leidet möglicherweise die Performance. Eine Locking-Strategie stellt immer einen Kompromiss aus Locking-Overhead und Datensicherheit dar; dieser Kompromiss beeinflusst schließlich die Performance. Die meisten kommerziellen Datenbankserver lassen Ihnen kaum eine Wahl: Sie können für Ihre Tabellen das sogenannte Row-Level Locking einsetzen, also das Locking auf Datensatzebene, bei dem Ihnen eine Vielzahl von oftmals komplexen Methoden zur Verfügung steht, um eine gute Performance mit vielen Locks zu erreichen. Bei MySQL haben Sie dagegen die Wahl. Seine Storage-Engines können eigene LockingRichtlinien und -Granularitäten implementieren. Die Locking-Verwaltung bildet eine wichtige Entscheidung beim Entwurf der Storage-Engine; das Festlegen der Granularität auf einer bestimmten Stufe kann für verschiedene Anwendungszwecke eine bessere Performance gewährleisten, die Engine jedoch für andere Fälle weniger gut geeignet erscheinen lassen. Da MySQL mehrere Storage-Engines bietet, ist es nicht nötig, sich auf eine einzige, allumfassende Lösung zu beschränken. Schauen wir uns die beiden wichtigsten Locking-Strategien an.
Nebenläufigkeitskontrolle | 5
Tabellen-Locks Die grundlegendste und mit dem geringsten Overhead verbundene Locking-Strategie in MySQL ist der Tabellen-Lock. Er entspricht dem bereits beschriebenen Mailbox-Locking: Er sperrt die gesamte Tabelle. Möchte ein Client etwas in die Tabelle schreiben (einfügen, löschen, aktualisieren usw.), beschafft er sich einen Schreib-Lock, der alle anderen Leseund Schreiboperationen in Schach hält. Wenn niemand schreibt, können lesende Prozesse Lese-Locks anfordern, die anderen Lese-Locks nicht in die Quere kommen. Tabellen-Locks bieten Variationen, um unter bestimmten Bedingungen eine gute Performance zu gewährleisten. Beispielsweise erlauben READ LOCAL-Tabellen-Locks einige Arten von nebenläufigen, d.h. parallel erfolgenden Schreiboperationen. Schreib-Locks besitzen außerdem eine höhere Priorität als Lese-Locks, so dass die Anforderung eines SchreibLocks an den Anfang der Locking-Warteschlange rückt, auch wenn sich bereits lesende Prozesse in der Warteschlange befinden (Schreib-Locks können in der Warteschlange an Lese-Locks vorbeiziehen, Lese-Locks dagegen können Schreib-Locks nicht überholen). Storage-Engines können zwar ihre eigenen Locks verwalten, dennoch verwendet auch MySQL eine Vielzahl von Locks für verschiedene Zwecke, bei denen es sich prinzipiell um Tabellen-Locks handelt. So setzt etwa der Server ungeachtet der Storage-Engine einen Tabellen-Lock für Anweisungen wie ALTER TABLE ein.
Row-Locks Die Form des Lockings, die die größtmögliche Nebenläufigkeit ermöglicht (und den meisten Overhead mitschleppt) ist das zeilenorientierte oder Row-Locking. Row-Locking gibt es unter anderem in den InnoDB- und Falcon-Storage-Engines. Row-Locks werden in der Storage-Engine implementiert, nicht im Server (werfen Sie einfach noch einmal einen Blick auf die Darstellung der logischen Architektur). Der Server ist völlig ahnungslos, was Locks betrifft, die in den Storage-Engines implementiert sind. Wie Sie später noch erfahren werden, implementieren die Storage-Engines das Locking alle auf ihre eigene Weise.
Transaktionen Es ist kaum möglich, sich mit den erweiterten Funktionen eines Datenbanksystems zu befassen, ohne auf Transaktionen zu stoßen. Eine Transaktion ist eine Gruppe von SQLAbfragen, die als atomar betrachtet werden, d.h. als zusammengehörende Einheit. Entweder wird die gesamte Gruppe der Abfragen von der Datenbank-Engine auf die Datenbank angewandt oder keine (z.B. aufgrund eines Absturzes oder aus einem anderen Grund). Hier gilt: alles oder nichts! Nur ein kleiner Teil dieses Abschnitts ist MySQL-spezifisch. Wenn Sie bereits mit ACIDTransaktionen vertraut sind, können Sie direkt mit »Transaktionen bei MySQL« auf Seite 11 fortfahren. Eine Bankanwendung ist das klassische Beispiel dafür, weshalb Transaktionen notwendig sind. Stellen Sie sich vor, die Datenbank der Bank würde aus zwei Tabellen bestehen:
6 | Kapitel 1: Die MySQL-Architektur
Girokonto und Sparkonto. Um 200 € von Karlas Girokonto auf ihr Sparkonto zu überwei-
sen, sind mindestens drei Schritte notwendig: 1. Sicherstellen, dass ihr Guthaben auf dem Girokonto mehr als 200 € aufweist 2. 200 € vom Girokonto abziehen 3. 200 € dem Sparkonto gutschreiben
Die gesamte Operation muss in einer Transaktion zusammengefasst werden, damit sie als Ganzes zurückgenommen werden kann, falls einer der Schritte fehlschlägt. Eine Transaktion wird mit der Anweisung START TRANSACTION gestartet und dann mit COMMIT bestätigt und gespeichert oder mit ROLLBACK zurückgenommen. Der SQL-Code für die Beispieltransaktion könnte so aussehen: 1 2 3 4 5
START TRANSACTION; SELECT balance FROM checking WHERE customer_id = 10233276; UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276; UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276; COMMIT;
Transaktionen allein reichen aber nicht aus. Was passiert, wenn der Datenbankserver abstürzt, während er gerade Zeile 4 ausführt? Wer weiß? Die Kundin hat wahrscheinlich gerade 200 € verloren. Und was passiert, wenn ein anderer Prozess zwischen den Zeilen 3 und 4 einfach das ganze Girokonto löscht? Die Bank hätte der Kundin einen Kredit von 200 € gewährt, ohne es überhaupt zu wissen. Transaktionen reichen nicht aus, wenn das System nicht den sogenannten ACID-Test besteht. ACID ist ein Akronym für »Atomicity, Consistency, Isolation, Durability«, also Atomarität, Konsistenz, Isolation, Dauerhaftigkeit, vier eng miteinander verknüpften Kriterien, die ein gut funktionierendes Transaktionsverarbeitungssystem erfüllen muss: Atomicity (Atomarität) Eine Transaktion muss als eine einzelne, nicht teilbare Arbeitseinheit ausgeführt werden: Entweder wird die gesamte Transaktion angewandt, oder sie wird komplett zurückgenommen. Wenn Transaktionen atomar sind, dann gibt es keine teilweise abgeschlossene Transaktion. Das Prinzip heißt: alles oder nichts. Consistency (Konsistenz) Die Datenbank muss sich immer von einem konsistenten Zustand in den nächsten bewegen. In unserem Beispiel sorgt die Konsistenz dafür, dass ein Absturz zwischen den Zeilen 3 und 4 nicht dazu führt, dass 200 € vom Girokonto verschwinden. Da die Transaktion niemals bestätigt wird (COMMIT), spiegelt sich keine der an der Datenbank durchgeführten Änderungen in der Datenbank wider. Isolation Die Ergebnisse einer Transaktion sind üblicherweise für andere Transaktionen unsichtbar, bis die Transaktion abgeschlossen ist. Das stellt in unserem Fall sicher, dass bei der Erstellung einer Kontoübersicht nach Zeile 3, aber vor Zeile 4 die 200 € immer noch auf dem Girokonto zu sehen sind. Wenn wir die verschiedenen Isolationsebenen besprechen, werden Sie verstehen, weshalb wir üblicherweise sagen.
Transaktionen | 7
Durability (Dauerhaftigkeit) Einmal mit COMMIT bestätigt, werden die Ergebnisse der Transaktion dauerhaft gespeichert. Das bedeutet, dass die Änderungen in einer Art festgehalten werden müssen, die dafür sorgt, dass die Daten bei einem Systemabsturz nicht verlorengehen. Das Konzept der Dauerhaftigkeit ist allerdings etwas unscharf, da es tatsächlich viele Stufen gibt. Manche Dauerhaftigkeitsstrategien bieten eine stärkere Sicherheitsgarantie als andere, und nichts ist wirklich hundertprozentig von Dauer. Wir werden später darauf eingehen, was Dauerhaftigkeit in MySQL wirklich bedeutet. Freuen Sie sich speziell auf »InnoDB-Ein-/Ausgabe-Tuning« auf Seite 306. ACID-Transaktionen sorgen dafür, dass die Banken Ihr Geld nicht verlieren. Es ist im Allgemeinen ausgesprochen schwierig, wenn nicht gar unmöglich, dies mit der Anwendungslogik zu erreichen. Ein ACID-konformer Datenbankserver muss alle möglichen komplizierten Dinge tun, die Sie möglicherweise gar nicht mitbekommen, um ACIDGarantien zu gewährleisten. Genau wie bei der erhöhten Locking-Granularität besteht der Nachteil dieser zusätzlichen Sicherheit darin, dass der Datenbankserver mehr Arbeit erledigen muss. Das bedeutet auch, dass ein Datenbankserver mit ACID-Transaktionen im Allgemeinen mehr CPULeistung, Speicher und Plattenplatz benötigt als ohne. Wie wir bereits mehrfach ausgeführt haben, spielt hier die Storage-Engine-Architektur von MySQL ihre Vorteile aus. Sie können selbst entscheiden, ob Ihre Anwendung Transaktionen benötigt. Wenn Sie sie nicht brauchen, können Sie bei einigen Arten von Abfragen eine höhere Leistung erreichen, wenn Sie eine Storage-Engine verwenden, die keine Transaktionen bietet. Sie könnten sogar LOCK TABLES einsetzen, um auch ohne Transaktionen die notwendige Sicherheit zu erhalten. Es liegt ganz bei Ihnen.
Isolationsebenen Isolation ist komplexer, als es auf den ersten Blick erscheinen mag. Der SQL-Standard definiert vier Isolationsebenen mit speziellen Regeln dafür, welche Änderungen innerhalb und außerhalb einer Transaktion sichtbar sind oder nicht. Niedrigere Isolationsebenen erlauben typischerweise eine stärkere Nebenläufigkeit und bringen einen geringeren Overhead mit sich. Jede Storage-Engine implementiert die Isolationsebenen etwas anders, und möglicherweise entspricht die Umsetzung nicht dem, was Sie erwarten würden, wenn Sie mit einem anderen Datenbankprodukt vertraut sind (wir werden deshalb in diesem Abschnitt nicht auf jedes einzelne Detail eingehen). Lesen Sie einfach das Handbuch der Storage-Engine, für die Sie sich letztendlich entschieden haben.
8 | Kapitel 1: Die MySQL-Architektur
Schauen wir uns die vier Isolationsebenen an: READ UNCOMMITTED
Auf der Isolationsebene READ UNCOMMITTED können Transaktionen die Ergebnisse noch nicht bestätigter Transaktionen sehen. Auf dieser Ebene können sehr viele Probleme auftreten, wenn Sie nicht ganz genau wissen, was Sie tun, und keinen guten Grund haben, es zu tun. In der Praxis wird diese Ebene selten genutzt, da ihre Performance kaum besser ist als die der anderen Ebenen, die wiederum viele Vorteile bieten. Das Lesen unbestätigter Daten wird auch als Dirty Read bezeichnet. READ COMMITTED
Die Standardisolationsebene der meisten Datenbanksysteme (aber nicht von MySQL!) ist READ COMMITTED. Sie entspricht der einfachen Definition der Isolation, die wir vorhin verwendet haben: Eine Transaktion sieht nur die Ergebnisse von Transaktionen, die bereits bestätigt waren, als sie begann. Ihre Änderungen wiederum werden erst sichtbar, nachdem sie bestätigt wurde. Auf dieser Ebene ist immer noch ein sogenannter Nonrepeatable Read (eine nicht wiederholbare Leseoperation) erlaubt. Dies bedeutet, dass Sie eine Anweisung zweimal ausführen können und unterschiedliche Daten zu sehen bekommen. REPEATABLE READ REPEATABLE READ löst die Probleme, die READ UNCOMMITTED zulässt. Diese Ebene garan-
tiert, dass alle Zeilen, die eine Transaktion liest, bei nachfolgenden Lesevorgängen innerhalb der gleichen Transaktion »gleich aussehen«. Theoretisch kann aber immer noch ein anderes schwerwiegendes Problem auftreten, nämlich das der PhantomReads. Vereinfacht gesagt, kann ein Phantom-Read auftreten, wenn Sie einen Bereich von Zeilen auswählen, eine andere Transaktion eine neue Zeile in diesen Bereich einfügt und Sie diesen Bereich erneut auswählen. Sie sehen dann diese neue »Phantom«-Zeile. InnoDB und Falcon lösen das Phantom-Read-Problem mithilfe der Multi-Version Concurrency Control, die wir weiter hinten in diesem Kapitel erklären. REPEATABLE READ ist die Standardisolationsebene von MySQL. Die Storage-Engines
InnoDB und Falcon respektieren diese Einstellung; Sie lernen in Kapitel 6, wie Sie sie ändern. Einige andere Engines beachten sie ebenfalls, das hängt aber von der Engine ab. SERIALIZABLE
Die höchste Isolationsebene, SERIALIZABLE, löst das Phantom-Read-Problem, indem die Transaktionen so angeordnet werden, dass sie sich nicht stören können. SERIALIZABLE setzt quasi auf jede Zeile, die es liest, eine Sperre. Auf dieser Ebene kann es zu vielen Timeouts und Kämpfen um einen Lock kommen. Wir kennen kaum Leute, die diese Isolationsebene benutzen, allerdings könnten die Anforderungen Ihrer Anwendung es notwendig machen, die verringerte Performance in Kauf zu nehmen, um die damit verbundene Datenstabilität zu erreichen. Tabelle 1-1 fasst die verschiedenen Isolationsebenen und die mit ihnen verbundenen Nachteile zusammen.
Transaktionen | 9
Tabelle 1-1: ANSI SQL-Isolationsebenen Isolationsebene
Dirty Reads möglich
Nonrepeatable Reads möglich
Phantom-Reads möglich
Locking-Reads
READ UNCOMMITTED
Ja
Ja
Ja
Nein
READ COMMITTED
Nein
Ja
Ja
Nein
REPEATABLE READ
Nein
Nein
Ja
Nein
SERIALIZABLE
Nein
Nein
Nein
Ja
Deadlocks Ein Deadlock tritt auf, wenn zwei oder mehr Transaktionen wechselseitig Locks auf denselben Ressourcen anfordern und halten und dadurch einen Kreis von Abhängigkeiten schaffen. Es kommt zu Deadlocks, wenn Transaktionen versuchen, Ressourcen in unterschiedlicher Reihenfolge zu sperren. Sie können auftreten, wenn mehrere Transaktionen die gleichen Ressourcen sperren. Nehmen wir als Beispiel diese beiden Transaktionen, die über die StockPrice-Tabelle laufen sollen: Transaktion #1 START TRANSACTION; UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2002-05-01'; UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02'; COMMIT;
Transaktion #2 START TRANSACTION; UPDATE StockPrice SET high UPDATE StockPrice SET high COMMIT;
= 20.12 WHERE stock_id = 3 and date = '2002-05-02'; = 47.20 WHERE stock_id = 4 and date = '2002-05-01';
Wenn Sie Pech haben, führt jede Transaktion die erste Anweisung aus und aktualisiert eine Datenzeile, wodurch diese in dem Prozess gesperrt wird. Beide Transaktionen versuchen dann, ihre zweite Zeile zu aktualisieren, wobei sie dann feststellen, dass diese bereits gesperrt ist. Die beiden Transaktionen warten nun bis in alle Ewigkeit darauf, dass die jeweils andere fertig wird – es sei denn, jemand schreitet ein und löst diesen Deadlock auf. Um dieses Problem zu lösen, implementieren Datenbanksysteme verschiedene Formen von Deadlock-Erkennung und Timeouts. Die schlaueren Systeme, wie etwa die InnoDBStorage-Engine, erkennen zyklische Abhängigkeiten und geben einen Fehler zurück. Das ist in der Tat sehr gut, da sich Deadlocks ansonsten in ausgesprochen langsam ablaufenden Abfragen äußern würden. Andere geben nach einer gewissen Zeit auf, wenn sie keinen Lock erhalten haben, was wiederum nicht so gut ist. Der Umgang von InnoDB mit Deadlocks sieht momentan so aus, dass es diejenige Transaktion rückgängig macht, die die wenigsten exklusiven Row-Locks besitzt (ein ungefähres Maß, für das es am einfachsten ist, es rückgängig zu machen).
10 |
Kapitel 1: Die MySQL-Architektur
Das Verhalten und die Reihenfolge von Locks hängen von der jeweiligen Storage-Engine ab. Einige Storage-Engines landen möglicherweise bei einer bestimmten Abfolge von Anweisungen in einem Deadlock, während andere dies nicht tun. Deadlocks haben eine Doppelnatur: Manche sind aufgrund echter Datenkonflikte unvermeidlich, während andere aufgrund der Funktionsweise einer Storage-Engine entstehen. Deadlocks können nicht unterbrochen werden, ohne eine der Transaktionen, entweder teilweise oder vollständig, zu widerrufen. Sie gehören zu Transaktionssystemen einfach dazu. Ihre Anwendungen müssen daher in der Lage sein, mit ihnen zurechtzukommen. Viele Anwendungen können einfach ihre Transaktionen wiederholen.
Transaktions-Log Mit einer Protokollierung der Transaktionen (Transaktions-Logging) können Transaktionen effizienter gestaltet werden. Anstatt die Tabellen auf der Festplatte nach jeder Änderung zu aktualisieren, kann die Storage-Engine die im Speicher befindliche Kopie der Daten ändern. Das geht sehr schnell. Die Storage-Engine schreibt dann einen Eintrag für die Änderung in das Transaktions-Log, das sich auf der Festplatte befindet und daher dauerhaft ist. Auch dies ist eine relativ schnelle Operation, da das Anhängen von LogEreignissen anstelle der zufälligen Ein-/Ausgabe an vielen Stellen nur eine sequenzielle Ein-/Ausgabe auf einem sehr kleinen Bereich der Festplatte umfasst. Zu einem späteren Zeitpunkt kann ein Prozess dann die Tabelle auf der Festplatte aktualisieren. Die meisten Storage-Engines, die diese (auch als Write-Ahead Logging bezeichnete) Technik einsetzen, müssen daher Änderungen zweimal auf die Festplatte schreiben.3 Kommt es zu einem Absturz, nachdem die Aktualisierung in das Transaktions-Log geschrieben wurde, aber bevor die Änderungen an den Daten selbst vorgenommen wurden, kann die Storage-Engine die Änderungen nach einem Neustart wiederherstellen. Die Wiederherstellungsmethode hängt von der jeweiligen Storage-Engine ab.
Transaktionen bei MySQL MySQL AB stellt drei transaktionsfähige Storage-Engines bereit: InnoDB, NDB Cluster und Falcon. Es gibt außerdem verschiedene Engines von Drittanbietern; die momentan bekanntesten Engines sind solidDB und PBXT. Im nächsten Abschnitt stellen wir spezielle Eigenschaften der einzelnen Engines vor.
AUTOCOMMIT MySQL arbeitet per Voreinstellung im AUTOCOMMIT-Modus. Das bedeutet, dass jede Abfrage als separate Transaktion ausgeführt wird, solange Sie nicht ausdrücklich eine Transaktion begonnen haben. Sie können AUTOCOMMIT für die aktuelle Verbindung aktivieren oder deaktivieren, indem Sie eine Variable setzen: 3 Die PBXT-Storage-Engine vermeidet clevererweise das Write-Ahead Logging.
Transaktionen |
11
mysql> SHOW VARIABLES LIKE 'AUTOCOMMIT'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec) mysql> SET AUTOCOMMIT = 1;
Die Werte 1 und ON sind äquivalent, genau wie 0 und OFF. Wenn Sie mit AUTOCOMMIT=0 arbeiten, sind Sie immer in einer Transaktion, bis Sie COMMIT oder ROLLBACK ausführen. MySQL startet dann sofort eine neue Transaktion. Das Ändern des Wertes von AUTOCOMMIT hat keine Wirkung auf nichttransaktionsfähige Tabellen wie MyISAM- oder Memory-Tabellen, die prinzipiell immer im AUTOCOMMIT-Modus arbeiten. Werden bestimmte Befehle während einer offenen Transaktion aufgerufen, dann veranlassen sie MySQL, die Transaktion zu bestätigen, bevor sie ausgeführt werden. Bei diesen Befehlen handelt es sich typischerweise um DDL-Befehle (Data Definition Language), die wesentliche Änderungen verursachen können, wie etwa ALTER TABLE. Aber auch LOCK TABLES und einige andere Anweisungen haben diese Wirkung. In der Dokumentation Ihrer Version finden Sie eine vollständige Liste der Befehle, die automatisch eine Transaktion bestätigen. MySQL erlaubt es Ihnen, mit dem Befehl SET TRANSACTION ISOLATION LEVEL die Isolationsebene festzulegen, die wirksam wird, wenn die nächste Transaktion beginnt. Sie können die Isolationsebene für den gesamten Server in der Konfigurationsdatei festlegen (siehe Kapitel 6) oder sich auf die aktuelle Sitzung beschränken: mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
MySQL erkennt alle vier Standard-ANSI-Isolationsebenen, InnoDB unterstützt sie alle. Andere Storage-Engines bieten unterschiedliche Unterstützung für die verschiedenen Isolationsebenen.
Storage-Engines in Transaktionen mischen Bei MySQL werden die Transaktionen nicht vom Server verwaltet; stattdessen implementieren die zugrunde liegenden Storage-Engines die Transaktionen selbst. Aus diesem Grund ist es nicht möglich, unterschiedliche Engines in einer einzigen Transaktion zu mischen. Bei MySQL AB arbeitet man daran, den Server um eine Transaktionsverwaltung auf höherer Ebene zu erweitern, wodurch es möglich werden wird, transaktionsfähige Tabellen in einer Transaktion zu mischen und zu vergleichen. Bis es so weit ist, müssen Sie vorsichtig sein. Wenn Sie transaktionsfähige und nichttransaktionsfähige Tabellen (z.B. InnoDB- und MyISAM-Tabellen) in einer Transaktion mischen, funktioniert die Transaktion (wenn alles gut geht). Wird jedoch ein Rollback erforderlich, dann können die Änderungen an der nichttransaktionsfähigen Tabelle nicht zurückgenommen werden. Die Datenbank verbleibt in einem inkonsistenten Zustand, der nur schwer wieder ausgeglichen werden
12 |
Kapitel 1: Die MySQL-Architektur
kann und die ganze Idee einer Transaktion ad absurdum führt. Daher ist es enorm wichtig, die richtige Storage-Engine für eine Tabelle zu wählen. Normalerweise warnt MySQL Sie nicht, wenn Sie transaktionsfähige Operationen auf einer nichttransaktionsfähigen Tabelle durchführen. Gelegentlich generiert die Zurücknahme einer Transaktion die Warnung »Some nontransactional changed tables couldn’t be rolled back«, meist allerdings gibt es kein Anzeichen dafür, dass Sie mit nichttransaktionsfähigen Tabellen arbeiten.
Implizites und explizites Locking InnoDB verwendet ein zweiphasiges Locking-Protokoll. Es kann sich jederzeit während einer Transaktion Locks beschaffen, gibt sie aber erst bei einem COMMIT oder ROLLBACK wieder frei. Alle Locks werden gleichzeitig freigegeben. Die früher beschriebenen LockingMechanismen sind alle implizit. InnoDB behandelt die Locks automatisch entsprechend Ihrer Isolationsebene. Allerdings unterstützt InnoDB auch explizites Locking, das vom SQL-Standard nicht einmal erwähnt wird: • SELECT ... LOCK IN SHARE MODE • SELECT ... FOR UPDATE MySQL unterstützt auch die Befehle LOCK TABLES und UNLOCK TABLES, die im Server und nicht in den Storage-Engines implementiert sind. Es gibt für sie Anwendungsfälle, allerdings stellen sie keinen Ersatz für Transaktionen dar. Wenn Sie Transaktionen benötigen, dann verwenden Sie eine transaktionsfähige Storage-Engine. Wir haben es oft mit Anwendungen zu tun, die von MyISAM nach InnoDB konvertiert wurden, aber weiterhin LOCK TABLES benutzen. Dies ist aufgrund des Row-Level-Locking nicht mehr notwendig und kann schwerwiegende Performance-Probleme verursachen. Die Interaktion zwischen LOCK TABLES und Transaktionen ist komplex, und es kann in manchen Serverversionen zu unerwarteten Verhaltensweisen kommen. Wir empfehlen daher, dass Sie – egal, welche Storage-Engine Sie verwenden – von der Verwendung von LOCK TABLES absehen, es sei denn, Sie sind in einer Transaktion, und AUTOCOMMIT ist deaktiviert.
Multi-Version Concurrency Control Die meisten der transaktionsfähigen Storage-Engines von MySQL, wie etwa InnoDB, Falcon und PBXT, verwenden keinen einfachen Row-Locking-Mechanismus, sondern benutzen Row-Level-Locking zusammen mit einer Technik zur Erhöhung der Nebenläufigkeit namens Multi-Version Concurrency Control (MVCC). MVCC ist nicht auf MySQL beschränkt, auch Oracle, PostgreSQL und einige andere Datenbanksysteme setzen es ein.
Multi-Version Concurrency Control |
13
Sie können sich MVCC als eine neue Variante des Row-Level-Lockings vorstellen. Sie müssen in vielen Fällen überhaupt nicht sperren und haben einen geringeren Overhead. Je nach seiner Implementierung erlaubt es nichtsperrende Leseoperationen und sperrt während der Schreiboperationen nur die notwendigen Datensätze. MVCC behält einen Schnappschuss der Daten von einem ganz bestimmten Zeitpunkt. Das bedeutet, dass Transaktionen eine konsistente Sicht der Daten sehen, unabhängig davon, wie lange ihre Ausführung dauert. Es bedeutet aber auch, dass unterschiedliche Transaktionen zur gleichen Zeit unterschiedliche Daten in den gleichen Tabellen sehen können! Wenn Sie das noch nie zuvor erlebt haben, mag es zunächst verwirrend sein, mit der Zeit aber werden Sie es immer besser verstehen. Jede Storage-Engine implementiert MVCC anders. Zu den Varianten gehören optimistische und pessimistische Concurrency Control. Wir werden eine Möglichkeit der Funktion von MVCC anhand einer vereinfachten Version des Verhaltens von InnoDB erläutern. InnoDB implementiert MVCC, indem es für jede Zeile zwei zusätzliche verborgene Werte speichert, die aufzeichnen, wann diese Zeile erzeugt wurde und wann sie verfällt (oder gelöscht wurde). Anstatt die tatsächlichen Zeiten festzuhalten, an denen diese Ereignisse eingetreten sind, speichert die Zeile die Systemversionsnummer zum Zeitpunkt des Ereignisses. Dabei handelt es sich um eine Zahl, die immer dann erhöht wird, wenn eine Transaktion beginnt. Jede Transaktion besitzt ab dem Zeitpunkt, zu dem sie begann, einen eigenen Datensatz mit der aktuellen Systemversion. Alle Abfragen müssen die Versionsnummern der Zeilen anhand der Version der Transaktion überprüfen. Wir wollen uns anschauen, wie das bei bestimmten Operationen funktioniert, wenn die Isolationsebene auf REPEATABLE READ gesetzt ist: SELECT
InnoDB muss jede Zeile untersuchen, um sicherzustellen, dass sie zwei Kriterien erfüllt: • InnoDB muss eine Version der Zeile finden, die mindestens so alt ist wie die Transaktion (d.h., ihre Version muss kleiner oder gleich der Version der Transaktion sein). Dies stellt sicher, dass die Zeile entweder bereits existierte, als die Transaktion begann, oder dass die Transaktion die Zeile erzeugte bzw. veränderte. • Die Löschversionsnummer (Deletion-ID) der Zeile muss undefiniert oder größer sein als die Version der Transaktion. Dies stellt sicher, dass die Zeile nicht gelöscht war, bevor die Transaktion begann. Zeilen, die beide Tests bestehen, können als Ergebnis der Abfrage zurückgeliefert werden. INSERT
InnoDB zeichnet die aktuelle Systemversionsnummer in der neuen Zeile auf. DELETE
InnoDB zeichnet die aktuelle Systemversionsnummer als Deletion-ID der Zeile auf.
14 |
Kapitel 1: Die MySQL-Architektur
UPDATE
InnoDB schreibt eine neue Kopie der Zeile und verwendet dabei die Systemversionsnummer für die Version der neuen Zeile. Außerdem schreibt es die Systemversionsnummer als Deletion-ID der alten Zeile. Das Ergebnis dieses zusätzlichen Aufwands besteht darin, dass die meisten lesenden Abfragen niemals Locks anfordern. Sie lesen die Daten ganz einfach so schnell sie können, wobei sie aber nur Zeilen auswählen, die den Kriterien entsprechen. Nachteilig ist allerdings, dass die Storage-Engine in den einzelnen Zeilen mehr Daten speichern muss, mehr Aufwand beim Untersuchen der Zeilen hat und mehr Verwaltungsoperationen durchführen muss. MVCC funktioniert nur mit den Isolationsebenen REPEATABLE READ und READ COMMITTED. READ UNCOMMITTED ist nicht MVCC-kompatibel, da die Abfragen nicht die Zeilenversion lesen, die ihrer Transaktionsversion entspricht; sie lesen einfach immer die neueste Version. SERIALIZABLE ist nicht MVCC-kompatibel, weil Leseoperationen jede Zeile sperren, die sie zurückliefern. In Tabelle 1-2 sind die verschiedenen Locking-Modelle und Concurrency-Ebenen in MySQL noch einmal zusammengefasst. Tabelle 1-2: Locking-Modelle und Concurrency in MySQL bei der Standardisolationsebene Locking-Strategie
Concurrency
Overhead
Engines
Tabellenorientiert
Niedrigste
Niedrigster
MyISAM, Merge, Memory
Zeilenorientiert
Hoch
Hoch
NDB Cluster
Zeilenorientiert mit MVCC
Höchste
Höchster
InnoDB, Falcon, PBXT, solidDB
Die Storage-Engines von MySQL In diesem Abschnitt erhalten Sie einen Überblick über die Storage-Engines von MySQL. Wir werden hier nicht allzu sehr ins Detail gehen, da wir die Storage-Engines und ihre speziellen Verhaltensweisen im Laufe des Buches noch genauer erläutern. Allerdings ist selbst dieses Buch keine komplette Dokumentation; Sie müssen daher unbedingt die MySQL-Handbücher für die Storage-Engines lesen, für die Sie sich entschieden haben. MySQL bietet darüber hinaus Foren für die einzelnen Storage-Engines, in denen Sie oft auch Links auf weiterführende Informationen und interessante Einsatzgebiete finden. Falls Sie die Engines nur grob miteinander vergleichen wollen, können Sie sich gleich zu Tabelle 1-3 auf Seite 32 begeben. MySQL speichert jede Datenbank (auch als Schema bezeichnet) als Unterverzeichnis seines Datenverzeichnisses im zugrunde liegenden Dateisystem. Wenn Sie eine Tabelle erzeugen, speichert MySQL die Tabellendefinition in einer .frm-Datei mit dem gleichen Namen wie die Tabelle. Das heißt, wenn Sie eine Tabelle namens MyTable anlegen, speichert MySQL die Tabellendefinition in MyTable.frm. Da MySQL das Dateisystem ver-
Die Storage-Engines von MySQL |
15
wendet, um Datenbanknamen und Tabellendefinitionen abzulegen, hängt es von der Plattform ab, ob Groß- und Kleinschreibung eine Rolle spielen. Bei einer MySQL-Instanz unter Windows ist die Schreibweise von Tabellen- und Datenbanknamen egal, auf Unixartigen Systemen wird zwischen Groß- und Kleinschreibung unterschieden. Jede StorageEngine speichert die Daten und Indizes der Tabelle anders, der Server selbst arbeitet mit der Tabellendefinition. Um festzustellen, welche Storage-Engine eine bestimmte Tabelle verwendet, benutzen Sie den Befehl SHOW TABLE STATUS. Führen Sie z.B. folgenden Befehl aus, um die Tabelle user in der Datenbank mysql zu untersuchen: mysql> SHOW TABLE STATUS LIKE 'user' \G *************************** 1. row *************************** Name: user Engine: MyISAM Row_format: Dynamic Rows: 6 Avg_row_length: 59 Data_length: 356 Max_data_length: 4294967295 Index_length: 2048 Data_free: 0 Auto_increment: NULL Create_time: 2002-01-24 18:07:17 Update_time: 2002-01-24 21:56:29 Check_time: NULL Collation: utf8_bin Checksum: NULL Create_options: Comment: Users and global privileges 1 row in set (0.00 sec)
Die Ausgabe zeigt, dass es sich hier um eine MyISAM-Tabelle handelt. Sie werden in der Ausgabe vermutlich noch viele weitere Informationen und Statistiken bemerken. Schauen wir uns die Bedeutung der einzelnen Zeilen kurz an: Name
Der Name der Tabelle. Engine
Die Storage-Engine der Tabelle. In den alten MySQL-Versionen hieß diese Spalte Type, nicht Engine. Row_format
Das Format der Zeile bzw. des Datensatzes. Bei einer MyISAM-Tabelle kann es Dynamic, Fixed oder Compressed sein. Dynamische Zeilen variieren in der Länge, weil sie Felder variabler Länge enthalten, etwa VARCHAR oder BLOB. Feste Zeilen, die immer die gleiche Größe aufweisen, bestehen entsprechend aus Feldern, deren Länge nicht variiert, wie etwa CHAR und INTEGER. Komprimierte Zeilen existieren nur in komprimierten Tabellen (siehe »Komprimierte MyISAM-Tabellen« auf Seite 20).
16 |
Kapitel 1: Die MySQL-Architektur
Rows
Die Anzahl der Zeilen (Datensätze) in der Tabelle. Bei nichttransaktionsfähigen Tabellen ist diese Zahl immer genau, sonst handelt es sich eher um eine Schätzung. Avg_row_length
Gibt an, wie viele Bytes die durchschnittliche Zeile enthält. Data_length
Gibt an, wie viele Daten (in Bytes) die gesamte Tabelle enthält. Max_data_length
Die maximale Datenmenge, die diese Tabelle aufnehmen kann. Näheres erfahren Sie in »Speicherung« auf Seite 18. Index_length
Gibt an, wie viel Festplattenplatz die Indexdaten einnehmen. Data_free
Bei einer MyISAM-Tabelle ist das die Menge des zugewiesenen, aber momentan unbenutzten Platzes. Diese Menge enthält die zuvor gelöschten Zeilen und kann von künftigen INSERT-Anweisungen wieder zurückgefordert werden. Auto_increment
Der nächste AUTO_INCREMENT-Wert. Create_time
Gibt an, wann die Tabelle angelegt wurde. Update_time
Gibt an, wann die Daten in der Tabelle zuletzt geändert wurden. Check_time
Gibt an, wann die Tabelle zuletzt mit CHECK TABLE oder myisamchk überprüft wurde. Collation
Der Standardzeichensatz und die Sortierreihenfolge für Zeichenspalten in der Tabelle. Nähere Informationen über diese Eigenschaften finden Sie in »Zeichensätze und Sortierreihenfolgen« auf Seite 255. Checksum
Bei Aktivierung liefert diese Information eine aktuelle Checksumme des Inhalts der gesamten Tabelle. Create_options
Weitere Optionen, die beim Anlegen der Tabelle angegeben wurden. Comment
Dieses Feld enthält eine Reihe zusätzlicher Informationen. Bei einer MyISAMTabelle sind hier mögliche Kommentare enthalten, die beim Erzeugen der Tabelle angegeben wurden. Verwendet die Tabelle die InnoDB-Storage-Engine, dann steht hier die Menge des freien Platzes im InnoDB-Tablespace. Ist die Tabelle eine Sicht, dann enthält der Kommentar den Text »VIEW«.
Die Storage-Engines von MySQL |
17
Die MyISAM-Engine Als Standard-Storage-Engine von MySQL bietet MyISAM einen guten Kompromiss aus Leistung und nützlichen Funktionen, wie etwa Volltextindizierung, Komprimierung und raumbezogenen (GIS) Funktionen. MyISAM unterstützt weder Transaktionen noch zeilenorientierte (Row-Level) Locks.
Speicherung MyISAM speichert jede Tabelle typischerweise in zwei Dateien: einer Datendatei und einer Indexdatei. Die beiden Dateien tragen die Erweiterungen .MYD bzw. .MYI. Das MyISAM-Format ist plattformneutral, was bedeutet, dass Sie die Daten- und Indexdateien problemlos von einem Intel-basierten Server auf einen PowerPC oder eine Sun SPARC kopieren können. MyISAM-Tabellen können dynamische oder statische Zeilen (d.h. Zeilen fester Länge) enthalten. MySQL entscheidet anhand der Tabellendefinition, welches Format es verwendet. Die Anzahl der Zeilen, die eine MyISAM-Tabelle aufnehmen kann, wird hauptsächlich durch den verfügbaren Festplattenplatz auf Ihrem Datenbankserver und die größtmögliche Dateigröße bestimmt, die Ihr Betriebssystem erzeugen kann. MyISAM-Tabellen, die in MySQL 5.0 mit Zeilen variabler Länge erzeugt werden, können standardmäßig mit 256 TByte Daten umgehen und verwenden 6 Byte große Zeiger auf die Datensätze. Frühere MySQL-Versionen benutzten standardmäßig 4-Byte-Zeige für bis zu 4 GByte Daten. Alle MySQL-Versionen können mit Zeigergrößen bis zu 8 Byte umgehen. Um die Zeigergröße in einer MyISAM-Tabelle nach oben oder unten zu ändern, müssen Sie Werte für die Optionen MAX_ROWS und AVG_ROW_LENGTH angeben, die ungefähr den benötigten Platz repräsentieren: CREATE TABLE mytable ( a INTEGER NOT NULL PRIMARY KEY, b CHAR(18) NOT NULL ) MAX_ROWS = 1000000000 AVG_ROW_LENGTH = 32;
In diesem Beispiel haben wir MySQL mitgeteilt, dass es sich auf die Speicherung von wenigstens 32 GByte Daten in der Tabelle einstellen muss. Um festzustellen, was MySQL getan hat, fragen Sie einfach den Tabellenstatus ab: mysql> SHOW TABLE STATUS LIKE 'mytable' \G *************************** 1. row *************************** Name: mytable Engine: MyISAM Row_format: Fixed Rows: 0 Avg_row_length: 0 Data_length: 0 Max_data_length: 98784247807 Index_length: 1024 Data_free: 0 Auto_increment: NULL
18 |
Kapitel 1: Die MySQL-Architektur
Create_time: 2002-02-24 17:36:57 Update_time: 2002-02-24 17:36:57 Check_time: NULL Create_options: max_rows=1000000000 avg_row_length=32 Comment: 1 row in set (0.05 sec)
Wie Sie sehen können, hat sich MySQL genau die Optionen gemerkt, die Sie angegeben haben. Und es hat sich für eine Repräsentation entschieden, die 91 GByte Daten aufnehmen kann! Die Zeigergröße können Sie später mit der Anweisung ALTER TABLE ändern. Allerdings werden dabei die gesamte Tabelle und alle ihre Indizes neu geschrieben, was recht lange dauern kann.
MyISAM-Eigenschaften Als eine der ältesten Storage-Engines in MySQL besitzt MyISAM viele Eigenschaften, die im Laufe der Jahre entwickelt wurden, um bestimmte Lücken zu schließen: Locking und Nebenläufigkeit MyISAM sperrt ganze Tabellen, keine Zeilen. Lesende Operationen fordern Shared Locks (Lese-Locks) für alle Tabellen an, die sie lesen müssen. Schreibende Operationen fordern exklusive Locks (Schreib-Locks) an. Es ist allerdings möglich, neue Zeilen in die Tabelle einzufügen, während Auswahlabfragen darauf ausgeführt werden (nebenläufige oder parallele Einfügungen). Dies ist eine wichtige und sinnvolle Eigenart. Automatische Reparatur MySQL unterstützt die automatische Überprüfung und Reparatur von MyISAMTabellen. In »MyISAM-Ein-/Ausgabe verbessern« auf Seite 304 finden Sie weitere Informationen. Manuelle Reparatur Sie können die Befehle CHECK TABLE mytable und REPAIR TABLE mytable verwenden, um eine Tabelle auf Fehler zu überprüfen und diese zu beheben. Wenn der Server offline ist, haben Sie die Möglichkeit, die Überprüfung und Reparatur der Tabellen mit dem Kommandozeilenwerkzeug myisamchk vorzunehmen. Index-Funktionen In MyISAM-Tabellen können Sie Indizes der ersten 500 Zeichen von BLOB- und TEXTSpalten erzeugen. MyISAM unterstützt Volltextindizes, die einzelne Wörter für komplexe Suchoperationen indizieren. Weitere Informationen über die Indizierung finden Sie in Kapitel 3. Verzögertes Schreiben von Schlüsseln MyISAM-Tabellen, die mit der Option DELAY_KEY_WRITE erzeugt wurden, schreiben am Ende einer Abfrage keine geänderten Indexdaten auf die Festplatte. Stattdessen puffert MyISAM die Änderungen in einem Schlüsselpuffer im Speicher. Die Indexblöcke werden erst dann auf die Platte geschrieben, wenn der Puffer geleert oder die Tabelle geschlossen wird. Bei sich häufig ändernden Tabellen kann dies zu einer deutlichen
Die Storage-Engines von MySQL |
19
Leistungssteigerung führen. Nach einem Server- oder Systemabsturz werden die Indizes allerdings mit hoher Wahrscheinlichkeit beschädigt und müssen repariert werden. Sie sollten dies mit einem Skript erledigen, das myisamchk ausführt, bevor Sie den Server neu starten, oder indem Sie automatische Wiederherstellungsoptionen verwenden. (Selbst wenn Sie DELAY_KEY_WRITE nicht benutzen, sind diese Sicherheitsvorkehrungen eine ausgezeichnete Idee.) Sie können das verzögerte Schreiben der Schlüssel global oder für einzelne Tabellen konfigurieren.
Komprimierte MyISAM-Tabellen Manche Tabellen ändern sich nie, nachdem sie erzeugt und mit Daten gefüllt wurden – etwa in CD-ROM- oder DVD-ROM-basierten Anwendungen und Embedded-Umgebungen. Diese eignen sich dann besonders für komprimierte MyISAM-Tabellen. Sie können Tabellen mit dem Dienstprogramm myisampack komprimieren (oder »packen«). Komprimierte Tabellen lassen sich nicht mehr ändern (obwohl Sie sie bei Bedarf dekomprimieren, verändern und erneut komprimieren können), benötigen aber im Allgemeinen weniger Festplattenplatz. Dadurch sind sie schneller, da kleinere Dateien weniger Suchoperationen erfordern, um Datensätze zu finden. Komprimierte MyISAMTabellen können Indizes haben, sind aber schreibgeschützt. Bei moderner Hardware ist der benötigte Overhead für die Dekomprimierung der Daten für die meisten Anwendungen unerheblich, wo der wirkliche Nutzen in der Reduzierung der Festplattenzugriffe liegt. Die Zeilen werden einzeln komprimiert, so dass MySQL nicht die ganze Tabelle (oder eine Seite) entpacken muss, nur um eine einzige Zeile zu lesen.
Die MyISAM-Merge-Engine Die Merge-Engine ist eine Variante von MyISAM. Eine Merge-Tabelle bildet eine Kombination mehrerer identischer MyISAM-Tabellen in einer virtuellen Tabelle. Sie bietet sich vor allem an, wenn Sie MySQL in Logging- und Data-Warehouse-Anwendungen einsetzen. Eine ausführliche Besprechung der Merge-Tabellen finden Sie in »Merge-Tabellen und Partitionierung« auf Seite 273.
Die InnoDB-Engine InnoDB wurde für die Transaktionsverarbeitung geschaffen – speziell für die Verarbeitung vieler kurzlebiger Transaktionen, die normalerweise abgeschlossen und nicht zurückgenommen werden. Für die transaktionsfähige Speicherung ist dies die beliebteste Storage-Engine. Ihre Leistungsfähigkeit und die Fähigkeit zur automatischen Wiederherstellung nach Abstürzen hat jedoch auch zu ihrer Popularität für nichttransaktionsfähige Speicheranforderungen beigetragen.
20 |
Kapitel 1: Die MySQL-Architektur
InnoDB speichert seine Daten in einer oder mehreren Datendateien, die zusammen als Tablespace bezeichnet werden. Bei einem Tablespace handelt es sich im Prinzip um eine Blackbox, die InnoDB ganz allein verwaltet. In MySQL 4.1 und neueren Versionen kann InnoDB die Daten und Indizes jeder Tabelle in separaten Dateien ablegen. InnoDB kann auch direkt Festplattenpartitionen zum Aufbau seines Tablespace nutzen. Näheres hierzu finden Sie in »Der InnoDB-Tablespace« auf Seite 314. InnoDB verwendet MVCC, um eine hohe Nebenläufigkeit zu erreichen, und implementiert alle vier Standard-SQL-Isolationsebenen. Standardmäßig wird die Isolationsebene REPEATABLE READ benutzt. InnoDB setzt eine Next-Key-Locking-Strategie ein, um Phantom-Reads in dieser Isolationsebene zu vermeiden: Anstatt nur die Zeilen zu sperren, die Sie in einer Abfrage berührt haben, sperrt InnoDB auch Lücken in der Indexstruktur, wodurch verhindert wird, dass Phantome eingefügt werden. InnoDB-Tabellen bauen auf einem cluster-orientierten Index auf, den wir in Kapitel 3 behandeln. Die Indexstrukturen von InnoDB unterscheiden sich stark von denen der meisten anderen MySQL-Storage-Engines. Dadurch sind sehr schnelle Anfragen nach Primärschlüsseln möglich. Allerdings enthalten Sekundärindizes (Indizes, die nicht der Primärschlüssel sind) die Spalten mit den Primärschlüsseln; falls Ihr Primärschlüssel also groß ist, werden auch die anderen Indizes groß sein. Sie sollten sich um einen kleinen Primärschlüssel bemühen, falls Sie viele Indizes in einer Tabelle haben. InnoDB komprimiert nämlich seine Indizes nicht. Momentan ist InnoDB im Gegensatz zu MyISAM nicht dazu in der Lage, Indizes durch Sortierung aufzubauen. Daher lädt InnoDB die Daten und erzeugt die Indizes viel langsamer als MyISAM. Jede Operation, die die Struktur einer InnoDB-Tabelle ändert, baut die gesamte Tabelle einschließlich aller Indizes neu auf. InnoDB wurde entworfen, als die meisten Server langsame Festplatten, nur eine einzige CPU und begrenzte Speicherkapazität besaßen. Heutzutage, da Multicore-Server mit riesigen Speichern und schnellen Festplatten immer preiswerter werden, kommt es bei InnoDB zu Problemen mit der Skalierbarkeit. Die Entwickler von InnoDB kümmern sich um diese Probleme, zurzeit sind aber noch nicht alle gelöst. Im Abschnitt »InnoDB bei Nebenläufigkeit anpassen« auf Seite 321 erfahren Sie genauer, wie Sie eine hohe Nebenläufigkeit mit InnoDB erreichen. Neben der ausgezeichneten Nebenläufigkeit ist die Fremdschlüssel-Beschränkung eine ausgesprochen beliebte Eigenschaft von InnoDB, die nicht einmal der MySQL-Server bietet. InnoDB bietet darüber hinaus außerordentlich schnelle Lookups bei Abfragen, die einen Primärschlüssel verwenden. InnoDB besitzt eine Vielzahl interner Optimierungen. Dazu gehört vorausschauendes Read-Ahead (Vorwärts-Lesen) zum Vorabladen von Daten von der Festplatte, ein adaptiver Hash, der für sehr schnelle Suchen automatisch Indizes im Speicher aufbaut, und ein Eingabepuffer, um das Einfügen zu beschleunigen. Wir werden später ausführlich auf diese Optimierungen eingehen.
Die Storage-Engines von MySQL |
21
Das Verhalten von InnoDB ist sehr kompliziert. Wir empfehlen Ihnen, den Abschnitt »InnoDB Transaction Model and Locking« des MySQL-Handbuchs zu lesen, falls Sie InnoDB einsetzen. Es gibt eine Menge Überraschungen und Ausnahmen, derer Sie sich bewusst sein sollten, bevor Sie eine Anwendung mit InnoDB erstellen.
Die Memory-Engine Memory-Tabellen (früher als HEAP-Tabellen bezeichnet) sind sinnvoll, wenn Sie einen schnellen Zugriff auf Daten benötigen, die sich entweder nie ändern oder die nach einem Neustart nicht fortbestehen müssen. Memory-Tabellen sind im Allgemeinen um etwa eine Größenordnung schneller als MyISAM-Tabellen. Alle ihre Daten werden im Speicher abgelegt, Abfragen müssen deshalb nicht auf die Festplatten-Ein-/Ausgabe warten. Die Tabellenstruktur einer Memory-Tabelle überlebt einen Serverneustart, die Daten tun dies allerdings nicht. Hier sind einige gute Anwendungsfälle für Memory-Tabellen: • für »Lookup«- oder »Mapping«-Tabellen, wie etwa Tabellen, die Postleitzahlen zu Orten zuordnen • zum Speichern der Ergebnisse periodisch gesammelter Daten in einem Cache • für Zwischenergebnisse beim Analysieren von Daten Memory-Tabellen unterstützen HASH-Indizes, die bei Lookup-Abfragen sehr schnell arbeiten. In »Hash-Indizes« auf Seite 108 finden Sie weitere Informationen über HASH-Indizes. Obwohl Memory-Tabellen sehr schnell sind, eignen sie sich oft nicht als allgemeiner Ersatz für festplattenbasierte Tabellen. Sie verwenden ein tabellenorientiertes Locking, das für Schreibvorgänge nur eine niedrige Nebenläufigkeit gewährt, und sie unterstützen keine TEXT- oder BLOB-Spaltentypen. Außerdem unterstützen sie nur Zeilen fester Größe, so dass sie VARCHARs als CHARs ablegen, was eine Verschwendung von Speicher darstellt. MySQL benutzt die Memory-Engine intern, wenn es Abfragen verarbeitet, die eine temporäre Tabelle für Zwischenergebnisse benötigen. Wenn das Zwischenergebnis zu groß für eine Memory-Tabelle wird oder TEXT- oder BLOB-Spalten besitzt, konvertiert MySQL es in eine MyISAM-Tabelle auf der Festplatte um. Mehr dazu erfahren Sie in späteren Kapiteln. Oft werden Memory-Tabellen mit temporären Tabellen verwechselt, bei denen es sich um flüchtige Tabellen handelt, die mit CREATE TEMPORARY TABLE erzeugt wurden. Temporäre Tabellen können jede Storage-Engine verwenden, sie sind nicht identisch mit Tabellen, die die Memory-StorageEngine benutzen. Temporäre Tabellen sind nur für eine einzige Verbindung sichtbar und verschwinden komplett, wenn die Verbindung geschlossen wird.
22 |
Kapitel 1: Die MySQL-Architektur
Die Archive-Engine Die Archive-Engine unterstützt nur INSERT- und SELECT-Abfragen und bot vor MySQL 5.1 auch keine Unterstützung für Indizes. Sie verursacht weniger Festplattenzugriffe als MyISAM, da sie Schreibzugriffe auf Daten puffert und jede Zeile beim Einfügen mit zlib komprimiert. Jede SELECT-Abfrage erfordert außerdem einen kompletten Scan der Tabelle. Archive-Tabellen eignen sich daher ideal für das Logging und die Datenerfassung, wo bei der Analyse oft eine ganze Tabelle durchsucht wird oder wo man schnelle INSERT-Abfragen auf einem Replikations-Master haben will. Replikations-Slaves können für die gleiche Tabelle eine andere Storage-Engine verwenden. Das bedeutet, dass die Tabelle auf dem Slave Indizes für eine schnellere Performance bei der Analyse haben kann. (In Kapitel 8 finden Sie weitere Informationen über die Replikation.) Archive unterstützt ein Row-Level-Locking und ein besonderes Puffersystem für stark nebenläufiges Einfügen. Es bietet konsistente Leseoperationen, indem es ein SELECT stoppt, nachdem es die Anzahl der Zeilen bezogen hat, die in der Tabelle existierten, als die Abfrage begann. Außerdem lässt es massenhafte Einfügungen so lange unsichtbar, bis sie abgeschlossen sind. Diese Funktionen emulieren einige Aspekte der transaktionsfähigen und MVCC-Verhaltensweisen, allerdings ist Archive keine transaktionsfähige Storage-Engine. Es ist einfach eine Storage-Engine, die für schnelles Einfügen und komprimierte Speicherung optimiert wurde.
Die CSV-Engine Die CSV-Engine kann Dateien mit kommaseparierten Werten (CSV-Dateien) als Tabellen behandeln, unterstützt aber keine Indizes dafür. Diese Engine ermöglicht es Ihnen, Dateien in eine Datenbank hinein- und aus ihr herauszukopieren, während der Server läuft. Eine CSV-Datei, die Sie aus einer Tabellenkalkulation exportieren und im Datenverzeichnis des MySQL-Servers speichern, kann der Server sofort lesen. In gleicher Weise kann ein externes Programm Daten, die Sie in eine CSV-Tabelle schreiben, sofort lesen. CSV-Tabellen eignen sich besonders als Datenaustauschformat und für bestimmte Arten des Logging.
Die Federated-Engine Die Federated-Engine speichert Daten nicht lokal. Jede Federated-Tabelle verweist auf eine Tabelle auf einem entfernten MySQL-Server. Sie stellt also für alle Operationen eine Verbindung zu einem entfernten Server her. Sie wird gelegentlich verwendet, um »Hacks« zu ermöglichen, wie etwa Tricks bei der Replikation. In der aktuellen Implementierung dieser Engine gibt es viele Merkwürdigkeiten und Beschränkungen. Wir denken, dass die Federated-Engine sich aufgrund ihrer Funktionsweise am besten für Lookups nach einzelnen Zeilen anhand des Primärschlüssels oder für INSERT-Abfragen auf einem entfernten Server eignet. Bei Aggregatabfragen (zusammengesetzten Abfragen), Verknüpfungen (Joins) oder anderen grundlegenden Operationen funktioniert sie nicht besonders gut.
Die Storage-Engines von MySQL |
23
Die Blackhole-Engine Die Blackhole-Engine besitzt überhaupt keinen Speichermechanismus. Sie verwirft alle INSERTs, anstatt sie zu speichern. Allerdings schreibt der Server Abfragen an BlackholeTabellen wie gewöhnlich in seine Protokolle, so dass sie auf Slaves repliziert oder einfach im Log aufbewahrt werden können. Dadurch eignet sich die Blackhole-Engine ganz gut für ausgefallene Replikationskonfigurationen und für das Logging von Überprüfungen.
Die NDB-Cluster-Engine MySQL AB erwarb die NDB-Cluster-Engine im Jahre 2003 von Sony Ericsson. Ursprünglich war sie für Echtzeitanforderungen gedacht und bot Redundanz und Lastausgleich. Die Engine schrieb zwar ein Log auf die Festplatte, behielt aber alle Daten im Speicher und war für Primärschlüssel-Lookups optimiert. MySQL hat seitdem weitere Indizierungsmethoden und viele Optimierungen hinzugefügt, und MySQL 5.1 erlaubt die Speicherung einiger Spalten auf der Festplatte. Die NDB-Architektur ist einzigartig: Ein NDB-Cluster ist völlig anders als beispielsweise ein Oracle-Cluster. Die NDB-Infrastruktur basiert auf einem Shared-Nothing-Konzept. Es gibt kein Speichernetzwerk oder eine andere große, zentralisierte Speicherlösung, auf die andere Cluster-Typen zurückgreifen. Eine NDB-Datenbank besteht aus Datenknoten, Verwaltungsknoten und SQL-Knoten (MySQL-Instanzen). Jeder Datenknoten enthält ein Segment (»Fragment«) der Daten des Clusters. Die Fragmente werden dupliziert, so dass das System mehrere Kopien der gleichen Daten auf unterschiedlichen Knoten besitzt. Aus Gründen der Redundanz und der Hochverfügbarkeit ist üblicherweise jedem Knoten ein physischer Server zugeordnet. In diesem Sinn ist NDB mit einem RAID auf Serverebene vergleichbar. Die Verwaltungsknoten werden verwendet, um die zentralisierte Konfiguration zu beziehen sowie um die Cluster-Knoten zu überwachen und zu steuern. Alle Datenknoten kommunizieren miteinander, und alle MySQL-Server sind mit allen Datenknoten verbunden. Eine niedrige Netzwerklatenz ist außerordentlich wichtig für NDB-Cluster. Eine Warnung: NDB-Cluster ist eine ziemlich »coole« Technik und sicherlich einiger Untersuchungen wert, um Ihre Neugierde zu befriedigen, allerdings neigen viele Techniker dazu, Ausreden zu suchen, um sie einsetzen zu können, und versuchen, sie an Anforderungen anzupassen, für die sie definitiv nicht geeignet sind. Unserer Erfahrung nach erkennen viele Leute trotz sorgfältiger Überlegungen nicht, wo diese Engine wirklich sinnvoll ist und wie sie funktioniert, wenn sie sie nicht tatsächlich installiert und eine Weile benutzt haben. Dadurch wird meist eine Menge Zeit verschwendet, weil sie wirklich nicht als allgemein einsetzbare Storage-Engine gedacht ist. Schockierend wirkt oft, dass NDB Joins momentan auf der MySQL-Serverebene ausführt und nicht in der Storage-Engine. Da alle Daten für NDB über das Netzwerk bezogen werden müssen, sind komplexe Joins unwahrscheinlich langsam. Andererseits können Suchen in nur einer Tabelle sehr schnell ablaufen, da mehrere Datenknoten jeweils einen
24 |
Kapitel 1: Die MySQL-Architektur
Teil des Ergebnisses liefern. Dies ist nur einer der vielen Aspekte, die Sie berücksichtigen und verstehen müssen, wenn Sie NDB-Cluster für eine bestimmte Anwendung in Betracht ziehen. Die NDB-Cluster-Engine ist so groß und komplex, dass wir sie in diesem Buch nicht weiter behandeln werden. Falls Sie daran interessiert sind, sollten Sie sich ein Buch suchen, das sich speziell diesem Thema widmet. Stellen Sie sich jedoch darauf ein, dass die NDBCluster-Engine voraussichtlich nicht Ihren Erwartungen entsprechen wird und für die meisten traditionellen Anwendungen nicht die richtige Lösung ist.
Die Falcon-Engine Jim Starkey, ein Datenbankpionier, zu dessen früheren Erfindungen Interbase, MVCC und der Spaltentyp BLOB gehören, hat die Falcon-Engine entworfen. MySQL AB erwarb die Falcon-Technik im Jahre 2006, und Jim arbeitet momentan für MySQL AB. Falcon wurde mit Blick auf moderne Hardware entworfen – speziell für Server mit mehreren 64-Bit-Prozessoren und Unmengen an Speicher –, kann aber auch in bescheideneren Umgebungen agieren. Falcon verwendet MVCC und versucht, laufende Transaktionen vollständig im Speicher zu behalten. Daher sind Rollback- und Wiederherstellungsoperationen außerordentlich schnell. Falcon ist momentan noch nicht fertiggestellt (z.B. synchronisiert es seine Commits noch nicht mit dem Binärlog), so dass wir darüber kaum etwas mit Bestimmtheit sagen können. Selbst die ersten Benchmarks, die wir damit durchgeführt haben, werden wahrscheinlich schon wieder veraltet sein, wenn es für den allgemeinen Gebrauch freigegeben wird. Die Falcon-Engine scheint ein gutes Potenzial für viele Online-Anwendungen zu bieten, aber Genaueres wird sich erst mit der Zeit zeigen.
Die solidDB-Engine Die solidDB-Engine, entwickelt von Solid Information Technology (http://www.soliddb. com), ist eine transaktionsfähige Engine, die MVCC benutzt. Sie unterstützt als momentan einzige Engine sowohl pessimistische als auch optimistische Nebenläufigkeitskontrolle. Die solidDB für MySQL enthält eine vollständige Unterstützung für Fremdschlüssel. Sie ähnelt in vielerlei Hinsicht InnoDB, etwa in ihrer Verwendung von Cluster-Indizes. Die solidDB für MySQL enthält eine kostenlose Online-Backup-Möglichkeit. Die solidDB für MySQL ist ein vollständiges Paket, das aus der solidDB-Storage-Engine, der MyISAM-Storage-Engine und dem MySQL-Server besteht. Der »Kitt« zwischen der solidDB-Storage-Engine und dem MySQL-Server wurde Ende 2006 eingeführt. Die zugrunde liegende Technik und der Code haben sich jedoch im Laufe der 15-jährigen Geschichte des Unternehmens entwickelt. Solid zertifiziert und unterstützt das gesamte Produkt. Es unterliegt der GPL und wird kommerziell unter einem dualen Lizenzierungsmodell angeboten, das identisch mit dem des MySQL-Servers ist.
Die Storage-Engines von MySQL |
25
Die PBXT-(Primebase XT-)Engine Die PBXT-Engine, entwickelt von Paul McCullagh von der SNAP Innovation GmbH in Hamburg (http://www.primebase.com), ist eine transaktionsfähige Storage-Engine mit einem einmaligen Design. Eines ihrer charakteristischen Merkmale ist die Art, wie sie Transaktions-Logs und Datendateien einsetzt, um ein Write-Ahead-Logging zu vermeiden, wodurch sich der Overhead der Transaktions-Commits entscheidend verringert. Aufgrund dieser Architektur hat PBXT das Potenzial zu einer sehr hohen Nebenläufigkeit bei Schreiboperationen. Tests haben bereits bewiesen, dass diese Engine bei bestimmten Operationen schneller sein kann als InnoDB. PBXT verwendet MVCC und unterstützt Fremdschlüssel-Beschränkungen, setzt aber keine Cluster-Indizes ein. PBXT ist eine relativ neue Engine und muss sich erst noch in Produktionsumgebungen bewähren. So wurde etwa die Implementierung für wirklich dauerhafte Transaktionen erst während der Entstehung dieses Buches abgeschlossen. Als Ergänzung zu PBXT arbeitet man bei SNAP Innovation an einer skalierbaren »BlobStreaming«-Infrastruktur (http://www.blobstreaming.org). Diese soll in der Lage sein, große Mengen an Binärdaten effizient zu speichern und abzurufen.
Die Maria-Storage-Engine Maria ist eine neue Storage-Engine, die von einigen der besten MySQL-Entwickler entwickelt wird, etwa von Michael Widenius, der MySQL geschaffen hat. Die Version 1.0 enthält nur einige der geplanten Funktionen. Das Ziel besteht darin, Maria als Ersatz für MyISAM zu verwenden, das momentan die Standard-Storage-Engine von MySQL ist und das der Server intern für solche Aufgaben wie Privilege Tables und temporäre Tabellen benutzt, die beim Ausführen von Abfragen erzeugt werden. Hier sind einige Besonderheiten aus dem Plan: • das tabellenweise Festlegen einer transaktionsfähigen oder nichttransaktionsfähigen Speicherung • die Wiederherstellung nach Abstürzen, selbst wenn die Tabelle im nichttransaktionsfähigen Modus läuft • Row-Level-Locking und MVCC • bessere BLOB-Verarbeitung
Weitere Storage-Engines Mehrere Dritthersteller bieten weitere (manchmal proprietäre) Engines an. Außerdem gibt es eine Unmenge spezialisierter und experimenteller Engines (z.B. eine Engine zum Abfragen von Webservices). Einige dieser Engines werden eher so nebenbei entwickelt, manchmal nur von einem oder zwei Entwicklern. Es ist nämlich recht einfach, eine Storage-Engine für MySQL herzustellen. Die meisten dieser Engines sind allerdings nicht weit verbreitet, weil sie oft nur begrenzt einsetzbar sind. Wir überlassen es Ihnen, diese Angebote zu erkunden.
26 |
Kapitel 1: Die MySQL-Architektur
Die richtige Engine auswählen Wenn Sie MySQL-basierte Anwendungen entwerfen, müssen Sie entscheiden, welche Storage-Engine Sie zum Speichern Ihrer Daten verwenden. Falls Sie nicht schon in der Entwurfsphase darüber nachdenken, werden Sie wahrscheinlich später mit Schwierigkeiten rechnen müssen. Möglicherweise merken Sie dann, dass die vorgegebene Engine eine gewünschte Funktion, wie etwa Transaktionen, nicht bietet oder dass der Mix aus Leseund Schreibabfragen, den Ihre Anwendung generiert, eine größere Granularität beim Locking erfordert, als die Tabellen-Locks von MyISAM erlauben. Da Sie die Storage-Engines tabellenweise angeben können, müssen Sie eine klare Vorstellung davon haben, wie die einzelnen Tabellen verwendet werden und welche Daten sie speichern sollen. Es ist außerdem hilfreich, wenn man ein gutes Verständnis für die Anwendung als Ganzes und ihr Wachstumspotenzial hat. Ausgestattet mit diesen Informationen können Sie darangehen, sinnvolle Entscheidungen darüber zu treffen, welche Storage-Engines letztendlich zum Zuge kommen. Es ist nicht unbedingt eine gute Idee, für unterschiedliche Tabellen auch unterschiedliche Storage-Engines zu verwenden. Wenn es sich einrichten lässt, sollten Sie sich für eine Storage-Engine entscheiden. Sie werden sich Ihr Leben damit deutlich erleichtern.
Überlegungen Es gibt zwar viele Faktoren, die Ihre Entscheidung beeinflussen können, welche StorageEngines zum Einsatz kommen, normalerweise läuft es aber auf ein paar wesentliche Überlegungen hinaus. Hier sind die wichtigsten Dinge, die Sie beachten sollten: Transaktionen Falls Ihre Anwendung Transaktionen erfordert, dann ist InnoDB momentan die stabilste, am besten integrierte und bewährte Wahl. Wir gehen aber davon aus, dass andere transaktionsfähige Engines mit der Zeit eine starke Konkurrenz bilden werden. MyISAM ist eine gute Wahl, wenn eine Aufgabe keine Transaktionen erfordert und vor allem SELECT- oder INSERT-Abfragen ausführt. Manchmal fallen bestimmte Komponenten einer Anwendung (wie etwa das Logging) in diese Kategorie. Nebenläufigkeit (Concurrency) Wie Sie am besten Ihre Anforderungen an die Nebenläufigkeit befriedigen, hängt von Ihrer Arbeitslast ab. Falls Sie nur parallel einfügen und lesen müssen, dann ist, ob Sie es glauben oder nicht, MyISAM eine gute Wahl! Müssen Sie dagegen dafür sorgen, dass ein Mix aus Operationen nebenläufig ausgeführt wird, ohne dass sie sich in die Quere kommen, dann sollten Sie sich für eine der Engines mit RowLevel-Locking entscheiden.
Die Storage-Engines von MySQL |
27
Backups Die Forderung nach regelmäßig durchgeführten Backups könnte ebenfalls die Wahl Ihrer Tabelle beeinflussen. Falls es kein Problem ist, den Server zum Erstellen von Backups regelmäßig herunterzufahren, dann ist es auch einfach, eine Storage-Engine zu wählen. Falls Sie dagegen in irgendeiner Form Online-Backups durchführen müssen, wird es weniger einfach. In Kapitel 11 wird dieses Thema näher betrachtet. Denken Sie auch daran, dass sich bei Verwendung mehrerer Storage-Engines die Komplexität der Backups und der Servereinrichtung erhöht. Wiederherstellung nach Abstürzen Wenn Sie viele Daten haben, müssen Sie ernsthaft darüber nachdenken, wie lange es dauert, das System nach einem Absturz wiederherzustellen. MyISAM-Tabellen werden z.B. im Allgemeinen leichter zerstört und brauchen viel länger für die Wiederherstellung als InnoDB-Tabellen. Um genau zu sein, ist das sogar einer der wichtigsten Gründe dafür, weshalb so viele Leute InnoDB verwenden, obwohl sie keine Transaktionen benötigen. Besondere Eigenschaften Manchmal werden Sie feststellen, dass eine Anwendung auf bestimmte Funktionen oder Optimierungen zurückgreift, die nur einige der MySQL-Storage-Engines zu bieten haben. Zum Beispiel sind viele Anwendungen auf Cluster-Index-Optimierungen angewiesen. Momentan beschränkt Sie dies auf InnoDB und solidDB. Andererseits unterstützt nur MyISAM eine Volltextsuche in MySQL. Wenn eine StorageEngine eine oder mehrere wichtige Anforderungen erfüllt, andere dagegen nicht, dann müssen Sie entweder einen Kompromiss eingehen oder eine clevere Designlösung finden. Oft bekommen Sie das, was Sie brauchen, von einer Storage-Engine, die Ihre Anforderungen scheinbar nicht unterstützt. Sie müssen sich nicht sofort entscheiden. Im Rest dieses Buches erfahren Sie noch eine Menge über die Stärken und Schwächen der einzelnen Storage-Engines und erhalten viele Tipps zu Architektur und Entwurf. Meist gibt es mehr Möglichkeiten, als Sie wahrscheinlich jetzt erkennen, und vermutlich hilft es, später auf die Frage der richtigen StorageEngine zurückzukommen.
Praktische Beispiele Ohne Beispiele aus dem richtigen Leben wirken diese Probleme und Überlegungen vermutlich ziemlich abstrakt. Betrachten wir deshalb einige verbreitete Datenbankanwendungen. Wir schauen uns verschiedene Tabellen an und stellen fest, welche Engine deren Anforderungen am besten entgegenkommt. Eine Zusammenfassung der Möglichkeiten finden Sie im nächsten Abschnitt.
Logging Nehmen wir an, Sie wollen mit MySQL in Echtzeit jeden Anruf über eine zentrale Telefonanlage festhalten. Oder vielleicht haben Sie mod_log_sql für Apache installiert, so 28 |
Kapitel 1: Die MySQL-Architektur
dass Sie alle Besuche Ihrer Website direkt in einer Tabelle festhalten können. Bei einer solchen Anwendung ist Geschwindigkeit wohl das wesentliche Ziel, und die Datenbank soll dabei nicht einen Flaschenhals bilden. Die Storage-Engines MyISAM und Archive würden hier ganz gut funktionieren, da sie einen relativ geringen Overhead verursachen und Tausende von Datensätzen pro Sekunde aufzeichnen können. Auch die PBXTStorage-Engine ist vermutlich besonders für Logging-Aufgaben geeignet. Interessant wird es, wenn Sie beschließen, Berichte anzufertigen, die die aufgezeichneten Daten zusammenfassen. Je nach den verwendeten Abfragen wird sich das Einfügen der Datensätze deutlich verlangsamen, während Sie die Daten für den Bericht sammeln. Was können Sie tun? Eine Lösung besteht darin, die Daten mithilfe der in MySQL fest eingebaute Replikation auf einen zweiten (Slave-)Server zu klonen. Die zeit- und CPU-intensiven Abfragen können dann auf dem Slave ausgeführt werden. Der Master kann die Datensätze weiterhin so schnell einfügen wie bisher, während Sie auf dem Slave beliebige Abfragen durchführen können, ohne sich Gedanken darüber machen zu müssen, ob diese das Echtzeit-Logging negativ beeinflussen. Sie können Abfragen auch in Zeiten mit niedriger Last durchführen, allerdings dürfen Sie sich nicht darauf verlassen, dass diese Strategie noch funktioniert, wenn Ihre Anwendung wächst. Eine weitere Möglichkeit ist die Verwendung einer Merge-Tabelle. Anstatt die Daten immer in die gleiche Tabelle zu schreiben, passen Sie die Anwendung so an, dass sie in eine Tabelle schreibt, die das Jahr oder den Namen bzw. die Nummer des Monats in ihrem Namen enthält: web_logs_2008_01 oder web_logs_2008_jan. Definieren Sie dann eine Merge-Tabelle, die die Daten enthält, die Sie zusammenfassen wollen, und verwenden Sie diese in Ihren Abfragen. Auch falls Sie die Daten täglich oder wöchentlich zusammenfassen müssen, funktioniert diese Strategie, Sie müssen einfach nur Tabellen mit spezielleren Namen anlegen, wie etwa web_logs_2008_01_01. Während Sie Abfragen in Tabellen durchführen, in die nicht mehr geschrieben wird, kann Ihre Anwendung ungestört Datensätze in der aktuellen Tabelle aufzeichnen.
Nur (oder hauptsächlich) gelesene Tabellen Aus Tabellen, deren Daten zum Aufbau eines Katalogs oder einer Liste (Jobs, Auktionen, Immobilien usw.) verwendet werden, wird normalerweise viel häufiger gelesen, als dass in sie geschrieben wird. Das macht sie zu guten Kandidaten für MyISAM – falls es Ihnen egal ist, was passiert, wenn MyISAM abstürzt. Unterschätzen Sie nicht, wie wichtig das ist; viele Anwender verstehen nicht, wie riskant es ist, eine Storage-Engine zu benutzen, die sich nicht einmal ernsthaft darum bemüht, ihre Daten auf die Platte zu schreiben.
Die Storage-Engines von MySQL |
29
Es ist eine ausgezeichnete Idee, eine realistische Lastsimulation auf einem Testserver durchzuführen und dann im wahrsten Sinne des Wortes den Stecker zu ziehen. Die Erfahrungen, die Sie bei der Wiederherstellung nach einem Absturz machen werden, sind unbezahlbar. Sie ersparen Ihnen hässliche Überraschungen im tatsächlichen Betrieb.
Vertrauen Sie nicht einfach auf die verbreitete Volksweisheit »MyISAM ist schneller als InnoDB«. Sie ist nicht grundsätzlich wahr. Wir können Ihnen Dutzende von Situationen nennen, in denen InnoDB MyISAM weit hinter sich lässt, vor allem bei Anwendungen, in denen Cluster-Indizes sinnvoll sind oder bei denen die Daten in den Speicher passen. Wenn Sie den Rest dieses Buches lesen, bekommen Sie ein Gespür dafür, welche Faktoren die Leistung einer Storage-Engine beeinflussen (Datengröße, Anzahl der notwendigen Ein-/Ausgabeoperationen, Primärschlüssel gegen Sekundärindizes usw.) und welche davon für Ihre Anwendung wichtig sind.
Auftragsverarbeitung Wenn Sie es mit irgendeiner Art von Auftragsverarbeitung zu tun haben, dann sind Transaktionen ein Muss. Halb abgearbeitete Bestellungen würden Ihren Dienst beim Kunden nicht unbedingt beliebt machen. Eine weitere wichtige Überlegung ist, ob die Engine Fremdschlüssel-Beschränkungen unterstützen muss. Zurzeit ist wahrscheinlich InnoDB am besten für Anwendungen zur Auftragsbearbeitung geeignet, obwohl auch die anderen transaktionsfähigen Storage-Engines heiße Kandidaten sind.
Aktienkurse Falls Sie Aktienkurse für die eigene Analyse sammeln, ist MyISAM – mit den üblichen Warnungen – eine gute Wahl. Betreiben Sie jedoch einen stark frequentierten Webservice, der Aktienkurse in Echtzeit sammelt und Tausende von Benutzern bedient, dann sollte eine Abfrage nie warten müssen. Zu jedem Zeitpunkt könnten viele Clients versuchen, die Tabelle zu lesen bzw. zu schreiben. Deshalb stellt Row-Level-Locking oder ein Entwurf, der Updates minimiert, die beste Lösung dar.
Schwarze Bretter und Thread-fähige Diskussionsforen Thread-fähige Diskussionsforen sind für MySQL-Benutzer ein interessantes Problem. Es gibt dafür Hunderte von frei verfügbaren PHP- und Perl-basierten Systemen. Bei vielen von ihnen wurde die Effizienz der Datenbank nicht berücksichtigt, so dass sie dazu neigen, für jede Anforderung eine große Anzahl von Abfragen auszuführen. Einige von ihnen wurden unabhängig von einer bestimmten Datenbank geschrieben, so dass sie die Vorteile von bestimmten Datenbanksystemen nicht ausnutzen. Gleichzeitig müssen die Zähler und Statistiken für die verschiedenen Diskussionen aktualisiert werden. Viele der Systeme benutzen darüber hinaus nur wenige monolithische Tabellen, um ihre Daten zu
30 |
Kapitel 1: Die MySQL-Architektur
speichern. Dementsprechend konzentrieren sich die starken Schreib-/Leseaktivitäten auf einige zentrale Tabellen, und die zur Wahrung der Konsistenz notwendigen Locks werden zu einer steten Quelle des Wettstreits. Ungeachtet ihrer Entwurfsmängel funktionieren die meisten Systeme bei kleiner und mittlerer Last ganz gut. Wenn eine Website jedoch wächst und eine Menge Verkehr generiert, kann sie sehr langsam werden. Die offensichtliche Lösung besteht darin, auf eine andere Storage-Engine zu wechseln, die in der Lage ist, mit dem gestiegenen Lese-/ Schreibaufkommen klarzukommen. Benutzer, die dies versuchen, werden manchmal allerdings überrascht sein, wenn sie feststellen, dass die Systeme sogar noch langsamer laufen als vorher! Diese Benutzer bemerken nicht, dass das System eine bestimmte Abfrage verwendet, die normalerweise ungefähr so aussieht: mysql> SELECT COUNT(*) FROM table;
Das Problem ist, dass nicht alle Engines diese Abfrage schnell ausführen können: MyISAM kann es, andere Engines möglicherweise nicht. Für jede Engine gibt es ähnliche Beispiele. In Kapitel 2 erfahren Sie, wie Sie sich vor solchen Überraschungen schützen und Lösungen für dieses Problem finden können.
CD-ROM-Anwendungen Wenn Sie eine CD-ROM- oder DVD-ROM-basierte Anwendung vertreiben müssen, die MySQL-Datendateien verwendet, dann sollten Sie in Betracht ziehen, MyISAM oder komprimierte MyISAM-Tabellen zu verwenden, die leicht isoliert und auf andere Medien kopiert werden können. Komprimierte MyISAM-Tabellen benötigen viel weniger Platz als unkomprimierte, sind allerdings schreibgeschützt. Bei bestimmten Anwendungen kann das problematisch werden, da die Daten aber sowieso auf ein schreibgeschütztes Medium kommen sollen, besteht wenig Grund dafür, unkomprimierte Tabellen für diese spezielle Aufgabe einzusetzen.
Storage-Engine-Zusammenfassung Tabelle 1-3 fasst die transaktions- und Locking-bezogenen Eigenschaften der beliebtesten MySQL-Storage-Engines zusammen. Die Spalte mit der MySQL-Version zeigt die minimale MySQL-Version, die Sie benötigen, um die Engine zu verwenden. Für einige Engines und MySQL-Versionen müssen Sie allerdings Ihren eigenen Server kompilieren. Das Wort »Alle« in dieser Spalte bedeutet alle Versionen seit MySQL 3.23.
Die Storage-Engines von MySQL |
31
Tabelle 1-3: Zusammenfassung der MySQL-Storage-Engines Storage-Engine
MySQL-Version
Transaktionen
MyISAM
Alle
Nein
LockingGranularität
Wichtige Anwendungen
Gegenanzeigen
Tabelle mit nebenläufigen Einfügungen
SELECT, INSERT, Laden
Gemischte Lese-/ Schreiblast
großer Mengen
MyISAM Merge
Alle
Nein
Tabelle mit nebenläufigen Einfügungen
Segmentierte Archivierung, Data- Warehouses
Viele globale Lookups
Memory (HEAP)
Alle
Nein
Tabelle
Zwischenberechnungen, statischer Lookup
Große Datensätze, persistente Speicherung
InnoDB
Alle
Ja
Zeilenorientiert (Row-Level) mit MVCC
Transaktionsfähige Verarbeitung
Keine
Falcon
6.0
Ja
Zeilenorientiert (Row-Level) mit MVCC
Transaktionsfähige Verarbeitung
Keine
Archive
4.1
Ja
Zeilenorientiert (Row-Level)
Logging, Gesamtanalyse
Zufälliger Zugriff, Updates, Löschungen
CSV
4.1
Nein
Tabelle
Logging, massenhaftes Laden externer Daten
Zufälliger Zugriff, Indizierung
Blackhole
4.1
Ja
Row-Level mit MVCC
geloggte oder replizierte Archivierung
Alle bis auf die vorgesehenen Anwendungen
Federated
5.0
N/A
N/A
Verteilte Datenquellen
Alle bis auf die vorgesehene Anwendung
NDB Cluster
5.0
Ja
Zeilenorientiert (Row-Level)
Hochverfügbarkeit
Die meisten typischen Anwendungen
PBXT
5.0
Ja
Zeilenorientiert (Row-Level) mit MVCC
Transaktionsfähige Verarbeitung, Logging
Bedarf an ClusterIndizes
solidDB
5.0
Ja
Zeilenorientiert (Row-Level) mit MVCC
Transaktionsfähige Verarbeitung
Keine
Maria (geplant)
6.x
Ja
Zeilenorientiert (Row-Level) mit MVCC
MyISAM-Ersatz
Keine
32 |
Kapitel 1: Die MySQL-Architektur
Tabellenkonvertierungen Es gibt verschiedene Möglichkeiten, eine Tabelle von einer Storage-Engine in eine andere zu konvertieren, die jeweils Vor- und Nachteile haben. In den folgenden Abschnitten beschreiben wir drei der gebräuchlichsten Methoden.
ALTER TABLE Die einfachste Möglichkeit, eine Tabelle von einer Engine zu einer anderen zu verschieben, besteht in der Anweisung ALTER TABLE. Der folgende Befehl konvertiert mytable zu Falcon: mysql> ALTER TABLE mytable ENGINE = Falcon;
Diese Syntax funktioniert bei allen Storage-Engines, es gibt allerdings einen Haken: Es kann sehr lange dauern. MySQL kopiert die alte Tabelle zeilenweise in die neue Tabelle. Während dieser Zeit wird wahrscheinlich die komplette Ein-/Ausgabekapazität der Festplatte des Servers beansprucht, und die Originaltabelle wird zum Lesen gesperrt, während die Konvertierung läuft. Denken Sie also zweimal nach, bevor Sie diese Technik auf einer ausgelasteten Tabelle einsetzen. Greifen Sie vielleicht lieber auf eine der anderen vorgestellten Methoden zurück, bei denen zuerst eine Kopie der Tabelle erstellt wird. Wenn Sie von einer Storage-Engine in eine andere konvertieren, gehen alle StorageEngine-spezifischen Eigenschaften verloren. Falls Sie etwa eine InnoDB-Tabelle nach MyISAM und wieder zurück konvertieren, verlieren Sie alle Fremdschlüssel, die Sie ursprünglich in der InnoDB-Tabelle definiert hatten.
Dump und Import Um den Konvertierungsvorgang besser kontrollieren zu können, könnten Sie die Tabelle zuerst mit dem Dienstprogramm mysqldump in eine Textdatei speichern. Anschließend bearbeiten Sie einfach die Dump-Datei, um die CREATE TABLE-Anweisung anzupassen, die sie enthält. Denken Sie daran, sowohl den Tabellennamen als auch ihren Typ zu ändern, da es nämlich nicht möglich ist, zwei Tabellen des gleichen Namens in der Datenbank vorzuhalten, selbst wenn sie einen unterschiedlichen Typ aufweisen – und mysqldump schreibt standardmäßig den Befehl DROP TABLE vor CREATE TABLE, so dass Ihre Daten verloren gehen, wenn Sie nicht aufpassen! In Kapitel 11 finden Sie weitere Hinweise über das effiziente Dumpen und Neuladen von Daten.
CREATE und SELECT Die dritte Konvertierungstechnik ist ein Kompromiss aus der Geschwindigkeit des ersten und der Sicherheit des zweiten Mechanismus. Anstatt die gesamte Tabelle als Dump zu speichern oder alles auf einmal zu konvertieren, legen Sie die neue Tabelle an und verwenden die MySQL-Syntax INSERT ... SELECT, um sie zu füllen:
Die Storage-Engines von MySQL |
33
mysql> CREATE TABLE innodb_table LIKE myisam_table; mysql> ALTER TABLE innodb_table ENGINE=InnoDB; mysql> INSERT INTO innodb_table SELECT * FROM myisam_table;
Das funktioniert gut, wenn Sie nicht viele Daten haben. Müssen dagegen viele Daten konvertiert werden, ist es oft effizienter, die Tabelle schrittweise zu füllen und die Transaktion zwischen jedem Schritt zu bestätigen, damit die »Undo-Logs« nicht zu groß werden. Nehmen Sie an, dass id der Primärschlüssel ist. Führen Sie dann diese Abfrage wiederholt aus (wobei Sie jedes Mal größere Werte für x und y benutzen), bis Sie alle Daten in die neue Tabelle kopiert haben: mysql> mysql> -> mysql>
START TRANSACTION; INSERT INTO innodb_table SELECT * FROM myisam_table WHERE id BETWEEN x AND y; COMMIT;
Anschließend haben Sie die Originaltabelle, die Sie schließen können, wenn Sie fertig sind, und die neue Tabelle, die nun vollständig gefüllt ist. Denken Sie daran, die Originaltabelle zu sperren, damit Sie keine inkonsistente Kopie der Daten erhalten!
34 |
Kapitel 1: Die MySQL-Architektur
KAPITEL 2
Engpässe finden: Benchmarking und Profiling
Irgendwann wird es so weit sein, dass Sie mehr Leistung von MySQL benötigen. Aber was sollten Sie versuchen zu verbessern? Eine bestimmte Abfrage? Ihr Schema? Ihre Hardware? Sie werden es nur herausfinden, wenn Sie messen, was Ihr System tut, und seine Leistung unter verschiedenen Bedingungen testen. Aus diesem Grund haben wir dieses Kapitel bereits so früh in das Buch eingefügt. Die beste Strategie besteht darin, die schwächste Verbindung in der Kette der Komponenten Ihrer Anwendung zu finden. Das ist vor allem dann sinnvoll, wenn Sie nicht wissen, was eine bessere Performance verhindert – oder was eine bessere Performance in der Zukunft verhindern wird. Benchmarking und Profiling sind zwei wichtige Techniken, um Flaschenhälse zu ermitteln. Sie sind miteinander verwandt, aber nicht identisch. Ein Benchmark misst die Leistung Ihres Systems. Das kann helfen, die Kapazität eines Systems festzustellen, zu zeigen, welche Änderungen eine Rolle spielen und welche nicht, oder zu zeigen, wie Ihre Anwendung mit unterschiedlichen Daten funktioniert. Im Gegensatz dazu hilft Ihnen Profiling dabei, herauszufinden, wo Ihre Anwendung die meiste Zeit verbringt oder die meisten Ressourcen beansprucht. Mit anderen Worten, Benchmarking beantwortet die Frage »Wie gut funktioniert das?«, und Profiling beantwortet die Frage »Wieso funktioniert es so?«. Wir haben dieses Kapitel in zwei Teile aufgeteilt: Der erste Teil handelt vom Benchmarking, im zweiten geht es um Profiling. Wir beginnen mit einer Diskussion der Gründe und Strategien für das Benchmarking und kommen dann zu speziellen Taktiken dafür. Wir zeigen Ihnen, wie man Benchmarks plant und entwirft, wie man für exakte Ergebnisse entwirft, Benchmarks durchführt und die Ergebnisse analysiert. Zum Ende des ersten Teils betrachten wir Benchmarking-Werkzeuge und schauen uns Beispiele für deren Einsatz an. Im zweiten Teil des Kapitels erfahren Sie, wie man sowohl Anwendungen als auch MySQL profiliert. Wir zeigen Beispiele für echten Profilierungscode, den wir in der Produktion verwendet haben, um uns bei der Analyse der Performance von Anwendungen
|
35
zu unterstützen. Darüber hinaus zeigen wir, wie man die Abfragen von MySQL protokolliert, die Logs analysiert und die Statuszähler von MySQL und andere Werkzeuge einsetzt, um festzustellen, was MySQL und Ihre Abfragen tun.
Wozu Benchmarks? Bei vielen mittleren bis großen MySQL-Anwendungsfällen gibt es Leute, die sich speziell um das Benchmarking kümmern. Allerdings sollte jeder Entwickler und Datenbankadministrator mit grundlegenden Benchmarking-Prinzipien und -Techniken vertraut sein. Benchmarks können sehr hilfreich für Sie sein: • Messen Sie, wie Ihre Anwendung momentan arbeitet. Wenn Sie nicht wissen, wie schnell sie im Moment läuft, können Sie nicht sicher sein, ob Änderungen, die Sie vornehmen, überhaupt einen Sinn haben. Mithilfe älterer Benchmark-Ergebnisse können Sie Probleme erkennen, die Sie nicht vorhergesehen haben. • Validieren Sie die Skalierbarkeit Ihres Systems. Mit einem Benchmark könnten Sie eine viel höhere Last simulieren, als Ihre Produktionssysteme verarbeiten, etwa den tausendfachen Anstieg der Benutzerzahl. • Berücksichtigen Sie das Wachstum bei Ihrer Planung. Benchmarks helfen Ihnen dabei, abzuschätzen, wie viel Hardware, Netzwerkkapazität und andere Ressourcen Sie für Ihre vorgesehene künftige Last benötigen. Damit verringern Sie das Risiko bei Upgrades oder größeren Änderungen der Anwendung. • Testen Sie die Fähigkeit Ihrer Anwendung, eine sich ändernde Umgebung zu tolerieren. Beispielsweise können Sie feststellen, wie Ihre Anwendung bei einer sporadisch auftretenden Spitze in der Nebenläufigkeit oder mit einer anderen Serverkonfiguration funktioniert oder wie Sie mit einer anderen Datenverteilung zurechtkommt. • Testen Sie unterschiedliche Hardware-, Software- und Betriebssystemkonfigurationen. Ist RAID 5 oder RAID 10 besser für Ihr System? Wie ändert sich die Leistung von zufälligen Schreiboperationen, wenn Sie von ATA-Festplatten auf SAN-Speicherung umstellen? Skaliert der 2.4-Linux-Kernel besser als der 2.6-Kernel? Nützt eine höhere MySQL-Version der Leistung? Was ist mit einer anderen Storage-Engine für Ihre Daten? Diese Fragen können Sie mit speziellen Benchmarks beantworten. Benchmarks können Sie auch für andere Zwecke einsetzen, etwa um eine einheitliche Testsuite für Ihre Anwendung zu schaffen; wir konzentrieren uns hier jedoch auf die leistungsbezogenen Aspekte.
Benchmarking-Strategien Es gibt zwei vorrangige Benchmarking-Strategien: Sie können die Anwendung als Ganzes bewerten oder MySQL allein betrachten. Diese beiden Strategien werden als Full-Stackbzw. Single-Component-Benchmarking bezeichnet. Es gibt verschiedene Gründe, eine Anwendung als Ganzes zu messen, anstatt sich nur auf MySQL zu beschränken:
36 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
• Sie testen die gesamte Anwendung, einschließlich des Webservers, des Anwendungscodes und der Datenbank. Das ist sinnvoll, weil Sie sich nicht um die Leistung von MySQL im Speziellen kümmern; Ihnen geht es um die ganze Anwendung. • MySQL bildet nicht immer die Engstelle der Anwendung. Ein Full-Stack-Benchmark kann dies beweisen. • Nur durch das Testen der ganzen Anwendung können Sie feststellen, wie sich der Cache der einzelnen Teile verhält. • Benchmarks sind nur so weit gut, dass sie das Verhalten der eigentlichen Anwendung widerspiegeln, was schwer festzustellen ist, wenn Sie nur einen Teil davon testen. Andererseits sind Anwendungs-Benchmarks unter Umständen schwer herzustellen und noch schwerer korrekt einzurichten. Wenn Sie einen Benchmark schlecht gestalten, treffen Sie möglicherweise falsche Entscheidungen, weil die Ergebnisse gar nicht die Wirklichkeit wiedergeben. Manchmal jedoch wollen Sie nicht über die ganze Anwendung informiert werden. Sie brauchen vielleicht nur einen MySQL-Benchmark, zumindest anfangs. Solch ein Benchmark ist sinnvoll, wenn: • Sie verschiedene Schemata oder Abfragen vergleichen wollen • Sie ein spezielles Problem testen wollen, das Sie in der Anwendung festgestellt haben • Sie einen langen Benchmark zu Gunsten eines kürzeren vermeiden wollen, der einen schnelleren Zyklus von Änderung und Messung erlaubt Es ist darüber hinaus sinnvoll, Benchmark-Tests mit MySQL durchzuführen, wenn Sie die Abfragen Ihrer Anwendung an einer echten Datenmenge wiederholen können. Die Daten selbst und die Größe der Datenmenge müssen realistisch sein. Nutzen Sie nach Möglichkeit einen Schnappschuss von tatsächlichen Produktionsdaten. Leider kann das Einrichten eines realistischen Benchmarks kompliziert und zeitaufwendig sein, und Sie haben Glück, wenn Sie eine Kopie von Produktionsdaten bekommen. Das kann natürlich auch unmöglich sein – stellen Sie sich etwa vor, Sie entwickeln eine neue Anwendung, die nur wenige Benutzer und kaum Daten hat. Wenn Sie wissen wollen, wie sie funktioniert, wenn sie sehr groß wird, bleibt Ihnen keine andere Wahl, als die Daten und die Arbeitslast der größeren Anwendung zu simulieren.
Was gemessen wird Bevor Sie mit dem Benchmarking beginnen, müssen Sie Ihre Ziele festlegen – genauer gesagt, bevor Sie Ihre Benchmarks entwerfen. Ihre Ziele bestimmen die Werkzeuge und Techniken, die Sie einsetzen, um exakte, sinnvolle Ergebnisse zu erhalten. Formulieren Sie Ihre Ziele als Fragen, wie etwa »Ist diese CPU besser als jene?« oder »Funktionieren die neuen Indizes besser als die aktuellen?«.
Benchmarking-Strategien |
37
Es ist vielleicht nicht so offensichtlich, aber manchmal brauchen Sie verschiedene Ansätze, um andere Dinge zu messen. So könnten Latenz und Durchsatz unterschiedliche Benchmarks erfordern. Betrachten Sie die folgenden Maße und wie sie zu Ihren Leistungszielen passen: Transaktionen pro Zeiteinheit Dies ist einer der Klassiker für das Benchmarking von Datenbankanwendungen. Standardisierte Benchmarks wie TPC-C (siehe http://www.tpc.org) sind weithin anerkannt, und viele Datenbankhersteller bemühen sich darum, sie gut zu erfüllen. Diese Benchmarks messen die Leistung bei der Online-Verarbeitung von Transaktionen (Online Transaction Processing; OLTP) und eignen sich am besten für interaktive Mehrbenutzeranwendungen. Die übliche Maßeinheit ist Transaktionen pro Sekunde. Der Begriff Durchsatz bedeutet üblicherweise das Gleiche wie Transaktionen (oder andere Arbeitseinheit) pro Zeiteinheit. Antwortzeit oder Latenz Dies misst die Gesamtzeit, die eine Aufgabe erfordert. Je nach Ihrer Anwendung müssen Sie sie in Millisekunden, Sekunden oder Minuten messen. Von diesem Wert können Sie dann durchschnittliche, minimale und maximale Antwortzeiten ableiten. Die maximale Antwortzeit ist meist kein sinnvolles Maß, da die maximale Antwortzeit wahrscheinlich umso länger wird, je länger der Benchmark läuft. Sie ist außerdem kaum reproduzierbar und wird wahrscheinlich zwischen den Durchläufen stark schwanken. Aus diesem Grund verwenden viele Leute stattdessen die Percentile Response Times (Antwortzeiten der Perzentile). Falls z.B. die Antwortzeit des 95. Perzentils bei 5 Millisekunden liegt, wissen Sie, dass die Aufgabe in 95 % der Fälle in weniger als 5 Millisekunden ausgeführt wird. Meist hilft es, wenn man die Ergebnisse dieser Benchmarks grafisch darstellt, entweder in Form von Linien (z.B. den Durchschnitt und das 95. Perzentil) oder als Streuungsdiagramm, damit Sie sehen können, wie die Werte verteilt sind. Diese Graphen zeigen Ihnen, wie sich die Benchmarks auf lange Sicht verhalten werden. Nehmen Sie einmal an, Ihr System setzt jede Stunde für eine Minute einen Kontrollpunkt. Während der Kontrolle hält das System an, und Transaktionen werden nicht abgeschlossen. Die Antwortzeit des 95. Perzentils zeigt keine Spitzen, die Ergebnisse weisen daher nicht auf das Problem hin. Eine grafische Darstellung dagegen zeigt periodisch auftretende Spitzen in der Antwortzeit. Abbildung 2-1 verdeutlicht dies. Abbildung 2-1 stellt auf der y-Achse die Anzahl der Transaktionen pro Minute dar (ADTPM). Diese Linie zeigt deutliche Spitzen, die im Gesamtdurchschnitt (gepunktete Linie) nicht zu erkennen sind. Die erste Spitze entsteht, weil die Caches des Servers noch leer sind (Cold Cache). Die anderen Spitzen zeigen, wann der Server intensiv »Dirty Pages« auf die Platte schreibt. Ohne das Diagramm sind die Abweichungen schlecht zu erkennen.
38 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
12000 10000
ADTPM
8000 6000 4000 2000 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Zeit in Minuten
Abbildung 2-1: Ergebnisse eines 30-minütigen dbt2-Benchmark-Durchlaufs
Skalierbarkeit Messungen der Skalierbarkeit sind für solche Systeme sinnvoll, die ihre Leistungsfähigkeit unter sich ändernden Lasten beibehalten sollen. »Performance unter sich ändernder Arbeitslast« ist ein ziemlich abstraktes Konzept. Die Performance wird typischerweise mit einem Maß wie Durchsatz oder Antwortzeit gemessen, die Arbeitslast schwankt bei Änderungen von Datenbankgröße, Anzahl der nebenläufigen Verbindungen oder Hardware. Skalierbarkeitsmessungen bieten sich an, wenn man die Kapazität planen will, da man mit ihnen leicht die Schwächen in der Anwendung zeigen kann, die andere Benchmark-Strategien nicht zeigen. Falls Sie z.B. Ihr System so gestalten, dass es bei einem Antwortzeit-Benchmark mit einer einzigen Verbindung gut funktioniert (eine ziemlich schwache Benchmark-Strategie), wird die Leistung der Anwendung wahrscheinlich stark abfallen, sobald eine bestimmte Anzahl gleichzeitiger Verbindungen erreicht wird. Ein Benchmark, der auf konsistente Antwortzeiten bei einer zunehmenden Anzahl von Verbindungen achtet, würde diesen Entwurfsfehler aufdecken. Manche Aktivitäten, wie etwa Batch-Jobs zum Erzeugen von Zusammenfassungen aus Einzeldaten brauchen einfach schnelle Antwortzeiten, Punkt. Bei ihnen ist es in Ordnung, Benchmarks mit Blick auf die Antwortzeiten durchzuführen; versuchen Sie aber auch daran zu denken, wie sie mit anderen Aktivitäten interagieren. BatchJobs können dafür sorgen, dass interaktive Abfragen leiden und umgekehrt. Nebenläufigkeit Nebenläufigkeit ist ein wichtiges, aber häufig missbrauchtes und falsch verstandenes Maß. Beispielsweise wird gern davon gesprochen, wie viele Benutzer gleichzei-
Benchmarking-Strategien |
39
tig auf einer Website zu Besuch sind. Allerdings ist HTTP zustandslos, und die meisten Benutzer lesen einfach nur, was in ihren Browsern angezeigt wird, das hat also nichts mit Nebenläufigkeit, also der parallelen Abarbeitung auf dem Webserver zu tun. Nebenläufigkeit auf dem Webserver überträgt sich auch nicht unbedingt auf den Datenbankserver; die einzige Sache, mit der sie direkt zusammenhängt, ist die Frage, wie viele Daten der Speichermechanismus der Sitzung verarbeiten können muss. Ein genaueres Maß der Nebenläufigkeit auf dem Webserver ist etwa, wie viele Anforderungen pro Sekunde die Benutzer zu Spitzenzeiten generieren. Sie können Nebenläufigkeit auch an verschiedenen Stellen in der Anwendung messen. Die höhere Nebenläufigkeit auf dem Webserver kann auch eine höhere Nebenläufigkeit auf der Datenbankebene verursachen, das wird allerdings von der Sprache und den verwendeten Werkzeugen beeinflusst. So verursacht z.B. Java mit einem Verbindungspool wahrscheinlich eine geringere Anzahl gleichzeitiger Verbindungen zum MySQL-Server als PHP mit persistenten Verbindungen. Wichtiger ist immer noch die Anzahl der Verbindungen, die zu einem bestimmten Zeitpunkt Abfragen ausführen. Eine gut gestaltete Anwendung könnte Hunderte von Verbindungen zum MySQL-Server offen haben, aber nur ein Bruchteil von ihnen sollte zu einem Zeitpunkt Abfragen ausführen. Eine Website mit »50.000 Benutzern gleichzeitig« bräuchte also nur 10 oder 15 gleichzeitig laufende Abfragen auf dem MySQL-Server! Mit anderen Worten: Worum Sie sich beim Benchmarking wirklich kümmern sollten, ist die Nebenläufigkeit im Betrieb oder die Anzahl der Threads oder Verbindungen, die gleichzeitig aktiv sind. Messen Sie, ob die Leistung stark abfällt, wenn die Nebenläufigkeit zunimmt. Ist dies der Fall, dann kommt Ihre Anwendung mit Lastspitzen nicht zurecht. Sie müssen entweder dafür sorgen, dass die Leistung nicht stark abfällt, oder die Anwendung so gestalten, dass sie in den Teilen, die damit nicht zurechtkommen, keine hohe Nebenläufigkeit zulässt. Im Allgemeinen werden Sie die Nebenläufigkeit auf dem MySQL-Server mit Methoden wie dem Application Queuing beschränken. In Kapitel 10 finden Sie mehr Informationen zu diesem Thema. Nebenläufigkeit ist völlig anders als Antwortzeit und Skalierbarkeit: Es ist kein Ergebnis, sondern eine Eigenschaft dafür, wie Sie den Benchmark einrichten. Anstatt die Nebenläufigkeit zu messen, die Ihre Anwendung erreicht, messen Sie die Leistung der Anwendung auf verschiedenen Ebenen der Nebenläufigkeit. In der letzten Analyse sollten Sie testen, was für Ihre Benutzer wichtig ist. Benchmarks messen die Leistung, aber »Leistung« hat für alle Leute eine andere Bedeutung. Sammeln Sie (offiziell oder inoffiziell) Informationen über die Anforderungen an die Skalierbarkeit des Systems darüber, was als akzeptable Antwortzeiten angesehen wird, welche Art von Nebenläufigkeit erwartet wird usw. Versuchen Sie dann, Ihre Benchmarks so zu gestalten, dass sie all diese Anforderungen berücksichtigen, ohne dass Sie einen Tunnelblick entwickeln oder einige Dinge zum Nachteil anderer bevorzugen.
40 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Benchmarking-Taktiken Nach den allgemeinen Anmerkungen wird es nun spezieller. Wir wollen uns anschauen, wie man Benchmarks gestaltet und ausführt. Zuerst schauen wir uns allerdings einige verbreitete Fehler an, die zu unnützen oder ungenauen Ergebnissen führen können: • die Verwendung einer Teilmenge der echten Datengröße, wie etwa die Verwendung nur eines Gigabyte an Daten, wenn die Anwendung eigentlich Hunderte von Gigabyte verarbeiten können muss, oder die Verwendung der aktuellen Datenmenge, wenn die Anwendung in der Zukunft eigentlich viel größer werden soll • die Verwendung falsch verteilter Daten, wie etwa gleichmäßig verteilter Daten, wenn die Daten des tatsächlichen Systems »Hot Spots« aufweisen (Zufällig generierte Daten weisen oft eine unrealistische Verteilung auf.) • die Verwendung unrealistisch verteilter Parameter, etwa wenn man so tut, als würden alle Benutzerprofile gleich wahrscheinlich betrachtet werden • die Verwendung eines Einbenutzerszenarios für eine Mehrbenutzeranwendung • Benchmarking einer verteilten Anwendung auf einem einzigen Server • falsch eingeschätztes Benutzerverhalten, wie etwa die »Zeit zum Nachdenken« auf einer Webseite. Echte Benutzer fordern eine Seite an und lesen sie dann – sie klicken nicht ohne Pause nacheinander auf Links. • die Ausführung identischer Abfragen in einer Schleife. Echte Abfragen sind nicht identisch, so dass der Cache nicht immer anspringt. Identische Abfragen liegen irgendwann vollständig oder teilweise im Cache vor. • das Verpassen der Fehlerprüfung. Wenn die Ergebnisse eines Benchmarks irgendwie sinnlos sind – z.B., wenn eine langsame Operation auf einmal sehr schnell abgeschlossen wird –, müssen Sie auf Fehler überprüfen. Vielleicht testen Sie mit dem Benchmark ja nur, wie schnell MySQL einen Syntaxfehler in der SQL-Abfrage entdecken kann! Prüfen Sie prinzipiell immer die Fehlerprotokolle nach einem Benchmark-Test. • ignorieren, wie das System läuft, wenn es noch nicht warmgelaufen ist, wie etwa direkt nach einem Neustart. Manchmal müssen Sie wissen, wie lange es dauert, bis der Server nach einem Neustart wieder die volle Kapazität erreicht hat, und schauen sich deshalb speziell die Aufwärmperiode an. Falls Sie andererseits die normale Leistung untersuchen wollen, müssen Sie daran denken, dass viele Caches nach einem Neustart noch kalt sind und die Benchmark-Tests nicht die gleichen Ergebnisse liefern wie nach einer gewissen Laufzeit des Servers. • die Verwendung von Standardservereinstellungen. In Kapitel 6 erfahren Sie mehr über das Optimieren der Servereinstellungen. Schon indem Sie diese Fehler vermeiden, können Sie die Qualität Ihrer Ergebnisse deutlich verbessern.
Benchmarking-Taktiken | 41
Abgesehen davon sollten Sie versuchen, die Tests so realistisch wie möglich zu gestalten. Manchmal allerdings ist es sinnvoll, einen etwas unrealistischen Benchmark einzusetzen. Nehmen Sie z.B. an, Ihre Anwendung befindet sich auf einem anderen Host als der Datenbankserver. Es wäre realistischer, die Benchmarks in der gleichen Konfiguration durchzuführen. Das würde aber gleichzeitig weitere Variablen einbringen, etwa wie schnell und belastet das Netzwerk ist. Benchmarking auf einem einzelnen Knoten ist normalerweise einfacher und in einigen Fällen genau genug. Sie müssen selbst abschätzen, welches Vorgehen passend ist.
Einen Benchmark entwerfen und planen Der erste Schritt beim Planen eines Benchmarks besteht darin, das Problem und das Ziel zu erkennen. Als Nächstes müssen Sie entscheiden, ob Sie einen Standard-Benchmark benutzen oder einen eigenen entwerfen wollen. Wenn Sie einen Standard-Benchmark verwenden, dann achten Sie darauf, dass dieser Ihren Anforderungen entspricht. Nehmen Sie z.B. keinen TPC-Benchmark, um ein ECommerce-System zu testen. Laut eigener Aussage veranschaulicht TPC Entscheidungsunterstützungssysteme, die große Datenmengen untersuchen. Daher ist dieser Benchmark nicht passend für ein OLTP-System. Das Entwerfen eines eigenen Benchmarks ist ein komplizierter und iterativ ablaufender Vorgang. Nehmen Sie als Einstieg einen Schnappschuss Ihrer Produktionsdatenmenge auf. Sorgen Sie dafür, dass Sie diese Daten für nachfolgende Durchläufe wiederherstellen können. Nun brauchen Sie Abfragen, die Sie an diesen Daten durchführen. Sie können eine Modultestsuite in einen rudimentären Benchmark umwandeln, indem Sie sie einfach viele Male durchlaufen lassen. Allerdings wird dies wahrscheinlich kaum der Art und Weise entsprechen, wie Sie die Datenbank wirklich benutzen. Ein besserer Ansatz sieht vor, alle Abfragen auf Ihrem Produktionssystem in einem repräsentativen Zeitrahmen zu protokollieren, etwa eine Stunde unter Spitzenlast oder einen ganzen Tag lang. Wenn Sie Abfragen während eines kurzen Zeitraums aufzeichnen, müssen Sie möglicherweise mehrere Zeitrahmen wählen. Auf diese Weise können Sie alle Systemaktivitäten erfassen, wie etwa Abfragen zur Erstellung von wöchentlichen Berichten oder Batch-Jobs, die Sie außerhalb der Spitzenzeiten eintakten.1 Sie können Abfragen auf unterschiedlichen Ebenen aufzeichnen. Falls Sie z.B. einen FullStack-Benchmark benötigen, können Sie die HTTP-Anforderungen auf einem Webserver protokollieren. Es ist natürlich auch möglich, das MySQL-Abfrage-Log zu aktivieren. Wenn Sie jedoch ein Abfrage-Log noch einmal abspielen, müssen Sie daran denken, die Threads zu trennen, anstatt einfach alle Abfragen linear abzuspielen. Es ist außerdem
1 All das natürlich unter der Voraussetzung, dass Sie einen perfekten Benchmark haben wollen. Normalerweise macht Ihnen das Leben einen Strich durch die Rechnung.
42 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
wichtig, einen eigenen Thread für jede Verbindung in dem Log einzurichten, anstatt die Abfragen in den Threads zu mischen. Das Abfrage-Log zeigt, über welche Verbindungen die einzelnen Abfragen kamen. Selbst wenn Sie keinen eigenen Benchmark herstellen, sollten Sie Ihren BenchmarkingPlan aufschreiben. Sie werden den Benchmark viele Male durchführen und müssen in der Lage sein, ihn exakt zu reproduzieren. Denken Sie bei der Planung auch an die Zukunft. Vielleicht sind Sie gar nicht derjenige, der den Benchmark das nächste Mal ausführt, und selbst wenn, erinnern Sie sich möglicherweise nicht mehr genau daran, wie Sie ihn das erste Mal ausgeführt haben. Ihr Plan sollte auch die Testdaten, die Schritte zum Einrichten des Systems und den Aufwärmplan umfassen. Denken Sie sich eine Methode zum Dokumentieren der Parameter und Ergebnisse aus, und dokumentieren Sie jeden Durchlauf sorgfältig. Ihre Dokumentationsmethode könnte so einfach wie eine Tabellenkalkulation oder ein Notizbuch sein oder so aufwendig wie eine selbst entworfene Datenbank. (Denken Sie daran, dass Sie wahrscheinlich einige Skripte schreiben werden, die Sie bei der Auswertung der Ergebnisse unterstützen. Je einfacher es daher ist, die Ergebnisse zu verarbeiten, ohne Tabellenkalkulations- oder Textdateien zu öffnen, umso besser.) Vermutlich ist es sinnvoll, ein Benchmark-Verzeichnis anzulegen, in dem es Unterverzeichnisse für die Ergebnisse der einzelnen Durchläufe gibt. Sie können dann die Ergebnisse, Konfigurationsdateien und Notizen für jeden Durchlauf in das entsprechende Unterverzeichnis legen. Falls Sie mit Ihrem Benchmark mehr messen, als Sie zunächst interessiert, zeichnen Sie die zusätzlichen Daten dennoch auf. Es ist besser, unnütze Daten zu haben, als wenn wichtige Daten fehlen, und vielleicht brauchen Sie die Zusatzdaten später doch noch. Versuchen Sie, so viele zusätzliche Informationen wie möglich während der Benchmarks aufzuzeichnen, wie etwa CPU-Auslastung, Festplatten-Ein-/ Ausgabe, Statistiken über den Netzwerkverkehr, Zähler von SHOW GLOBAL STATUS usw.
Exakte Ergebnisse erhalten Um wirklich exakte Ergebnisse zu erhalten, müssen Sie Ihren Benchmark so entwerfen, dass er tatsächlich Ihre möglichen Fragen beantwortet. Haben Sie den richtigen Benchmark gewählt? Erfassen Sie alle Daten, die Sie benötigen, um die Fragen zu beantworten? Testen Sie mit dem Benchmark die falschen Kriterien? Führen Sie etwa einen CPUgebundenen Benchmark durch, um die Leistung einer Anwendung vorherzusagen, von der Sie wissen, dass sie ein-/ausgabegebunden ist? Sorgen Sie als Nächstes dafür, dass Ihre Benchmark-Ergebnisse wiederholbar sind. Versuchen Sie sicherzustellen, dass das System sich am Anfang jedes Durchlaufs im gleichen Zustand befindet. Wenn der Benchmark wichtig ist, sollten Sie zwischen den Durchläufen booten. Falls Sie, was normal wäre, einen aufgewärmten Server testen müssen, sollten Sie dafür sorgen, dass die Aufwärmphase lang genug und dass sie wiederholbar ist. Besteht das Aufwärmen z.B. aus zufälligen Abfragen, werden Ihre Benchmark-Ergebnisse nicht reproduzierbar sein. Benchmarking-Taktiken | 43
Wenn der Benchmark Daten oder das Schema ändert, dann setzen Sie ihn zwischen den Durchläufen mit einem frischen Schnappschuss zurück. Das Einfügen in eine Tabelle mit 1000 Zeilen liefert nicht die gleichen Ergebnisse wie das Einfügen in eine Tabelle mit einer Million Zeilen! Auch die Datenfragmentierung und die Aufteilung auf der Festplatte können der Reproduzierbarkeit der Ergebnisse im Wege stehen. Damit das physische Layout immer annähernd gleich ist, könnten Sie eine Partition schnell formatieren und neu mit Daten füllen. Achten Sie auf externe Last-, Profilierungs- und Überwachungssysteme, ausführliches Logging, regelmäßig wiederkehrende Jobs und andere Faktoren, die Ihre Ergebnisse verfälschen können. Eine typische Überraschung ist ein cron-Job, der mitten in einem Benchmark-Durchlauf beginnt, oder ein Patrol-Read-Zyklus oder eine planmäßige Konsistenzprüfung auf Ihrer RAID-Karte. Sorgen Sie dafür, dass alle Ressourcen, die der Benchmark während des Durchlaufs benötigt, auch ihm zugeordnet sind. Wenn jemand anderes die Netzwerkkapazität belegt oder der Benchmark auf einem SAN stattfindet, den auch andere Server benutzen, werden die Ergebnisse möglicherweise nicht genau sein. Versuchen Sie, während der Durchführung eines Benchmarks so wenige Parameter wie möglich zu ändern. Man nennt dies in der Wissenschaft »die Variable isolieren«. Wenn Sie mehrere Dinge auf einmal ändern müssen, dann verpassen Sie vermutlich etwas. Parameter können auch voneinander abhängig sein, so dass sie sich manchmal gar nicht unabhängig voneinander ändern lassen. Vielleicht wissen Sie nicht einmal, dass sie miteinander verbunden sind. Das erhöht natürlich die Komplexität. Manchmal spielt das keine Rolle. Falls Sie etwa über eine Migration von einem Solaris-System auf SPARCHardware zu GNU/Linux auf x86 nachdenken, wäre es sinnlos, als Zwischenschritt Solaris auf x86 einem Benchmark-Test zu unterziehen! Im Allgemeinen hilft es, wenn man die Benchmark-Parameter schrittweise ändert und nicht alle auf einmal. Setzen Sie z.B. Techniken wie »Teile und Herrsche« (zum Halbieren der Unterschiede zwischen den Durchläufen) ein, um sich an einen guten Wert für die Servereinstellung heranzutasten. Wir kennen viele Benchmarks, die versuchen, die Leistung nach einer Migration, etwa von Oracle nach MySQL vorherzusagen. Diese sind oft problematisch, weil MySQLmit völlig anderen Arten von Abfragen gut funktioniert als Oracle. Falls Sie wissen wollen, wie gut eine Anwendung, die auf Oracle erstellt wurde, funktioniert, nachdem Sie sie nach MySQL migriert haben, müssen Sie normalerweise das Schema und die Abfragen für MySQL umgestalten. (In manchen Fällen, z.B. wenn Sie eine plattformübergreifende Anwendung herstellen, wird es Sie interessieren, wie die gleichen Abfragen auf beiden Plattformen laufen, aber das ist eher ungewöhnlich.) Auch aus den vorgegebenen Einstellungen der MySQL-Konfiguration werden Sie kaum sinnvolle Ergebnisse gewinnen, da diese auf winzige Anwendungen abgestimmt sind, die wenig Speicher beanspruchen.
44 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Sollten Sie schließlich ein seltsames Ergebnis erhalten, dann verwerfen Sie es nicht einfach. Versuchen Sie lieber herauszufinden, was passiert ist. Vielleicht finden Sie ein wertvolles Ergebnis, ein großes Problem oder einfach einen Fehler in Ihrem BenchmarkDesign.
Den Benchmark ausführen und die Ergebnisse analysieren Nachdem Sie alles vorbereitet haben, können Sie den Benchmark ausführen und damit beginnen, Daten zu sammeln und zu analysieren. Meist ist es eine gute Idee, die Benchmark-Durchläufe zu automatisieren. Damit verbessern Sie Ihre Ergebnisse und deren Genauigkeit, da Sie auf diese Weise keine Schritte vergessen oder in den einzelnen Durchläufen unterschiedlich ausführen. Es hilft außerdem bei der Dokumentation des Benchmark-Tests. Jede Automatisierungsmethode eignet sich, etwa ein Makefile oder eine Reihe eigener Skripten. Sie können sich für eine beliebige Skriptsprache entscheiden: für eine Shell, PHP, Perl usw. Versuchen Sie, so viel wie möglich zu automatisieren, einschließlich des Ladens der Daten, des Aufwärmens des Systems, des Ausführens des Benchmarks und des Aufzeichnens der Ergebnisse. Wenn Sie das Benchmarking richtig eingerichtet haben, können Sie es in einem Schritt durchführen. Soll es dagegen eine einmalige Angelegenheit bleiben, dann lohnt sich das Automatisieren nicht.
Normalerweise führt man einen Benchmark mehrmals durch. Die genaue Anzahl der Durchgänge hängt von Ihrem Vorgehen bei der Auswertung ab und davon, wie wichtig die Ergebnisse sind. Falls Sie größere Sicherheit haben wollen, müssen Sie den Benchmark mehrfach durchführen. Es ist üblich, das beste Ergebnis zu suchen, einen Durchschnitt aus allen Ergebnissen zu bilden oder den Benchmark fünfmal ablaufen zu lassen und dann den Durchschnitt aus den drei besten Ergebnissen zu bilden. Sie können so exakt sein, wie Sie wollen. Möglicherweise möchten Sie ja statistische Methoden auf Ihre Ergebnisse anwenden, Konfidenzintervalle ermitteln usw., aber oft benötigen Sie diese große Sicherheit gar nicht.2 Wenn er Ihre Fragen zu Ihrer Zufriedenheit beantwortet, können Sie den Benchmark ruhig mehrere Male ausführen und feststellen, wie stark die Ergebnisse sich unterscheiden. Falls sie stark schwanken, führen Sie entweder den Benchmark öfter durch oder lassen ihn länger laufen, wodurch sich normalerweise die Abweichungen verringern. Sobald Sie Ihre Ergebnisse haben, müssen Sie sie analysieren, d.h., Zahlen in Wissen umwandeln. Das Ziel besteht darin, die Frage zu beantworten, die der Benchmark formu-
2 Falls Sie wirklich wissenschaftlich exakte, harte Ergebnisse haben wollen, sollten Sie ein gutes Buch darüber lesen, wie Sie kontrollierte Tests entwerfen und durchführen. Dieses Thema können wir nämlich aufgrund seines Umfangs hier nicht behandeln.
Benchmarking-Taktiken | 45
liert. Idealerweise werden Sie jetzt Aussagen treffen können wie »Das Aufrüsten auf vier CPUs erhöht den Durchsatz um 50 % bei gleichbleibender Latenz« oder »Die Indizes haben die Abfragen beschleunigt«. Wie Sie die Zahlen »würfeln«, hängt davon ab, wie Sie die Ergebnisse gesammelt haben. Es bietet sich an, zur Analyse der Ergebnisse Skripten zu schreiben, nicht nur, um den Arbeitsaufwand zu verringern, sondern aus den gleichen Gründen, aus denen Sie auch die Benchmarks automatisieren sollen: Reproduzierbarkeit und Dokumentation.
Benchmarking-Werkzeuge Sie müssen kein eigenes Benchmarking-System aufziehen. Das ist sowieso nur dann notwendig, wenn es einen wirklich dringenden Grund dafür gibt, dass Sie keines der verfügbaren Systeme einsetzen können. Ihnen stehen eine Vielzahl von Werkzeugen zur Verfügung. In den folgenden Abschnitten werden wir einige davon vorstellen.
Full-Stack-Werkzeuge Erinnern Sie sich daran, dass es zwei Arten von Benchmarks gibt: Full-Stack und SingleComponent. Es wird Sie nicht überraschen zu hören, dass es Werkzeuge gibt, um ganze Anwendungen einem Benchmark-Test zu unterziehen, und Werkzeuge, um an MySQL und anderen Komponenten isoliert Stresstests durchzuführen. Durch das Testen des vollständigen Systems erhalten Sie normalerweise ein klareres Bild von seiner Leistung. Zu den Werkzeugen für solche Tests gehören: ab ab ist ein bekanntes Benchmarking-Werkzeug für den Apache-HTTP-Server. Es zeigt, wie viele Anforderungen pro Sekunde Ihr HTTP-Server bedienen kann. Falls Sie eine Webanwendung testen, bedeutet dies, wie viele Anforderungen pro Sekunde die gesamte Anwendung erfüllen kann. Es ist ein sehr einfaches Werkzeug, sein Nutzen ist allerdings beschränkt, da es einfach nur, so schnell es kann, eine URL abarbeitet. Weitere Informationen über ab finden Sie unter http://httpd.apache. org/docs/2.0/programs/ab.html. http_load Dieses Werkzeug ist vom Konzept her mit ab vergleichbar; es ist ebenfalls dazu gedacht, einen Webserver zu laden, allerdings bietet es größere Flexibilität. Sie können eine Eingabedatei mit vielen verschiedenen URLs anlegen, http_load wählt dann zufällig eine aus. Darüber hinaus können Sie die Rate vorgeben, mit der die Anforderungen ausgeführt werden, anstatt sie einfach so schnell wie möglich abarbeiten zu lassen. Näheres erfahren Sie unter http://www.acme.com/software/http_load/. JMeter JMeter ist ein Java-Programm, das eine andere Anwendung laden und seine Performance messen kann. Es wurde zum Testen von Webanwendungen geschaffen, Sie können damit aber auch FTP-Server testen und über JDBC Abfragen an eine Datenbank auslösen. 46 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
JMeter ist viel komplexer als ab und http_load. Es besitzt z.B. Funktionen, mit denen Sie viel flexibler echte Benutzer simulieren können, indem Sie solche Parameter wie die Anlaufzeit (ramp-up time) kontrollieren. Es verfügt über eine grafische Oberfläche mit einer integrierten Darstellung der Ergebnisse in Diagrammform und bietet die Möglichkeit, Ergebnisse aufzuzeichnen und erneut abzuspielen. Weitere Informationen finden Sie unter http://jakarta.apache.org/jmeter/.
Single-Component-Werkzeuge Hier sind einige nützliche Werkzeuge, um die Leistung von MySQL und des Systems zu testen, auf dem es läuft. Im nächsten Abschnitt zeigen wir Beispiel-Benchmarks mit einigen dieser Werkzeuge: mysqlslap mysqlslap (http://dev.mysql.com/doc/refman/5.1/en/mysqlslap.html) simuliert die Last auf dem Server und liefert Zeitinformationen. Es ist Teil der MySQL-5.1-Server-Distribution, sollte aber auch mit MySQL 4.1 und neueren Servern funktionieren. Sie können angeben, wie viele nebenläufige Verbindungen es benutzen soll, und übergeben entweder eine SQL-Anweisung auf der Kommandozeile oder eine Datei, die auszuführende SQL-Anweisungen enthält. Falls Sie dem Programm keine Anweisungen übergeben, kann es auch automatisch SELECT-Anweisungen generieren, indem es das Schema des Servers untersucht. sysbench sysbench (http://sysbench.sourceforge.net) ist ein Multithread-fähiges System-Benchmarking-Werkzeug. Sein Ziel besteht darin, ein Gefühl für die Systemleistung zu gewinnen, und zwar in Bezug auf Faktoren, die zum Ausführen eines Datenbankservers wichtig sind. Sie können z.B. die Leistung von Datei-Ein-/Ausgabe, Betriebssystem-Scheduler, Speicherzuweisung und Übertragungsgeschwindigkeit, der POSIXThreads sowie des Datenbankservers selbst messen. sysbench unterstützt Skripten in der Sprache Lua (http://www.lua.org), wodurch man sehr flexibel eine Vielzahl von Szenarien testen kann. Database Test Suite Die Database Test Suite, die von den Open-Source Development Labs (OSDL) geschaffen wurde und auf SourceForge unter http://sourceforge.net/projects/osdldbt/ angeboten wird, ist ein Test-Kit zum Ausführen von Benchmarks, die mit einigen Industriestandard-Benchmarks vergleichbar sind, wie etwa denen des Transaction Processing Performance Council (TPC). Speziell beim dbt2-Testprogramm handelt es sich um eine freie (allerdings nicht zertifizierte) Implementierung des TPC-C-OLTPTests. Sie unterstützt InnoDB und Falcon. Zurzeit ist der Status für die anderen transaktionsfähigen MySQL-Storage-Engines unbekannt. MySQL Benchmark Suite (sql-bench) MySQL bietet zusammen mit dem MySQL-Server seine eigene Benchmark-Suite an, mit der Sie unterschiedliche Datenbankserver testen können. Sie ist Single-Thread-
Benchmarking-Werkzeuge |
47
fähig und misst, wie schnell der Server Abfragen ausführt. Die Ergebnisse zeigen, welche Arten von Operationen der Server gut ausführt. Der größte Vorteil dieser Benchmark-Suite besteht darin, dass sie viele vordefinierte Tests enthält, die einfach zu benutzen sind, so dass es leicht ist, unterschiedliche Storage-Engines oder Konfigurationen miteinander zu vergleichen. Man kann damit sogar sehr gut die Gesamtleistung zweier Server miteinander vergleichen. Sie können auch nur einen Teil ihrer Tests ausführen (um z.B. nur die UPDATE-Leistung zu testen). Die Tests sind größtenteils CPU-gebunden, einige kurze Zeiträume erfordern aber auch viele Festplatten-Ein-/Ausgaben. Die größten Nachteile dieses Werkzeugs bestehen darin, dass es nicht mehrbenutzerfähig ist, dass es eine sehr kleine Datenmenge benutzt, dass Sie die Site-spezifischen Daten nicht testen können und dass seine Ergebnisse zwischen den Durchläufen variieren können. Da es nur Single-Thread-fähig und vollständig seriell ist, hilft es Ihnen nicht dabei, die Vorteile mehrerer CPUs abzuschätzen. Allerdings lassen sich mehrere Server mit jeweils nur einer CPU miteinander vergleichen. Für den Datenbankserver, den Sie testen wollen, sind Perl- und DBD-Treiber erforderlich. Die Dokumentation finden Sie unter http://dev.mysql.com/doc/en/mysqlbenchmarks.html/. Super Smack Super Smack (http://vegan.net/tony/supersmack/) ist ein Stresstest-Werkzeug für MySQL und PostgreSQL. Es handelt sich hierbei um ein komplexes, leistungsfähiges Werkzeug, mit dem Sie mehrere Benutzer simulieren, Testdaten in die Datenbank laden und Tabellen mit zufällig generierten Daten füllen können. Die Benchmarks sind in sogenannten »Smack«-Dateien enthalten, die eine einfache Sprache verwenden, um Clients, Tabellen, Abfragen usw. zu definieren.
Benchmarking-Beispiele In diesem Abschnitt zeigen wir Ihnen einige Beispiele für tatsächliche Benchmarks mit einigen der oben erwähnten Werkzeuge. Wir können nicht auf jedes Werkzeug ausführlich eingehen, aber dennoch sollten Ihnen diese Beispiele bei der Entscheidung für einen sinnvollen Benchmark und bei den ersten Schritten helfen.
http_load Beginnen wir mit einem einfachen Beispiel für die Verwendung von http_load. Dazu benutzen wir die folgenden URLs, die wir in der Datei urls.txt gespeichert haben: http://www.mysqlperformanceblog.com/ http://www.mysqlperformanceblog.com/page/2/ http://www.mysqlperformanceblog.com/mysql-patches/ http://www.mysqlperformanceblog.com/mysql-performance-presentations/ http://www.mysqlperformanceblog.com/2006/09/06/slow-query-log-analyzes-tools/
48 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Die MySQL-Funktion BENCHMARK( ) MySQL besitzt eine praktische BENCHMARK( )-Funktion, mit der Sie die Ausführungsgeschwindigkeit bestimmter Arten von Operationen testen können. Sie geben einen Ausdruck an, der ausgeführt werden soll, und legen fest, wie oft dies zu geschehen hat. Der Ausdruck kann ein beliebiger Skalar sein, wie etwa eine skalare Unterabfrage oder eine Funktion. Damit lässt sich sehr schön die relative Geschwindigkeit einiger Operationen testen, wie etwa ob MD5( ) schneller als SHA1( ) ist: mysql> SET @input := 'hello world'; mysql> SELECT BENCHMARK(1000000, MD5(@input)); +---------------------------------+ | BENCHMARK(1000000, MD5(@input)) | +---------------------------------+ | 0 | +---------------------------------+ 1 row in set (2.78 sec) mysql> SELECT BENCHMARK(1000000, SHA1(@input)); +----------------------------------+ | BENCHMARK(1000000, SHA1(@input)) | +----------------------------------+ | 0 | +----------------------------------+ 1 row in set (3.50 sec)
Der Rückgabewert ist immer 0. Sie messen die Ausführung, indem Sie nachschauen, welche Zeit die Clientanwendung für die Ausführung der Abfrage angegeben hat. In diesem Fall sieht es so aus, als sei MD5( ) schneller. Es ist allerdings nicht ganz einfach, BENCHMARK( ) richtig einzusetzen, wenn man nicht weiß, was es eigentlich tut. Es misst nämlich, wie schnell der Server den Ausdruck ausführen kann, liefert jedoch keinen Hinweis auf den Overhead für das Parsen und Optimieren. Und wenn der Ausdruck keine Benutzervariable enthält, dann wird unter Umständen ab der zweiten Ausführung des Ausdrucks durch den Server ein Ergebnis aus dem Cache zurückgeliefert.3 Auch wenn es praktisch ist, verwenden wir BENCHMARK( ) nicht für echte Benchmarks. Es ist einfach zu schwer festzustellen, was es wirklich misst, und beschränkt sich außerdem zu sehr auf einen kleinen Teil des gesamten Ausführungsprozesses.
Am einfachsten benutzt man http_load, indem man die URLs in einer Schleife abruft. Das Programm ruft sie so schnell wie möglich auf:3 $ http_load -parallel 1 -seconds 10 urls.txt 19 fetches, 1 max parallel, 837929 bytes, in 10.0003 seconds 44101.5 mean bytes/connection 1.89995 fetches/sec, 83790.7 bytes/sec msecs/connect: 41.6647 mean, 56.156 max, 38.21 min 3 Einer der Autoren machte diesen Fehler und stellte fest, dass 10.000 Ausführungen eines bestimmten Ausdrucks genauso schnell liefen wie eine Ausführung. Es war ein Cache-Ergebnis. Im Allgemeinen müssen Sie bei einem solchen Verhalten immer von einem Cache-Ergebnis oder einem Fehler ausgeben.
Benchmarking-Beispiele | 49
msecs/first-response: 320.207 mean, 508.958 max, 179.308 min HTTP response codes: code 200 – 19
Die Ergebnisse sind eigentlich selbsterklärend: Sie zeigen einfach Statistiken über die Anforderungen. Ein etwas komplexeres Benutzungsszenario sieht vor, dass die URLs so schnell wie möglich in einer Schleife abgerufen, damit aber fünf gleichzeitig agierende Benutzer emuliert werden: $ http_load -parallel 5 -seconds 10 urls.txt 94 fetches, 5 max parallel, 4.75565e+06 bytes, in 10.0005 seconds 50592 mean bytes/connection 9.39953 fetches/sec, 475541 bytes/sec msecs/connect: 65.1983 mean, 169.991 max, 38.189 min msecs/first-response: 245.014 mean, 993.059 max, 99.646 min HTTP response codes: code 200 – 94
Alternativ können wir, anstatt die URLs so schnell wie möglich abzurufen, die Last für eine vorherbestimmte Anforderungsrate (wie etwa fünf pro Sekunde) emulieren: $ http_load -rate 5 -seconds 10 urls.txt 48 fetches, 4 max parallel, 2.50104e+06 bytes, in 10 seconds 52105 mean bytes/connection 4.8 fetches/sec, 250104 bytes/sec msecs/connect: 42.5931 mean, 60.462 max, 38.117 min msecs/first-response: 246.811 mean, 546.203 max, 108.363 min HTTP response codes: code 200 – 48
Schließlich emulieren wir eine noch größere Last mit einer Rate von 20 eingehenden Anforderungen pro Sekunde. Beachten Sie, dass die Verbindungs- und Antwortraten mit höherer Last zunehmen: $ http_load -rate 20 -seconds 10 urls.txt 111 fetches, 89 max parallel, 5.91142e+06 bytes, in 10.0001 seconds 53256.1 mean bytes/connection 11.0998 fetches/sec, 591134 bytes/sec msecs/connect: 100.384 mean, 211.885 max, 38.214 min msecs/first-response: 2163.51 mean, 7862.77 max, 933.708 min HTTP response codes: code 200 -- 111
sysbench Das Programm sysbench kann eine Vielzahl von Benchmarks ausführen, die es als »Tests« bezeichnet. Es soll nicht nur die Datenbankleistung testen, sondern auch, wie gut ein System wahrscheinlich als Datenbankserver arbeiten wird. Wir beginnen mit einigen Tests, die nicht MySQL-spezifisch sind, und messen die Leistung für Subsysteme, die die Gesamtgrenzen des Systems bestimmen. Anschließend zeigen wir, wie die Datenbankleistung gemessen wird.
50 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Der sysbench-CPU-Benchmark Der offensichtlichste Subsystemtest ist der CPU-Benchmark, der 64-Bit-Integer-Werte verwendet, um Primzahlen bis zu einem angegebenen Maximalwert zu berechnen. Wir führen diesen Test auf zwei Servern aus, die beide unter GNU/Linux laufen, und vergleichen die Ergebnisse miteinander. Hier sehen Sie die Hardware des ersten Servers: [server1 ~]$ cat /proc/cpuinfo ... model name : AMD Opteron(tm) Processor 246 stepping : 1 cpu MHz : 1992.857 cache size : 1024 KB
Und so führen wir den Benchmark aus: [server1 ~]$ sysbench --test=cpu --cpu-max-prime=20000 run sysbench v0.4.8: multi-threaded system evaluation benchmark ... Test execution summary: total time: 121.7404s
Der zweite Server besitzt eine andere CPU: [server2 ~]$ cat /proc/cpuinfo ... model name : Intel(R) Xeon(R) CPU stepping : 6 cpu MHz : 1995.005
5130
@ 2.00GHz
Hier ist sein Benchmark-Ergebnis: [server1 ~]$ sysbench --test=cpu --cpu-maxprime=20000 run sysbench v0.4.8: multi-threaded system evaluation benchmark ... Test execution summary: total time: 61.8596s
Das Ergebnis zeigt einfach die Gesamtzeit an, die erforderlich war, um die Primzahlen zu berechnen. Das lässt sich ganz leicht vergleichen. In diesem Fall führte der zweite Server den Benchmark fast doppelt so schnell aus wie der erste Server.
Der sysbench-Datei-Ein-/Ausgabe-Benchmark Der fileio-Benchmark misst, wie Ihr System unter verschiedenen Arten von Ein-/Ausgabe-Lasten arbeitet. Damit kann man Festplatten, RAID-Karten und RAID-Modi vergleichen und das Ein-/Ausgabe-Subsystem anpassen und verbessern. Zuerst müssen einige Dateien für den Benchmark vorbereitet werden. Sie sollten deutlich mehr Daten generieren, als in den Speicher passen. Passen die Daten komplett in den Speicher, schiebt das Betriebssystem die meisten von ihnen in den Cache, und das Ergebnis spiegelt die ein-/ausgabegebundene Last nicht korrekt wider. Wir erzeugen zuerst eine Datenmenge: $ sysbench --test=fileio --file-total-size=150G prepare
Benchmarking-Beispiele | 51
Im zweiten Schritt wird der Benchmark ausgeführt. Zum Testen verschiedener Arten von Ein-/Ausgabe-Leistung stehen mehrere Optionen zur Verfügung: seqwr
Sequenzielles Schreiben seqrewr
Sequenzielles Neuschreiben seqrd
Sequenzielles Lesen rndrd
Zufälliges Lesen rndwr
Zufälliges Schreiben rndrw
Kombiniertes zufälliges Lesen/Schreiben Der folgende Befehl führt den Benchmark mit zufälligem Lese-/Schreibzugriff aus: $ sysbench --test=fileio --file-total-size=150G --file-test-mode=rndrw --init-rnd=on --max-time=300 --max-requests=0 run
Hier sind die Ergebnisse: sysbench v0.4.8:
multi-threaded system evaluation benchmark
Running the test with following options: Number of threads: 1 Initializing random number generator from timer. Extra file open flags: 0 128 files, 1.1719Gb each 150Gb total file size Block size 16Kb Number of random requests for random IO: 10000 Read/Write ratio for combined random IO test: 1.50 Periodic FSYNC enabled, calling fsync( ) each 100 requests. Calling fsync( ) at the end of test, Enabled. Using synchronous I/O mode Doing random r/w test Threads started! Time limit exceeded, exiting... Done. Operations performed: 40260 Read, 26840 Write, 85785 Other = 152885 Total Read 629.06Mb Written 419.38Mb Total transferred 1.0239Gb (3.4948Mb/sec) 223.67 Requests/sec executed Test execution summary: total time: 300.0004s total number of events: 67100 total time taken by event execution: 254.4601 per-request statistics:
52 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
min: avg: max: approx. 95 percentile: Threads fairness: events (avg/stddev): execution time (avg/stddev):
0.0000s 0.0038s 0.5628s 0.0099s
67100.0000/0.00 254.4601/0.00
Die Ausgabe enthält eine Menge an Informationen. Die interessantesten Zahlen zum Verfeinern des Ein-/Ausgabe-Subsystems sind die Anzahl der Anforderungen pro Sekunde und der Gesamtdurchsatz. In diesem Fall zeigen die Ergebnisse 223,67 Anforderungen/Sekunde bzw. 3,4948 MByte/Sekunde. Diese Werte liefern einen guten Hinweis auf die Performance der Festplatten. Wenn Sie fertig sind, löschen Sie die Dateien, die sysbench für die Benchmarks angelegt hat: $ sysbench --test=fileio –-file-total-size=150G cleanup
Der sysbench-OLTP-Benchmark Der OLTP-Benchmark emuliert die Last bei der Transaktionsverarbeitung. Wir zeigen ein Beispiel mit einer Tabelle, die eine Million Zeilen enthält. Der erste Schritt besteht darin, die Tabelle für den Test vorzubereiten: $ sysbench --test=oltp --oltp-table-size=1000000 --mysql-db=test --mysqluser=root prepare sysbench v0.4.8: multi-threaded system evaluation benchmark No DB drivers specified, using mysql Creating table 'sbtest'... Creating 1000000 records in table 'sbtest'...
Das war schon die gesamte Vorbereitung. Nun lassen wir den Benchmark 60 Sekunden lang mit 8 nebenläufigen Threads im Read-Only-Modus laufen: $ sysbench --test=oltp --oltp-table-size=1000000 --mysql-db=test --mysql-user=root --maxtime=60 --oltp-read-only=on --max-requests=0 --num-threads=8 run sysbench v0.4.8: multi-threaded system evaluation benchmark No DB drivers specified, using mysql WARNING: Preparing of "BEGIN" is unsupported, using emulation (last message repeated 7 times) Running the test with following options: Number of threads: 8 Doing OLTP test. Running mixed OLTP test Doing read-only test Using Special distribution (12 iterations, Using "BEGIN" for starting transactions Using auto_inc on the id column Threads started!
1 pct of values are returned in 75 pct cases)
Benchmarking-Beispiele | 53
Time limit exceeded, exiting... (last message repeated 7 times) Done. OLTP test statistics: queries performed: read: write: other: total: transactions: deadlocks: read/write requests: other operations:
179606 0 25658 205264 12829 0 179606 25658
Test execution summary: total time: total number of events: total time taken by event execution: per-request statistics: min: avg: max: approx. 95 percentile: Threads fairness: events (avg/stddev): execution time (avg/stddev):
(213.07 per sec.) (0.00 per sec.) (2982.92 per sec.) (426.13 per sec.)
60.2114s 12829 480.2086 0.0030s 0.0374s 1.9106s 0.1163s
1603.6250/70.66 60.0261/0.06
Wie gehabt finden Sie auch hier viele Informationen in den Ergebnissen. Die interessantesten Teile sind: • der Transaktionszähler • die Rate der Transaktionen pro Sekunde • die Statistiken für die einzelnen Anforderungen (minimale, durchschnittliche und maximale Zeit sowie die Zeit des 95. Perzentils) • Statistiken für die Thread-Fairness, die zeigen, wie fair die simulierte Last war
Weitere sysbench-Funktionen Das sysbench-Programm kann verschiedene weitere System-Benchmarks ausführen, die nicht direkt die Leistung eines Datenbankservers messen: memory
Führt sequenzielle Speicher-Lese- oder Schreiboperationen aus. threads
Misst die Performance des Thread-Schedulers. Damit kann man vor allem das Verhalten des Schedulers unter hoher Last testen.
54 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
mutex
Misst die Mutex-Leistung, indem eine Situation emuliert wird, in der meist alle Threads parallel ablaufen, wobei nur kurzzeitig Mutex-Locks geholt werden. (Bei einem Mutex handelt es sich um eine Datenstruktur, die einen Zugriff unter wechselseitigem Ausschluss [engl. Mutual Exclusive] auf eine Ressource garantiert, so dass verhindert wird, dass ein nebenläufiger Zugriff Probleme verursacht.) seqwr
Misst die Leistung bei sequenziellen Schreiboperationen. Dies ist sehr wichtig, um die praktischen Leistungsgrenzen eines Systems zu testen. Es kann zeigen, wie gut der Cache Ihres RAID-Controllers funktioniert, und alarmiert Sie, wenn die Ergebnisse ungewöhnlich ausfallen. Falls Sie z.B. keinen batteriegesicherten SchreibCache haben, Ihre Platte aber 3.000 Anforderungen pro Sekunde erhält, dann stimmt etwas nicht und Ihre Daten sind nicht mehr sicher. Zusätzlich zum Benchmark-spezifischen Parameter (--test) akzeptiert sysbench weitere gebräuchliche Parameter, wie etwa --num-threads, --max-requests und --max-time. In der Dokumentation finden Sie Informationen dazu.
dbt2 TPC-C auf der Database Test Suite Das dbt2-Werkzeug der Database Test Suite ist eine freie Implementierung des TPC-CTests. TPC-C ist eine Spezifikation, die vom TPC (Transaction Processing Performing Council) veröffentlicht wurde und eine komplexe Online-Transaktionsverarbeitungslast emuliert. Es weist seine Ergebnisse in Transaktionen pro Minute (tpmC) aus, zusammen mit den Kosten für die einzelnen Transaktionen (Preis/tpmC). Die Ergebnisse hängen stark von der Hardware ab, weshalb veröffentlichte TPC-C-Ergebnisse ausführliche Spezifikationen der Server enthalten, die in dem Benchmark verwendet wurden. Der dbt2-Test ist eigentlich nicht TPC-C. Er ist nicht vom TPC zertifiziert, und seine Ergebnisse können auch nicht direkt mit TPC-C-Ergebnissen verglichen werden.
Schauen wir uns ein Beispiel für das Einrichten und Ausführen eines dbt2-Benchmarks an. Wir haben Version 0.37 von dbt2 verwendet. Das ist die neueste Version, die wir zusammen mit MySQL einsetzen konnten (neuere Versionen enthalten Erweiterungen, die MySQL nicht vollständig unterstützt). Wir haben folgende Schritte unternommen: 1. Vorbereitung der Daten
Der folgende Befehl erzeugt Daten für 10 Warehouses in dem angegebenen Verzeichnis. Die Warehouses nehmen zusammen einen Platz von etwa 700 MByte ein. Der erforderliche Platzbedarf ändert sich proportional zur Anzahl der Warehouses, Sie können also den Parameter -w ändern, um eine Datenmenge mit der von Ihnen benötigten Größe anzulegen.
Benchmarking-Beispiele | 55
# src/datagen -w 10 -d /mnt/data/dbt2-w10 warehouses = 10 districts = 10 customers = 3000 items = 100000 orders = 3000 stock = 100000 new_orders = 900 Output directory of data files: /mnt/data/dbt2-w10 Generating data files for 10 warehouse(s)... Generating item table data... Finished item table data... Generating warehouse table data... Finished warehouse table data... Generating stock table data...
2. Laden der Daten in die MySQL-Datenbank
Der nächste Befehl erzeugt eine Datenbank namens dbt2w10 und lädt in sie die Daten, die wir im vorhergehenden Schritt erzeugt haben (-d ist der Datenbankname, und -f ist das Verzeichnis mit den generierten Daten): # scripts/mysql/mysql_load_db.sh -d dbt2w10 -f /mnt/data/dbt2-w10 s /var/lib/mysql/mysql.sock
3. Ausführen des Benchmarks
Im letzten Schritt wird der folgende Befehl aus dem Verzeichnis scripts ausgeführt: # run_mysql.sh -c 10 -w 10 -t 300 -n dbt2w10 -u root -o /var/lib/mysql/mysql.sock -e ************************************************************************ * DBT2 test for MySQL started * * * * Results can be found in output/9 directory * ************************************************************************ * * * Test consists of 4 stages: * * * * 1. Start of client to create pool of databases connections * * 2. Start of driver to emulate terminals and transactions generation * * 3. Test * * 4. Processing of results * * * ************************************************************************ DATABASE NAME: DATABASE USER: DATABASE SOCKET: DATABASE CONNECTIONS: TERMINAL THREADS: SCALE FACTOR(WARHOUSES): TERMINALS PER WAREHOUSE: DURATION OF TEST(in sec): SLEEPY in (msec) ZERO DELAYS MODE:
56 |
dbt2w10 root /var/lib/mysql/mysql.sock 10 100 10 10 300 300 1
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Stage 1. Starting up client... Delay for each thread 300 msec. Will sleep for 4 sec to start 10 database connections CLIENT_PID = 12962 Stage 2. Starting up driver... Delay for each thread 300 msec. Will sleep for 34 sec to start 100 terminal threads All threads has spawned successfuly. Stage 3. Starting of the test. Duration of the test 300 sec Stage 4. Processing of results... Shutdown clients. Send TERM signal to 12962. Response Time (s) Transaction % Average : 90th % Total ------------ ----- ----------------- -----Delivery 3.53 2.224 : 3.059 1603 New Order 41.24 0.659 : 1.175 18742 Order Status 3.86 0.684 : 1.228 1756 Payment 39.23 0.644 : 1.161 17827 Stock Level 3.59 0.652 : 1.147 1630
Rollbacks --------0 172 0 0 0
% ----0.00 0.92 0.00 0.00 0.00
3396.95 new-order transactions per minute (NOTPM) 5.5 minute duration 0 total unknown errors 31 second(s) ramping up
Das wichtigste Ergebnis ist diese Zeile kurz vor dem Ende: 3396.95 new-order transactions per minute (NOTPM)
Sie zeigt uns, wie viele Transaktionen pro Minute das System verarbeiten kann – je mehr, desto besser. (Der Begriff »new-order« steht nicht für eine bestimmte Art von Transaktion, sondern bedeutet einfach, dass der Test jemanden simuliert hat, der eine neue Bestellung auf der imaginären E-Commerce-Website abgegeben hat.) Sie können einige der Parameter ändern, um andere Benchmarks zu erzeugen: -c
-e
-t
Die Anzahl der Verbindungen zur Datenbank. Sie können dies ändern, um unterschiedliche Nebenläufigkeitsstufen zu emulieren und festzustellen, wie das System skaliert. Dies aktiviert den Zero-Delay-Modus, was bedeutet, dass es zwischen den Abfragen keine Verzögerungen gibt. Das ist ein Stresstest für die Datenbank, der aber in gewisser Weise unrealistisch ist, da echte Benutzer Zeit zum Nachdenken benötigen, bevor sie neue Abfragen generieren. Die Gesamtdauer des Benchmarks. Wählen Sie diese Zeit sorgfältig, da Ihre Ergebnisse ansonsten keine Bedeutung haben. Eine zu kurze Zeit für den Benchmark-Test einer ein-/ausgabegebundenen Last liefert unkorrekte Ergebnisse, da das System nicht genug Zeit hat, um die Caches zu füllen und eine normale Arbeit zu beginnen. Falls Sie andererseits eine CPU-gebundene Last einem Benchmark unterziehen wollen, dürfen Sie die Zeit nicht zu lang wählen, da sonst die Datenmenge möglicherweise zu stark anwächst und ein-/ausgabegebunden wird. Benchmarking-Beispiele | 57
Die Ergebnisse dieses Benchmarks können Informationen über mehr als nur die Performance liefern. Falls Sie z.B. zu viele Rollbacks sehen, wissen Sie, dass wahrscheinlich etwas nicht stimmt.
Die MySQL-Benchmark-Suite Die MySQL-Benchmark-Suite besteht aus einem Satz von Perl-Benchmarks; Sie brauchen also Perl, um sie auszuführen. Sie finden die Benchmarks im Unterverzeichnis sql-bench/ Ihrer MySQL-Installation. Auf Debian GNU/Linux-Systemen befinden sie sich z.B. in /usr/share/mysql/sql-bench/. Bevor Sie beginnen, lesen Sie die mitgelieferte README-Datei, in der erläutert wird, wie die Suite benutzt wird, und die die Kommandozeilenargumente dokumentiert. Um alle Tests auszuführen, verwenden Sie solche Befehle: $ cd /usr/share/mysql/sql-bench/ sql-bench$ ./run-all-tests --server=mysql --user=root --log --fast Test finished. You can find the result in: output/RUN-mysql_fast-Linux_2.4.18_686_smp_i686
Die Ausführung der Benchmarks kann, je nach verwendeter Hardware und Konfiguration, länger als eine Stunde dauern. Wenn Sie die Kommandozeilenoption --log angeben, dann können Sie den Fortschritt während der Abarbeitung beobachten. Die Tests schreiben ihre Ergebnisse in ein Unterverzeichnis namens output. Jede Datei besteht aus einer Reihe von Zeitangaben für die Operationen in dem jeweiligen Benchmark. Hier ist ein Ausschnitt, der für den Druck nur ein wenig schöner formatiert wurde: sql-bench$ tail -5 output/select-mysql_fast-Linux_2.4.18_686_smp_i686 Time for count_distinct_group_on_key (1000:6000): 34 wallclock secs ( 0.20 usr 0.08 sys + 0.00 cusr 0.00 csys = 0.28 Time for count_distinct_group_on_key_parts (1000:100000): 34 wallclock secs ( 0.57 usr 0.27 sys + 0.00 cusr 0.00 csys = 0.84 Time for count_distinct_group (1000:100000): 34 wallclock secs ( 0.59 usr 0.20 sys + 0.00 cusr 0.00 csys = 0.79 Time for count_distinct_big (100:1000000): 8 wallclock secs ( 4.22 usr 2.20 sys + 0.00 cusr 0.00 csys = 6.42 Total time: 868 wallclock secs (33.24 usr 9.55 sys + 0.00 cusr 0.00 csys = 42.79
CPU) CPU) CPU) CPU) CPU)
Der count_distinct_group_on_key (1000:6000)-Test brauchte z.B. für die Ausführung 34 »Wall-Clock-Sekunden« (also echte Sekunden). Das ist die Gesamtzeit, die der Client für die Ausführung des Tests benötigte. Die anderen Werte (usr, sys, cursr, csys), die sich zu 0,28 aufsummiert haben, geben den Overhead für diesen Test an. So viel Zeit wurde also zum Ausführen des Client-Codes aufgebracht, anstatt auf die Antwort des MySQL-Servers zu warten. Der für uns interessante Wert – also die Zeit, die mit Dingen verbracht wurde, die nicht der Kontrolle des Clients unterliegen – beträgt daher 33,72 Sekunden. Statt der gesamten Suite können Sie die Tests auch einzeln ausführen. Vielleicht wollen Sie sich z.B. auf den Insert-Test konzentrieren. Dieser bietet Ihnen mehr Details als die Zusammenfassung, die von der kompletten Test-Suite erzeugt wird:
58 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
sql-bench$ ./test-insert Testing server 'MySQL 4.0.13 log' at 2003-05-18 11:02:39 Testing the speed of inserting data into 1 table and do some selects on it. The tests are done with a table that has 100000 rows. Generating random keys Creating tables Inserting 100000 rows in order Inserting 100000 rows in reverse order Inserting 100000 rows in random order Time for insert (300000): 42 wallclock secs ( 7.91 usr 5.03 sys + Testing insert of duplicates Time for insert_duplicates (100000): 16 wallclock secs ( 2.28 usr 1.89 sys +
0.00 cusr
0.00 csys = 12.94 CPU)
0.00 cusr
0.00 csys =
4.17 CPU)
Profiling Beim Profiling erfahren Sie, wie stark die einzelnen Teile eines Systems zu den Gesamtkosten beitragen, die zum Herstellen eines Ergebnisses nötig sind. Das einfachste Maß für die Kosten ist die Zeit. Beim Profiling kann aber auch die Anzahl der Funktionsaufrufe, Ein-/Ausgabe-Operationen, Datenbankabfragen usw. gemessen werden. Das Ziel besteht darin, zu verstehen, weshalb ein System so arbeitet, wie es das tut.
Eine Anwendung profilieren Wie beim Benchmarking können Sie ein Profil auf Anwendungsebene oder auf einer einzelnen Komponente, wie dem MySQL-Server, erstellen. Ein Profiling auf Anwendungsebene liefert normalerweise eine bessere Einsicht in die Frage, wie man die Anwendung optimieren kann, und stellt genauere Ergebnisse bereit, da die Ergebnisse die Arbeit umfassen, die von der gesamten Anwendung geleistet wurde. Falls Sie etwa die MySQLAbfragen der Anwendung optimieren wollen, werden Sie möglicherweise versucht sein, die Abfragen auszuführen und zu analysieren. In diesem Fall verpassen Sie allerdings viele wichtige Informationen über die Abfragen, wie etwa Einblicke in die Arbeit, die die Anwendung erledigen muss, wenn die Ergebnisse in den Speicher gelesen und verarbeitet werden.4 Da Webanwendungen einen sehr verbreiteten Anwendungsfall für MySQL darstellen, verwenden wir eine PHP-Website als Beispiel. Typischerweise müssen Sie die Anwendung global profilieren, um festzustellen, wie das System geladen wurde. Vielleicht wol-
4 Wenn Sie einen Engpass untersuchen, dann können Sie das möglicherweise abkürzen und feststellen, wo er liegt, indem Sie einige grundlegende Systemstatistiken unter die Lupe nehmen. Falls die Webserver untätig sind und der MySQL-Server 100 % der CPU nutzt, müssen Sie wahrscheinlich nicht die gesamte Anwendung profilieren, vor allem, wenn es dringend ist. Die ganze Anwendung können Sie sich immer noch anschauen, nachdem Sie die Krise überwunden haben.
Profiling | 59
len Sie aber auch einige interessante Subsysteme isolieren, wie etwa die Suchfunktion. Jedes teure, d.h. aufwendige Subsystem bildet einen guten Kandidaten für ein isoliertes Profiling. Wenn wir optimieren müssen, wie eine PHP-Website MySQL verwendet, dann ziehen wir es vor, Statistiken über die Granularität der Objekte (oder Module) im PHP-Code zu sammeln. Das Ziel besteht darin, zu messen, wie viel der Antwortzeit der einzelnen Seiten von den Datenbankoperationen verbraucht wird. Der Zugriff auf die Datenbank stellt in Anwendungen oft, aber nicht immer, den Flaschenhals dar. Flaschenhälse können auch von folgenden Faktoren verursacht werden: • externen Ressourcen, wie etwa Aufrufen an Webservices oder Suchmaschinen • Operationen, die die Verarbeitung großer Datenmengen in der Anwendung erfordern, wie etwa das Parsen großer XML-Dateien • aufwendigen Operationen in engen Schleifen, wie beim Missbrauch von regulären Ausdrücken • schlecht optimierten Algorithmen, wie etwa naiven Suchalgorithmen zum Suchen von Objekten in Listen Bevor Sie sich MySQL-Abfragen anschauen, müssen Sie die tatsächliche Quelle Ihrer Performance-Probleme ermitteln. Das Profiling von Anwendungen kann Sie bei der Suche nach Engpässen unterstützen und ist ein wichtiger Schritt bei der Überwachung und Verbesserung der Gesamtleistung.
Wie und was gemessen wird Für die meisten Anwendungen ist die Zeit ein gutes Maß für das Profiling, da die meisten Benutzer sich am meisten um die Zeit sorgen. Bei Webanwendungen benutzen wir gern einen Debug-Modus, der für jede Seite die Abfragen zusammen mit ihren Zeiten und der Anzahl der Zeilen anzeigt. Bei langsamen Abfragen können wir dann EXPLAIN ausführen (in späteren Kapiteln finden Sie weitere Informationen über EXPLAIN). Zur genaueren Analyse kombinieren wir diese Daten mit Maßen vom MySQL-Server. Wir empfehlen Ihnen, in jedes neue Projekt, das Sie beginnen, Profiling-Code aufzunehmen. Bei bestehenden Anwendungen mag es schwierig sein, in eine neue Anwendung können Sie dagegen leicht Profiling-Code einbinden. Viele Bibliotheken enthalten Funktionen, die das erleichtern. So besitzen etwa Javas JDBC und PHPs mysqli Funktionen für das Profiling des Datenbankzugriffs. Profiling-Code ist darüber hinaus ausgesprochen sinnvoll zum Verfolgen seltsamer Probleme, die nur im Betrieb auftreten und in der Entwicklung nicht reproduziert werden können. Ihr Profiling-Code sollte wenigstens die folgenden Daten sammeln und protokollieren: • die Gesamtausführungszeit oder »Wall-Clock«-Zeit (Bei Webanwendungen ist das die Gesamtzeit zum Seitenaufbau.)
60 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
• jede ausgeführte Abfrage sowie deren Ausführungszeit • jede Verbindung, die zum MySQL-Server geöffnet wurde • jeden Aufruf einer externen Ressource, wie etwa von Webservices, memcached und extern ausgeführten Skripten • potenziell teure Funktionsaufrufe, wie etwa XML-Analysen • Benutzer- und System-CPU-Zeit Mit diesen Informationen können Sie die Leistung besser überwachen. Sie liefern Ihnen Einblicke in die Performance, die Sie sonst nur schwer erlangen können, wie etwa: • • • •
globale Leistungsprobleme sporadisch erhöhte Antwortzeiten Systemengpässe, die nicht bei MySQL liegen müssen Ausführungszeiten »unsichtbarer« Benutzer, wie etwa der Spider von Suchmaschinen
Bremst Profiling Ihre Server? Ja. Profiling und routinemäßige Überwachung erhöhen den Overhead. Die wichtigen Fragen lauten, wie viel Overhead sie hinzufügen und ob der Vorteil den zusätzlichen Aufwand rechtfertigt. Viele Leute, die leistungsstarke Anwendungen entwerfen und herstellen, glauben, dass man alles messen muss, was messbar ist, und dass man die Kosten des Messens einfach als Teil der Arbeit der Anwendung akzeptieren sollte. Selbst wenn Sie dem nicht zustimmen, ist es doch eine gute Idee, wenigstens eine Art von leichtem Profiling hinzuzufügen, das Sie permanent aktivieren können. Es ist schließlich nicht lustig, auf einen Leistungsengpass zu treffen, den Sie nicht kommen sahen, weil Ihre Systeme nicht darauf ausgelegt sind, täglich auftretende Leistungsänderungen zu erfassen. Wenn Ihnen ein Problem auffällt, können historische Daten von unschätzbarem Wert sein. Mithilfe von Profiling-Daten ist es sogar möglich, Hardware-Anschaffungen zu planen, Ressourcen zuzuweisen und die Last für Spitzenzeiten vorherzusagen. Was meinen wir mit »leichtem« Profiling? Das Aufzeichnen der Zeiten für die SQL-Abfragen sowie der Gesamtausführungszeit des Skripts ist mit Sicherheit nicht sehr teuer. Sie müssen dies auch nicht für jeden Seitenaufruf erledigen. Bei einem anständigen Verkehrsaufkommen können Sie auch einfach ein zufälliges Sample profilieren, indem Sie in der Setup-Datei Ihrer Anwendung das Profiling aktivieren: 99; ?>
Schon das Profiling von 1 % Ihrer Seitenbesuche sollte Ihnen dabei helfen, die schlimmsten Probleme zu finden. Denken Sie daran, die Kosten für Logging, Profiling und Messungen mit in Ihre Betrachtungen aufzunehmen, wenn Sie Benchmarks ausführen, da diese Ihre Benchmark-Ergebnisse verfälschen können.
Profiling | 61
Ein PHP-Profiling-Beispiel Um Ihnen eine Vorstellung davon zu vermitteln, wie einfach und unauffällig das Profiling einer PHP-Webanwendung sein kann, wollen wir uns einige Codebeispiele anschauen. Das erste Beispiel zeigt, wie man die Anwendung anpasst, die Abfragen und weitere Profiling-Daten in einer MySQL-Tabelle aufzeichnet und die Ergebnisse analysiert. Um die Wirkung des Logging zu reduzieren, erfassen wir alle Logging-Informationen im Speicher und schreiben sie erst dann in eine einzelne Zeile, wenn die Ausführung der Seite beendet ist. Das ist besser, als wenn man jede Abfrage einzeln protokolliert, da das Logging jeder einzelnen Abfrage die Anzahl der Abfragen verdoppelt, die an den MySQLServer gesandt werden müssen. Das separate Protokollieren aller Profiling-Daten würde es noch schwerer machen, Engstellen zu analysieren, da Sie selten eine solche Granularität vorfinden, um Probleme in der Anwendung zu erkennen und zu beheben. Wir beginnen mit dem Code, den Sie benötigen, um die Profiling-Informationen zu erfassen. Hier ist ein vereinfachtes Beispiel einer grundlegenden PHP-5-Logging-Klasse namens class.Timer.php, in die bereits Funktionen wie getrusage( ) integriert sind, um den Ressourceneinsatz des Skripts festzustellen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
62 |
aTIMES[$point]['start'] = microtime(TRUE); $this->aTIMES[$point]['start_utime'] = $dat["ru_utime.tv_sec"]*1e6+$dat["ru_utime.tv_usec"]; $this->aTIMES[$point]['start_stime'] = $dat["ru_stime.tv_sec"]*1e6+$dat["ru_stime.tv_usec"]; } function stopTime($point, $comment='') { $dat = getrusage( ); $this->aTIMES[$point]['end'] = microtime(TRUE); $this->aTIMES[$point]['end_utime'] = $dat["ru_utime.tv_sec"] * 1e6 + $dat["ru_utime.tv_usec"]; $this->aTIMES[$point]['end_stime'] = $dat["ru_stime.tv_sec"] * 1e6 + $dat["ru_stime.tv_usec"]; $this->aTIMES[$point]['comment'] .= $comment; $this->aTIMES[$point]['sum'] += $this->aTIMES[$point]['end'] - $this->aTIMES[$point]['start'];
Kapitel 2: Engpässe finden: Benchmarking und Profiling
33 $this->aTIMES[$point]['sum_utime'] += 34 ($this->aTIMES[$point]['end_utime'] 35 $this->aTIMES[$point]['start_utime']) / 1e6; 36 $this->aTIMES[$point]['sum_stime'] += 37 ($this->aTIMES[$point]['end_stime'] 38 $this->aTIMES[$point]['start_stime']) / 1e6; 39 } 40 41 function logdata( ) { 42 43 $query_logger = DBQueryLog::getInstance('DBQueryLog'); 44 $data['utime'] = $this->aTIMES['Page']['sum_utime']; 45 $data['wtime'] = $this->aTIMES['Page']['sum']; 46 $data['stime'] = $this->aTIMES['Page']['sum_stime']; 47 $data['mysql_time'] = $this->aTIMES['MySQL']['sum']; 48 $data['mysql_count_queries'] = $this->aTIMES['MySQL']['cnt']; 49 $data['mysql_queries'] = $this->aTIMES['MySQL']['comment']; 50 $data['sphinx_time'] = $this->aTIMES['Sphinx']['sum']; 51 52 $query_logger->logProfilingData($data); 53 54 } 55 56 // This helper function implements the Singleton pattern 57 function getInstance( ) { 58 static $instance; 59 60 if(!isset($instance)) { 61 $instance = new Timer( ); 62 } 63 64 return($instance); 65 } 66 } 67 ?>
Es ist ganz leicht, die Timer-Klasse in Ihrer Anwendung einzusetzen. Sie müssen potenziell teure (oder anderweitig interessante) Aufrufe einfach mit dem Timer umhüllen. Und so packen Sie z.B. einen Timer um jede MySQL-Abfrage. Die neue PHP-Schnittstelle mysqli erlaubt es Ihnen, die grundlegende Klasse mysqli zu erweitern und die Methode query neu zu deklarieren: 1 2 3 4 5 6 7 8 9 10 11
startTime('MySQL'); $res = parent::query($query, $resultmode); $timer->stopTime('MySQL', "Query: $query\n"); return $res; } } ?>
Profiling | 63
Diese Technik erfordert nur wenige Codeänderungen. Sie können einfach mysqli global auf mysqlx ändern, und Ihre Anwendung beginnt, alle Abfragen zu protokollieren. Mit diesem Ansatz haben Sie die Möglichkeit, den Zugriff auf jede externe Ressource zu messen, wie etwa Abfragen an die Volltextsuchmaschine Sphinx: $timer->startTime('Sphinx'); $this->sphinxres = $this->sphinx_client->Query ( $query, "index" ); $timer->stopTime('Sphinx', "Query: $query\n");
Jetzt wollen wir uns anschauen, wie wir die Daten aufzeichnen, die Sie sammeln. In diesem Beispiel wäre es klug, die Storage-Engine MyISAM oder Archive zu verwenden. Beide eignen sich gut zum Speichern von Logs. Wir benutzen INSERT DELAYED, wenn wir Zeilen zu den Logs hinzufügen, damit das INSERT auf dem Datenbankserver als Hintergrund-Thread ausgeführt wird. Dies bedeutet, dass die Abfrage sofort zurückkehrt und damit die Antwortzeit der Anwendung nicht merklich beeinflusst. (Selbst wenn wir nicht INSERT DELAYED benutzen, geschieht das Einfügen nebenläufig, solange wir es nicht ausdrücklich deaktivieren, so dass externe SELECT-Abfragen nicht das Logging blockieren.) Schließlich richten wir ein datumsbasiertes Partitionierungsschema ein, indem wir jeden Tag eine neue Log-Tabelle anlegen. Hier ist die CREATE TABLE-Anweisung für unsere Logging-Tabelle: CREATE TABLE logs.performance_log_template ( ip INT UNSIGNED NOT NULL, page VARCHAR(255) NOT NULL, utime FLOAT NOT NULL, wtime FLOAT NOT NULL, mysql_time FLOAT NOT NULL, sphinx_time FLOAT NOT NULL, mysql_count_queries INT UNSIGNED NOT NULL, mysql_queries TEXT NOT NULL, stime FLOAT NOT NULL, logged TIMESTAMP NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, user_agent VARCHAR(255) NOT NULL, referer VARCHAR(255) NOT NULL ) ENGINE=ARCHIVE;
Tatsächlich fügen wir niemals Daten in diese Tabelle ein; es handelt sich nur um ein Template für die CREATE TABLE LIKE-Anweisungen, die wir benutzen, um die Tabellen für die Daten der einzelnen Tage anzulegen. Mehr dazu erläutern wir in Kapitel 3. Im Moment sollten Sie nur wissen, dass es ganz gut ist, wenn man den kleinsten Datentyp benutzt, der die vorgesehenen Daten aufnehmen kann. Wir verwenden ein vorzeichenloses Integer, um die IP-Adresse zu speichern. Darüber hinaus verwenden wir eine 255 Zeichen lange Spalte für die Seite und den Referrer. Diese Werte können länger als 255 Zeichen sein, allerdings genügen für unsere Zwecke normalerweise die ersten 255.
64 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Das letzte Stück des Puzzles besteht darin, die Ergebnisse aufzuzeichnen, wenn die Ausführung der Seite beendet ist. Hier ist der dazu notwendige PHP-Code: 1 2 3 4 5 6 7 8 9
startTime('Page'); // ... other code ... // End of the page execution $timer->stopTime('Page'); $timer->logdata( ); ?>
Die Timer-Klasse verwendet die Helper-Klasse DBQueryLog, die dafür verantwortlich ist, dass in die Datenbank protokolliert und jeden Tag eine neue Log-Tabelle erzeugt wird. Hier ist der Code: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
mysqlx->query($query); // Handle "table not found" error - create new table for each new day if ((!$res) && ($this->mysqlx->errno == 1146)) { // 1146 is table not found $res = $this->mysqlx->query( "CREATE TABLE $table_name LIKE logs.performance_log_template"); $res = $this->mysqlx->query($query); } } } ?>
Sobald wir einige Daten aufgezeichnet haben, können wir die Logs analysieren. Das Schöne am Verwenden von MySQL für das Logging ist, dass Ihnen die Flexibilität von SQL für die Analyse zugute kommt; so können Sie einfach Abfragen schreiben, um beliebige Berichte aus den Logs zu erstellen. Um etwa Seiten zu ermitteln, deren Ausführung am ersten Tag des Februar 2007 länger als 10 Sekunden dauerte, schreiben Sie:
Profiling | 65
mysql> SELECT page, wtime, mysql_time -> FROM performance_log_070201 WHERE wtime > 10 LIMIT 7; +-------------------------------------------+---------+------------+ | page | wtime | mysql_time | +-------------------------------------------+---------+------------+ | /page1.php | 50.9295 | 0.000309 | | /page1.php | 32.0893 | 0.000305 | | /page1.php | 40.4209 | 0.000302 | | /page3.php | 11.5834 | 0.000306 | | /login.php | 28.5507 | 28.5257 | | /access.php | 13.0308 | 13.0064 | | /page4.php | 32.0687 | 0.000333 | +-------------------------------------------+---------+------------+
(Normalerweise würden wir in einer solchen Abfrage mehr Daten auswählen. Zur besseren Verdeutlichung haben wir das hier abgekürzt.) Wenn Sie die wtime (Wall-Clock-Zeit) und die Abfragezeit vergleichen, werden Sie feststellen, dass die Ausführungszeit der MySQL-Abfrage bei nur zwei der sieben Seiten für die langsame Antwortzeit verantwortlich war. Da wir die Abfragen bei den ProfilingDaten gespeichert haben, können wir sie uns zur näheren Untersuchung heraussuchen: mysql> SELECT mysql_queries -> FROM performance_log_070201 WHERE mysql_time > 10 LIMIT 1\G *************************** 1. row *************************** mysql_queries: Query: SELECT id, chunk_id FROM domain WHERE domain = 'domain.com' Time: 0.00022602081298828 Query: SELECT server.id sid, ip, user, password, domain_map.id as chunk_id FROM server JOIN domain_map ON (server.id = domain_map.master_id) WHERE domain_map.id = 24 Time: 0.00020599365234375 Query: SELECT id, chunk_id, base_url,title FROM site WHERE id = 13832 Time: 0.00017690658569336 Query: SELECT server.id sid, ip, user, password, site_map.id as chunk_id FROM server JOIN site_map ON (server.id = site_map.master_id) WHERE site_map.id = 64 Time: 0.0001990795135498 Query: SELECT from_site_id, url_from, count(*) cnt FROM link24.link_in24 FORCE INDEX (domain_message) WHERE domain_id=435377 AND message_day IN (...) GROUP BY from_site_id ORDER BY cnt desc LIMIT 10 Time: 6.3193740844727 Query: SELECT revert_domain, domain_id, count(*) cnt FROM art64.link_out64 WHERE from_ site_id=13832 AND message_day IN (...) GROUP BY domain_id ORDER BY cnt desc LIMIT 10 Time: 21.3649559021
Dabei werden zwei problematische Abfragen aufgedeckt, die mit Ausführungszeiten von 6,3 und 21,3 Sekunden einer Optimierung bedürfen. Alle Abfragen auf diese Weise zu protokollieren ist sehr teuer, weshalb wir entweder nur einen Bruchteil der Seiten aufzeichnen oder das Logging nur im Debug-Modus aktivieren. Wie können Sie feststellen, ob es eine Engstelle in dem Teil des Systems gibt, den Sie nicht profilieren? Am einfachsten ist es, wenn Sie sich die »verlorene Zeit« anschauen. Im Allgemeinen ist die Wall-Clock-Zeit (wtime) die Summe aus Benutzerzeit, Systemzeit, Zeit 66 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
für die SQL-Abfrage und jeder anderen Zeit, die Sie messen können, sowie der »verlorenen Zeit«, die Sie nicht messen können. Es gibt natürlich Überschneidungen, wie etwa die CPU-Zeit, die der PHP-Code benötigt, um die SQL-Abfragen zu verarbeiten, diese kann man aber üblicherweise vernachlässigen. Abbildung 2-2 ist eine hypothetische Darstellung für die Aufteilung der Wall-Clock-Zeit. 13%
Benutzerzeit
23%
Systemzeit Abfragen Netzwerk-Ein/Ausgaben
2%
24%
Verlorene Zeit
38%
Abbildung 2-2: Die verlorene Zeit ist die Differenz zwischen der Wall-Clock-Zeit und der Zeit, für die Sie einen Nachweis haben.
Idealerweise sollte die »verlorene Zeit« so gering wie möglich ausfallen. Wenn Sie alles, was Sie gemessen haben, von wtime abziehen, und immer noch viel übrig bleibt, dann erhöht irgendetwas, das Sie nicht messen, die Ausführungszeit Ihres Skripts. Das könnte die Zeit sein, die zum Generieren der Seite erforderlich ist, oder eine irgendwie geartete Wartezeit.5 Es gibt zwei Arten von Wartezeiten: das Warten in der Warteschlange auf CPU-Zeit und das Warten auf Ressourcen. Ein Prozess wartet in der Warteschlange, wenn er bereit zur Ausführung ist, aber alle CPUs ausgelastet sind. Normalerweise ist es nicht möglich festzustellen, wie viel Zeit ein Prozess wartend in der CPU-Warteschlange verbringt, aber das ist im Allgemeinen nicht das Problem. Es ist wahrscheinlicher, das Sie einen externen Ressourcenaufruf ausführen und ihn nicht profilieren. Wenn Ihr Profiling einigermaßen vollständig ist, sollten Sie leicht in der Lage sein, Engpässe zu finden. Es ist eigentlich ganz einfach: Besteht die Ausführungszeit Ihres Skripts zum größten Teil aus CPU-Zeit, müssen Sie wahrscheinlich Ihren PHP-Code optimieren. Manchmal werden Messungen allerdings durch andere verdeckt. So haben Sie z.B. vielleicht eine hohe CPU-Auslastung, weil es einen Bug gibt, der das Cache-System ineffizient macht und Ihre Anwendung zwingt, zu viele SQL-Abfragen auszuführen. Wie dieses Beispiel demonstriert, bildet das Profiling auf Anwendungsebene die flexibelste und nützlichste Technik. Falls es sich ermöglichen lässt, sollten Sie das Profiling in jede Anwendung einbinden, die Sie auf potenzielle Leistungsengpässe hin untersuchen müssen. 5 Angenommen, der Webserver puffert das Ergebnis, so dass die Ausführung des Skripts endet und Sie nicht die Zeit messen, die es in Anspruch nimmt, das Ergebnis an den Client zu senden.
Profiling | 67
Zuletzt sollten wir noch darauf hinweisen, dass wir hier nur grundlegende AnwendungsProfiling-Techniken gezeigt haben. Wir wollen Ihnen in diesem Abschnitt einfach zeigen, wie Sie feststellen, ob MySQL das Problem ist. Vielleicht wollen Sie ja auch den Code der Anwendung selbst profilieren. Falls Sie z.B. beschließen, Ihren PHP-Code zu optimieren, weil er zu viel CPU-Zeit beansprucht, können Sie die CPU-Benutzung mit Werkzeugen wie etwa xdebug, Valgrind und cachegrind profilieren. In manche Sprachen ist bereits eine Unterstützung für das Profiling integriert. So lässt sich z.B. Ruby-Code mit der Kommandozeilenoption -r profilieren. Bei Perl gehen Sie folgendermaßen vor: $ perl -d:DProf <Skriptdatei> $ dprofpp tmon.out
Eine Websuche nach »Profiling <Sprache>« ist ein guter Einstieg.
MySQL-Profiling Wir befassen uns nun ausführlicher mit MySQL-Profiling, weil dies weniger stark von Ihrer speziellen Anwendung abhängt. Manchmal sind sowohl Anwendungs-Profiling als auch Server-Profiling erforderlich. Obwohl das Anwendungs-Profiling Ihnen ein vollständigeres Bild der Leistung des gesamten Systems liefern kann, bekommen Sie beim Profiling von MySQL möglicherweise Informationen, die Ihnen verborgen bleiben, wenn Sie sich die Anwendung als Ganzes anschauen. So erfahren Sie etwa beim Profiling Ihres PHP-Codes nicht, wie viele Zeilen MySQL zum Ausführen der Abfragen untersucht hat. Wie beim Profiling von Anwendungen besteht das Ziel darin, festzustellen, wo MySQL einen Großteil seiner Zeit verbringt. Wir werden uns nicht mit dem Profiling des Quellcodes von MySQL befassen, auch wenn das für eigene MySQL-Installationen manchmal sinnvoll sein kann. Dies könnte das Thema eines weiteren Buches sein. Stattdessen zeigen wir Ihnen einige Techniken, mit deren Hilfe Sie Informationen über die verschiedenen Arbeiten bekommen, die MySQL erledigt, um Abfragen auszuführen, und mit denen Sie diese Informationen analysieren können. Sie können auf der Granularitätsstufe arbeiten, die Ihnen am besten passt: Profilieren Sie den Server als Ganzes, oder untersuchen Sie einzelne Abfragen oder Folgen von Abfragen. Folgende Informationen können Sie unter anderem sammeln: • auf welche Daten MySQL am meisten zugreift • welche Arten von Abfragen MySQL am meisten ausführt • in welchen Zuständen die MySQL-Threads die meiste Zeit verbringen • welche Subsysteme MySQL am häufigsten benutzt, um eine Abfrage auszuführen • auf welche Arten von Daten MySQL während einer Abfrage zugreift • wie viele verschiedene Aktivitäten, wie etwa Indexscans, MySQL durchführt Wir beginnen mit der am breitesten angelegten Stufe, dem Profiling des gesamten Servers, und arbeiten uns dann zu den detaillierteren Bereichen vor. 68 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Logging von Abfragen MySQL besitzt zwei Arten von Abfrage-Logs: das allgemeine Log (General-Log) und das langsame Log (Slow-Log). Beide protokollieren Abfragen, allerdings an entgegengesetzten Enden der Abfrageverarbeitung. Das allgemeine Log zeichnet alle Abfragen auf, die der Server empfängt, so dass er auch Abfragen enthält, die möglicherweise aufgrund von Fehlern gar nicht ausgeführt wurden. Das allgemeine Log zeichnet alle Abfragen auf sowie weitere Ereignisse, wie das Starten und Beenden von Verbindungen auf. Sie können es mit einer einzigen Konfigurationsdirektive aktivieren: log =
Aufgrund seiner Gestaltung enthält das allgemeine Log keine Ausführungszeiten oder andere Informationen, die erst verfügbar sind, wenn die Abfrage abgeschlossen ist. Im Gegensatz dazu enthält das Slow-Log nur Abfragen, die ausgeführt wurden. Es zeichnet speziell Abfragen auf, die für die Ausführung länger als eine festgelegte Zeit benötigt haben. Beide Logs können für das Profiling nützlich sein, allerdings eignet sich vor allem das Slow-Log zum Erkennen problematischer Abfragen. Wir empfehlen Ihnen normalerweise, es zu aktivieren. Das folgende Konfigurationsbeispiel aktiviert das Log, erfasst alle Abfragen, die länger als zwei Sekunden für die Ausführung benötigen, und protokolliert Abfragen, die keine Indizes verwenden. Es zeichnet außerdem langsame administrative Anweisungen auf, wie etwa OPTIMIZE TABLE: log-slow-queries = long_query_time = 2 log-queries-not-using-indexes log-slow-admin-statements
Sie sollten dieses Beispiel anpassen und in Ihre my.cnf-Serverkonfigurationsdatei setzen. Mehr Informationen über die Serverkonfiguration finden Sie in Kapitel 6. Der vorgegebene Wert für long_query_time beträgt 10 Sekunden. Das ist für die meisten Gegebenheiten zu lang, weshalb wir meist zwei Sekunden empfehlen. Es gibt allerdings auch Anwendungsfälle, für die schon eine Sekunde zu lang ist. Im nächsten Abschnitt zeigen wir Ihnen, wie Sie ein feiner aufgelöstes Logging erhalten. In MySQL 5.1 bieten die globalen Systemvariablen slow_query_log und slow_query_ log_file eine Kontrolle zur Laufzeit über das Slow-Query-Log. In MySQL 5.0 dagegen können Sie das Slow-Log nicht ein- oder ausschalten, ohne den MySQL-Server neu zu starten. Die übliche Lösung für MySQL 5.0 besteht in der Variablen long_query_time, die Sie dynamisch ändern können. Der folgende Befehl deaktiviert das Slow-Query-Log zwar nicht, hat aber praktisch die gleiche Wirkung. (Wenn eine Ihrer Abfragen länger als 10.000 Sekunden für die Ausführung braucht, sollten Sie sowieso optimieren!) mysql> SET GLOBAL long_query_time = 10000;
Eine verwandte Konfigurationsvariable, log_queries_not_using_indexes, veranlasst den Server dazu, alle Abfragen in das Slow-Log zu protokollieren, die keine Indizes verwenden, und zwar ungeachtet dessen, wie schnell sie ausgeführt werden. Obwohl das Akti-
Profiling | 69
vieren des Slow-Logs normalerweise nur einen geringen Logging-Overhead verursacht, der sich relativ zu der Zeit verhält, die eine »langsame« Abfrage zur Ausführung braucht, können Abfragen ohne Indizes häufig auftreten und sehr schnell sein (wie etwa Scans sehr kleiner Tabellen). Das Protokollieren dieser Abfragen kann daher den Server verlangsamen und darüber hinaus viel Plattenplatz für das Log belegen. Leider können Sie in MySQL 5.0 das Logging dieser Abfragen nicht mit einer dynamisch einstellbaren Variablen aktivieren oder deaktivieren. Sie müssen die Konfigurationsdatei bearbeiten und dann MySQL neu starten. Eine Möglichkeit, den Aufwand zu reduzieren, ohne einen Neustart zu benötigen, besteht darin, die Log-Datei zu einem symbolischen Link auf /dev/null zu machen, wenn Sie sie deaktivieren wollen (dieser Trick funktioniert auch bei jeder anderen Log-Datei). Sie müssen lediglich FLUSH LOGS ausführen, nachdem Sie die Änderung vorgenommen haben, um sicherzustellen, dass MySQL seinen aktuellen Log-Datei-Deskriptor schließt und den in /dev/null neu öffnet. Im Gegensatz zu MySQL 5.0 erlaubt es Ihnen MySQL 5.1, das Logging zur Laufzeit zu ändern. Sie können außerdem das Log in Tabellen speichern, die Sie mit SQL abfragen können. Das ist eine tolle Verbesserung.
Feinere Kontrolle über das Logging Das Slow-Query-Log in MySQL 5.0 und früheren Versionen unterliegt einigen Einschränkungen, die es für manche Zwecke ungeeignet machen. Ein Problem besteht darin, dass seine Granularität nur in Sekunden besteht und der Minimalwert für long_query_ time in MySQL 5.0 eine Sekunde ist. Für die meisten interaktiven Anwendungen ist das viel zu lang. Falls Sie eine leistungsstarke Webanwendung entwickeln, dann soll wahrscheinlich die ganze Seite in viel weniger als einer Sekunde generiert werden. Außerdem führt die Seite vermutlich viele Abfragen aus, während sie generiert wird. In diesem Zusammenhang würde eine Abfrage, die 150 Millisekunden für die Ausführung braucht, sicherlich als sehr langsam angesehen. Ein weiteres Problem besteht darin, dass Sie nicht alle Abfragen, die der Server ausführt, in das Slow-Log schreiben können (dies betrifft vor allem die Abfragen des SlaveThreads). Das allgemeine Log protokolliert alle Abfragen, allerdings bereits, bevor sie überprüft werden, so dass es keine Informationen über die Ausführungszeit, die LockZeit und die Anzahl der untersuchten Zeilen enthält. Diese Informationen gibt es nur im Slow-Log. Falls Sie schließlich die Option log_queries_not_using_indexes aktivieren, könnte Ihr Slow-Log mit Einträgen für schnelle, effiziente Abfragen überflutet werden, die vollständige Tabellenscans durchführen. Wenn Sie z.B. eine Dropdown-Liste mit Zuständen aus SELECT * FROM STATES generieren, dann wird diese Abfrage protokolliert, da sie ein vollständiger Tabellenscan ist. Beim Profiling zum Zwecke der Leistungsoptimierung suchen Sie nach Abfragen, die die meiste Arbeit auf dem MySQL-Server verursachen. Das bedeutet nicht immer langsame Abfragen, weshalb die Idee des Protokollierens »langsamer« Abfragen nicht unbedingt 70 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
sinnvoll ist. So würde etwa eine 10-Millisekunden-Abfrage, die 1.000-mal pro Sekunde ausgeführt wird, den Server stärker belasten als eine 10-Sekunden-Abfrage, die einmal pro Sekunde läuft. Um ein solches Problem zu erkennen, müssen Sie jede Abfrage protokollieren und die Ergebnisse analysieren. Im Allgemeinen bietet es sich an, sowohl die langsamen Abfragen anzuschauen (auch wenn sie nicht oft ausgeführt werden) als auch die Abfragen, die insgesamt dem Server die meiste Arbeit machen. Auf diese Weise können Sie unterschiedliche Arten von Problemen aufdecken, wie z.B. Abfragen, die schlechte Benutzererfahrungen verursachen. Wir haben auf der Grundlage der Arbeit von Georg Richter einen Patch für den MySQLServer entwickelt, der es Ihnen erlaubt, langsame Abfragezeiten in Mikrosekunden anstatt in Sekunden anzugeben. Er ermöglicht Ihnen außerdem, alle Abfragen in das Slow-Log zu protokollieren, indem Sie long_query_time=0 setzen. Der Patch steht unter http://www.mysqlperformanceblog.com/mysql-patches/ zur Verfügung. Allerdings müssen Sie MySQL möglicherweise selbst kompilieren, wenn Sie ihn benutzen wollen, da er vor MySQL Version 5.1 nicht in der offiziellen MySQL-Distribution enthalten ist. Momentan ändert die Version des Patches, die in MySQL 5.1 enthalten ist, nur die Zeitauflösung. Eine neue Version, die es noch nicht in irgendeiner offiziellen MySQL-Distribution gibt, bringt weitere nützliche Funktionen mit. Der Patch enthält die VerbindungsID der Abfrage sowie Informationen über den Abfrage-Cache, den Join-Typ, temporäre Tabellen und die Sortierung. Außerdem liefert er InnoDB-Statistiken, wie etwa Informationen über das Ein-/Ausgabeverhalten und die Lock-Wartezeiten. Der neue Patch erlaubt es Ihnen, Abfragen zu protokollieren, die vom SQL-Slave-Thread ausgeführt werden, was sehr wichtig ist, wenn es Probleme mit Replikations-Slaves gibt, die hinterherhinken. (In »Übermäßiger Rückstand bei der Replikation« auf Seite 434 finden Sie weitere Informationen darüber, wie Sie den Slaves helfen können, Schritt zu halten.) Außerdem können Sie sich beim Logging auf einige Sessions beschränken. Für die Zwecke des Profilings reicht das normalerweise. Wir glauben, dass dies ein gutes Vorgehen ist. Dieser Patch ist relativ neu. Sie müssen deshalb mit einer gewissen Vorsicht zu Werke gehen, wenn Sie ihn selbst anwenden. Unserer Meinung nach ist er ziemlich sicher, wurde aber natürlich noch nicht so ausgiebig im Betrieb getestet wie der Rest des MySQL-Servers. Falls Sie sich Sorgen wegen der Stabilität des gepatchten Servers machen, müssen Sie die gepatchte Version nicht die ganze Zeit benutzen. Sie können sie einfach für ein paar Stunden starten, um einige Abfragen zu protokollieren, und dann zur ungepatchten Version zurückkehren. Beim Profiling sollte man alle Abfragen mit long_query_time=0 protokollieren. Wenn ein Großteil Ihrer Last von sehr einfachen Abfragen kommt, werden Sie dies sicher erfahren wollen. Das Logging all dieser Abfragen beeinflusst in gewissem Maße die Leistung und erfordert eine Menge Plattenplatz – ein weiterer Grund, weshalb Sie nicht jede einzelne Abfrage protokollieren sollten. Glücklicherweise können Sie long_query_time ändern,
Profiling | 71
ohne den Server neu zu starten, so dass Sie sich leicht für einen bestimmten Zeitraum alle Abfragen beschaffen können; anschließend protokollieren Sie wieder nur sehr langsame Abfragen.
Wie man das Slow-Query-Log liest Hier ist ein Beispiel aus einem Slow-Query-Log: 1 2 3 4
# Time: 030303 0:51:27 # User@Host: root[root] @ localhost [] # Query_time: 25 Lock_time: 0 Rows_sent: 3949 SELECT ...
Rows_examined: 378036
Zeile 1 zeigt, wann die Abfrage protokolliert wurde, und Zeile 2 zeigt, wer sie ausgeführt hat. In Zeile 3 steht, wie viele Sekunden es gedauert hat, die Abfrage auszuführen, wie lange sie auf Tabellen-Locks auf der MySQL-Serverebene (nicht auf der Storage-EngineEbene) gewartet hat, wie viele Zeilen die Abfrage zurückgeliefert hat und wie viele Zeilen untersucht wurden. Diese Zeilen sind auskommentiert, sie werden also nicht ausgeführt, wenn Sie das Log einem MySQL-Client übergeben. Die letzte Zeile enthält die Abfrage. Hier ist ein Ausschnitt von einem MySQL 5.1-Server: 1 2 3 4
# Time: 070518 9:47:00 # User@Host: root[root] @ localhost [] # Query_time: 0.000652 Lock_time: 0.000109 SELECT ...
Rows_sent: 1
Rows_examined: 1
Die Informationen sind größtenteils gleich, allerdings werden die Zeiten in Zeile 3 genauer angegeben. Eine neuere Version des Patchs liefert sogar noch mehr Informationen: 1 2 3 4 5 6 7 8 9 10
# Time: 071031 20:03:16 # User@Host: root[root] @ localhost [] # Thread_id: 4 # Query_time: 0.503016 Lock_time: 0.000048 Rows_sent: 56 Rows_examined: 1113 # QC_Hit: No Full_scan: No Full_join: No Tmp_table: Yes Disk_tmp_table: No # Filesort: Yes Disk_filesort: No Merge_passes: 0 # InnoDB_IO_r_ops: 19 InnoDB_IO_r_bytes: 311296 InnoDB_IO_r_wait: 0.382176 # InnoDB_rec_lock_wait: 0.000000 InnoDB_queue_wait: 0.067538 # InnoDB_pages_distinct: 20 SELECT ...
Zeile 5 zeigt, ob die Abfrage aus dem Abfrage-Cache bedient wurde, ob ein vollständiger Tabellenscan erfolgte, ob ein Join ohne Indizes durchgeführt wurde, ob eine temporäre Tabelle zum Einsatz kam und, falls dies der Fall war, ob die temporäre Tabelle auf der Festplatte erzeugt wurde. Zeile 6 gibt an, ob die Abfrage ein Filesort durchgeführt hat und ob, falls dies geschehen ist, dies auf der Festplatte passierte, und wie viele SortMerge-Durchläufe sie ausgeführt hat. Die Zeilen 7, 8 und 9 tauchen auf, falls die Abfrage InnoDB verwendet hat. Zeile 7 zeigt, wie viele Seitenleseoperationen InnoDB während der Abfrage eingetaktet hat, und gibt die dazugehörenden Werte in Byte an. Der letzte Wert in Zeile 7 zeigt, wie lange InnoDB
72 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
zum Lesen der Daten von der Festplatte gebraucht hat. Auf Zeile 8 sehen Sie, wie lange die Abfrage auf Row-Locks gewartet hat und wie lange sie warten musste, um in den InnoDB-Kernel zu gelangen.6 Zeile 9 zeigt ungefähr, auf wie viele einzelne InnoDB-Seiten die Abfrage zugegriffen hat. Je größer dieser Wert wird, umso ungenauer wird er wahrscheinlich. Eine Anwendung für diese Information besteht darin, die Arbeitsmenge der Abfrage in Seiten abzuschätzen, die angibt, wie der InnoDB-Pufferpool die Daten im Cache speichert. Sie kann Ihnen auch zeigen, wie hilfreich Cluster-Indizes wirklich sind. Wenn die Zeilen der Abfrage gut in Cluster aufgeteilt sind, passen sie in weniger Seiten. Mehr zu diesem Thema finden Sie in »Cluster-Indizes« auf Seite 118. Die Benutzung des Slow-Query-Logs, um Probleme mit langsamen Abfragen zu beheben ist nicht immer ganz einfach. Das Log enthält zwar eine Menge an nützlichen Informationen, eine wichtige Information allerdings fehlt: ein Hinweis darauf, wieso eine Abfrage langsam war. Manchmal ist es offensichtlich. Wenn das Log angibt, dass 12.000.000 Zeilen untersucht wurden und 1.200.000 an den Client geschickt wurden, dann wissen Sie, wieso die Ausführung so lange dauerte – es war einfach eine riesige Abfrage! Es ist allerdings nur selten so deutlich. Passen Sie auf, dass Sie nicht zu viel in das Slow-Query-Log hineinlesen. Wenn Sie die gleiche Abfrage oft in dem Log sehen, kann das durchaus bedeuten, dass sie langsam ist und optimiert werden muss. Aber nur, weil eine Abfrage im Log auftaucht, muss sie nicht unbedingt schlecht oder gar langsam sein. Vielleicht finden Sie eine langsame Abfrage, führen sie selbst aus und stellen fest, dass sie im Bruchteil einer Sekunde erledigt ist. Das Auftauchen im Log bedeutet einfach, dass die Abfrage damals lange brauchte, das muss jetzt oder in Zukunft nicht so sein. Es gibt viele Gründe, wieso eine Abfrage manchmal langsam und manchmal schnell ist: • Eine Tabelle könnte gesperrt gewesen sein, weshalb die Abfrage warten musste. Der Lock_time-Wert gibt an, wie lange die Abfrage darauf warten musste, dass Sperren freigegeben wurden. • Die Daten oder Indizes stehen noch nicht im Cache zur Verfügung. Das kommt häufig vor, wenn MySQL erst gestartet wurde oder nicht gut eingestellt ist. • Es läuft ein nächtlicher Backup-Prozess, der alle Festplattenzugriffe verlangsamt. • Der Server führt gleichzeitig noch weitere Abfragen aus, so dass diese Abfrage gebremst wird. Sie sollten also das Slow-Query-Log nur als Teil der Aufzeichnung dessen betrachten, was passiert ist. Verwenden Sie es etwa, um eine Liste der möglichen Verdächtigen herzustellen, untersuchen Sie die einzelnen Kandidaten dann aber genauer.
6 Siehe »InnoDB bei Nebenläufigkeit anpassen« auf Seite 321 für weitere Informationen über den InnoDB-Kernel.
Profiling | 73
Die Patches für das Slow-Query-Log sollen Ihnen dabei helfen zu verstehen, weshalb eine Abfrage langsam ist. Speziell im Fall von InnoDB können die InnoDB-Statistiken Sie unterstützen: Sie können sehen, ob die Abfrage auf eine Ein-/Ausgabe von der Platte gewartet hat, ob sie viel Zeit mit Warten in der InnoDB-Warteschlange verbringen musste usw.
Log-Analysewerkzeuge Nachdem Sie einige Abfragen protokolliert haben, müssen Sie die Ergebnisse analysieren. Die allgemeine Strategie sieht so aus: Suchen Sie die Abfragen, die den Server am meisten beeinflussen, überprüfen Sie deren Ausführungspläne mit EXPLAIN, und passen Sie sie gegebenenfalls an. Wiederholen Sie die Analyse nach dem Anpassen, da Ihre Änderungen andere Abfragen beeinflussen können. Oft sind z.B. Indizes für SELECT-Abfragen hilfreich, verlangsamen aber INSERT- und UPDATE-Abfragen. Sie sollten im Allgemeinen in den Logs auf die folgenden drei Dinge achten: Lange Abfragen Routinemäßig ablaufende Batch-Jobs generieren lange Abfragen. Ihre normalen Abfragen dagegen sollten nicht sehr lange dauern. Einflussreiche Abfragen Suchen Sie die Abfragen, die den Großteil der Ausführungszeit des Servers ausmachen. Denken Sie daran, dass kurze Abfragen, die oft ausgeführt werden, ebenfalls viel Zeit in Anspruch nehmen können. Neue Abfragen Suchen Sie nach Abfragen, die gestern noch nicht unter den Top 100 waren, es heute aber sind. Dabei kann es sich um neue Abfragen handeln, aber auch um solche, die eigentlich schnell ausgeführt werden, jetzt aber von einer unterschiedlichen Indizierung oder einer anderen Änderung negativ beeinflusst werden. Wenn Ihr Slow-Query-Log relativ klein ist, können Sie die Analyse leicht von Hand erledigen, falls Sie jedoch alle Abfragen protokollieren (wie wir es empfehlen), brauchen Sie ein Werkzeug. Hier ist eine Auswahl: mysqldumpslow MySQL liefert mysqldumpslow mit dem MySQL-Server. Es handelt sich hierbei um ein Perl-Skript, das das Slow-Query-Log zusammenfassen kann und Ihnen zeigt, wie oft die einzelnen Abfragen jeweils im Log auftauchen. Auf diese Weise verschwenden Sie keine Zeit damit, eine langsame 30-Sekunden-Abfrage zu optimieren, die nur einmal am Tag ausgeführt wird, wenn es viele andere kürzere langsame Abfragen gibt, die Tausende Male am Tag laufen. Der Vorteil von mysqldumpslow besteht darin, dass es bereits installiert ist. Allerdings ist es etwas weniger flexibel als einige der anderen Werkzeuge. Außerdem ist es schlecht dokumentiert und versteht Logs von Servern nicht, die mit dem oben beschriebenen Mikrosekunden-Patch ausgestattet wurden.
74 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
mysql_slow_log_filter Dieses Werkzeug, das unter http://www.mysqlperformanceblog.com/files/utils/ mysql_slow_log_filter zur Verfügung steht, versteht das Mikrosekunden-Log-Format. Sie können es einsetzen, um Abfragen zu extrahieren, die länger sind als ein vorgegebener Schwellenwert, oder die mehr als eine bestimmte Anzahl von Zeilen untersuchen. Es eignet sich großartig zum »Nachziehen« Ihrer Log-Datei, wenn Sie den Mikrosekunden-Patch verwenden, der möglicherweise dafür sorgt, dass Ihr Log zu schnell anwächst, so dass Sie ihm ohne eine Filterung gar nicht mehr folgen können. Sie können das Programm eine Weile mit hohen Schwellenwerten ausführen, dann eine Optimierung vornehmen, bis die schlimmsten Missetäter beseitigt sind, anschließend die Parameter ändern, um weitere Abfragen zu erfassen, und das Einstellen fortsetzen. Hier ist ein Befehl, der Abfragen zeigt, die entweder länger als eine halbe Sekunde laufen oder mehr als 1.000 Zeilen untersuchen: $ tail -f mysql-slow.log | mysql_slow_log_filter -T 0.5 -R 1000
mysql_slow_log_parser Mit diesem Werkzeug, das Sie unter http://www.mysqlperformanceblog.com/files/ utils/mysql_slow_log_parser finden, können Sie das Mikrosekunden-Slow-Log zusammenfassen. Darüber hinaus zeigt es Minimal- und Maximalwerte für die Ausführungszeit sowie die Anzahl der analysierten Zeilen an, gibt die »kanonisierte« Abfrage aus und liefert einen tatsächlichen Auszug, den Sie mit EXPLAIN untersuchen können. Hier ist ein Auszug aus seiner Ausgabe: ### 3579 Queries ### Total time: 3.348823, Average time: 0.000935686784017883 ### Taking 0.000269 to 0.130820 seconds to complete ### Rows analyzed 1 - 1 SELECT id FROM forum WHERE id=XXX; SELECT id FROM forum WHERE id=12345;
mysqlsla Der MySQL Statement Log Analyzer, verfügbar unter http://hackmysql.com/ mysqlsla, kann nicht nur das Slow-Log, sondern auch das allgemeine Log sowie »raw« Logs analysieren, die begrenzte SQL-Anweisungen enthalten. Genau wie mysql_ slow_log_parser kann es kanonisieren und zusammenfassen, außerdem kann es Abfragen mit EXPLAIN untersuchen (es schreibt viele Nicht-SELECT-Anweisungen für EXPLAIN um) und ausgefeilte Berichte erzeugen. Mit den Statistiken für das Slow-Log lässt sich vorhersagen, um wie viel Sie den Ressourcenverbrauch des Servers reduzieren können. Nehmen Sie an, Sie erfassen eine Stunde lang (3.600 Sekunden) Abfragen und stellen fest, dass die gesamte kombinierte Ausführungszeit für alle Abfragen im Log 10.000 Sekunden beträgt. (Die Gesamtzeit ist größer als die tatsächlich verstrichene, d.h. die Wall-Clock-Zeit, weil die Abfragen parallel ausgeführt werden.) Wenn die Log-Analyse Ihnen zeigt, dass die schlechteste Abfrage für 3.000 Sekunden der Ausführungszeit verantwortlich ist, dann wissen Sie, dass diese Abfrage 30 % der Last erzeugt. Jetzt wissen Sie auch, um wie viel Sie den Ressourcenverbrauch des Servers senken können, indem Sie diese Abfrage optimieren. Profiling | 75
Profiling eines MySQL-Servers Eine der besten Methoden, um einen Server zu profilieren, d.h., um festzustellen, was er die meiste Zeit tut, ist mit SHOW STATUS. SHOW STATUS liefert viele Statusinformationen zurück. Wir erwähnen hier nur einige der Variablen in seiner Ausgabe. SHOW STATUS zeigt einige seltsame Verhaltensweisen, die in MySQL 5.0 und neueren Versionen schlechte Ergebnisse liefern können. In Kapitel 13 finden Sie weitere Details über das Verhalten von SHOW STATUS und seine Pferdefüße.
Um festzustellen, wie der Server in (ungefährer) Echtzeit arbeitet, erfassen Sie regelmäßig SHOW STATUS und vergleichen das Ergebnis mit der vorhergehenden Probe. Verwenden Sie dazu folgenden Befehl: mysqladmin extended -r -i 10
Einige der Variablen sind keine streng ansteigenden Zähler, weshalb Ihnen auch seltsame Ausgaben unterkommen können wie eine negative Zahl für Threads_running. Darüber müssen Sie sich keine Sorgen machen. Das bedeutet lediglich, dass der Zähler seit der letzten Erfassung verkleinert wurde. Die Ausgabe ist sehr ausführlich, lassen Sie die Ergebnisse deshalb am besten durch grep laufen, um Variablen herauszufiltern, die Sie nicht beobachten wollen. Alternativ können Sie innotop oder ein anderes der in Kapitel 14 erwähnten Werkzeuge einsetzen, um die Ergebnisse zu untersuchen. Hier sind einige der Variablen, die Sie sinnvollerweise genauer unter die Lupe nehmen sollten: Bytes_received und Bytes_sent
Der Verkehr zum und vom Server Com_*
Die Befehle, die der Server ausführt Created_*
Temporäre Tabellen und Dateien, die während der Ausführung der Abfrage erzeugt wurden Handler_*
Operationen der Storage-Engine Select_*
Verschiedene Arten von Join-Ausführungsplänen Sort_*
Verschiedene Arten von Sortierinformationen Mit diesem Ansatz können Sie interne Operationen von MySQL überwachen, wie etwa die Anzahl der Schlüsselzugriffe, Schlüssellesevorgänge von der Festplatte für MyISAM, die Rate des Datenzugriffs, Datenlesevorgänge von der Festplatte für InnoDB usw. Es hilft Ihnen festzustellen, wo sich in Ihrem System die echten oder potenziellen Engstellen
76 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
befinden, ohne dass Sie eine einzige Abfrage anschauen müssen. Es gibt auch Werkzeuge zur Analyse von SHOW STATUS, z.B. mysqlreport, die Ihnen einen Eindruck vom Gesamtzustand des Servers vermitteln. Wir werden die Bedeutung der Statusvariablen hier nicht allzu detailliert betrachten, erläutern sie aber, wenn wir sie in Beispielen einsetzen. Machen Sie sich also keine Sorgen, wenn Sie nicht wissen, was sie alle bedeuten. Eine andere gute Methode, um einen MySQL-Server zu profilieren, ist mit SHOW PROCESSLIST. Damit sehen Sie nicht nur, welche Arten von Abfragen ausgeführt werden, sondern auch, welchen Zustand Ihre Verbindungen aufweisen. Manche Dinge, wie z.B. eine große Anzahl von Verbindungen im Zustand Locked, deuten ganz offensichtlich auf Engpässe hin. Wie bei SHOW STATUS ist die Ausgabe von SHOW PROCESSLIST so umfangreich, dass es normalerweise bequemer ist, ein Werkzeug wie innotop einzusetzen, als sie manuell zu untersuchen.
Profiling von Abfragen mit SHOW STATUS Die Kombination aus FLUSH STATUS und SHOW SESSION STATUS eignet sich sehr gut, um festzustellen, was passiert, während MySQL eine Abfrage oder eine ganze Reihe von Abfragen ausführt. Damit können Sie die Abfragen optimieren. Schauen wir uns an einem Beispiel an, wie Sie die Aktionen einer Abfrage interpretieren. Führen Sie zuerst FLUSH STATUS aus, um die Statusvariablen der Sitzung auf null zurückzusetzen und sehen zu können, wie viel Arbeit MySQL erledigen muss, um die Abfrage durchzuführen: mysql> FLUSH STATUS;
Nun starten Sie die Abfrage. Wir setzen SQL_NO_CACHE hinzu, damit MySQL die Abfrage nicht einfach aus dem Cache beantwortet: mysql> SELECT SQL_NO_CACHE film_actor.actor_id, COUNT(*) -> FROM sakila.film_actor -> INNER JOIN sakila.actor USING(actor_id) -> GROUP BY film_actor.actor_id -> ORDER BY COUNT(*) DESC; ... 200 rows in set (0.18 sec)
Die Abfrage lieferte 200 Zeilen zurück, aber was hat sie wirklich getan? SHOW STATUS kann Ihnen einen Einblick liefern. Schauen wir uns zuerst an, welche Art von Abfrageplan der Server gewählt hat: mysql> SHOW SESSION STATUS LIKE 'Select%'; +------------------------+-------+ | Variable_name | Value | +------------------------+-------+ | Select_full_join | 0 | | Select_full_range_join | 0 | | Select_range | 0 |
Profiling | 77
| Select_range_check | 0 | | Select_scan | 2 | +------------------------+-------+
Es sieht so aus, als hätte MySQL einen vollständigen Tabellenscan durchgeführt. (Es sieht sogar so aus, als wären es zwei gewesen, aber das ist nur ein Überbleibsel von SHOW STATUS; wir kommen später darauf zurück.) Hätte sich die Abfrage auf mehr als eine Tabelle bezogen, dann wären möglicherweise mehrere Variablen größer als null gewesen. Falls MySQL z.B. einen Bereichsscan durchgeführt hätte, um passende Zeilen in der nachfolgenden Tabelle zu finden, dann hätte Select_full_range_join ebenfalls einen Wert erhalten. Wir können noch mehr Erkenntnisse gewinnen, wenn wir uns die systemnahen Storage-Engine-Operationen anschauen, die die Abfrage durchgeführt hat: mysql> SHOW SESSION STATUS LIKE 'Handler%'; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | Handler_commit | 0 | | Handler_delete | 0 | | Handler_discover | 0 | | Handler_prepare | 0 | | Handler_read_first | 1 | | Handler_read_key | 5665 | | Handler_read_next | 5662 | | Handler_read_prev | 0 | | Handler_read_rnd | 200 | | Handler_read_rnd_next | 207 | | Handler_rollback | 0 | | Handler_savepoint | 0 | | Handler_savepoint_rollback | 0 | | Handler_update | 5262 | | Handler_write | 219 | +----------------------------+-------+
Die hohen Werte der »read«-Operationen deuten darauf hin, dass MySQL mehr als eine Tabelle scannen musste, um diese Abfrage zu beantworten. Würde MySQL nur eine Tabelle mit einem vollständigen Tabellenscan lesen, dann würden wir hohe Werte für Handler_read_rnd_next sehen, und Handler_read_rnd wäre null. In diesem Fall weisen die Werte, die nicht null sind, darauf hin, dass MySQL eine temporäre Tabelle benutzt haben muss, um die verschiedenen GROUP BY- und ORDER BY-Klauseln zu bedienen. Aus diesem Grund sind die Werte für Handler_write und Handler_update nicht null: MySQL hat vermutlich in die temporäre Tabelle geschrieben, diese zum Sortieren gescannt und sie dann noch einmal gescannt, um die Ergebnisse sortiert auszugeben. Schauen wir uns an, was MySQL getan hat, um die Ergebnisse zu sortieren: mysql> SHOW SESSION STATUS LIKE 'Sort%'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | Sort_merge_passes | 0 | | Sort_range | 0 |
78 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
| Sort_rows | 200 | | Sort_scan | 1 | +-------------------+-------+
Wie vorausgesehen, hat MySQL die Zeilen sortiert, indem es eine temporäre Tabelle gescannt hat, die alle Zeilen in der Ausgabe enthielt. Wäre der Wert größer als 200 Zeilen gewesen, dann hätte es unserer Vermutung nach die Sortierung an einer anderen Stelle während der Ausführung der Abfrage vorgenommen. Wir können außerdem erkennen, wie viele temporäre Tabellen MySQL für die Abfrage erzeugt hat: mysql> SHOW SESSION STATUS LIKE 'Created%'; +-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | Created_tmp_disk_tables | 0 | | Created_tmp_files | 0 | | Created_tmp_tables | 5 | +-------------------------+-------+
Es ist schön zu sehen, dass die Abfrage nicht die Festplatte für die temporären Tabellen benutzen musste, da dies sehr langsam ist. Ein bisschen seltsam ist es aber schon; sicher hat MySQL für diese eine Abfrage nicht fünf temporäre Tabellen angelegt, oder? Um genau zu sein, benötigt die Abfrage nur eine temporäre Tabelle. Es handelt sich hier um das gleiche Überbleibsel, das uns schon einmal aufgefallen ist. Was ist passiert? Wir führen das Beispiel auf MySQL 5.0.45 aus. In MySQL 5.0 wählt SHOW STATUS tatsächlich Daten aus den INFORMATION_SCHEMA-Tabellen aus, wodurch »Kosten für die Beobachtung« eingeführt werden.7 Das verfälscht die Ergebnisse ein wenig, was Sie erkennen, wenn Sie SHOW STATUS noch einmal ausführen: mysql> SHOW SESSION STATUS LIKE 'Created%'; +-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | Created_tmp_disk_tables | 0 | | Created_tmp_files | 0 | | Created_tmp_tables | 6 | +-------------------------+-------+
Sehen Sie, der Wert hat sich wieder erhöht. Der Handler und die anderen Variablen sind gleichermaßen betroffen. Ihre Ergebnisse werden, je nach Ihrer MySQL-Version, etwas anders aussehen. Sie können den gleichen Vorgang – FLUSH STATUS, Ausführen der Abfrage und Ausführen von SHOW STATUS – auch in MySQL 4.1 und älteren Versionen durchführen. Sie brauchen nur einen untätigen Server, da ältere Versionen über globale Zähler verfügen, die von anderen Prozessen geändert werden können.
7 Das Problem der »Kosten für die Beobachtung« wurde in MySQL 5.1 für SHOW SESSION STATUS behoben.
Profiling | 79
Am besten kompensieren Sie die »Kosten für die Beobachtung«, die durch das Ausführen von SHOW STATUS verursacht werden, indem Sie es zweimal ausführen und das zweite Ergebnis vom ersten abziehen. Sie können dies dann von SHOW STATUS abziehen, um die wahren Kosten der Abfrage zu ermitteln. Um exakte Ergebnisse zu erhalten, müssen Sie den Geltungsbereich der Variablen kennen, damit Sie wissen, welche Variablen Kosten für die Beobachtung aufwerfen; manche gelten nur sitzungsweit, andere sind global. Sie können diesen komplizierten Vorgang mit mk-query-profiler automatisieren. Integrieren Sie diese Art des automatischen Profilings in den Datenbankverbindungscode Ihrer Anwendung. Wenn das Profiling aktiviert ist, kann der Verbindungscode den Status vor jeder Abfrage zurücksetzen und die Unterschiede hinterher protokollieren. Alternativ können Sie auf Seitenbasis profilieren und nicht auf Abfragebasis. Beide Strategien eignen sich, um Ihnen zu zeigen, wie viel Arbeit MySQL während der Abfragen erledigt hat.
SHOW PROFILE SHOW PROFILE ist ein Patch, den Jeremy Cole der Community-Version von MySQL hinzugefügt hat. Es gibt ihn dort seit MySQL 5.0.37.8 Das Profiling ist standardmäßig deaktiviert, kann aber auf Sitzungsebene aktiviert werden. Nach dem Aktivieren veranlasst es den MySQL-Server, Informationen über die Ressourcen zu sammeln, die der Server verwendet, um eine Abfrage auszuführen. Setzen Sie dazu die Variable profiling auf 1: mysql> SET profiling = 1;
Führen Sie nun eine Abfrage aus: mysql> SELECT COUNT(DISTINCT actor.first_name) AS cnt_name, COUNT(*) AS cnt -> FROM sakila.film_actor -> INNER JOIN sakila.actor USING(actor_id) -> GROUP BY sakila.film_actor.film_id -> ORDER BY cnt_name DESC; ... 997 rows in set (0.03 sec)
Die Profiling-Daten dieser Abfrage wurden in der Sitzung gespeichert. Um Abfragen zu sehen, die profiliert wurden, verwenden Sie SHOW PROFILES: mysql> SHOW PROFILES\G *************************** 1. row *************************** Query_ID: 1 Duration: 0.02596900 Query: SELECT COUNT(DISTINCT actor.first_name) AS cnt_name,...
Sie gelangen mit der Anweisung SHOW PROFILE an die gespeicherten Profildaten. Wenn Sie diese Anweisung ohne Argument aufrufen, zeigt sie die Statuswerte und Zeitdauer der neuesten Anweisung: 8 Momentan ist SHOW PROFILE noch nicht in den Enterprise-Versionen von MySQL enthalten, selbst in denen nicht, die neuer sind als 5.0.37.
80 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
mysql> SHOW PROFILE; +------------------------+-----------+ | Status | Duration | +------------------------+-----------+ | (initialization) | 0.000005 | | Opening tables | 0.000033 | | System lock | 0.000037 | | Table lock | 0.000024 | | init | 0.000079 | | optimizing | 0.000024 | | statistics | 0.000079 | | preparing | 0.00003 | | Creating tmp table | 0.000124 | | executing | 0.000008 | | Copying to tmp table | 0.010048 | | Creating sort index | 0.004769 | | Copying to group table | 0.0084880 | | Sorting result | 0.001136 | | Sending data | 0.000925 | | end | 0.00001 | | removing tmp table | 0.00004 | | end | 0.000005 | | removing tmp table | 0.00001 | | end | 0.000011 | | query end | 0.00001 | | freeing items | 0.000025 | | removing tmp table | 0.00001 | | freeing items | 0.000016 | | closing tables | 0.000017 | | logging slow query | 0.000006 | +------------------------+-----------+
Jede Zeile repräsentiert eine Zustandsänderung für den Prozess und gibt an, wie lange er in diesem Zustand war. Die Status-Spalte korrespondiert mit der State-Spalte in der Ausgabe von SHOW FULL PROCESSLIST. Die Werte stammen aus der Variablen thd->proc_info, Sie schauen also auf Werte, die direkt aus dem Inneren von MySQL kommen. Sie sind im MySQL-Handbuch dokumentiert, obwohl die meisten intuitiv benannt sind und nicht so schwer zu verstehen sein sollten. Sie können angeben, dass eine Abfrage profiliert werden soll, indem Sie ihre Query_ID aus der Ausgabe von SHOW PROFILES übergeben, und Sie können zusätzliche Ausgabespalten festlegen. Um z.B. die Benutzer- und System-CPU-Zeiten für die vorangegangene Abfrage zu sehen, benutzen Sie den folgenden Befehl: mysql> SHOW PROFILE CPU FOR QUERY 1;
SHOW PROFILE liefert viele Einblicke in die Arbeit, die der Server verrichtet, um eine
Abfrage auszuführen, und kann Ihnen helfen zu verstehen, womit Ihre Abfragen wirklich ihre Zeit verbringen. Einige seiner Beschränkungen bestehen in seinen nichtimplementierten Funktionen, der Unfähigkeit, die Abfragen einer anderen Verbindung zu sehen und zu profilieren, sowie in dem Overhead, der durch das Profiling verursacht wird.
Profiling | 81
Andere Methoden, um MySQL zu profilieren Wir haben Ihnen in diesem Kapitel genügend Details gezeigt, um zu verdeutlichen, wie Sie anhand der internen Statusinformationen von MySQL feststellen können, was im Server passiert. Sie können aber auch mit verschiedenen anderen Statusausgaben von MySQL ein Profiling vornehmen. Weitere nützliche Befehle sind SHOW INNODB STATUS und SHOW MUTEX STATUS. Wir betrachten diese und andere Befehle in Kapitel 13 näher.
Wann können Sie keinen Profiling-Code hinzufügen? Manchmal ist es nicht möglich, Profiling-Code hinzuzufügen oder den Server zu patchen oder gar die Konfiguration des Servers zu ändern. Normalerweise gibt es aber eine Möglichkeit, wenigstens eine Art des Profiling vorzunehmen. Versuchen Sie Folgendes: • Passen Sie Ihre Webserver-Logs so an, dass sie die Wall-Clock-Zeit und die CPUZeit für die einzelnen Anforderungen aufzeichnen. • Verwenden Sie Paket-Sniffer, um Abfragen zu erfassen und zu messen (einschließlich der Netzwerk-Latenz), wenn sie das Netzwerk durchqueren. Frei verfügbare Sniffer sind u.a. mysqlsniffer (http://hackmysql.com/mysqlsniffer) und tcpdump; unter http://forge.mysql.com/snippets/view.php?id=15 finden Sie ein Beispiel für den Einsatz von tcpdump. • Verwenden Sie einen Proxy, wie etwa MySQL Proxy, um Abfragen zu erfassen und zu messen.
Profiling des Betriebssystems Oft ist es ganz sinnvoll, einen Blick in die Statistiken des Betriebssystems zu werfen und zu versuchen herauszufinden, was Betriebssystem und Hardware tun. Das kann nicht nur beim Profiling einer Anwendung helfen, sondern auch bei der Fehlersuche. Dieser Abschnitt ist absichtlich auf Unix-artige Betriebssysteme zugeschnitten, da wir damit am häufigsten arbeiten. Sie können diese Techniken aber auch auf andere Betriebssysteme anwenden, vorausgesetzt, diese liefern Statistiken. Wir benutzen meist die Werkzeuge vmstat, iostat, mpstat und strace. Sie bieten jeweils eine etwas andere Sicht auf eine Kombination aus Prozess, CPU, Speicher und Ein-/Ausgabeaktivität. Diese Werkzeuge stehen auf den meisten Unix-artigen Betriebssystemen zur Verfügung. Wir zeigen im Laufe des Buches Beispiele für ihre Anwendung, vor allem am Ende von Kapitel 7. Seien Sie vorsichtig mit strace unter GNU/Linux auf Produktionsservern. Manchmal scheint es Probleme mit Multi-Thread-Prozessen zu geben, und wir haben damit schon Server zum Absturz gebracht.
82 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Fehlerbehebung bei MySQL-Verbindungen und -Prozessen Eine Gruppe von Werkzeugen, die wir nirgendwo ausführlich betrachten, sind Werkzeuge zum Untersuchen der Netzwerkaktivität und für die grundlegende Fehlerbehebung. Als Beispiel dafür, wie man das erledigen kann, zeigen wir Ihnen, wie Sie eine MySQL-Verbindung zurück zu ihrem Ursprung auf einem anderen Server verfolgen können. Beginnen Sie mit der Ausgabe von SHOW PROCESSLIST in MySQL, und schauen Sie sich die Host-Spalte in einem der Prozesse an. Wir benutzen das folgende Beispiel: *************************** 21. row *************************** Id: 91296 User: web Host: sargon.cluster3:37636 db: main Command: Sleep Time: 10 State: Info: NULL
Die Host-Spalte zeigt, woher die Verbindung stammte und, das ist genauso wichtig, von welchem TCP-Port sie kam. Sie können anhand dieser Information feststellen, welcher Prozess die Verbindung geöffnet hat. Falls Sie Root-Zugang zu sargon haben, können Sie netstat und die Portnummer benutzen, um den Prozess zu ermitteln, der die Verbindung geöffnet hat: root@sargon# netstat -ntp | grep :37636 tcp 0 0 192.168.0.12:37636 192.168.0.21:3306 ESTABLISHED 16072/apache2
Die Prozessnummer und den Namen finden Sie im letzten Feld der Ausgabe: Prozess 16072 startete diese Verbindung, er kam von Apache. Sobald Sie die Prozess-ID kennen, haben Sie die Möglichkeit, viele weitere Dinge über diesen Prozess herauszufinden, wie etwa, welche weiteren Netzwerkverbindungen der Prozess besitzt: root@sargon# netstat -ntp | grep 16072/apache2 tcp 0 0 192.168.0.12:37636 192.168.0.21:3306 ESTABLISHED 16072/apache2 tcp 0 0 192.168.0.12:37635 192.168.0.21:3306 ESTABLISHED 16072/apache2 tcp 0 0 192.168.0.12:57917 192.168.0.3:389 ESTABLISHED 16072/apache2
Es sieht so aus, als hätte dieser Apache-Prozess zwei MySQL-Verbindungen (Port 3306) offen sowie etwas zu Port 389 auf einer anderen Maschine. Was ist Port 389? Es gibt keine Garantie, aber viele Programme benutzen standardisierte Portnummern, wie z.B. MySQL mit dem Standardport 3306. Eine Liste befindet sich oft in /etc/services. Schauen wir also einmal nach, was sie besagt: root@sargon# grep 389 /etc/services ldap 389/tcp # Lightweight Directory Access Protocol ldap 389/udp
Wir wissen zufällig, dass dieser Server LDAP-Authentifizierung einsetzt, LDAP scheint also sinnvoll zu sein. Was können wir noch über Prozess 16072 herausfinden? Mit ps ist
Profiling des Betriebssystems | 83
es ziemlich einfach festzustellen, was der Prozess tut. Das hübsche Muster, das wir grep hinwerfen, hilft uns, die erste Zeile der Ausgabe anzuschauen, die uns die Spaltenüberschriften zeigt: root@sargon# ps -eaf | grep 'UID\|16072' UID PID PPID C STIME TTY TIME CMD apache 16072 22165 0 09:20 ? 00:00:00 /usr/sbin/apache2 -D DEFAULT_VHOST...
Mit dieser Information können Sie potenziell auch andere Probleme finden. Seien Sie z.B. nicht überrascht, wenn Sie feststellen, dass ein Dienst wie LDAP oder NFS Probleme für Apache bereitet und die Seitengenerierung verlangsamt. Mit dem lsof-Befehl können Sie sich auch eine Liste der offenen Dateien eines Prozesses anzeigen lassen. Das liefert Ihnen alle möglichen Informationen, da unter Unix alles eine Datei ist. Wir verzichten hier auf die Ausgabe, weil sie sehr ausführlich ist, aber Sie können lsof | grep 16072 ausführen, um die offenen Dateien des Prozesses zu ermitteln. Mit lsof lassen sich auch Netzwerkverbindungen feststellen, wenn netstat nicht verfügbar ist. So verwendet etwa der folgende Befehl lsof, um ungefähr die gleichen Informationen auszugeben wie netstat. Für den Druck haben wir die Ausgabe ein wenig umformatiert: root@sargon# lsof -i -P | grep 16072 apache2 16072 apache 3u IPv4 25899404 TCP *:80 (LISTEN) apache2 16072 apache 15u IPv4 33841089 TCP sargon.cluster3:37636-> hammurabi.cluster3:3306 (ESTABLISHED) apache2 16072 apache 27u IPv4 33818434 TCP sargon.cluster3:57917-> romulus.cluster3:389 (ESTABLISHED) apache2 16072 apache 29u IPv4 33841087 TCP sargon.cluster3:37635-> hammurabi.cluster3:3306 (ESTABLISHED)
Unter GNU/Linux bildet das /proc-Dateisystem eine weitere wertvolle Hilfe bei der Fehlersuche. Jeder Prozess hat unter /proc sein eigenes Verzeichnis, und Sie finden viele Informationen darüber, wie etwa sein aktuelles Arbeitsverzeichnis, die Speicherbenutzung usw. Apache verfügt außerdem über eine Funktion ähnlich dem Unix-Befehl ps: die URL /server-status/. Falls z.B. in Ihrem Intranet Apache unter http://intranet/ läuft, können Sie sich mit Ihrem Webbrowser zu http://intranet/server-status/ begeben, um festzustellen, was Apache tut. Mit dieser hilfreichen Methode kann man herausfinden, welche URL ein Prozess bedient. Die Seite besitzt eine Legende, die ihre Ausgabe erläutert.
Erweitertes Profiling und Fehlerbehebung Falls Sie einen Prozess genauer erforschen müssen, um festzustellen, was er tut – z.B. weshalb er sich im unterbrechungsfreien Sleep-Status befindet –, verwenden Sie strace -p und/oder gdb -p. Diese Befehle zeigen Systemaufrufe und Backtraces, die Sie genauer darüber in Kenntnis setzen, was der Prozess gerade getan hat, als er hängengeblieben ist. Es gibt viele Möglichkeiten, weshalb ein Prozess hängenbleiben könnte, wie etwa NFSLocking-Dienste, die abstürzen, ein Aufruf an einen entfernten Webservice, der nicht antwortet, usw.
84 |
Kapitel 2: Engpässe finden: Benchmarking und Profiling
Sie können auch Systeme oder Teile von Systemen genauer profilieren, um näher zu erfahren, was sie tun. Wenn Sie wirklich hohe Leistung benötigen und Probleme auftreten, dann werden Sie vielleicht sogar die Interna von MySQL profilieren. Das scheint zwar nicht Ihre Aufgabe zu sein (sondern eher die des MySQL-Entwicklerteams, oder?), aber es kann Ihnen dabei helfen, den Teil des Systems zu isolieren, der Ärger macht. Vielleicht sind Sie nicht gewillt oder in der Lage, das Problem zu lösen, aber zumindest können Sie Ihre Anwendung so gestalten, dass sie die Schwachstelle meidet. Dies sind einige Werkzeuge, die Ihnen nützlich sein könnten: OProfile OProfile (http://oprofile.sourceforge.net) ist ein System-Profiler für Linux. Er besteht aus einem Kernel-Treiber und einem Daemon zum Sammeln von Testdaten, sowie verschiedenen Werkzeugen, mit deren Hilfe Sie die gesammelten Profiling-Daten analysieren können. Er profiliert den gesamten Code, einschließlich der InterruptHandler, des Kernels, der Kernel-Module, der Anwendungen und der Shared Libraries. Wenn eine Anwendung mit Debug-Symbolen kompiliert wurde, kann OProfile die Quelle mit Anmerkungen versehen, aber das ist nicht notwendig; Sie können ein System auch profilieren, ohne alles neu zu kompilieren. Der Overhead ist relativ gering und bewegt sich im Bereich von ein paar Prozent. gprof gprof ist der GNU-Profiler, der Ausführungsprofile von Programmen herstellen kann, die mit der Option -pg kompiliert wurden. Er berechnet die Zeit, die in jeder Routine verbracht wurde. gprof kann Berichte über die Häufigkeit und Dauer von Funktionsaufrufen, ein Aufrufdiagramm und annotierte Quelllistings liefern. Weitere Werkzeuge Es gibt noch viele weitere Werkzeuge, einschließlich spezialisierter und/oder proprietärer Programme. Dazu gehören Intel VTune, der Sun Performance Analyzer (Teil von Sun Studio) und DTrace auf Solaris und anderen Systemen.
Profiling des Betriebssystems | 85
KAPITEL 3
Schema-Optimierung und Indizierung
Das Optimieren eines schlecht gestalteten oder indizierten Schemas kann die Leistung um Größenordnungen verbessern. Falls Sie eine hohe Leistung erwarten, müssen Sie Ihr Schema und die Indizes an die speziellen Abfragen anpassen, die Sie ausführen werden. Sie sollten außerdem die Leistungsanforderungen für die verschiedenen Arten von Abfragen abschätzen, da Änderungen an einer Abfrage oder an einem Teil des Schemas sich auch woanders auswirken können. Eine Optimierung geht oft mit Nebenwirkungen einher. Beispielsweise sorgt das Hinzufügen von Indizes zur Beschleunigung der Datenbereitstellung dafür, dass sich Updates verlangsamen. Ein denormalisiertes Schema wiederum kann einige Arten von Abfragen schneller machen, andere dagegen ausbremsen. Zähler und Summary-Tabellen bilden eine großartige Möglichkeit, um Abfragen zu optimieren, sind aber eventuell in der Wartung recht teuer. Manchmal müssen Sie vielleicht über die Rolle eines Entwicklers hinausgehen und die geschäftlichen Anforderungen hinterfragen, die an Sie gestellt werden. Leute, die keine Experten für Datenbanksysteme sind, schreiben manchmal Geschäftsanforderungen, ohne deren Auswirkungen auf die Leistung zu verstehen. Wenn Sie erklären, dass eine kleine Funktion die Anforderungen an die Server-Hardware verdoppelt, wird man möglicherweise entscheiden, dass man auch ohne diese Funktion auskommen kann. Schema-Optimierung und Indizierung verlangen einen Ansatz, der das Große und Ganze beachtet, sowie Aufmerksamkeit für Details. Sie müssen das ganze System verstehen, um zu verstehen, wie die einzelnen Teile aufeinander wirken. Dieses Kapitel beginnt mit einer Erläuterung der Datentypen und behandelt dann Indizierungsstrategien und Normalisierung. Es endet mit einigen Bemerkungen über Storage-Engines. Wahrscheinlich müssen Sie noch einmal zu diesem Kapitel zurückkommen, nachdem Sie das Kapitel über die Abfragenoptimierung gelesen haben. Viele der hier besprochenen Themen – speziell die Indizierung – können nicht isoliert betrachtet werden. Sie müssen mit Abfragenoptimierung und Server-Tuning vertraut sein, um sinnvolle Entscheidungen über Indizes treffen zu können.
86 |
Optimale Datentypen auswählen MySQL unterstützt eine Vielzahl von Datentypen. Die Auswahl des richtigen Typs zum Speichern Ihrer Daten ist entscheidend für eine gute Performance. Die folgenden einfachen Hinweise sollen Ihnen helfen, bessere Entscheidungen zu treffen – ungeachtet der Frage, welche Arten von Daten Sie speichern: Kleiner ist meist besser Versuchen Sie im Allgemeinen den kleinsten Datentyp zu benutzen, der Ihre Daten korrekt speichern und darstellen kann. Kleinere Datentypen sind normalerweise schneller, da sie weniger Platz auf der Festplatte, im Speicher und im CPU-Cache belegen. Meist erfordern sie auch weniger CPU-Zyklen für die Verarbeitung. Unterschätzen Sie jedoch nicht den Bereich der Werte, die Sie speichern müssen, da das Vergrößern des Datentypbereichs an vielen Stellen in Ihrem Schema unerfreulich und zeitaufwendig ist. Falls Sie sich nicht sicher sind, welcher Datentyp sich für Ihre Zwecke am besten eignet, wählen Sie den kleinsten, der Ihnen möglich erscheint. (Wenn das System nicht sehr ausgelastet ist oder nicht viele Daten enthält oder Sie sich in einer frühen Phase des Entwurfsprozesses befinden, können Sie das hinterher noch ändern.) Einfach ist gut Um Operationen auf einfacheren Datentypen zu verarbeiten, sind typischerweise weniger CPU-Zyklen erforderlich. Zum Beispiel sind Integer-Werte einfacher zu vergleichen als Zeichen, da Zeichensätze und Sortierregeln die Zeichenvergleiche verkomplizieren. Hier sind zwei Beispiele: Sie sollten Daten und Zeiten in den MySQLeigenen Typen anstatt in Strings speichern, und Sie sollten Integer-Werte für IPAdressen verwenden. Wir gehen auf diese Themen später näher ein. Vermeiden Sie NULL, falls möglich Definieren Sie Felder als NOT NULL, falls es sich einrichten lässt. Viele Tabellen enthalten einfach nur aufgrund ihrer Vorgabe »Nullable«-Spalten, auch wenn die Anwendung NULL (also die Abwesenheit eines Wertes) nicht speichern muss. Achten Sie darauf, Spalten als NOT NULL anzugeben, es sei denn, Sie haben ausdrücklich vor, NULL in ihnen zu speichern. Es ist für MySQL schwieriger, Abfragen zu optimieren, die sich auf Nullable-Spalten beziehen, weil diese die Vergleiche von Indizes, Indexstatistiken und Werten deutlich komplizierter machen. Eine Nullable-Spalte belegt mehr Speicherplatz und erfordert in MySQL eine spezielle Verarbeitung. Wenn eine Nullable-Spalte indiziert wird, braucht sie pro Eintrag ein zusätzliches Byte und kann in MyISAM sogar dafür sorgen, dass ein Index fester Größe (wie etwa der Index einer Single-IntegerSpalte) in einen Index variabler Größe konvertiert wird. Selbst wenn Sie die Tatsache eines »kein Wert« in einer Tabelle speichern müssen, müssen Sie nicht unbedingt NULL verwenden. Nehmen Sie stattdessen lieber Null, einen speziellen Wert oder einen leeren String.
Optimale Datentypen auswählen |
87
Die Leistungssteigerung aufgrund der Änderung von NULL-Spalten in NOT NULL ist normalerweise klein. Räumen Sie diesem Vorhaben daher bei einem existierenden Schema keine allzu hohe Priorität ein, es sei denn, Sie wissen, dass diese Spalten Probleme verursachen. Falls Sie dagegen planen, Spalten zu indizieren, vermeiden Sie es, diese nullable zu machen. Der erste Schritt bei der Entscheidung für einen Datentyp für eine Spalte besteht darin, festzustellen, welche allgemeine Klasse von Typen passen würde: numerisch, String, temporal usw. Das ist normalerweise relativ einfach, wir erwähnen hier allerdings einige Sonderfälle, bei denen die Wahl nicht ganz so intuitiv ist. Im nächsten Schritt muss der spezielle Typ gewählt werden. Viele der Datentypen von MySQL können die gleiche Art von Daten speichern, unterscheiden sich aber im möglichen Wertebereich, in der Genauigkeit, die sie zulassen, oder in dem physischen Platz (auf der Festplatte oder im Speicher), den sie erfordern. Manche Datentypen weisen auch besondere Verhaltensweisen oder Eigenschaften auf. So können z.B. eine DATETIME- und eine TIMESTAMP-Spalte die gleiche Art von Daten speichern: Datum und Uhrzeit mit einer Genauigkeit von einer Sekunde. Allerdings belegt TIMESTAMP nur halb so viel Speicherplatz, ist in der Lage, die Zeitzonen zu beachten, und verfügt über besondere Fähigkeiten zur automatischen Aktualisierung. Andererseits ist der Bereich der erlaubten Werte kleiner, und seine besonderen Fähigkeiten können manchmal hinderlich sein. Wir diskutieren hier grundlegende Datentypen. MySQL unterstützt aus Gründen der Kompatibilität viele Aliase, wie etwa INTEGER, BOOL und NUMERIC. Das sind nur Aliase. Sie können verwirrend sein, beeinflussen aber nicht die Leistung.
Ganze Zahlen Es gibt zwei Arten von Zahlen: ganze Zahlen und reelle Zahlen (Zahlen mit einem gebrochenen Anteil). Wenn Sie ganze Zahlen speichern, verwenden Sie einen der IntegerTypen: TINYINT, SMALLINT, MEDIUMINT, INT oder BIGINT. Diese verlangen 8, 16, 24, 32 bzw. 64 Bit Speicherplatz. Sie können Werte von –2(N–1) bis 2(N–1)–1 speichern, wobei N die Anzahl der Bits des von ihnen benutzten Speicherplatzes ist. Integer-Typen können optional das Attribut UNSIGNED aufweisen, das negative Werte verbietet und die obere Grenze der positiven Werte, die Sie speichern können, ungefähr verdoppelt. Beispielsweise kann ein TINYINT UNSIGNED Werte von 0 bis 255 speichern anstatt von –128 bis 127. Vorzeichenbehaftete und vorzeichenlose Typen belegen den gleichen Speicherplatz und weisen die gleiche Leistung auf. Nehmen Sie deshalb das, was für Ihren Datenbereich am besten geeignet ist. Ihre Wahl bestimmt, wie MySQL die Daten im Speicher und auf der Festplatte speichert. Integer-Berechnungen dagegen verwenden im Allgemeinen 64-Bit-BIGINT-Integer-Werte,
88 | Kapitel 3: Schema-Optimierung und Indizierung
selbst auf 32-Bit-Architekturen. (Ausnahmen bilden einige Aggregatfunktionen, die DECIMAL oder DOUBLE einsetzen, um Berechnungen auszuführen.) MySQL erlaubt es Ihnen, eine »Breite« für Integer-Typen anzugeben, z.B. INT(11). Für die meisten Anwendungen hat das keine Bedeutung: Es beschränkt den zulässigen Wertebereich nicht, sondern legt einfach die Anzahl der Zeichen fest, die die interaktiven Werkzeuge von MySQL (wie etwa der Kommandozeilenclient) für die Anzeige reservieren. Bei der Speicherung und bei Berechnungen ist INT(1) identisch mit INT(20). Die Falcon-Storage-Engine unterscheidet sich von den anderen StorageEngines, die MySQL AB anbietet, dahingehend, dass sie Integer-Werte in ihrem eigenen internen Format speichert. Der Benutzer hat keine Kontrolle über die tatsächliche Größe der gespeicherten Daten. Storage-Engines von Drittanbietern, wie etwa Brighthouse, besitzen ebenfalls eigene Speicherformate und Komprimierungsschemata.
Reelle Zahlen Reelle Zahlen sind Zahlen, die einen gebrochenen Anteil aufweisen. Sie eignen sich jedoch nicht nur für gebrochene Zahlen, sondern Sie können DECIMAL auch einsetzen, um Integer-Werte zu speichern, die so groß sind, dass sie nicht in BIGINT passen. MySQL unterstützt sowohl exakte als auch näherungsweise Typen. Die Typen FLOAT und DOUBLE unterstützen näherungsweise Berechnungen mit normaler Fließkommaarithmetik. Falls Sie genau wissen müssen, wie die Fließkommaergebnisse berechnet werden, müssen Sie in der Fließkommaimplementierung Ihrer Plattform nachforschen. Der Typ DECIMAL dient zur Speicherung exakter gebrochener Zahlen. Seit MySQL 5.0 unterstützt DECIMAL exakte Berechnungen. MySQL 4.1 und frühere Versionen verwendeten Fließkommaarithmetik, um Berechnungen mit DECIMAL-Werten durchzuführen. Das konnte aufgrund des Mangels an Genauigkeit zu seltsamen Ergebnissen führen. In diesen Versionen von MySQL war DECIMAL nur ein »Speichertyp«. Seit MySQL 5.0 führt der Server selbst die DECIMAL-Berechnungen durch, weil die CPUs die Berechnungen nicht direkt unterstützen. Fließkommaberechnungen sind etwas schneller, weil die CPU diese nativ ausführt. Sowohl Fließkomma- als auch DECIMAL-Typen erlauben es Ihnen, eine Genauigkeit anzugeben. Für eine DECIMAL-Spalte können Sie die maximal erlaubten Stellen vor und nach dem Dezimalpunkt festlegen. Das hat dann auch Einfluss auf den Platzbedarf der Spalte. MySQL 5.0 und neuere Versionen packen die Stellen in einen Binärstring (neun Stellen pro vier Bytes). Zum Beispiel speichert DECIMAL(18, 9) neun Stellen von jeder Seite des Dezimalpunkts und braucht insgesamt neun Bytes: vier für die Stellen vor dem Dezimalpunkt, eines für den Dezimalpunkt selbst und vier für die Stellen hinter dem Dezimalpunkt.
Optimale Datentypen auswählen |
89
Eine DECIMAL-Zahl in MySQL ab Version 5.0 kann bis zu 65 Stellen besitzen. Frühere MySQL-Versionen hatten eine Grenze von 254 Stellen und speicherten die Werte als ungepackte Strings (ein Byte pro Stelle). Allerdings konnten diese Versionen von MySQL solche großen Zahlen gar nicht in Berechnungen verwenden, weil DECIMAL lediglich ein Speicherformat war; für Berechnungen wurden DECIMAL-Zahlen in DOUBLE-Werte umgewandelt. Es gibt verschiedene Möglichkeiten, die gewünschte Genauigkeit einer Fließkommaspalte festzulegen. Diese Möglichkeiten können allerdings MySQL dazu veranlassen, stillschweigend einen anderen Datentyp zu wählen oder die Werte beim Speichern zu runden. Diese Genauigkeitsspezifikatoren sind nicht standardisiert, so dass wir Ihnen empfehlen, zwar den gewünschten Typ, nicht jedoch die Genauigkeit anzugeben. Fließkommatypen belegen typischerweise weniger Platz als DECIMAL für den gleichen Wertebereich. Eine FLOAT-Spalte verwendet vier Bytes für die Speicherung. DOUBLE belegt acht Bytes und bietet eine größere Genauigkeit und einen größeren Wertebereich. Wie bei den Integer-Werten wählen Sie nur den Speichertyp; MySQL verwendet DOUBLE für seine internen Berechnungen mit Fließkommatypen. Aufgrund der zusätzlichen Platzanforderungen und Berechnungskosten sollten Sie DECIMAL nur dann einsetzen, wenn Sie exakte Werte für gebrochene Zahlen benötigen –
z.B., wenn Sie Finanzdaten speichern.
Stringtypen MySQL unterstützt etliche Stringdatentypen, die jeweils noch viele Variationen bieten. Diese Datentypen haben sich in den Versionen 4.1 und 5.0 stark geändert, was sie noch komplizierter machte. Seit MySQL 4.1 kann jede Stringspalte ihren eigenen Zeichensatz mit eigenen Sortierregeln für diesen Zeichensatz besitzen (Kollation; siehe Kapitel 5 für weitere Informationen zu diesen Themen). Das kann die Performance stark beeinflussen.
VARCHAR- und CHAR-Typen Die beiden wichtigsten Stringtypen sind VARCHAR und CHAR, die Zeichenwerte speichern. Leider ist es schwierig, exakt zu erklären, wie diese Werte auf der Festplatte und im Speicher gespeichert werden, da die Implementierungen von der jeweiligen Storage-Engine abhängen (so benutzt etwa Falcon für fast jeden Datentyp seine eigenen Speicherformate). Wir gehen davon aus, dass Sie InnoDB und/oder MyISAM benutzen. Falls nicht, lesen Sie die Dokumentation Ihrer Storage-Engine. Schauen wir uns an, wie VARCHAR- und CHAR-Werte typischerweise auf der Festplatte gespeichert werden. Beachten Sie, dass eine Storage-Engine einen CHAR- oder VARCHARWert im Speicher anders als auf der Festplatte speichern kann und dass der Server den Wert möglicherweise in noch ein anderes Storage-Engine-Format übersetzt, wenn er ihn von der Storage-Engine bezieht. Hier ist ein allgemeiner Vergleich der beiden Typen:
90 | Kapitel 3: Schema-Optimierung und Indizierung
VARCHAR VARCHAR speichert Zeichenstrings variabler Länge und ist der am weitesten verbrei-
tete Stringdatentyp. Er kann weniger Speicherplatz als die Typen fester Länge beanspruchen, weil er nur so viel Platz benutzt, wie er wirklich braucht (d.h., für kürzere Werte wird weniger Platz benötigt). Eine Ausnahme bildet eine MyISAM-Tabelle, die mit ROW_FORMAT=FIXED erzeugt wurde, bei der also für jede Zeile eine festgelegte Menge an Platz verwendet und daher unter Umständen Platz verschwendet wird. VARCHAR nutzt 1 oder 2 Extrabytes zum Aufzeichnen der Länge des Wertes: 1 Byte, wenn die maximale Länge der Spalte 255 Bytes oder weniger beträgt, und 2 Bytes, wenn es mehr ist. Beim Latin1-Zeichensatz belegt ein VARCHAR(10) bis zu 11 Bytes an Speicherplatz. Ein VARCHAR(1000) kann bis zu 1002 Bytes belegen, da er 2 Bytes für die Längeninformationen braucht. VARCHAR nützt der Leistung, weil es Platz spart. Da die Zeilen jedoch eine variable Länge haben, können sie bei einer Aktualisierung größer werden, was möglicherweise zusätzlichen Aufwand verursacht. Wenn eine Zeile anwächst und nicht mehr an ihre ursprüngliche Stelle passt, hängt es von der Storage-Engine ab, was geschieht. MyISAM könnte z.B. die Zeile fragmentieren, InnoDB muss vielleicht die Seite aufteilen, damit die Zeile hineinpasst. Andere Storage-Engines aktualisieren die vorhandenen Daten möglicherweise niemals. Meist lohnt es sich, VARCHAR zu verwenden, wenn die maximale Spaltenlänge viel größer ist als die durchschnittliche Länge, wenn das Feld selten aktualisiert wird, so dass Fragmentierung kein Problem darstellt, und wenn Sie einen komplexen Zeichensatz wie UTF-8 verwenden, bei dem jedes Zeichen eine variable Anzahl von Bytes zur Speicherung benutzt. Ab Version 5.0 bewahrt MySQL Leerzeichen am Ende, wenn Sie Werte speichern und abrufen. Bis Version 4.1 hat MySQL abschließende Leerzeichen entfernt. CHAR CHAR besitzt eine feste Länge: MySQL reserviert immer genügend Platz für die angegebene Anzahl von Zeichen. Wenn es einen CHAR-Wert speichert, entfernt MySQL alle abschließenden Leerzeichen. (Das gilt auch für VARCHAR bis MySQL 4.1 – CHAR und VARCHAR waren logisch gesehen identisch und unterschieden sich nur im Speicherformat.) Für Vergleichszwecke werden Werte mit Leerzeichen aufgefüllt. CHAR ist sinnvoll, wenn Sie sehr kurze Strings speichern wollen oder wenn alle Werte annähernd gleich lang sind. Zum Beispiel ist CHAR eine gute Wahl für MD5-Werte für Benutzerpasswörter, die immer gleich lang sind. Auch bei Daten, die sich häufig ändern, eignet sich CHAR besser als VARCHAR, da eine Zeile fester Länge nicht von Fragmentierung betroffen ist. Für sehr kurze Spalten ist CHAR ebenfalls effizienter als VARCHAR; ein CHAR(1), das dazu gedacht ist, nur die Werte Y und N aufzunehmen, verwendet in einem ein Byte langen Zeichensatz nur ein Byte,1 ein VARCHAR(1) dagegen würde wegen des Längenbytes zwei Bytes benötigen. 1 Denken Sie daran, dass die Länge in Zeichen und nicht in Bytes festgelegt wird. Ein Multibyte-Zeichensatz kann mehr als ein Byte zur Speicherung der einzelnen Zeichen verlangen.
Optimale Datentypen auswählen |
91
Dieses Verhalten kann ein wenig verwirrend sein, weshalb wir es mit einem Beispiel verdeutlichen. Zuerst erzeugen wir eine Tabelle mit einer einzigen CHAR(10)-Spalte und speichern einige Werte in ihr: mysql> CREATE TABLE char_test( char_col CHAR(10)); mysql> INSERT INTO char_test(char_col) VALUES -> ('string1'), (' string2'), ('string3 ');
Wenn wir die Werte abrufen, wurden die Leerzeichen am Ende entfernt: mysql> SELECT CONCAT("'", char_col, "'") FROM char_test; +----------------------------+ | CONCAT("'", char_col, "'") | +----------------------------+ | 'string1' | | ' string2' | | 'string3' | +----------------------------+
Speichern wir die gleichen Werte in einer VARCHAR(10)-Spalte, erhalten wir beim Abrufen folgendes Ergebnis: mysql> SELECT CONCAT("'", varchar_col, "'") FROM varchar_test; +-------------------------------+ | CONCAT("'", varchar_col, "'") | +-------------------------------+ | 'string1' | | ' string2' | | 'string3 ' | +-------------------------------+
Wie die Daten gespeichert werden, liegt an den Storage-Engines, und nicht alle StorageEngines behandeln Daten fester Länge genauso wie Daten variabler Länge. Die MemoryStorage-Engine verwendet Zeilen mit fester Größe, muss also auch dann den maximal möglichen Platz für jeden Wert reservieren, wenn es sich um ein Feld variabler Länge habelt. Andererseits benutzt Falcon Spalten variabler Länge auch für CHAR-Felder fester Länge. Das Verhalten beim Auffüllen und Abschneiden ist jedoch bei allen Storage-Engines gleich, weil dies der MySQL-Server selbst erledigt. Verwandte Typen zu CHAR und VARCHAR sind BINARY und VARBINARY, die Binär-Strings speichern. Binär-Strings sind herkömmlichen Strings sehr ähnlich, speichern allerdings Bytes anstelle von Zeichen. Auch das Auffüllen geschieht anders: MySQL füllt BINARY-Werte mit \0 (dem Null-Byte) anstatt mit Leerzeichen auf und entfernt beim Abrufen den Auffüllwert nicht.2 Diese Typen sind nützlich, wenn Sie Binärdaten speichern müssen und MySQL die Werte als Byte anstatt als Zeichen vergleichen soll. Der Vorteil der byteweisen Vergleiche besteht in mehr als in der Frage, ob Groß- und Kleinschreibung beachtet wird. MySQL vergleicht BINARY-Strings tatsächlich ein Byte nach dem anderen, entsprechend der nume2 Seien Sie vorsichtig mit dem BINARY-Typ, falls der Wert nach dem Abrufen unverändert bleiben muss. MySQL füllt ihn mit \0 auf die erforderliche Länge auf.
92 | Kapitel 3: Schema-Optimierung und Indizierung
rischen Werte der einzelnen Bytes. Daraus folgt, dass Binärvergleiche viel einfacher sein können als Zeichenvergleiche – und demzufolge auch schneller.
Großzügigkeit ist nicht unbedingt klug Beim Speichern des Wertes 'hello' wird in einer VARCHAR(5)- und in einer VARCHAR(200)Spalte jeweils der gleiche Platz benötigt. Bietet es irgendeinen Vorteil, die kürzere Spalte zu benutzen? Wie es sich zeigt, gibt es einen großen Vorteil. Die größere Spalte kann viel mehr Speicher belegen, weil MySQL oft Speicherbereiche fester Größe reserviert, um Werte intern aufzunehmen. Das erweist sich als besonders ungünstig für Sortierungen oder Operationen, die im Speicher befindliche temporäre Tabellen verwenden. Das gilt auch für Sortierungen, die temporäre Tabellen benutzen, die auf der Festplatte liegen. Die beste Strategie besteht darin, nur so viel Platz zuzuweisen, wie Sie tatsächlich brauchen.
BLOB- und TEXT-Typen BLOB und TEXT sind Stringdatentypen, die große Datenmengen als Binär- bzw. Zeichen-
strings aufnehmen sollen. Es handelt sich bei ihnen eigentlich um Familien von Datentypen: die Zeichentypen sind TINYTEXT, SMALLTEXT, TEXT, MEDIUMTEXT und LONGTEXT, und die Binärtypen sind TINYBLOB, SMALLBLOB, BLOB, MEDIUMBLOB und LONGBLOB. BLOB ist ein Synonym für SMALLBLOB, TEXT ist ein Synonym für SMALLTEXT.
Im Gegensatz zu allen anderen Datentypen behandelt MySQL alle BLOB- und TEXT-Werte als Objekte mit eigener Identität. Storage-Engines speichern sie oft besonders. InnoDB setzt für sie möglicherweise sogar einen separaten »externen« Speicherbereich ein, wenn sie groß sind. Jeder Wert verlangt ein bis vier Byte Speicherplatz in der Zeile und genügend Platz im externen Speicher, um den Wert dann tatsächlich aufzunehmen. Der einzige Unterschied zwischen den BLOB- und den TEXT-Familien besteht darin, dass BLOB-Typen die Binärdaten ohne Sortierregel oder Zeichensatz speichern, während TEXTTypen einen Zeichensatz und eine Sortierregel (Kollation) besitzen. MySQL sortiert BLOB- und TEXT-Spalten anders als andere Typen: Anstatt die vollständige Länge des Strings zu sortieren, sortiert es nur die ersten max_sort_length Bytes solcher Spalten. Wenn Sie nur nach den ersten paar Zeichen sortieren müssen, können Sie entweder die Servervariable max_sort_length verkleinern oder ORDER BY SUBSTRING(Spalte, Länge) verwenden. MySQL kann die vollständige Länge dieser Datentypen nicht indizieren und die Indizes auch nicht für die Sortierung benutzen. (Mehr zu diesen Themen finden Sie weiter hinten in diesem Kapitel.)
Optimale Datentypen auswählen |
93
Wie Sie temporäre Tabellen auf der Festplatte vermeiden Da die Memory-Storage-Engine die Typen BLOB und TEXT nicht unterstützt, müssen Abfragen, die BLOB- oder TEXT-Spalten verwenden und eine implizite temporäre Tabelle benötigen, selbst für nur wenige Zeilen temporäre MyISAM-Tabellen auf der Festplatte benutzen. Das kann zu einem ernsthaften Overhead führen. Auch wenn Sie MySQL so konfigurieren, dass es temporäre Tabellen in einer RAM-Disk speichert, sind viele teure Betriebssystemaufrufe notwendig. (Die Maria-Storage-Engine sollte dieses Problem lindern, indem sie alles im Cache ablegt, nicht nur die Indizes.) Am besten ist es, BLOB- und TEXT-Typen nur dann zu verwenden, wenn Sie sie wirklich brauchen. Wenn Sie sie nicht vermeiden können, dann probieren Sie vielleicht den ORDER BY SUBSTRING(Spalte, Länge)-Trick, um die Werte in Zeichenstrings umzuwandeln, die wiederum temporäre Tabellen im Speicher zulassen. Denken Sie nur daran, einen ausreichend kurzen Teilstring zu verwenden, damit die temporäre Tabelle nicht länger wird als max_heap_table_size oder tmp_table_size, da MySQL die Tabelle ansonsten in eine MyISAM-Tabelle umwandelt, die auf der Festplatte gespeichert wird. Falls die Extra-Spalte von EXPLAIN »Using temporary« enthält, benutzt die Abfrage eine implizite temporäre Tabelle.
ENUM anstelle eines Stringtyps benutzen Manchmal können Sie eine ENUM-Spalte anstelle von herkömmlichen Stringtypen verwenden. Eine ENUM-Spalte kann bis zu 65.535 verschiedene Stringwerte aufnehmen. MySQL speichert sie sehr kompakt, je nach der Anzahl der Werte in der Liste in nur ein oder zwei Bytes gepackt. Es legt jeden Wert intern als Integer ab, der seine Position in der Felddefinitionsliste repräsentiert, und bewahrt die »Lookup-Tabelle«, die die Zuordnung der Zahlen zu den Strings definiert, in der .frm-Datei der Tabelle auf. Hier ist ein Beispiel: mysql> CREATE TABLE enum_test( -> e ENUM('fish', 'apple', 'dog') NOT NULL -> ); mysql> INSERT INTO enum_test(e) VALUES('fish'), ('dog'), ('apple');
Tatsächlich speichern die drei Zeilen Integer-Werte und keine Strings. Sie erkennen die duale Natur der Werte, wenn Sie sie in einem numerischen Kontext abrufen: mysql> SELECT e + 0 FROM enum_test; +-------+ | e + 0 | +-------+ | 1 | | 3 | | 2 | +-------+
Diese Dualität kann fürchterlich verwirrend sein, wenn Sie Zahlen für Ihre ENUM-Konstanten angeben, wie in ENUM('1', '2', '3'). Wir empfehlen Ihnen, dies nicht zu tun.
94 | Kapitel 3: Schema-Optimierung und Indizierung
Überraschend ist weiterhin, dass ein ENUM-Feld anhand der internen Integer-Werte sortiert wird und nicht anhand der Strings selbst: mysql> SELECT e FROM enum_test ORDER BY e; +-------+ | e | +-------+ | fish | | apple | | dog | +-------+
Dem können Sie abhelfen, indem Sie die ENUM-Mitglieder in der Reihenfolge angeben, in der sie sortiert werden sollen. Oder Sie geben mit FIELD( ) explizit eine Sortierreihenfolge in Ihren Abfragen an. Allerdings verhindert dies, dass MySQL den Index für die Sortierung benutzt: mysql> SELECT e FROM enum_test ORDER BY FIELD(e, 'apple', 'dog', 'fish'); +-------+ | e | +-------+ | apple | | dog | | fish | +-------+
Der größte Nachteil von ENUM besteht darin, dass die Liste der Strings fest ist. Das Hinzufügen oder Entfernen von Strings erfordert den Einsatz von ALTER TABLE. Es ist daher wahrscheinlich keine so gute Idee, ENUM als Stringdatentyp zu benutzen, wenn sich die Liste der erlaubten Stringwerte irgendwann in Zukunft ändern wird. MySQL benutzt ENUM in seinen eigenen Privilege-Tabellen, um die Werte Y und N zu speichern. Da MySQL jeden Wert als Integer speichert und eine Suche durchführen muss, um diesen in seine Stringrepräsentation umzuwandeln, verursachen ENUM-Spalten einen gewissen Overhead. Das wird normalerweise durch ihre geringere Größe ausgeglichen, allerdings nicht immer. Es kann insbesondere langsamer sein, eine CHAR- oder VARCHARSpalte zu einer ENUM-Spalte zusammenzufügen als zu einer anderen CHAR- oder VARCHARSpalte. Zur Verdeutlichung haben wir mit einem Benchmark gemessen, wie schnell MySQL solch einen Join mit einer Tabelle in einer unserer Anwendungen durchführt. Die Tabelle besitzt einen relativ breiten Primärschlüssel: CREATE TABLE webservicecalls ( day date NOT NULL, account smallint NOT NULL, service varchar(10) NOT NULL, method varchar(50) NOT NULL, calls int NOT NULL, items int NOT NULL, time float NOT NULL, cost decimal(9,5) NOT NULL,
Optimale Datentypen auswählen |
95
updated datetime, PRIMARY KEY (day, account, service, method) ) ENGINE=InnoDB;
Die Tabelle enthält ungefähr 110.000 Zeilen und ist nur etwa 10 MByte groß, passt also komplett in den Speicher. Die Spalte service enthält 5 einzelne Werte mit einer durchschnittlichen Länge von 4 Zeichen, und die Spalte method enthält 71 Werte mit einer durchschnittlichen Länge von 20 Zeichen. Wir haben eine Kopie dieser Tabelle hergestellt und die Spalten service und method in ENUM umgewandelt: CREATE TABLE webservicecalls_enum ( ... weggelassen ... service ENUM(...values omitted...) NOT NULL, method ENUM(...values omitted...) NOT NULL, ... weggelassen ... ) ENGINE=InnoDB;
Anschließend haben wir die Leistung beim Zusammenführen der Tabellen anhand der Primärschlüsselspalten gemessen. Hier ist die von uns verwendete Abfrage: mysql> SELECT SQL_NO_CACHE COUNT(*) -> FROM webservicecalls -> JOIN webservicecalls USING(day, account, service, method);
Wir variierten diese Abfrage, um die VARCHAR- und ENUM-Spalten in unterschiedlichen Kombinationen zusammenzuführen. Tabelle 3-1 zeigt die Ergebnisse. Tabelle 3-1: Geschwindigkeit beim Zusammenführen von VARCHAR- und ENUM-Spalten Test
Abfragen pro Sekunde
VARCHAR zusammengeführt zu VARCHAR
2,6
VARCHAR zusammengeführt zu ENUM
1,7
ENUM zusammengeführt zu VARCHAR
1,8
ENUM zusammengeführt zu ENUM
3,5
Der Join verläuft nach dem Umwandeln der Spalten in ENUM schneller, allerdings ist das Zusammenführen der ENUM-Spalten zu VARCHAR-Spalten langsamer. Es scheint sich in diesem Fall anzubieten, diese Spalten zu konvertieren, solange sie nicht zu VARCHAR-Spalten zusammengeführt werden müssen. Das Umwandeln der Spalten bringt jedoch noch einen Vorteil mit sich: Laut der Data_length-Spalte aus SHOW TABLE STATUS wurde die Tabelle nach dem Konvertieren dieser beiden Spalten nach ENUM um etwa 1/3 kleiner. Manchmal ist das sogar dann ganz günstig, wenn die ENUM-Spalten zu VARCHAR-Spalten zusammengeführt werden müssen. Der Primärschlüssel ist nach der Konvertierung ebenfalls nur noch etwa halb so groß. Da es sich hier um eine InnoDB-Tabelle handelt, würde die Verringerung der Primärschlüsselgröße dazu führen, dass möglicherweise vorhandene Indizes ebenfalls viel kleiner werden. Wir erklären das später noch genauer.
96 | Kapitel 3: Schema-Optimierung und Indizierung
Datums- und Zeittypen MySQL verfügt über viele Typen für verschiedene Arten von Datums- und Zeitwerten, wie etwa YEAR und DATE. Die feinste Auflösung, die MySQL für eine Zeit speichern kann, beträgt eine Sekunde. Es kann allerdings temporäre Berechnungen mit Mikrosekundenauflösung durchführen, und wir zeigen Ihnen auch, wie Sie die Speicherbeschränkungen umgehen. Für die meisten der temporalen Typen gibt es keine Alternativen, es ist also nicht die Frage, welches die beste Wahl ist. Die einzige Frage ist, was Sie tun, wenn Sie sowohl ein Datum als auch eine Uhrzeit speichern müssen. MySQL bietet für diesen Zweck zwei sehr ähnliche Datentypen: DATETIME und TIMESTAMP. Für viele Anwendungen funktionieren beide, manchmal ist jedoch ein Typ besser als der andere. Werfen wir einen Blick darauf: DATETIME
Dieser Typ kann bei einer Genauigkeit von einer Sekunde einen großen Wertebereich, und zwar vom Jahr 1001 bis zum Jahr 9999, aufnehmen. Er speichert das Datum und die Uhrzeit, gepackt in einen Integer-Wert im Format YYYYMMDDHHMMSS, unabhängig von der Zeitzone. Dazu sind acht Bytes Speicherplatz nötig. Standardmäßig zeigt MySQL DATETIME-Werte in einem sortierbaren, unmissverständlichen Format an, also etwa 2008-01-16 22:37:08. Dies ist die ANSI-Standardmethode zum Darstellen von Datums- und Uhrzeitangaben. TIMESTAMP
Wie der Name vermuten lässt, speichert der Typ TIMESTAMP die Anzahl der Sekunden, die seit Mitternacht des 1. Januar 1970 (Greenwich Mean Time) vergangen sind – das ist also identisch mit dem Unix-Zeitstempel. TIMESTAMP benutzt nur vier Bytes zur Speicherung, umfasst daher einen deutlich kleineren Bereich als DATETIME: von 1970 bis in die Mitte des Jahres 2038. MySQL bietet die Funktionen FROM_UNIXTIME( ) und UNIX_TIMESTAMP( ), um einen Unix-Zeitstempel in ein Datum umzuwandeln und umgekehrt. Neuere MySQL-Versionen formatieren TIMESTAMP-Werte genau wie DATETIME-Werte, ältere MySQL-Versionen dagegen zeigen sie ohne Interpunktion zwischen den Teilen an. Dieser Unterschied besteht nur in der Anzeigeformatierung; das TIMESTAMPSpeicherformat ist in allen MySQL-Versionen gleich. Welchen Wert ein TIMESTAMP anzeigt, hängt von der Zeitzone ab. MySQL-Server, Betriebssystem und Clientverbindungen besitzen jeweils ihre eigenen Zeitzoneneinstellungen. Ein TIMESTAMP, der den Wert 0 speichert, zeigt tatsächlich den 1969-12-31 19:00:00 in Eastern Standard Time an, eine Zeitzone, die fünf Stunden Unterschied zu GMT hat. TIMESTAMP besitzt besondere Eigenschaften, die DATETIME nicht hat. Standardmäßig setzt MySQL die erste TIMESTAMP-Spalte auf die aktuelle Zeit, wenn Sie eine Zeile ein-
Optimale Datentypen auswählen |
97
fügen, ohne einen Wert für die Spalte anzugeben.3 MySQL aktualisiert außerdem den Wert der ersten TIMESTAMP-Spalte, wenn Sie die Zeile aktualisieren, es sei denn, Sie weisen in der UPDATE-Anweisung ausdrücklich einen Wert zu. Sie können das Verhalten beim Einfügen und bei der Aktualisierung für jede TIMESTAMP-Spalte konfigurieren. Schließlich sind TIMESTAMP-Spalten standardmäßig NOT NULL, wodurch sie sich von jedem anderen Datentyp unterscheiden. Abgesehen von dem besonderen Verhalten gilt, dass Sie im Allgemeinen TIMESTAMP benutzen sollten, wenn dies möglich ist, da es effizienter mit dem Platz umgeht als DATETIME. Manchmal speichern Leute die Unix-Zeitstempel als Integer-Werte, das bringt aber normalerweise gar nichts. Da dieses Format im Umgang oft weniger bequem ist, raten wir davon ab. Was ist, wenn Sie einen Datums- und Zeitwert mit einer Auflösung von unter einer Sekunde speichern müssen? MySQL besitzt momentan keinen passenden Datentyp dafür, aber Sie können Ihr eigenes Speicherformat einsetzen: Verwenden Sie den Datentyp BIGINT, und speichern Sie den Wert als Zeitstempel in Mikrosekunden, oder benutzen Sie DOUBLE, und speichern Sie den gebrochenen Anteil hinter den Sekunden nach dem Dezimalpunkt. Beide Ansätze funktionieren gut.
Bit-gepackte Datentypen MySQL verfügt über einige Speichertypen, die einzelne Bits in einem Wert verwenden, um Daten kompakt zu speichern. Technisch gesehen sind all diese Typen Stringtypen, ungeachtet des zugrunde liegenden Speicherformats und der Manipulationen: BIT
Vor MySQL 5.0 ist BIT einfach nur ein Synonym für TINYINT. Seit MySQL 5.0 jedoch ist es ein völlig anderer Datentyp mit besonderen Eigenschaften. Wir besprechen das neue Verhalten hier. Sie können eine BIT-Spalte einsetzen, um einen oder mehrere Wahr/falsch-Werte in einer einzigen Spalte zu speichern. BIT(1) definiert ein Feld, das ein einziges Bit enthält, BIT(2) speichert zwei Bits usw. Die maximale Länge einer BIT-Spalte beträgt 64 Bits. Das Verhalten von BIT variiert zwischen den verschiedenen Storage-Engines. MyISAM packt die Spalten zum Zwecke der Speicherung zusammen, so dass 17 einzelne BIT-Spalten für die Speicherung nur 17 Bits erfordern (vorausgesetzt, keine dieser Spalten erlaubt NULL). MyISAM rundet das auf drei Bytes für die Speicherung. Andere Storage-Engines, wie etwa Memory und InnoDB, speichern jede Spalte als kleinsten Integer-Typ, der groß genug ist, um die Bits aufzunehmen. Sie sparen also keinen Speicherplatz.
3 Die Regeln für das Verhalten von TIMESTAMP sind komplex und haben sich in den verschiedenen MySQL-Versionen geändert, Sie müssen also sicherstellen, dass Sie das gewünschte Verhalten bekommen. Normalerweise ist es eine gute Idee, die Ausgabe von SHOW CREATE TABLE zu untersuchen, nachdem Sie Änderungen an den TIMESTAMP-Spalten vorgenommen haben.
98 | Kapitel 3: Schema-Optimierung und Indizierung
MySQL behandelt BIT als Stringtyp, nicht als numerischen Typ. Wenn Sie einen BIT(1)-Wert abrufen, ist das Ergebnis ein String, der Inhalt ist aber der Binärwert 0 oder 1, nicht der ASCII-Wert »0« oder »1«. Falls Sie jedoch den Wert in einem numerischen Kontext abrufen, ist das Ergebnis die Zahl, in die der Bitstring konvertiert wird. Denken Sie daran, wenn Sie das Ergebnis mit einem anderen Wert vergleichen müssen. Falls Sie z.B. den Wert b'00111001' (das binäre Äquivalent von 57) in einer BIT(8)-Spalte speichern und ihn dann abrufen, erhalten Sie den String, der den Zeichencode 57 enthält. Das ist zufällig der ASCII-Zeichencode für »9«. In einem numerischen Kontext dagegen erhalten Sie den Wert 57: mysql> CREATE TABLE bittest(a bit(8)); mysql> INSERT INTO bittest VALUES(b'00111001'); mysql> SELECT a, a + 0 FROM bittest; +------+-------+ | a | a + 0 | +------+-------+ | 9 | 57 | +------+-------+
Das kann sehr verwirrend sein, setzen Sie deshalb BIT mit Bedacht ein. Wir denken, dass es für die meisten Anwendungen am besten ist, diesen Typ zu vermeiden. Falls Sie einen Wahr/falsch-Wert in einem einzigen Bit speichern wollen, besteht eine weitere Möglichkeit darin, eine nullable CHAR(0)-Spalte anzulegen. Diese Spalte ist in der Lage, entweder die Abwesenheit eines Wertes (NULL) oder einen Wert der Länge null (den leeren String) zu speichern. SET
Falls Sie viele Wahr/falsch-Werte speichern müssen, dann sollten Sie darüber nachdenken, ob Sie viele Spalten mit dem MySQL-eigenen SET-Datentyp zu einer kombinieren. MySQL stellt den Datentyp SET intern als gepackte Menge aus Bits dar. Er nutzt den Speicher effizient aus und kann mithilfe von MySQL-Funktionen wie FIND_IN_SET( ) und FIELD( ) leicht in Abfragen genutzt werden. Der größte Nachteil sind die Kosten für das Ändern der Spaltendefinition: Es erfordert ein ALTER TABLE, das bei großen Tabellen sehr teuer ist (weiter hinten in diesem Kapitel lernen Sie allerdings eine Lösung dafür kennen). Im Allgemeinen können Sie auch keine Indizes für Suchen in SET-Spalten benutzen. Bitweise Operationen auf Integer-Spalten Eine Alternative zu SET besteht darin, ein Integer als gepacktes Feld aus Bits zu verwenden. So können Sie z.B. acht Bits in ein TINYINT packen und sie mit bitweisen Operatoren manipulieren. Sie vereinfachen dies, indem Sie benannte Konstanten für jedes Bit in Ihrem Anwendungscode definieren. Der größte Vorteil dieses Ansatzes gegenüber SET ist, dass Sie die »Aufzählung«, die das Feld repräsentiert, ohne ALTER TABLE ändern können. Nachteilig ist, dass die Abfragen schwieriger zu schreiben und zu verstehen sind (was bedeutet es, wenn Bit 5 gesetzt ist?). Manche Leute kommen mit bitweisen Manipulationen gut zurecht, andere nicht; ob Sie diese Technik also ausprobieren, ist vor allem eine Frage Ihrer persönlichen Vorlieben. Optimale Datentypen auswählen |
99
Eine Beispielanwendung für gepackte Bits ist eine Zugriffskontrollliste (Access Control List; ACL), die Berechtigungen speichert. Jedes Bit oder SET-Element repräsentiert einen Wert wie CAN_READ, CAN_WRITE oder CAN_DELETE. Falls Sie eine SET-Spalte benutzen, erlauben Sie es MySQL, die Bit-zu-Wert-Zuordnung in der Spaltendefinition zu speichern. Bei einer Integer-Spalte speichern Sie die Zuordnung im Anwendungscode. So würden die Abfragen bei einer SET-Spalte aussehen: mysql> CREATE TABLE acl ( -> perms SET('CAN_READ', 'CAN_WRITE', 'CAN_DELETE') NOT NULL -> ); mysql> INSERT INTO acl(perms) VALUES ('CAN_READ,CAN_DELETE'); mysql> SELECT perms FROM acl WHERE FIND_IN_SET('CAN_READ', perms); +---------------------+ | perms | +---------------------+ | CAN_READ,CAN_DELETE | +---------------------+
Hätten Sie ein Integer verwendet, könnten Sie das Beispiel folgendermaßen schreiben: mysql> SET @CAN_READ := 1 << 0, -> @CAN_WRITE := 1 << 1, -> @CAN_DELETE := 1 << 2; mysql> CREATE TABLE acl ( -> perms TINYINT UNSIGNED NOT NULL DEFAULT 0 -> ); mysql> INSERT INTO acl(perms) VALUES(@CAN_READ + @CAN_DELETE); mysql> SELECT perms FROM acl WHERE perms & @CAN_READ; +-------+ | perms | +-------+ | 5 | +-------+
Wir haben hier Variablen eingesetzt, um die Werte zu defnieren, Sie können in Ihrem Code aber auch Konstanten benutzen.
Bezeichner wählen Es ist sehr wichtig, für eine Bezeichnerspalte einen guten Datentyp zu wählen. Sie werden diese Spalten eher mit anderen Werten vergleichen (z.B. in Joins) und sie für Lookups verwenden als andere Spalten. Und vermutlich werden Sie sie in anderen Tabellen als Fremdschlüssel einsetzen, so dass Sie beim Festlegen eines Datentyps für eine Bezeichnerspalte den Typ auch in den zugehörigen Tabellen wählen werden. (Wie wir weiter vorn in diesem Kapitel demonstriert haben, bietet es sich an, in zusammengehörenden Tabellen den gleichen Datentyp zu wählen, da Ihnen dies auch bei Joins entgegenkommt.) Wenn Sie einen Typ für eine Bezeichnerspalte wählen, müssen Sie nicht nur den Speichertyp beachten, sondern auch, wie MySQL Berechnungen und Vergleiche mit diesem Typ ausführt. Beispielsweise speichert MySQL ENUM- und SET-Typen als Integer, konvertiert sie aber in Strings, wenn es Vergleiche in einem Stringumfeld durchführt.
100 | Kapitel 3: Schema-Optimierung und Indizierung
Haben Sie sich für einen Typ entschieden, dann nehmen Sie diesen Typ auch in allen zugehörigen Tabellen. Die Typen sollten genau übereinstimmen, einschließlich solcher Eigenschaften wie UNSIGNED.4 Das Mischen unterschiedlicher Datentypen kann zu Performance-Problemen führen. Und selbst dann, wenn das nicht so ist, könnten implizite Typkonvertierungen im Laufe von Vergleichen Fehler verursachen, die nur schwer zu finden sind. Solche Fehler können sogar später noch auftauchen, wenn Sie schon längst vergessen haben, dass Sie unterschiedliche Datentypen vergleichen. Wählen Sie die kleinste Größe, die Ihren erforderlichen Wertebereich aufnehmen kann, und lassen Sie notfalls Raum für ein künftiges Wachstum. Falls Sie z.B. eine state_idSpalte haben, in der die Namen von US-Bundesstaaten gespeichert sind, benötigen Sie nicht Tausende oder Millionen Werte: Nehmen Sie deshalb kein INT. Ein TINYINT sollte ausreichen und ist drei Bytes kleiner. Wenn Sie diesen Wert als Fremdschlüssel in anderen Tabellen benutzen, können drei Bytes schon einen großen Unterschied machen. Integer-Typen Integer sind normalerweise die beste Wahl für Bezeichner, da sie schnell sind und mit AUTO_INCREMENT funktionieren. ENUM und SET
Die Typen ENUM und SET stellen im Allgemeinen eine schlechte Wahl für Bezeichner dar, obwohl sie ganz gut für statische »Definitionstabellen« sein können, die Statusoder »Typ«-Werte enthalten. ENUM- und SET-Spalten eignen sich zum Aufnehmen von Informationen wie dem Status einer Bestellung, einem Produkttyp oder dem Geschlecht einer Person. Falls Sie z.B. ein ENUM-Feld benutzen, um den Typ eines Produkts zu definieren, wollen Sie möglicherweise eine Lookup-Tabelle haben, die über einen Primärschlüssel mit einem identischen ENUM-Feld verbunden ist. (Sie könnten in der Lookup-Tabelle Spalten für beschreibenden Text hinzufügen, um ein Glossar zu generieren oder um sinnvolle Bezeichnungen in einem Pulldown-Menü auf einer Website anzubieten.) In diesem Fall werden Sie ENUM als Bezeichner verwenden, für die meisten anderen Aufgaben sollten Sie dies hingegen vermeiden. Stringtypen Vermeiden Sie nach Möglichkeit Stringtypen für Bezeichner, da sie eine Menge Platz beanspruchen und im Allgemeinen langsamer sind als Integer-Typen. Seien Sie besonders vorsichtig, wenn Sie Stringbezeichner bei MyISAM-Tabellen einsetzen. MyISAM verwendet standardmäßig gepackte Indizes für Strings, wodurch sich Suchen deutlich verlangsamen. In unseren Tests haben wir mit gepackten Indizes unter MyISAM eine sechsmal langsamere Leistung festgestellt.
4 Wenn Sie die InnoDB-Storage-Engine benutzen, sind Sie möglicherweise gar nicht in der Lage, Fremdschlüssel zu erzeugen, wenn die Datentypen nicht exakt zueinander passen. Die resultierende Fehlermeldung »ERROR 1005 (HY000): Can’t create table« kann je nach Kontext verwirrend sein, und oft wird in MySQL-Mailinglisten danach gefragt. (Eigenartigerweise kann man Fremdschlüssel zwischen VARCHAR-Spalten unterschiedlicher Längen anlegen.)
Optimale Datentypen auswählen | 101
Seien Sie außerdem vorsichtig mit vollständig »zufälligen« Strings, wie sie etwa von MD5( ), SHA1( ) oder UUID( ) erzeugt werden. Jeder neue Wert, den Sie mit ihnen generieren, wird beliebig über einen großen Raum verteilt. Dadurch verlangsamen sich INSERT- und einige Arten von SELECT-Abfragen:5 • Sie verlangsamen INSERT-Abfragen, weil der eingefügte Wert an eine zufällige Stelle in den Indizes gelangen muss. Das verursacht Seitenaufteilungen, zufällige Festplattenzugriffe und eine Fragmentierung des Cluster-Index bei ClusterStorage-Engines. • Sie verlangsamen SELECT-Abfragen, weil logisch benachbarte Zeilen weit über die Festplatte und im Speicher verstreut werden. • Zufällige Werte sorgen bei allen Arten von Abfragen dafür, dass die Caches nur schlecht funktionieren, da sie die Lokalitätseigenschaft vereiteln, die für das Funktionieren von Caches verantwortlich ist. Wenn der gesamte Datensatz gleichermaßen »heiß« ist, dann bringt es keine Vorteile, bestimmte Teile der Daten im Cache abzulegen. Und wenn der Arbeitssatz nicht in den Speicher passt, dann weist der Cache viele Lücken auf. Falls Sie UUID-Werte speichern, sollten Sie die Bindestriche entfernen oder, besser noch, die UUID-Werte mit UNHEX( ) in 16-Byte-Zahlen konvertieren und diese in einer BINARY(16)-Spalte speichern. Sie können die Werte im Hexadezimalformat mit der Funktion HEX( ) abrufen. Werte, die von UUID( ) generiert wurden, haben andere Eigenschaften als solche, die von einer kryptografischen Hash-Funktion wie SHA1( ) erzeugt wurden: Die UUIDWerte sind ungleichmäßig verteilt und in gewisser Weise sequenziell. Sie sind allerdings nicht so gut wie ein monoton ansteigender Integer-Wert.
Besondere Arten von Daten Manche Arten von Daten korrespondieren nicht direkt mit den verfügbaren eingebauten Typen. Ein Zeitstempel mit einer Auflösung von unter einer Sekunde ist ein Beispiel dafür; wir haben Ihnen weiter vorn in diesem Kapitel bereits einige Möglichkeiten für die Speicherung solcher Daten gezeigt. Ein weiteres Beispiel ist eine IP-Adresse. Oft werden VARCHAR(15)-Spalten verwendet, um IP-Adressen zu speichern. Eine IP-Adresse ist aber eigentlich ein vorzeichenloser 32-BitInteger-Wert, kein String. Die Dotted-Quad-Notation stellt nur eine Möglichkeit dar, sie so zu schreiben, dass Menschen sie leichter lesen können. Sie sollten IP-Adressen als vorzeichenlose Integer-Werte speichern. MySQL bietet die Funktionen INET_ATON( ) und INET_NTOA( ) für Konvertierungen zwischen den beiden Darstellungen. Künftige Versionen von MySQL enthalten dann vielleicht einen eigenen Datentyp für IP-Adressen. 5 Andererseits können solche pseudozufälligen Werte bei sehr großen Tabellen mit vielen Schreiboperationen dabei helfen, »Hot-Spots« zu eliminieren.
102 | Kapitel 3: Schema-Optimierung und Indizierung
Hüten Sie sich vor automatisch generierten Schemata Wir haben die wichtigsten Überlegungen in Bezug auf Datentypen behandelt (einige mit ernsthaften, andere mit weniger starken Auswirkungen auf die Performance), sind aber noch nicht auf die Schrecken von automatisch generierten Schemata eingegangen. Schlecht geschriebene Schema-Migrationsprogramme und Programme, die automatisch Schemata generieren, können schwerwiegende Performance-Probleme hervorrufen. Einige Programme benutzen große VARCHAR-Felder für alles oder verwenden unterschiedliche Datentypen für Spalten, die in Joins verglichen werden. Überprüfen Sie ein Schema auf jeden Fall besonders gründlich, wenn es automatisch erzeugt wurde. Objektrelationale Abbildungssysteme (Object-relational Mapping; ORM) sowie die »Frameworks«, die sie einsetzen, sind ein weiterer Performance-Albtraum. Einige dieser Systeme erlauben es Ihnen, beliebige Arten von Daten in einem beliebigen Typ von BackendDatenspeicher zu speichern, was normalerweise bedeutet, dass sie gar nicht darauf ausgelegt sind, die Stärken dieser Datenspeicher auszunutzen. Manchmal speichern sie jede Eigenschaft eines Objekts in einer eigenen Zeile, wobei sie sogar eine zeitstempelbasierte Versionierung einsetzen, es also mehrere Versionen jeder Eigenschaft gibt! Ein solches Design mag Entwicklern gefallen, die damit objektorientiert arbeiten können, ohne darüber nachdenken zu müssen, wie die Daten gespeichert werden. Anwendungen jedoch, die ihre »Komplexität vor den Entwicklern verstecken«, skalieren normalerweise nicht besonders gut. Wir empfehlen Ihnen, genau nachzudenken, bevor Sie Leistung gegen Entwicklungsproduktivität eintauschen. Führen Sie außerdem immer Tests an realistischen Datenmengen durch, damit Ihnen Leistungsprobleme nicht zu spät auffallen.
Grundlagen der Indizierung Indizes sind Datenstrukturen, die MySQL dabei helfen, Daten effizient abzurufen. Sie sind für eine gute Leistung entscheidend, allerdings werden sie oft vergessen oder missverstanden. Indizierung bildet deshalb den Hauptgrund für viele auftretende Performance-Probleme. Deshalb behandeln wir dieses Thema bereits so früh in diesem Buch – sogar noch vor der Abfragenoptimierung. Indizes (in MySQL auch »Schlüssel« genannt) werden mit zunehmender Datenmenge immer wichtiger. Kleine, leicht ladbare Datenbanken laufen oft auch ohne passende Indizes gut. Wenn die Datenmengen dann aber größer werden, kann die Leistung sehr schnell einbrechen. Am einfachsten verstehen Sie, wie ein Index in MySQL funktioniert, wenn Sie an den Index in einem Buch denken. Um festzustellen, an welcher Stelle in einem Buch ein Thema besprochen wird, schauen Sie in den Index. Dieser verrät Ihnen die Seitenzahlen, an denen der Begriff auftaucht.
Grundlagen der Indizierung | 103
MySQL setzt Indizes auf ähnliche Weise ein. Es durchsucht die Datenstruktur eines Index nach einem Wert. Wenn es einen Treffer entdeckt, kann es die Zeile suchen, die diesen Treffer enthält. Nehmen Sie an, Sie führen folgende Abfrage durch: mysql> SELECT first_name FROM sakila.actor WHERE actor_id = 5;
Es gibt einen Index für die Spalte actor_id, MySQL verwendet also den Index, um Zeilen zu finden, deren actor_id gleich 5 ist. Mit anderen Worten, es führt eine Suche in den Werten des Index aus und liefert alle Zeilen zurück, die den angegebenen Wert enthalten. Ein Index enthält Werte aus einer oder mehreren bestimmten Spalten in einer Tabelle. Wenn Sie mehr als eine Spalte indizieren, dann ist die Spaltenreihenfolge sehr wichtig, weil MySQL nur auf einem ganz links gelegenen Präfix des Index effizient suchen kann. Wie Sie sehen werden, ist das Erzeugen eines Index über zwei Spalten nicht das Gleiche wie das Erzeugen zweier getrennter Indizes über jeweils eine Spalte.
Arten von Indizes Es gibt viele Arten von Indizes, die sich jeweils für unterschiedliche Zwecke eignen. Indizes werden auf der Storage-Engine-Ebene implementiert, nicht auf der Serverebene. Daher sind sie nicht standardisiert: Die Indizierung funktioniert in jeder Engine etwas anders, und die einzelnen Engines unterstützen auch nicht unbedingt alle Indexarten. Selbst wenn mehrere Engines den gleichen Indextyp unterstützen, implementieren sie ihn möglicherweise intern unterschiedlich. Unter dieser Maßgabe wollen wir uns nun die Indexarten anschauen, für die MySQL momentan Unterstützung bietet, und ihre Vor- und Nachteile betrachten.
B-Baum-Indizes Wenn Informatiker über Indizes reden, ohne einen Typ zu erwähnen, meinen sie mit großer Wahrscheinlichkeit einen B-Baum-Index (B-Tree-Index), der typischerweise eine B-Baum-Datenstruktur benutzt, um seine Daten zu speichern.6 Die meisten der MySQLStorage-Engines unterstützen diesen Indextyp. Eine Ausnahme bildet die ArchiveEngine: Sie hat bis MySQL 5.1 überhaupt keine Indizes unterstützt. Seitdem erlaubt sie eine einzelne indizierte AUTO_INCREMENT-Spalte. Wir verwenden den Begriff »B-Baum« für diese Indizes, weil MySQL ihn in CREATE TABLE und anderen Anweisungen verwendet. Intern können Storage-Engines jedoch auch andere Speicherstrukturen benutzen. Beispielsweise setzt die NDB Cluster-StorageEngine eine T-Baum-Datenstruktur für solche Indizes ein, nennt sie aber BTREE.
6 Viele Storage-Engines benutzen tatsächlich einen B+Baum-Index, in dem jeder Blattknoten einen Verweis auf den nächsten enthält, um ein schnelles Absteigen durch die Knoten zu ermöglichen. Ausführliche Erläuterungen zu B-Baum-Indizes finden Sie in der Informatik-Fachliteratur.
104 | Kapitel 3: Schema-Optimierung und Indizierung
Storage-Engines speichern B-Baum-Indizes auf verschiedene Weise auf der Festplatte, wodurch die Leistung beeinflusst werden kann. So benutzt etwa MyISAM eine Präfixkomprimierungstechnik, die Indizes verkleinert, während InnoDB die Indizes unkomprimiert lässt, weil es für einige seiner Optimierungen keine komprimierten Indizes benutzen kann. Außerdem verweisen MyISAM-Indizes auf die indizierten Zeilen anhand der physischen Positionen der gespeicherten Zeilen, während InnoDB auf sie anhand ihrer Primärschlüsselwerte verweist. Jede Variante hat ihre Vor- und Nachteile. Der grundsätzliche Gedanke hinter einem B-Baum besteht darin, dass alle Werte in ihrer Reihenfolge gespeichert werden und jedes Blatt den gleichen Abstand von der Wurzel aufweist. Abbildung 3-1 zeigt eine abstrakte Darstellung eines B-Baums, die ungefähr dem entspricht, wie die InnoDB-Indizes funktionieren (InnoDB verwendet eine B+BaumStruktur). MyISAM verwendet eine andere Struktur, die Prinzipien sind aber ähnlich. Wert in Seite Zeiger auf Kindseite Zeiger von höher gelegener Knotenseite
Zeiger von höher gelegener Knotenseite
Schlüssel1
SchlüsselN
Blattseite: Werte < Schlüssel1 Wert1.1 Wert1.2
Wert1.m
Zeiger auf Daten (variiert zwischen den Storage-Engines)
Verweis auf nächstes Blatt Schlüssel1 <= Wert < Schlüssel2 Wert2.1 Wert2.2
Wert2.m Werte>= SchlüsselN WertN.1 WertN.2
WertN.m
Logische Seite. Größe hängt von der Storage-Engine ab. 16K für InnoDB.
Abbildung 3-1: Ein Index, der auf einer B-Baum-Struktur (technisch gesehen, einer B+Baum-Struktur) aufbaut
Ein B-Baum-Index beschleunigt den Datenzugriff, weil die Storage-Engine nicht die gesamte Tabelle scannen muss, um die gewünschten Daten zu finden. Stattdessen beginnt sie beim Wurzelknoten (in der Abbildung nicht zu sehen). Die Positionen im Wurzelknoten enthalten Zeiger auf Kindknoten, und die Storage-Engine folgt diesen Zeigern. Sie findet den richtigen Zeiger, indem sie sich die Werte in den Knotenseiten anschaut, die die oberen und unteren Grenzen der Werte in den Kindknoten definieren. Schließlich stellt die Storage-Engine entweder fest, dass der gewünschte Wert nicht existiert, oder erreicht erfolgreich eine Blattseite.
Grundlagen der Indizierung | 105
Blattseiten sind besonders, weil sie anstelle von Zeigern auf andere Seiten Zeiger auf die indizierten Daten enthalten. (Bei den verschiedenen Storage-Engines gibt es unterschiedliche Arten von »Zeigern« auf die Daten.) Unsere Illustration zeigt nur eine Knotenseite und ihre Blattseiten, es kann aber auch viele Ebenen aus Knotenseiten zwischen der Wurzel und den Blättern geben. Die Tiefe des Baums hängt davon ab, wie groß die Tabelle ist. Da B-Bäume die indizierten Spalten der Reihe nach speichern, eignen sie sich gut zum Suchen von Datenbereichen. Beim Absteigen durch den Baum auf der Suche nach einem Index auf ein Textfeld durchläuft man z.B. die Werte in alphabetischer Reihenfolge, so dass es effizient ist, »alle zu suchen, deren Namen mit den Buchstaben I bis K beginnen«. Nehmen Sie an, Sie haben die folgende Tabelle: CREATE TABLE People ( last_name varchar(50) not null, first_name varchar(50) not null, dob date not null, gender enum('m', 'f') not null, key(last_name, first_name, dob) );
Der Index enthält für jede Zeile in der Tabelle die Werte aus den Spalten last_name, first_name und dob. Abbildung 3-2 verdeutlicht, wie der Index die Daten anordnet, die er speichert. Allen Cuba 1960-01-01
Akroyd Christian 1958-12-07
Akroyd Debbie 1990-03-18
Astaire Angelina 1980-03-04
Barrymore Julia 2000-05-16
Akroyd Kirsten 1978-11-02
Allen Cuba 1960-01-01
Allen Kim 1930-07-12
Allen Meryl 1980-12-12
B arrymore Julia 2000-05-16
Basinger Viven 1976-12-08
Basinger Vivien 1979-01-24
Abbildung 3-2: Beispieleinträge aus einem B-Baum-Index (technisch betrachtet einem B+Baum-Index)
106 | Kapitel 3: Schema-Optimierung und Indizierung
Beachten Sie, dass der Index die Werte entsprechend der Reihenfolge der Spalten anordnet, die dem Index in der CREATE TABLE-Anweisung vorgegeben wurde. Schauen Sie sich die beiden letzten Einträge an: Zwei Leute tragen den gleichen Namen, haben aber unterschiedliche Geburtsdaten. Sie wurden nach dem Geburtsdatum sortiert. Arten von Abfragen, die einen B-Baum-Index benutzen können: B-Baum-Indizes funktionieren gut bei Lookups anhand des vollständigen Schlüsselwertes, eines Schlüsselbereichs oder eines Schlüsselpräfixes. Sie sind nur dann sinnvoll, wenn der Lookup mit dem ganz links gelegenen Präfix des Index erfolgt.7 Der Index, den wir im vorherigen Abschnitt gezeigt haben, eignet sich für folgende Abfragearten: Vergleich des vollständigen Wertes Ein Vergleich des vollständigen Schlüsselwertes gibt Werte für alle Spalten in dem Index an. Dieser Index kann Ihnen z.B. helfen, eine Person namens Cuba Allen zu suchen, die am 1. Januar 1960 (1960-01-01) geboren wurde. Vergleich eines ganz links gelegenen Präfixes Dieser Index kann Ihnen helfen, alle Leute mit dem Nachnamen Allen zu finden. Dabei wird nur die erste Spalte im Index benutzt. Vergleich eines Spaltenpräfixes Sie können den ersten Teil eines Spaltenwertes vergleichen. Dieser Index kann Ihnen helfen, alle Leute zu finden, deren Nachnamen mit J beginnen. Dabei wird nur die erste Spalte in dem Index benutzt. Vergleich eines Wertebereichs Dieser Index kann Ihnen helfen, Leute zu finden, deren Nachnamen zwischen Allen und Barrymore liegen. Dabei wird auch nur die erste Spalte benutzt. Ein Teil wird exakt verglichen, bei einem anderen Teil wird ein Bereich verglichen Dieser Index kann Ihnen helfen, alle Leute zu finden, deren Nachname Allen ist und deren Vornamen mit dem Buchstaben K beginnen (Kim, Karl usw.). Dabei handelt es sich um einen exakten Vergleich auf last_name und eine Bereichsabfrage auf first_name. Auf den Index beschränkte Abfragen B-Baum-Indizes können normalerweise Abfragen unterstützen, die auf den Index beschränkt sind. Das sind Abfragen, die nur auf den Index, nicht jedoch auf den Zeilenspeicher zugreifen. Wir besprechen diese Optimierung in »Abdeckende Indizes« auf Seite 128. Da die Knoten des Baums sortiert sind, können sie sowohl für Lookups (zum Auffinden von Werten) als auch für ORDER BY-Abfragen (zum Auffinden von Werten in einer sortierten Reihenfolge) benutzt werden. Falls ein B-Baum Ihnen dabei helfen kann, eine Zeile auf eine bestimmte Weise zu finden, kann er Ihnen im Allgemeinen auch dabei helfen, 7 Das ist MySQL- und sogar versionsspezifisch. Andere Datenbanken können nichtführende Indexteile benutzen, obwohl es normalerweise effizienter ist, ein vollständiges Präfix zu verwenden. Vielleicht bietet MySQL diese Möglichkeit in Zukunft; wir zeigen Ihnen weiter hinten in diesem Kapitel, wie Sie das umgehen.
Grundlagen der Indizierung | 107
Zeilen nach dem gleichen Kriterium zu sortieren. Unser Index ist also auch für ORDER BYKlauseln hilfreich, die alle Arten von Lookups erfassen, die wir gerade aufgeführt haben. Hier sind einige Beschränkungen von B-Baum-Indizes: • Sie sind nicht geeignet, wenn die Suche nicht auf der ganz linken Seite der indizierten Spalten beginnt. So würde Ihnen z.B. dieser Index nicht dabei helfen, alle Leute namens Bill zu finden oder alle Leute, die an einem bestimmten Datum geboren wurden, weil diese Spalten nicht ganz links im Index liegen. Sie können den Index auch nicht dazu benutzen, um Leute zu finden, deren Nachname mit einem bestimmten Buchstaben endet. • Sie können Spalten im Index nicht überspringen. Das heißt, Sie haben keine Möglichkeit, alle Leute zu finden, deren Nachname Smith lautet und die an einem bestimmten Datum geboren wurden. Wenn Sie keinen Wert für die Spalte first_name festlegen, kann MySQL nur die erste Spalte des Index verwenden. • Die Storage-Engine kann Zugriffe mit Spalten rechts der ersten Bereichsbedingung nicht optimieren. Würde z.B. Ihre Abfrage WHERE last_name="Smith" AND first_name LIKE 'J%' AND dob='1976-12-23' lauten, dann würde der Index nur auf die ersten beiden Spalten in dem Index zugreifen, weil LIKE eine Bereichsbedingung ist (der Server kann die anderen Spalten allerdings für andere Zwecke verwenden). Bei einer Spalte, die nur eine begrenzte Anzahl von Werten aufweist, können Sie das oft umgehen, indem Sie Gleichheitsbedingungen anstelle von Bereichsbedingungen festlegen. Wir zeigen weiter hinten in diesem Kapitel in der Indizierungsfallstudie ausführliche Beispiele dafür. Jetzt wissen Sie, wieso wir behauptet haben, dass die Spaltenreihenfolge ausgesprochen wichtig ist: Diese Beschränkungen haben alle mit der Spaltenreihenfolge zu tun. Für Hochleistungsanwendungen müssen Sie möglicherweise Indizes mit den gleichen Spalten in unterschiedlichen Anordnungen herstellen, um Ihre Abfragen zu bedienen. Einige dieser Beschränkungen sind nicht spezifisch für B-Baum-Indizes, sondern sind das Ergebnis dessen, wie der MySQL-Abfrageoptimierer und die Storage-Engines Indizes benutzen. Möglicherweise verschwinden einige von ihnen in Zukunft.
Hash-Indizes Ein Hash-Index wird aus einer Hash-Tabelle erzeugt und eignet sich nur für exakte Lookups, die jede Spalte in dem Index benutzen.8 Für jede Zeile berechnet die StorageEngine einen Hash-Code der indizierten Spalten, bei dem es sich um einen kleinen Wert handelt, der sich wahrscheinlich von den Hash-Codes unterscheidet, die für andere Zeilen mit anderen Schlüsselwerten berechnet wurden. Sie legt die Hash-Codes im Index ab und speichert einen Zeiger auf jede Zeile in einer Hash-Tabelle.
8 In der Informatik-Fachliteratur finden Sie mehr Informationen über Hash-Tabellen.
108 | Kapitel 3: Schema-Optimierung und Indizierung
In MySQL unterstützt nur die Memory-Storage-Engine explizite Hash-Indizes. Diese bilden den Standard-Index-Typ für Memory-Tabellen, obwohl Memory-Tabellen auch B-Baum-Indizes besitzen können. Die Memory-Engine unterstützt uneindeutige HashIndizes, was in der Welt der Datenbanken ungewöhnlich ist. Wenn mehrere Werte denselben Hash-Code aufweisen, speichert der Index ihre Zeilenzeiger im gleichen HashTabelleneintrag mittels einer verknüpften Liste. Hier ist ein Beispiel. Nehmen Sie an, wir haben die folgende Tabelle: CREATE TABLE testhash ( fname VARCHAR(50) NOT NULL, lname VARCHAR(50) NOT NULL, KEY USING HASH(fname) ) ENGINE=MEMORY;
mit diesen Daten: mysql> SELECT * FROM testhash; +--------+-----------+ | fname | lname | +--------+-----------+ | Arjen | Lentz | | Baron | Schwartz | | Peter | Zaitsev | | Vadim | Tkachenko | +--------+-----------+
Nehmen Sie nun noch an, der Index verwendet eine imaginäre Hash-Funktion namens f( ), die folgende Werte zurückliefert (das sind nur Beispiele, keine echten Werte): f('Arjen') f('Baron') f('Peter') f('Vadim')
= = = =
2323 7437 8784 2458
Die Datenstruktur des Index sieht dann so aus: Slot
Wert
2323
Zeiger auf Zeile 1
2458
Zeiger auf Zeile 4
7437
Zeiger auf Zeile 2
8784
Zeiger auf Zeile 3
Beachten Sie, dass die Slots sortiert sind, die Zeilen aber nicht. Wenn wir nun folgende Abfrage durchführen mysql> SELECT lname FROM testhash WHERE fname='Peter';
berechnet MySQL den Hash von 'Peter' und verwendet diesen, um den Zeiger im Index nachzuschlagen. Da f('Peter') = 8784 ist, sucht MySQL im Index nach 8784 und findet den Zeiger auf Zeile 3. Der letzte Schritt besteht darin, den Wert in Zeile 3 mit 'Peter' zu vergleichen, um sicherzustellen, dass dies die richtige Zeile ist.
Grundlagen der Indizierung | 109
Weil die Indizes selbst nur kurze Hash-Werte speichern, sind Hash-Indizes sehr kompakt. Die Länge des Hash-Wertes ist nicht von der Art der Spalten abhängig, die man indiziert – ein Hash-Index eines TINYINT ist genauso groß wie ein Hash-Index einer Spalte mit vielen Zeichen. Aus diesem Grund verlaufen Lookups normalerweise blitzschnell. Allerdings unterliegen Hash-Indizes einigen Einschränkungen: • Da der Index anstelle der Werte selbst nur Hash-Codes und Zeilenzeiger enthält, kann MySQL die Werte in dem Index nicht anstelle der Zeilen lesen. Glücklicherweise ist der Zugriff auf die Zeilen, die sich im Speicher befinden, sehr schnell, so dass die Leistung meist nicht leidet. • MySQL kann Hash-Indizes nicht für die Sortierung benutzen, da die Zeilen nicht in sortierter Reihenfolge gespeichert werden. • Hash-Indizes bieten keine Unterstützung für teilweise Schlüsselvergleiche, da sie den Hash aus dem gesamten indizierten Wert berechnen. Das heißt: Wenn Sie einen Index von (A,B) haben und die WHERE-Klausel Ihrer Abfrage sich nur auf A bezieht, hilft Ihnen der Index nicht weiter. • Hash-Indizes unterstützen nur Gleichheitsvergleiche mit den Operatoren =, IN( ) und <=> (beachten Sie, dass die Operatoren <> und <=> nicht identisch sind). Sie können Bereichsabfragen, wie etwa WHERE preis > 100 nicht beschleunigen. • Der Zugriff auf die Daten in einem Hash-Index ist sehr schnell, es sei denn, es gibt viele Kollisionen (mehrere Werte mit dem gleichen Hash). Wenn es Kollisionen gibt, muss die Storage-Engine jedem Zeilenzeiger in der verknüpften Liste folgen und seinen Wert mit dem Lookup-Wert vergleichen, um die richtige(n) Zeile(n) zu ermitteln. • Manche Operationen zur Indexpflege können langsam sein, wenn es viele HashKollisionen gibt. Falls Sie z.B. einen Hash-Index aus einer Spalte mit einer sehr niedrigen Selektivität (also mit vielen Hash-Kollisionen) erzeugen und dann eine Zeile aus der Tabelle löschen, kann es sehr aufwendig sein, den Zeiger aus dem Index auf diese Zeile zu finden. Die Storage-Engine muss jede Zeile in der verknüpften Liste dieses Hash-Schlüssels untersuchen, um die Referenz auf die eine Zeile, die Sie gelöscht haben, zu finden und zu entfernen. Aufgrund dieser Beschränkungen sind Hash-Indizes nur in Sonderfällen von Nutzen. Wenn Sie allerdings den Anforderungen der Anwendung entsprechen, können sie die Leistung deutlich steigern. Ein Beispiel sind Data-Warehousing-Anwendungen, bei denen ein klassisches »Stern«-Schema viele Joins in Lookup-Tabellen erfordert. HashIndizes sind genau das, was eine Lookup-Tabelle braucht. Zusätzlich zu den expliziten Hash-Indizes der Memory-Storage-Engine unterstützt die NDB Cluster-Storage-Engine eindeutige Hash-Indizes. Ihre Funktionalität ist spezifisch für die NDB Cluster-Storage-Engine, die wir in diesem Buch nicht behandeln.
110 | Kapitel 3: Schema-Optimierung und Indizierung
Die InnoDB-Storage-Engine besitzt eine besondere Eigenschaft namens adaptive HashIndizes. Wenn InnoDB bemerkt, dass auf einige Indexwerte besonders oft zugegriffen wird, baut es für sie im Speicher auf B-Baum-Indizes einen Hash-Index auf. Dies verleiht seinen B-Baum-Indizes einige Eigenschaften der Hash-Indizes, wie etwa sehr schnelle Hash-Lookups. Das funktioniert völlig automatisch, man kann es nicht kontrollieren oder konfigurieren. Eigene Hash-Indizes aufbauen: Falls Ihre Storage-Engine keine Hash-Indizes unterstützt, können Sie sie emulieren, und zwar etwa so wie bei InnoDB. Dadurch haben Sie Zugriff auf einige der wünschenswerten Eigenschaften von Hash-Indizes, z.B. sehr kleine Indexgrößen bei sehr langen Schlüsseln. Die Idee dahinter ist einfach: Sie erzeugen einen Pseudohash-Index auf einem normalen B-Baum-Index. Das ist zwar nicht ganz identisch mit einem echten Hash-Index, weil für Lookups immer noch der B-Baum-Index benutzt wird. Allerdings werden für die Lookups die Hash-Werte der Schlüssel verwendet und nicht die Schlüssel selbst. Sie müssen nur manuell die Hash-Funktion in der WHERE-Klausel der Abfrage angeben. Dieser Ansatz funktioniert z.B. gut für URL-Lookups. URLs sorgen aufgrund ihrer großen Länge im Allgemeinen dafür, dass B-Baum-Indizes riesig werden. Normalerweise würden Sie eine Tabelle mit URLs so abfragen: mysql> SELECT id FROM url WHERE url="http://www.mysql.com";
Falls Sie allerdings den Index der url-Spalte entfernen und der Tabelle eine indizierte url_crc-Spalte hinzufügen, können Sie eine solche Abfrage einsetzen: mysql> SELECT id FROM url WHERE url="http://www.mysql.com" -> AND url_crc=CRC32("http://www.mysql.com");
Das funktioniert, weil der MySQL-Abfrageoptimierer bemerkt, dass es einen kleinen, ausgesprochen selektiven Index in der Spalte url_crc gibt, und einen Index-Lookup für Einträge mit diesem Wert (1560514994, in diesem Fall) durchführt. Selbst wenn mehrere Zeilen den gleichen url_crc-Wert besitzen, ist es sehr einfach, diese Zeilen mit einem schnellen Integer-Vergleich zu ermitteln und zu untersuchen, um die Zeile zu finden, die exakt der vollständigen URL entspricht. Die Alternative besteht darin, die komplette URL als String zu indizieren, was viel langsamer ist. Ein Nachteil dieses Ansatzes ist die Notwendigkeit, die Hash-Werte zu pflegen. Sie können dies manuell erledigen oder seit MySQL 5.0 Trigger einsetzen. Das folgende Beispiel zeigt, wie Trigger die Pflege der url_crc-Spalte beim Einsetzen und Aktualisieren von Werten unterstützen. Zuerst legen wir die Tabelle an: CREATE TABLE pseudohash ( id int unsigned NOT NULL auto_increment, url varchar(255) NOT NULL, url_crc int unsigned NOT NULL DEFAULT 0, PRIMARY KEY(id) );
Grundlagen der Indizierung | 111
Jetzt erzeugen wir die Trigger. Wir ändern zeitweise das Trennzeichen für die Anweisungen, damit wir als Trennzeichen für den Trigger ein Semikolon benutzen können: DELIMITER | CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url); END; | CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url); END; | DELIMITER ;
Jetzt müssen wir uns nur noch vergewissern, dass der Trigger den Hash pflegt: mysql> INSERT INTO pseudohash (url) VALUES ('http://www.mysql.com'); mysql> SELECT * FROM pseudohash; +----+----------------------+------------+ | id | url | url_crc | +----+----------------------+------------+ | 1 | http://www.mysql.com | 1560514994 | +----+----------------------+------------+ mysql> UPDATE pseudohash SET url='http://www.mysql.com/' WHERE id=1; mysql> SELECT * FROM pseudohash; +----+-----------------------+------------+ | id | url | url_crc | +----+-----------------------+------------+ | 1 | http://www.mysql.com/ | 1558250469 | +----+-----------------------+------------+
Bei diesem Ansatz sollten Sie keine SHA1( )- oder MD5( )-Hash-Funktionen verwenden. Diese liefern sehr lange Strings zurück, die Platz verschwenden und Vergleiche verlangsamen. Es handelt sich bei ihnen um kryptografisch starke Funktionen, die dazu gedacht sind, Kollisionen virtuell zu eliminieren, was hier aber nicht unser Ziel ist. Einfache Hash-Funktionen können eine bessere Leistung bei akzeptablen Kollisionsraten bieten. Besitzt Ihre Tabelle viele Zeilen und verursacht CRC32( ) zu viele Kollisionen, dann implementieren Sie doch Ihre eigene 64-Bit-Hash-Funktion. Achten Sie darauf, dass die Funktion einen Integer und keinen String zurückliefert. Zum Implementieren einer 64-BitHash-Funktion könnten Sie z.B. nur einen Teil des Wertes verwenden, der von MD5( ) zurückgeliefert wird. Das ist wahrscheinlich nicht so effizient, als wenn Sie Ihre eigene Routine als benutzerdefinierte Funktion schreiben (siehe »Benutzerdefinierte Funktionen« auf Seite 248), geht aber auch: mysql> SELECT CONV(RIGHT(MD5('http://www.mysql.com/'), 16), 16, 10) AS HASH64; +---------------------+ | HASH64 | +---------------------+ | 9761173720318281581 | +---------------------+
112 | Kapitel 3: Schema-Optimierung und Indizierung
Maatkit (http://maatkit.sourceforge.net) enthält eine benutzerdefinierte Funktion, die einen Fowler/Noll/Vo-64-Bit-Hash implementiert, der sehr schnell ist. Mit Hash-Kollisionen zurechtkommen: Wenn Sie einen Wert anhand seines Hashs suchen, müssen Sie auch den eigentlichen Wert in Ihre WHERE-Klausel aufnehmen: mysql> SELECT id FROM url WHERE url_crc=CRC32("http://www.mysql.com") -> AND url="http://www.mysql.com";
Die folgende Abfrage funktioniert nicht richtig, denn wenn eine andere URL den CRC32( )-Wert 1560514994 hat, liefert die Abfrage beide Zeilen zurück: mysql> SELECT id FROM url WHERE url_crc=CRC32("http://www.mysql.com");
Die Wahrscheinlichkeit einer Hash-Kollision wächst aufgrund des sogenannten Geburtstagsparadoxon viel schneller, als man glaubt. CRC32( ) liefert einen 32-Bit-Integer-Wert zurück, so dass die Wahrscheinlichkeit einer Kollision schon bei nur 93.000 Werten bei 1 % liegt. Um das zu verdeutlichen, luden wir alle Wörter aus /usr/share/dict/words zusammen mit ihren CRC32( )-Werten in eine Tabelle, die daraufhin 98.569 Zeilen umfasste. In dieser Datenmenge gibt es bereits eine Kollision! Die Kollision sorgt dafür, dass die folgende Abfrage mehr als eine Zeile zurückliefert: mysql> SELECT word, crc FROM words WHERE crc = CRC32('gnu'); +---------+------------+ | word | crc | +---------+------------+ | codding | 1774765869 | | gnu | 1774765869 | +---------+------------+
Die korrekte Abfrage sieht so aus: mysql> SELECT word, crc FROM words WHERE crc = CRC32('gnu') AND word = 'gnu'; +------+------------+ | word | crc | +------+------------+ | gnu | 1774765869 | +------+------------+
Um Probleme mit Kollisionen zu vermeiden, müssen Sie beide Bedingungen in der WHEREKlausel angeben. Wenn Kollisionen kein Problem darstellen – etwa weil Sie statistische Abfragen durchführen und keine exakten Ergebnisse benötigen –, können Sie sie vereinfachen und eine gewisse Effizienz erreichen, indem Sie nur den CRC32( )-Wert in der WHERE-Klausel verwenden.
Räumliche (R-Baum-)Indizes MyISAM unterstützt räumliche (auch: mehrdimensionale) Indizes, die Sie mit georäumlichen Typen wie GEOMETRY benutzen können. Im Gegensatz zu B-Baum-Indizes verlangen räumliche Indizes nicht, dass Ihre WHERE-Klauseln auf dem ganz links gelegenen Präfix des Index operieren. Sie indizieren die Daten gleichzeitig in allen Dimensionen. Dadurch
Grundlagen der Indizierung | 113
können Lookups effizient eine Kombination der Dimensionen einsetzen. Damit das funktioniert, müssen Sie jedoch die MySQL-GIS-Funktionen verwenden, wie z.B. MBRCONTAINS( ).
Volltextindizes FULLTEXT ist ein besonderer Indextyp für MyISAM-Tabellen. Er sucht Schlüsselwörter im
Text, anstatt Werte direkt mit den Werten im Index zu vergleichen. Die Volltextsuche unterscheidet sich völlig von anderen Arten von Vergleichen. Sie verfügt über viele Feinheiten, wie Stoppwörter, Wortstämme und Pluralformen sowie Boolesche Suchen. Das ist eher analog zu dem, was eine Suchmaschine tut, als zu simplen WHERE-Parametervergleichen. Das Vorhandensein eines Volltextindex in einer Spalte mindert nicht den Wert eines B-Baum-Index in der gleichen Spalte. Volltextindizes sind für MATCH AGAINST-Operationen gedacht, nicht für ordinäre WHERE-Klauseloperationen. Wir besprechen die Volltextindizierung in »Volltextsuche« auf Seite 263 näher.
Indizierungsstrategien für High Performance Um eine gute Abfrageleistung zu erreichen, ist es wesentlich, dass man korrekte Indizes erzeugt und sie richtig anwendet. Wir haben die verschiedenen Arten von Indizes vorgestellt und ihre Stärken und Schwächen erkundet. Jetzt wollen wir einmal schauen, wie man die Stärke von Indizes wirklich ausnutzen kann. Es gibt viele Möglichkeiten, Indizes auszuwählen und effektiv zu benutzen, weil es viele Optimierungen für Spezialfälle und spezialisierte Verhaltensweisen gibt. Mit der Zeit werden Sie lernen, was Sie wann benutzen sollten und wie Sie die Folgen Ihrer Wahl auf die Leistungsfähigkeit zu bewerten haben. Die folgenden Abschnitte helfen Ihnen dabei, zu verstehen, wie Sie Indizes effektiv einsetzen. Vergessen Sie allerdings nicht, Benchmark-Tests durchzuführen!
Die Spalte isolieren MySQL kann im Allgemeinen nur dann Indizes aus Spalten erzeugen, wenn die Spalten in der Abfrage isoliert werden. »Isolieren« der Spalte bedeutet, dass diese nicht Teil eines Ausdrucks sein oder sich innerhalb einer Funktion in der Abfrage befinden sollte. Hier ist z.B. eine Abfrage, die den Index auf actor_id nicht benutzen kann: mysql> SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
Ein Mensch kann leicht erkennen, dass die WHERE-Klausel äquivalent zu actor_id = 4 ist, MySQL dagegen kann die Gleichung für actor_id nicht lösen. Das müssen Sie erledigen. Sie müssen sich angewöhnen, Ihre WHERE-Kriterien zu vereinfachen, damit die indizierte Spalte allein auf einer Seite des Vergleichsoperators bleibt.
114 | Kapitel 3: Schema-Optimierung und Indizierung
Hier ist ein weiteres Beispiel für einen verbreiteten Fehler: mysql> SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;
Diese Abfrage sucht alle Zeilen, bei denen der Wert date_col jünger als 10 Tage ist, benutzt aufgrund der Funktion TO_DAYS( ) aber keine Indizes. So formulieren Sie diese Abfrage besser: mysql> SELECT ... WHERE date_col >= DATE_SUB(CURRENT_DATE, INTERVAL 10 DAY);
Diese Abfrage kann problemlos einen Index verwenden, bietet aber noch Raum für Verbesserungen. Der Verweis auf CURRENT_DATE verhindert, dass der Abfrage-Cache die Ergebnisse speichert. Sie können CURRENT_DATE durch ein Literal ersetzen, um dieses Problem zu beheben: mysql> SELECT ... WHERE date_col >= DATE_SUB('2008-01-17', INTERVAL 10 DAY);
In Kapitel 5 finden Sie weitere Einzelheiten zum Abfrage-Cache.
Präfixindizes und Index-Selektivität Manchmal muss man sehr lange Zeichenspalten indizieren, was die Indizes groß und schwerfällig macht. Eine Strategie besteht darin, einen Hash-Index zu simulieren, wie wir bereits weiter vorn in diesem Kapitel gezeigt haben. Manchmal ist das aber nicht gut genug. Was tun? Oft spart man Platz und erzielt eine gute Leistung, wenn man nur die ersten Zeichen anstelle des ganzen Wertes indiziert. Die Indizes benötigen dann weniger Platz, sie sind aber auch weniger selektiv. Indexselektivität bezeichnet das Verhältnis aus der Anzahl der verschiedenen indizierten Werte (Kardinalität) und der Gesamtanzahl der Zeilen in der Tabelle (#T) und reicht von 1/#T bis 1. Ein stark selektiver Index ist gut, weil er MySQL bei seiner Suche nach Treffern bereits mehr Zeilen ausfiltern lässt. Ein eindeutiger Index hat eine Selektivität von 1, besser geht es nicht. Ein Präfix für die Spalte bietet häufig eine ausreichende Selektivität für eine gute Leistung. Wenn Sie BLOB- oder TEXT-Spalten oder sehr lange VARCHAR-Spalten indizieren, dann müssen Sie Präfixindizes definieren, weil MySQL es nicht erlaubt, die vollständige Länge dieser Spalten zu indizieren. Der Trick besteht darin, ein Präfix zu wählen, das lang genug ist, um eine gute Selektivität zu gewährleisten, aber kurz genug, um Platz zu sparen. Das Präfix muss so lang sein, dass der Index fast so nützlich ist, als hätte man die ganze Spalte indiziert. Die Kardinalität des Präfix soll mit anderen Worten fast der Kardinalität der kompletten Spalte entsprechen. Um eine gute Präfixlänge festzulegen, suchen Sie die am häufigsten auftretenden Werte und vergleichen sie mit einer Liste der am häufigsten auftretenden Präfixe. Es gibt keine gute Tabelle, um das in der Sakila-Beispieldatenbank zu demonstrieren, weshalb wir eine aus der city-Tabelle ableiten, damit wir genügend Daten zum Arbeiten haben:
Indizierungsstrategien für High Performance | 115
CREATE TABLE sakila.city_demo(city VARCHAR(50) NOT NULL); INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city; -- Repeat the next statement five times: INSERT INTO sakila.city_demo(city) SELECT city FROM sakila.city_demo; -- Now randomize the distribution (inefficiently but conveniently): UPDATE sakila.city_demo SET city = (SELECT city FROM sakila.city ORDER BY RAND( ) LIMIT 1);
Jetzt haben wir einen Beispieldatensatz. Die Ergebnisse sind nicht realistisch verteilt, außerdem benutzen wir RAND( ), so dass Ihre Ergebnisse anders aussehen werden, aber das spielt bei dieser Übung keine Rolle. Zuerst suchen wir die am häufigsten auftretenden Städte: mysql> SELECT COUNT(*) AS cnt, city -> FROM sakila.city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10; +-----+----------------+ | cnt | city | +-----+----------------+ | 65 | London | | 49 | Hiroshima | | 48 | Teboksary | | 48 | Pak Kret | | 48 | Yaound | | 47 | Tel Aviv-Jaffa | | 47 | Shimoga | | 45 | Cabuyao | | 45 | Callao | | 45 | Bislig | +-----+----------------+
Sie sehen, dass jeder Wert ungefähr 45- bis 65-mal auftritt. Jetzt suchen wir die am häufigsten auftretenden Stadtnamenpräfixe. Wir beginnen hierbei mit dreibuchstabigen Präfixen: mysql> SELECT COUNT(*) AS cnt, LEFT(city, 3) AS pref -> FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10; +-----+------+ | cnt | pref | +-----+------+ | 483 | San | | 195 | Cha | | 177 | Tan | | 167 | Sou | | 163 | al- | | 163 | Sal | | 146 | Shi | | 136 | Hal | | 130 | Val | | 129 | Bat | +-----+------+
Die einzelnen Präfixe tauchen viel häufiger auf, so dass es viel weniger eindeutige Präfixe als eindeutige Städtenamen vollständiger Länge gibt. Der Grundgedanke besteht darin, dass die Präfixlänge erhöht werden soll, bis das Präfix fast so selektiv ist wie die vollständige Länge der Spalte. Ein wenig Herumprobieren ergibt, dass 7 ein guter Wert ist: 116 | Kapitel 3: Schema-Optimierung und Indizierung
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref -> FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10; +-----+---------+ | cnt | pref | +-----+---------+ | 70 | Santiag | | 68 | San Fel | | 65 | London | | 61 | Valle d | | 49 | Hiroshi | | 48 | Teboksa | | 48 | Pak Kre | | 48 | Yaound | | 47 | Tel Avi | | 47 | Shimoga | +-----+---------+
Eine weitere Möglichkeit zum Berechnen einer guten Präfixlänge besteht darin, die Selektivität der vollständigen Spalte zu berechnen und zu versuchen, die Selektivität des Präfix an diesen Wert anzunähern. So ermitteln Sie die Selektivität der kompletten Spalte: mysql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo; +-------------------------------+ | COUNT(DISTINCT city)/COUNT(*) | +-------------------------------+ | 0.0312 | +-------------------------------+
Das Präfix wird im Durchschnitt ungefähr so gut, wenn wir eine Selektivität nahe .031 anstreben. Es ist möglich, viele verschiedene Längen in einer Abfrage zu bewerten, was sich bei sehr großen Tabellen als sinnvoll erweist. So ermittelt man die Selektivität mehrerer Präfixlängen in einer Abfrage: mysql> SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3, -> COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4, -> COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5, -> COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS sel6, -> COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7 -> FROM sakila.city_demo; +--------+--------+--------+--------+--------+ | sel3 | sel4 | sel5 | sel6 | sel7 | +--------+--------+--------+--------+--------+ | 0.0239 | 0.0293 | 0.0305 | 0.0309 | 0.0310 | +--------+--------+--------+--------+--------+
Diese Abfrage zeigt, dass bei Zunahme der Präfixlängen bis hin zu sieben Zeichen die Verbesserungen schrittweise immer kleiner werden. Es reicht nicht, sich nur die durchschnittliche Selektivität anzuschauen. Sie müssen auch über die Worst-Case-Selektivität denken. Die durchschnittliche Selektivität macht Sie vielleicht glauben, dass ein Präfix mit vier oder fünf Zeichen gut genug ist, aber wenn Ihre Daten sehr ungleichmäßig sind, könnte sich das als Falle herausstellen. Wenn Sie sich die Anzahl der Vorkommen der verbreitetsten Präfixe von Städtenamen mit dem Wert 4 anschauen, dann springt die Ungleichmäßigkeit förmlich ins Auge: Indizierungsstrategien für High Performance | 117
mysql> SELECT COUNT(*) AS cnt, LEFT(city, 4) AS pref -> FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 5; +-----+------+ | cnt | pref | +-----+------+ | 205 | San | | 200 | Sant | | 135 | Sout | | 104 | Chan | | 91 | Toul | +-----+------+
Bei vier Zeichen treten die häufigsten Präfixe etwas öfter auf als die häufigsten Werte voller Länge. Das bedeutet, dass die Selektivität mit diesen Werten niedriger ist als die durchschnittliche Selektivität. Wenn Sie einen realistischeren Datensatz haben als dieses zufällig erzeugte Beispiel, wird dieser Effekt wahrscheinlich noch offensichtlicher. So würde etwa der Aufbau eines Index aus echten Städtenamen mit Vier-Zeichen-Präfixen eine ungeheuerliche Selektivität bei Städten ergeben, die mit »San« und »New« beginnen, da es von ihnen so viele gibt. Nachdem wir einen guten Wert für unsere Beispieldaten gefunden haben, erfahren Sie nun, wie man einen Präfixindex aus der Spalte erzeugt: mysql> ALTER TABLE sakila.city_demo ADD KEY (city(7));
Mit Präfixindizes können Indizes kleiner und schneller werden, sie bringen aber auch Nachteile mit sich: MySQL kann Präfixindizes nicht für ORDER BY- oder GROUP BY-Abfragen verwenden noch kann es sie als abdeckende Indizes (Covering Index) einsetzen. Manchmal sind Suffixindizes sinnvoll (z.B. zum Ermitteln aller E-MailAdressen einer bestimmten Domain). MySQL unterstützt von sich aus keine umgekehrten Indizes, allerdings können Sie einen umgekehrten String speichern und ein Präfix davon indizieren. Sie pflegen den Index dann z.B. mit Triggern; siehe »Eigene Hash-Indizes aufbauen« auf Seite 111 weiter vorn in diesem Kapitel.
Cluster-Indizes Cluster-Indizes9 bilden keine eigene Art von Indizes, sondern stellen einen Ansatz zur Datenspeicherung dar. Die genauen Einzelheiten variieren zwischen den Implementierungen. Die Cluster-Indizes von InnoDB speichern allerdings tatsächlich einen B-BaumIndex und die Zeilen zusammen in der gleichen Struktur. Wenn eine Tabelle einen Cluster-Index besitzt, werden ihre Zeilen eigentlich in den Blattseiten des Index gespeichert. Der Begriff »Cluster« bezieht sich auf die Tatsache, dass Zeilen mit aufeinanderfolgenden Schlüsselwerten dicht nebeneinander gespeichert
9 Oracle-Benutzern ist der Begriff »indexorganisierte Tabelle« vertraut, der das Gleiche bedeutet.
118 | Kapitel 3: Schema-Optimierung und Indizierung
werden.10 Pro Tabelle kann es nur einen Cluster-Index geben, da es nicht möglich ist, die Zeilen an zwei Stellen auf einmal zu speichern. (Allerdings können Sie mit abdeckenden Indizes mehrere Cluster-Indizes emulieren; mehr dazu später.) Da die Storage-Engines für das Implementieren der Indizes verantwortlich sind, werden sie nicht von allen Storage-Engines unterstützt. Momentan sind solidDB und InnoDB die einzigen, die es tun. Wir konzentrieren uns in diesem Abschnitt auf InnoDB; die Prinzipien, die wir diskutieren, werden aber wahrscheinlich zumindest teilweise für alle Storage-Engines gelten, die Cluster-Indizes jetzt oder in Zukunft unterstützen. Abbildung 3-3 zeigt, wie die Datensätze in einem Cluster-Index angeordnet sind. Wie Sie sehen, enthalten die Blattseiten vollständige Zeilen, die Knotenseiten dagegen nur die indizierten Spalten. In diesem Fall enthält die indizierte Spalte Integer-Werte. 11
1
2
10
Akroyd Christian 1958-12-07
Akroyd Debbie 1990-03-18
Akroyd Kirsten 1978-11-02
21
91
11
12
20
Allen Cuba 1960-01-01
Allen Kim 1930-07-12
Allen Meryl 1980-12-12
91
92
100
Barrymore Julia 2000-05-16
Basinger Vivien 1976-12-08
Basinger Viven 1979-01-24
Abbildung 3-3: Layout von Indexdaten, die in Clustern zusammengefasst sind
Einige Datenbankserver erlauben es Ihnen auszuwählen, welcher Index »geclustert« werden soll, bei den MySQL-Storage-Engines ist das momentan allerdings nicht der Fall. InnoDB packt die Daten anhand des Primärschlüssels in Clustern zusammen. Das bedeutet, dass die »indizierte Spalte« in Abbildung 3-3 die Primärschlüsselspalte ist.
10 Das stimmt nicht immer, wie Sie gleich sehen werden.
Indizierungsstrategien für High Performance | 119
Falls Sie keinen Primärschlüssel definieren, versucht InnoDB stattdessen, einen eindeutigen non-nullable Index zu verwenden. Wenn es keinen solchen Index gibt, definiert InnoDB einen verborgenen Primärschlüssel für Sie und erzeugt die Cluster daraus.11 InnoDB fasst nur Datensätze innerhalb einer Seite zusammen. Zwischen Seiten mit aufeinanderfolgenden Schlüsselwerten kann es einen Abstand geben. Ein zu einem Cluster zusammengepackter Primärschlüssel kann die Leistung verbessern, aber auch zu ernsthaften Leistungsproblemen führen. Sie sollten daher ernsthaft darüber nachdenken, ob Sie mit einem Cluster-Index arbeiten wollen, vor allem, wenn Sie die Storage-Engine einer Tabelle von InnoDB in etwas anderes ändern oder umgekehrt. Das Zusammenpacken von Daten zu Clustern bringt einige wichtige Vorteile mit sich: • Sie können zusammengehörende Daten zusammenhalten. Wenn Sie z.B. eine Mailbox implementieren, dann erzeugen Sie den Cluster anhand von user_id. Auf diese Weise beziehen Sie alle Nachrichten eines Benutzers, indem Sie nur einige Seiten von der Festplatte holen. Ohne Cluster erfordert jede Nachricht ihren eigenen Festplattenzugriff. • Der Datenzugriff erfolgt schnell. Ein Cluster-Index enthält sowohl den Index als auch die Daten zusammen in einem B-Baum, so dass das Beziehen von Zeilen aus einem Cluster-Index normalerweise schneller geht als ein vergleichbarer Lookup in einem Index ohne Cluster. • Abfragen, die abdeckende Indizes verwenden, können die Primärschlüsselwerte benutzen, die im Blattknoten enthalten sind. Diese Vorteile können die Leistung deutlich ankurbeln, wenn Sie Ihre Tabellen und Abfragen so gestalten, dass sie sie ausnutzen. Cluster-Indizes haben aber auch Nachteile: • Das Anlegen von Clustern bringt die größte Verbesserung für ein-/ausgabegebundene Operationen. Wenn die Daten in den Speicher passen, dann spielt die Reihenfolge, in der auf sie zugegriffen wird, eigentlich keine Rolle, und Cluster sind kaum sinnvoll. • Die Einfügegeschwindigkeiten hängen stark von der Einfügereihenfolge ab. Das Einfügen von Zeilen in der Reihenfolge der Primärschlüssel bildet die schnellste Möglichkeit, um Daten in eine InnoDB-Tabelle zu laden. Es könnte ganz günstig sein, die Tabelle nach dem Laden vieler Daten mit OPTIMIZE TABLE neu zu organisieren, wenn die Zeilen nicht in der Primärschlüsselreihenfolge geladen wurden. • Das Aktualisieren der Spalten in einem Cluster-Index ist sehr aufwendig, da InnoDB dabei gezwungen wird, jede aktualisierte Zeile an eine neue Stelle zu verschieben. • Tabellen, die auf Cluster-Indizes aufbauen, neigen zu Seitenaufteilungen, wenn neue Zeilen eingefügt werden oder wenn die Aktualisierung des Primärschlüssels einer Zeile dazu führt, dass die Zeile verschoben werden muss. Zu einer Seitenaufteilung kommt es, wenn der Schlüsselwert einer Zeile vorschreibt, dass die Zeile auf eine 11 Die solidDB-Storage-Engine macht das auch.
120 | Kapitel 3: Schema-Optimierung und Indizierung
Seite platziert werden muss, die bereits voller Daten ist. Die Storage-Engine muss die Seite in zwei Seiten aufteilen, um die Zeile unterzubringen. Seitenaufteilungen können dazu führen, dass eine Tabelle mehr Platz auf der Festplatte erfordert. • Cluster-Tabellen können bei vollständigen Tabellenscans viel langsamer sein, vor allem, wenn Zeilen weniger dicht gepackt sind oder aufgrund von Seitenaufteilungen nicht sequenziell gespeichert werden. • Sekundäre (nonclustered) Indizes können größer werden, als man vermuten würde, da ihre Blattknoten die Primärschlüsselspalten der referenzierten Zeilen enthalten. • Zugriffe auf den sekundären Index erfordern zwei Index-Lookups statt nur einem. Der letzte Punkt kann etwas verwirrend sein. Weshalb sollte ein sekundärer Index zwei Index-Lookups verlangen? Die Antwort liegt in der Natur der »Zeilenzeiger« begründet, die der sekundäre Index speichert. Erinnern Sie sich, ein Blattknoten speichert nicht den Zeiger auf den physischen Ort der referenzierten Zeile, sondern die Primärschlüsselwerte der Zeile. Das heißt, um eine Zeile aus einem sekundären Index zu finden, sucht die StorageEngine zuerst den Blattknoten in dem sekundären Index und verwendet dann die Primärschlüsselwerte, die dort gespeichert sind, um zu dem Primärschlüssel zu gelangen und die Zeile zu finden. Das bedeutet doppelten Aufwand: zwei B-Baum-Navigationen anstelle einer. (In InnoDB kann der adaptive Hash-Index dabei helfen, dieses Handicap zu verringern.)
Vergleich des Datenlayouts von InnoDB und MyISAM Die Unterschiede zwischen Datenlayouts mit und ohne Cluster und die entsprechenden Unterschiede zwischen primären und sekundären Indizes können verwirrend und überraschend sein. Schauen wir uns an, wie InnoDB und MyISAM die folgende Tabelle gestalten: CREATE TABLE layout_test ( col1 int NOT NULL, col2 int NOT NULL, PRIMARY KEY(col1), KEY(col2) );
Nehmen Sie an, in der Tabelle stehen die Primärschlüsselwerte 1 bis 10.000, eingefügt in zufälliger Reihenfolge und dann optimiert mit OPTIMIZE TABLE. Mit anderen Worten: Die Daten sind auf der Festplatte optimal angeordnet, die Zeilen können aber in zufälliger Reihenfolge vorliegen. Die Werte für col2 sind zufällig zwischen 1 und 100 zugewiesen, so dass es viele Duplikate gibt. Das Datenlayout von MyISAM: Das Datenlayout von MyISAM ist einfacher, weshalb wir dies zuerst darstellen. MyISAM speichert die Zeilen auf der Festplatte in der Reihenfolge, in der sie eingefügt wurden, wie in Abbildung 3-4 zu sehen ist.
Indizierungsstrategien für High Performance | 121
Zeilennummer Spalte1 Spalte2 0 99 8 1 12 56 2 3000 62
9997
18
8
9998 9999
4700 3
13 93
Abbildung 3-4: Das MyISAM-Datenlayout für die Tabelle layout_test
Neben den Zeilen stehen die Zeilennummern, beginnend bei 0. Da die Zeilen eine feste Größe besitzen, kann MyISAM eine beliebige Zeile finden, indem es die erforderliche Anzahl an Bytes vom Anfang der Tabelle durchsucht. (MyISAM setzt nicht immer »Zeilennummern« ein, wie wir hier gezeigt haben, es benutzt unterschiedliche Strategien, je nachdem, ob die Zeilen eine feste Größe oder eine variable Größe haben.) Dieses Layout vereinfacht den Aufbau eines Index. Wir verdeutlichen dies hier anhand einer Reihe von Diagrammen, bei denen physische Einzelheiten wie Seiten außer Acht gelassen wurden und nur »Knoten« im Index gezeigt werden. Jeder Blattknoten im Index kann einfach die Zeilennummer enthalten. In Abbildung 3-5 sehen Sie den Primärschlüssel der Tabelle. Spaltenwert Zeilennummer
Interne Knoten
3
99
4700
9999
0
9998
Blattknoten in Spalte1-Anordnung
Abbildung 3-5: MyISAM-Primärschlüssellayout für die Tabelle layout_test
Einige der Details haben wir weggelassen, etwa, wie viele interne B-Baum-Knoten von dem vorhergehenden abstammen, aber das ist auch nicht wichtig, um das grundlegende Datenlayout einer Nicht-Cluster-Storage-Engine zu verstehen. Was ist mit dem Index in col2? Ist damit etwas Besonderes? Wie es aussieht, nicht – es ist einfach ein Index wie jeder andere auch. Abbildung 3-6 zeigt den Index von col2.
122 | Kapitel 3: Schema-Optimierung und Indizierung
Spaltenwert Zeilennummer
Interne Knoten
8
8
13
0
9997
9998
Blattknoten in Spalte2-Anordnung
Abbildung 3-6: MyISAM-Indexlayout von col2 für die Tabelle layout_test
In der Tat gibt es in MyISAM keinen strukturellen Unterschied zwischen einem Primärschlüssel und einem anderen Index. Ein Primärschlüssel ist einfach ein eindeutiger, nonnullable Index namens PRIMARY. Das Datenlayout von InnoDB: InnoDB speichert die gleichen Daten aufgrund seiner Clusterbezogenen Organisation ganz anders. Eine Darstellung der Speicherung sehen Sie in Abbildung 3-7. Primärschlüsselspalten (Spalte1) TID RP
Transaktions-ID Rollback-Zeiger Nichtprimärschlüsselspalten (Spalte2)
Interne Knoten
3
99
4700
TID
TID RP 8
TID RP 13
RP 93
InnoDB-ClusterIndexblattknoten
Abbildung 3-7: Das Primärschlüssellayout von InnoDB für die Tabelle layout_test
Auf den ersten Blick scheint sich das nicht sehr von Abbildung 3-5 zu unterscheiden. Schauen Sie jedoch noch einmal hin. Sie werden bemerken, dass diese Illustration die ganze Tabelle zeigt, nicht nur den Index. Weil der Cluster-Index in der Tabelle in InnoDB »ist«, gibt es keine getrennte Zeilenspeicherung wie bei MyISAM. Jeder Blattknoten im Cluster-Index enthält den Primärschlüsselwert, die Transaktions-ID und den Rollback-Zeiger, den InnoDB für transaktionsbezogene und MVCC-Zwecke
Indizierungsstrategien für High Performance | 123
verwendet, sowie die restlichen Spalten (in diesem Fall col2). Wenn der Primärschlüssel auf einem Spaltenpräfix liegt, fügt InnoDB den restlichen Spalten den vollständigen Spaltenwert hinzu. Ebenso im Gegensatz zu MyISAM sind sekundäre Indizes in InnoDB ganz anders als Cluster-Indizes. Anstatt »Zeilenzeiger« zu speichern, enthalten die Blattknoten des sekundären Index von InnoDB die Primärschlüsselwerte, die als »Zeiger« auf die Zeilen dienen. Diese Strategie verringert die Arbeit, die erforderlich ist, um die sekundären Indizes zu pflegen, wenn die Zeilen verschoben werden oder wenn eine Datenseite aufgeteilt wird. Durch die Verwendung der Primärschlüsselwerte einer Zeile als Zeiger vergrößert sich der Index, allerdings bedeutet es auch, dass InnoDB eine Zeile verschieben kann, ohne die Zeiger auf diese Zeile aktualisieren zu müssen. Abbildung 3-8 zeigt den col2-Index für die Beispieltabelle. Jeder Blattknoten enthält die indizierten Spalten (in diesem Fall nur col2), gefolgt von den Primärschlüsselwerten (col1). Schlüsselspalten (Spalte2) Primärschlüsselspalten (Spalte1)
Interne Knoten
8
8
13
93
18
99
4700
3
InnoDB-SekundärIndexblattknoten
Abbildung 3-8: Das Sekundärschlüssellayout von InnoDB für die Tabelle layout_test
Diese Diagramme haben die B-Baum-Blattknoten dargestellt, wir haben aber absichtlich die Einzelheiten über Knoten weggelassen, die keine Blattknoten sind. Die InnoDB-BBaum-Knoten, die keine Blattknoten sind, enthalten jeweils die indizierte(n) Spalte(n) sowie einen Zeiger auf den nächsttieferen Knoten (bei dem es sich entweder um einen Nichtblattknoten oder um einen Blattknoten handeln kann). Das gilt für alle Indizes, Cluster-Indizes und sekundäre Indizes. Abbildung 3-9 zeigt in einem abstrakten Diagramm, wie InnoDB und MyISAM die Tabelle aufbauen. Anhand dieser Illustration können Sie leicht erkennen, wie unterschiedlich InnoDB und MyISAM Daten und Indizes speichern. Es macht nichts, falls Sie nicht verstehen, warum und wie sich die Cluster- und die NichtCluster-Speicherung unterscheiden und wieso das so wichtig ist. Das wird im weiteren Verlauf dieses Kapitels und im nächsten Kapitel noch deutlicher werden. Diese Konzepte sind kompliziert, und es dauert eine Weile, bis man sie vollständig verstanden hat.
124 | Kapitel 3: Schema-Optimierung und Indizierung
Primärschlüssel
Primärschlüssel
Sekundärschlüssel
Row
Row
Row
Row
Row
Row
Row
Row
Sekundärschlüssel
+ Key
+ Key
+ Key
+ Key
ols PK c
ols PK c
ols PK c
ols PK c
InnoDB- (Cluster-) Tabellenlayout
MyISAM (Nicht-Cluster-) Tabellenlayout
Abbildung 3-9: Cluster- und Nicht-Cluster-Tabellen nebeneinander
Mit InnoDB Zeilen in Primärschlüsselreihenfolge einfügen Wenn Sie InnoDB verwenden und kein spezielles Clustern benötigen, dann sollten Sie möglicherweise einen Surrogatschlüssel definieren. Das ist ein Primärschlüssel, dessen Wert nicht aus den Daten der Anwendung abgeleitet wird. Am einfachsten geht das normalerweise mit einer AUTO_INCREMENT-Spalte. Dies stellt sicher, dass die Zeilen der Reihe nach eingefügt werden, und bietet eine bessere Leistung für Joins mit Primärschlüsseln. Es ist am besten, wenn Sie zufällig (nichtsequeziell) zu Clustern zusammengepackte Schlüssel vermeiden. Beispielsweise ist die Verwendung von UUID-Werten aus Sicht der Leistung eine schlechte Wahl: Das Einfügen in den Cluster-Index erfolgt zufällig – was ganz schlecht ist –, und Sie erhalten keine sinnvollen Daten-Cluster. Zur Demonstration haben wir zwei Fälle Benchmark-Tests unterzogen. Im ersten Fall wird in eine userinfo-Tabelle eine Zeile mit einer Integer-ID eingefügt: CREATE TABLE userinfo ( id int unsigned NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL DEFAULT '',
Indizierungsstrategien für High Performance | 125
email varchar(64) NOT NULL DEFAULT '', password varchar(64) NOT NULL DEFAULT '', dob date DEFAULT NULL, address varchar(255) NOT NULL DEFAULT '', city varchar(64) NOT NULL DEFAULT '', state_id tinyint unsigned NOT NULL DEFAULT '0', zip varchar(8) NOT NULL DEFAULT '', country_id smallint unsigned NOT NULL DEFAULT '0', gender ('M','F') NOT NULL DEFAULT 'M', account_type varchar(32) NOT NULL DEFAULT '', verified tinyint NOT NULL DEFAULT '0', allow_mail tinyint unsigned NOT NULL DEFAULT '0', parrent_account int unsigned NOT NULL DEFAULT '0', closest_airport varchar(3) NOT NULL DEFAULT '', PRIMARY KEY (id), UNIQUE KEY email (email), KEY country_id (country_id), KEY state_id (state_id), KEY state_id_2 (state_id,city,address) ) ENGINE=InnoDB
Beachten Sie den automatisch inkrementierenden Integer-Primärschlüssel. Der zweite Fall ist eine Tabelle namens userinfo_uuid. Sie ist mit der Tabelle userinfo identisch, allerdings ist ihr Primärschlüssel eine UUID anstelle eines Integer-Wertes: CREATE TABLE userinfo_uuid ( uuid varchar(36) NOT NULL, ...
Wir haben beide Tabellendesigns mit Benchmarks getestet. Zuerst fügten wir in beide Tabellen auf einem Server mit ausreichend Speicher, um die Indizes aufzunehmen, eine Million Datensätze ein. Anschließend fügten wir drei Millionen Zeilen in dieselben Tabellen ein, wodurch die Indizes größer wurden als der Speicher des Servers. In Tabelle 3-2 werden die Benchmark-Ergebnisse verglichen. Tabelle 3-2: Benchmark-Ergebnisse für das Einfügen von Zeilen in InnoDB-Tabellen Tabelle
Zeilen
Zeit (sek)
Indexgröße (MByte)
userinfo
1.000.000
137
342
userinfo_uuid
1.000.000
180
544
userinfo
3.000.000
1233
1036
userinfo_uuid
3.000.000
4525
1707
Sie sehen, dass es nicht nur länger dauert, die Zeilen mit dem UUID-Primärschlüssel einzufügen, die resultierenden Indizes sind auch etwas größer. Zum Teil liegt das an dem größeren Primärschlüssel, aber andererseits zweifellos auch an Seitenaufteilungen und daraus folgender Fragmentierung. Um herauszufinden, warum das so ist, wollen wir uns anschauen, was im Index geschah, als wir Daten in die erste Tabelle einfügten. Abbildung 3-10 zeigt, wie erst eine Seite gefüllt und das Einfügen dann auf einer zweiten Seite fortgesetzt wird.
126 | Kapitel 3: Schema-Optimierung und Indizierung
Sequenzielles Einfügen in die Seite: jeder neue Datensatz wird nach dem vorhergehenden eingefügt 1
2
3
4
4
5
Wenn die Seite voll ist, wird das Einfügen auf einer neuen Seite fortgesetzt ...
...
300
301
301
5
302
302
Abbildung 3-10: Einfügen aufeinanderfolgender Indexwerte in einen Cluster-Index
Wie Abbildung 3-10 verdeutlicht, speichert InnoDB jeden Datensatz unmittelbar nach dem vorhergehenden, da die Primärschlüsselwerte der Reihe nach geordnet sind. Wenn die Seite ihren maximalen Füllfaktor erreicht (bei InnoDB liegt der anfängliche Füllfaktor bei einer 15/16-Füllung, so dass Platz für spätere Änderungen bleibt), kommt der nächste Datensatz auf eine neue Seite. Sobald die Daten auf diese sequenzielle Weise geladen wurden, sind die Seiten fast vollständig mit Datensätzen gefüllt, die bereits geordnet wurden. Das ist ausgesprochen wünschenswert. Vergleichen Sie das einmal mit dem zweiten Szenario, bei dem die Daten mit dem UUIDCluster-Index in die zweite Tabelle eingefügt wurden (siehe Abbildung 3-11). Einfügen von UUIDs: neue Datensätze können zwischen zuvor eingefügten Datensätzen eingefügt werden, wodurch diese verschoben werden 000944 0016c9 002f21 16-6175 1a-6175 8e-6177
002775 64-6178
000e2f 20-6180
Seiten, die gefüllt und auf die Festplatte übertragen wurden, müssen möglicherweise erneut gelesen werden 000944 000e2f 0016c9 002775 002f21 16-6175 20-6180 1a-6175 64-6178 8e-6177
001475 64-6181 *Es werden nur die ersten 13 Zeichen der UUID gezeigt
Abbildung 3-11: Einfügen von nichtsequeziellen Werten in einen Cluster-Index
Indizierungsstrategien für High Performance | 127
Da eine neue Zeile nicht unbedingt einen größeren Primärschlüsselwert besitzt als die vorhergehende Zeile, kann InnoDB sie nicht immer an das Ende des Index setzen. Es muss die passende Stelle für die Zeile finden – im Durchschnitt irgendwo in der Mitte der vorhandenen Daten – und Platz dafür schaffen. Dies verursacht eine Menge Zusatzaufwand und sorgt für eine suboptimale Anordnung der Daten. Hier ist eine Zusammenfassung der Nachteile: • Die Zielseite könnte auf die Platte geschrieben und aus dem Cache entfernt worden sein. In diesem Fall muss InnoDB sie suchen und erneut von der Festplatte einlesen, bevor es die neue Zeile einfügen kann. Dies verursacht viele zufällige Ein-/Ausgaben. • Manchmal muss InnoDB Seiten aufteilen, um Platz für neue Zeilen zu schaffen. Das erfordert ein Verschieben vieler Daten. • Seiten werden aufgrund der Aufteilungen spärlich und unregelmäßig gefüllt, so dass die fertigen Daten fragmentiert sind. Nach dem Laden solcher willkürlichen Werte in einen Cluster-Index müssen Sie wahrscheinlich erst einmal OPTIMIZE TABLE ausführen, um die Tabelle neu aufzubauen und die Seiten optimal zu füllen. Die Moral von der Geschichte lautet, dass Sie danach streben sollten, die Daten in der Primärschlüsselreihenfolge einzufügen, wenn Sie InnoDB verwenden, und dass Sie versuchen sollten, einen Cluster-fähigen Schlüssel zu benutzen, der Ihnen für jede neue Zeile einen monoton ansteigenden Wert liefert.
Wann die Primärschlüsselreihenfolge nachteilig ist Bei stark nebenläufiger Auslastung kann das Einfügen in Primärschlüsselreihenfolge bei InnoDB, so, wie es momentan implementiert ist, eine Problemstelle schaffen. Dieser »Hotspot« liegt beim oberen Ende des Primärschlüssels. Da alle Einfügungen dort stattfinden, kann es passieren, dass parallel erfolgende Einfügungen einander die Locks für den nächsten Schlüssel und/oder die AUTO_INCREMENT-Locks streitig machen (der Hotspot kann bei einem von ihnen oder bei beiden liegen). Wenn bei Ihnen dieses Problem auftritt, dann könnten Sie Ihre Tabelle oder Anwendung umgestalten oder InnoDB so anpassen, dass es mit dieser speziellen Situation besser zurechtkommt. In Kapitel 6 erfahren Sie mehr über das Anpassen von InnoDB.
Abdeckende Indizes Indizes bieten eine Methode, um Zeilen effizient zu suchen. MySQL kann einen Index aber auch benutzen, um die Daten einer Spalte zu holen, so dass es die Zeile gar nicht lesen muss. Schließlich enthalten die Blattknoten des Index die Werte, die sie indizieren; weshalb sollte man daher die Zeile lesen, wenn das Lesen des Index bereits die gewünschten Daten liefern kann? Ein Index, der alle Daten enthält (oder »abdeckt«), die nötig sind, um eine Abfrage zu beantworten, wird als abdeckender Index (Covering Index) bezeichnet. 128 | Kapitel 3: Schema-Optimierung und Indizierung
Abdeckende Indizes sind ein starkes Werkzeug und können die Leistung deutlich verbessern. Bedenken Sie die Vorteile, die sich ergeben, wenn nur der Index anstelle der Daten gelesen wird: • Indexeinträge sind normalerweise viel kleiner als die vollständige Zeile. MySQL muss also auf deutlich weniger Daten zugreifen, wenn es nur den Index liest. Das ist für im Cache gespeicherte Operationen sehr wichtig, bei denen sich ein Großteil der Antwortzeit durch das Kopieren der Daten ergibt. Auch für ein-/ausgabegebundene Operationen ist das hilfreich, da die Indizes kleiner als die Daten sind und besser in den Speicher passen. (Das gilt vor allem für MyISAM, das Indizes packen kann, so dass sie noch kleiner werden.) • Indizes werden nach ihren Indexwerten sortiert (zumindest innerhalb der Seite), so dass ein-/ausgabegebundene Bereichszugriffe im Vergleich zu Zugriffen, bei denen jede Zeile von einer anderen Stelle auf der Festplatte geholt werden muss, weniger Ein-/Ausgaben benötigen. Bei manchen Storage-Engines, wie etwa MyISAM, können Sie die Tabelle sogar mit OPTIMIZE optimieren, um vollständig sortierte Indizes zu erhalten. Dadurch wären für einfache Bereichsabfragen vollständig sequenzielle Indexzugriffe möglich. • Die meisten Storage-Engines speichern Indizes besser als Daten im Cache. (Falcon bildet eine bemerkenswerte Ausnahme.) Manche Storage-Engines, wie etwa MyISAM, speichern nur den Index im MySQL-Cache. Da das Betriebssystem die Daten für MyISAM im Cache speichert, ist für den Zugriff darauf typischerweise ein Systemaufruf erforderlich. Das hat möglicherweise starke Auswirkungen auf die Leistung, vor allem bei im Cache ablaufenden Operationen, für die der Systemaufruf den teuersten Teil des Datenzugriffs darstellt. • Abdeckende Indizes sind wegen der Cluster-Indizes von InnoDB besonders für InnoDB-Tabellen sinnvoll. Die sekundären Indizes von InnoDB enthalten die Primärschlüsselwerte der Zeile in ihren Blattknoten. Ein sekundärer Index, der eine Abfrage abdeckt, verhindert daher einen weiteren Index-Lookup im Primärschlüssel. In all diesen Szenarien ist es typischerweise deutlich preiswerter, eine Abfrage aus einem Index zu beantworten, als die Zeilen zu durchsuchen. Ein abdeckender Index ist nicht einfach eine beliebige Art von Index. Der Index muss die Werte aus den Spalten speichern, die er enthält. Hash-Indizes, räumliche Indizes und Volltextindizes speichern diese Werte nicht; MySQL kann daher nur B-Baum-Indizes verwenden, um Abfragen abzudecken. Auch abdeckende Indizes werden von den verschiedenen Storage-Engines unterschiedlich implementiert, und nicht alle Storage-Engines unterstützen sie (momentan tun die Memory- und die Falcon-Storage-Engine dies nicht). Wenn Sie eine Abfrage starten, die von einem Index abgedeckt wird (eine index-covered Query), dann steht »Using index« in der Extra-Spalte in EXPLAIN.12 Beispielsweise besitzt 12 Man kann »Using index« in der Extra-Spalte leicht mit »index« in der type-Spalte verwechseln. Sie sind jedoch völlig unterschiedlich. Die type-Spalte hat nichts mit abdeckenden Indizes zu tun; sie zeigt den Zugriffstyp der Abfrage oder wie die Abfrage die Zeilen sucht.
Indizierungsstrategien für High Performance | 129
die Tabelle sakila.inventory einen mehrspaltigen Index in (store_id, film_id). MySQL kann diesen Index für eine Abfrage benutzen, die nur auf diese beiden Spalten zugreift: mysql> EXPLAIN SELECT store_id, film_id FROM sakila.inventory\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: inventory type: index possible_keys: NULL key: idx_store_id_film_id key_len: 3 ref: NULL rows: 4673 Extra: Using index
Indexabdeckende Abfragen enthalten Feinheiten, die diese Optimierung beeinträchtigen können. Der MySQL-Abfrageoptimierer entscheidet vor dem Ausführen einer Abfrage, ob ein Index sie abdeckt. Nehmen Sie an, der Index deckt eine WHERE-Bedingung ab, aber nicht die gesamte Abfrage. Wenn die Bedingung als falsch bewertet wird, holen MySQL 5.1 und frühere Versionen die Zeile dennoch, obwohl sie sie nicht brauchen und herausfiltern. Schauen wir uns an, wie das passieren kann und wie man die Abfrage verändern muss, um das Problem zu umgehen. Wir beginnen mit der folgenden Abfrage: mysql> EXPLAIN SELECT * FROM products WHERE actor='SEAN CARREY' -> AND title like '%APOLLO%'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: products type: ref possible_keys: ACTOR,IX_PROD_ACTOR key: ACTOR key_len: 52 ref: const rows: 10 Extra: Using where
Der Index kann diese Abfrage aus zwei Gründen nicht abdecken: • Kein Index deckt die Abfrage ab, da wir alle Spalten aus der Tabelle ausgewählt haben und kein Index alle Spalten abdeckt. Theoretisch gibt es eine Abkürzung, die MySQL benutzen könnte, allerdings führt die WHERE-Klausel nur Spalten auf, die der Index abdeckt, MySQL könnte deshalb den Index einsetzen, um den Schauspieler zu finden, dann überprüfen, ob der Titel passt, und nur dann die vollständige Zeile lesen. • MySQL kann die LIKE-Operation im Index nicht ausführen. Das ist eine Einschränkung der Storage-Engine-API, die nur einfache Vergleiche in Indexoperationen erlaubt. MySQL kann präfixvergleichende LIKE-Muster im Index ausführen, weil es sie in einfache Vergleiche umwandeln kann, das führende Wildcard in der Abfrage
130 | Kapitel 3: Schema-Optimierung und Indizierung
verhindert jedoch, dass die Storage-Engine den Vergleich auswertet. Daher muss der MySQL-Server selbst die Werte der Zeile holen und vergleichen und kann dies nicht mit den Werten des Index erledigen. Es gibt eine Kombination aus cleverer Indizierung und Umschreiben der Abfrage, mit der man beide Probleme umgehen kann. Wir können den Index so erweitern, dass er (artist, title, prod_id) abdeckt, und schreiben die Abfrage um: mysql> EXPLAIN SELECT * -> FROM products -> JOIN ( -> SELECT prod_id -> FROM products -> WHERE actor='SEAN CARREY' AND title LIKE '%APOLLO%' -> ) AS t1 ON (t1.prod_id=products.prod_id)\G *************************** 1. row *************************** id: 1 select_type: PRIMARY table: <derived2> ...weggelassen... *************************** 2. row *************************** id: 1 select_type: PRIMARY table: products ...weggelassen... *************************** 3. row *************************** id: 2 select_type: DERIVED table: products type: ref possible_keys: ACTOR,ACTOR_2,IX_PROD_ACTOR key: ACTOR_2 key_len: 52 ref: rows: 11 Extra: Using where; Using index
MySQL verwendet den abdeckenden Index im ersten Stadium der Abfrage, wenn es passende Zeilen in der Unterabfrage der FROM-Klausel sucht. Es deckt mit dem Index nicht die ganze Abfrage ab, aber insgesamt ist es besser als nichts. Die Effektivität dieser Optimierung hängt davon ab, wie viele Zeilen die WHERE-Klausel findet. Nehmen Sie an, die Tabelle products enthält eine Million Zeilen. Wir wollen uns anschauen, wie diese beiden Abfragen mit den drei unterschiedlichen Datenmengen funktionieren, die jeweils eine Million Zeilen enthalten: 1. In der ersten haben 30.000 Produkte Sean Carrey als Schauspieler, und 20.000 von
ihnen enthalten Apollo im Titel. 2. In der zweiten haben 30.000 Produkte Sean Carrey als Schauspieler, und 40 von ihnen enthalten Apollo im Titel. 3. In der dritten haben 50 Produkte Sean Carrey als Schauspieler, und 10 von ihnen enthalten Apollo im Titel.
Indizierungsstrategien für High Performance | 131
Wir verwendeten diese drei Datenmengen für Benchmarks der zwei Variationen der Abfrage und erhielten die Ergebnisse, die in Tabelle 3-3 gezeigt werden. Tabelle 3-3: Benchmark-Ergebnisse für indexabdeckende Abfragen im Vergleich mit nichtindexabdeckenden Abfragen Datenmenge
Originalabfrage
Optimierte Abfrage
Beispiel 1
5 Abfragen pro Sekunde
5 Abfragen pro Sekunde
Beispiel 2
7 Abfragen pro Sekunde
35 Abfragen pro Sekunde
Beispiel 3
2400 Abfragen pro Sekunde
2000 Abfragen pro Sekunde
Und so kann man diese Ergebnisse interpretieren: • In Beispiel 1 liefert die Abfrage eine große Ergebnismenge zurück, so dass die Wirkung der Optimierung nicht zu erkennen ist. Der größte Teil der Zeit wird mit dem Lesen und Senden der Daten verbracht. • Beispiel 2, bei dem der zweite Bedingungsfilter nach der Indexfilterung nur eine kleine Ergebnismenge hinterlässt, zeigt, wie effektiv die vorgeschlagene Optimierung ist: Die Leistung ist mit unseren Daten fünfmal besser. Die Effizienz ergibt sich daraus, dass nur 40 vollständige Zeilen gelesen werden müssen und nicht 30.000 wie in der ersten Abfrage. • Beispiel 3 zeigt den Fall einer ineffizienten Unterabfrage. Die Menge der Ergebnisse, die nach der Indexfilterung bleibt, ist so klein, dass die Unterabfrage teurer ist als das Lesen aller Daten aus der Tabelle. Diese Optimierung ist manchmal eine effektive Methode, um in MySQL 5.1 und früheren Versionen das Lesen unnötiger Zeilen zu vermeiden. MySQL 6.0 kann diese zusätzliche Arbeit selbst umgehen, so dass es Ihnen möglich sein dürfte, Ihre Abfragen zu vereinfachen, wenn Sie auf diese höhere Version umsteigen. Bei den meisten Storage-Engines kann ein Index nur Abfragen abdecken, die auf Spalten zugreifen, die Teil des Index sind. InnoDB dagegen treibt diese Optimierung ein wenig weiter. Erinnern Sie sich daran, dass die sekundären Indizes von InnoDB in ihren Blattknoten Primärschlüsselwerte enthalten. Das bedeutet, dass die sekundären Indizes von InnoDB gewissermaßen »Zusatzspalten« enthalten, die InnoDB benutzen kann, um Abfragen abzudecken. Beispielsweise benutzt die Tabelle sakila.actor InnoDB und hat einen Index in last_name. Der Index kann also Abfragen abdecken, die die Primärschlüsselspalte actor_id beziehen, obwohl diese Spalte technisch gesehen kein Teil des Index ist: mysql> EXPLAIN SELECT actor_id, last_name -> FROM sakila.actor WHERE last_name = 'HOPPER'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: ref
132 | Kapitel 3: Schema-Optimierung und Indizierung
possible_keys: key: key_len: ref: rows: Extra:
idx_actor_last_name idx_actor_last_name 137 const 2 Using where; Using index
Indexscans für Sortierungen verwenden MySQL bietet zwei Möglichkeiten, um sortierte Ergebnisse herzustellen: Entweder setzt es ein Filesort ein oder es fragt einen Index in der Reihenfolge ab.13 Man kann feststellen, wann MySQL vorhat, einen Index zu scannen, indem man nach »index« in der typeSpalte in EXPLAIN sucht. (Verwechseln Sie das nicht mit »Using index« in der ExtraSpalte.) Das Scannen des Index selbst geht schnell, weil dabei nur von einem Indexeintrag zum nächsten gegangen werden muss. Falls allerdings MySQL den Index nicht benutzt, um die Abfrage abzudecken, muss es jede Zeile nachschauen, die es im Index findet. Dabei handelt es sich prinzipiell um zufällige Ein-/Ausgaben. Das Lesen der Daten in der Indexreihenfolge ist deshalb normalerweise viel langsamer als ein sequenzieller Tabellenscan, vor allem bei ein-/ausgabegebundenen Operationen. MySQL kann den gleichen Index zum Sortieren und zum Suchen von Zeilen verwenden. Falls möglich, sollten Sie Ihre Indizes so gestalten, dass sie sich gut für beide Aufgaben eignen. Das Anordnen der Ergebnisse nach dem Index funktioniert nur dann, wenn die Reihenfolge des Index exakt die gleiche ist wie die ORDER BY-Klausel und alle Spalten in der gleichen Richtung sortiert sind (aufsteigend oder absteigend). Wenn die Abfrage mehrere Tabellen zusammenführt, funktioniert das nur, wenn alle Spalten in der ORDER BY-Klausel auf die erste Tabelle verweisen. Die ORDER BY-Klausel unterliegt außerdem den gleichen Beschränkungen wie Lookup-Abfragen: Sie muss einen ganz links liegenden Präfix des Index formulieren. In allen anderen Fällen verwendet MySQL ein Filesort. Ein Fall, bei dem die ORDER BY-Klausel kein ganz links liegendes Präfix des Index angeben muss, tritt auf, wenn es Konstanten für die führenden Spalten gibt. Legen die WHERE- oder eine JOIN-Klausel Konstanten für diese Spalten fest, können sie die »Lücken im Index füllen«. Zum Beispiel hat die Tabelle rental in der normalen Sakila-Beispieldatenbank einen Index in (rental_date, inventory_id, customer_id): CREATE TABLE rental ( ... PRIMARY KEY (rental_id), UNIQUE KEY rental_date (rental_date,inventory_id,customer_id),
13 MySQL besitzt zwei Filesort-Mechanismen; Sie erfahren darüber mehr in »Sortieroptimierungen« auf Seite 190.
Indizierungsstrategien für High Performance | 133
KEY idx_fk_inventory_id (inventory_id), KEY idx_fk_customer_id (customer_id), KEY idx_fk_staff_id (staff_id), ... );
MySQL benutzt den rental_date-Index, um die folgende Abfrage zu sortieren, wie Sie am Fehlen eines Filesort in EXPLAIN erkennen: mysql> EXPLAIN SELECT rental_id, staff_id FROM sakila.rental -> WHERE rental_date = '2005-05-25' -> ORDER BY inventory_id, customer_id\G *************************** 1. row *************************** type: ref possible_keys: rental_date key: rental_date rows: 1 Extra: Using where
Das funktioniert, obwohl die ORDER BY-Klausel selbst nicht ein am weitesten links gelegenes Präfix des Index ist, weil wir eine Gleichheitsbedingung für die erste Spalte im Index angegeben haben. Hier sind einige weitere Abfragen, die den Index für die Sortierung benutzen können. Diese hier funktioniert, weil die Abfrage eine Konstante für die erste Spalte des Index liefert und ein ORDER BY in der zweiten Spalte angibt. Zusammengenommen formen diese zwei ein ganz links gelegenes Präfix des Index: ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
Die folgende Abfrage funktioniert auch, weil die zwei Spalten in ORDER BY ein ganz links gelegenes Präfix des Index bilden: ... WHERE rental_date > '2005-05-25' ORDER BY rental_date, inventory_id;
Hier sehen Sie einige Abfragen, die den Index nicht für die Sortierung verwenden können: • Diese Abfrage benutzt zwei verschiedene Sortierrichtungen, die Spalten des Index sind jedoch alle aufsteigend sortiert: ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC;
• Hier bezieht sich ORDER BY auf eine Spalte, die nicht im Index liegt: ... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id;
• Hier formen die WHERE- und die ORDER BY-Klausel nicht das am weitesten links gelegene Präfix des Index: ... WHERE rental_date = '2005-05-25' ORDER BY customer_id;
• Diese Abfrage hat eine Bereichsbedingung in der ersten Spalte, so dass MySQL den Rest des Index nicht verwendet: ... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
• Hier gibt es eine mehrfache Gleichheit in der Spalte inventory_id. Für die Zwecke der Sortierung ist das im Prinzip das Gleiche wie ein Bereich: ... WHERE rental_date = '2005-05-25' AND inventory_id IN(1,2) ORDER BY customer_id;
134 | Kapitel 3: Schema-Optimierung und Indizierung
• Hier ist ein Beispiel, bei dem MySQL theoretisch einen Index benutzen könnte, um ein Join zu sortieren. Das tut es aber nicht, weil der Optimierer die Tabelle film_actor als Zweites in den Join setzt (Kapitel 4 zeigt Möglichkeiten, um die JoinReihenfolge zu ändern): mysql> EXPLAIN SELECT actor_id, title FROM sakila.film_actor -> INNER JOIN sakila.film USING(film_id) ORDER BY actor_id\G +------------+----------------------------------------------+ | table | Extra | +------------+----------------------------------------------+ | film | Using index; Using temporary; Using filesort | | film_actor | Using index | +------------+----------------------------------------------+
Eine der wichtigsten Anwendungen für eine Anordnung nach dem Index ist eine Abfrage, die sowohl eine ORDER BY- als auch eine LIMIT-Klausel aufweist. Wir gehen später genauer darauf ein.
Gepackte (Präfixkomprimierte) Indizes MyISAM setzt Präfixkomprimierung ein, um die Indexgröße zu reduzieren, wodurch ein größerer Teil des Index in den Speicher passt und die Leistung sich gelegentlich deutlich verbessert. Stringwerte werden standardmäßig gepackt, Sie können MyISAM jedoch auch anweisen, Integer-Werte zu komprimieren. MyISAM packt die einzelnen Indexblöcke, indem es den ersten Wert des Blocks vollständig speichert. Anschließend speichert es jeden weiteren Wert im Block, indem es die Anzahl der Bytes aufzeichnet, die das gleiche Präfix besitzen, sowie die tatsächlichen Daten des Suffix, das sich von den anderen Suffixen unterscheidet. Falls z.B. der erste Wert »perform« ist und der zweite »performance« lautet, dann wird der zweite Wert analog als »7,ance« gespeichert. MyISAM kann auch benachbarte Zeilenzeiger präfixkomprimieren. Komprimierte Blöcke brauchen weniger Platz, verlangsamen aber auch bestimmte Operationen. Da das Komprimierungspräfix eines Wertes vom Wert davor abhängt, kann MyISAM keine Binärsuchen durchführen, um einen gewünschten Eintrag im Block zu finden, und muss den Block vom Anfang an scannen. Sequenzielle Vorwärtsscans funktionieren gut, umgekehrte Scans dagegen – wie etwa ORDER BY DESC – nicht so gut. Jede Operation, bei der eine einzelne Zeile in der Mitte des Blocks gefunden werden muss, erfordert im Durchschnitt das Scannen des halben Blocks. Unsere Benchmarks haben gezeigt, dass gepackte Schlüssel dafür sorgen, dass IndexLookups in MyISAM-Tabellen bei einer CPU-gebundenen Operation mehrfach langsamer verlaufen, weil die Scans willkürliche Lookups erfordern. Umgekehrte Scans von gepackten Schlüsseln sind sogar noch langsamer. Man muss zwischen CPU- und Speicherressourcen auf der einen und Festplattenressourcen auf der anderen Seite wählen. Gepackte Indizes nehmen nur etwa ein Zehntel des Platzes auf der Festplatte ein, und wenn Sie ein-/ausgabegebundene Operationen haben, dann gleichen sie wahrscheinlich die Kosten für bestimmte Abfragen mehr als aus.
Indizierungsstrategien für High Performance | 135
Mit der CREATE TABLE-Option PACK_KEYS können Sie kontrollieren, wie die Indizes einer Tabelle gepackt sind.
Redundante und duplizierte Indizes MySQL erlaubt es Ihnen, mehrere Indizes in derselben Spalte herzustellen; es erkennt und verhindert diesen Fehler nicht. MySQL muss jeden duplizierten Index gesondert pflegen. Der Abfrageoptimierer berücksichtigt alle Indizes beim Optimieren von Abfragen. Das kann ernsthafte Auswirkungen auf die Leistung haben. Duplizierte Indizes sind Indizes desselben Typs, erzeugt aus dem gleichen Satz Spalten in der gleichen Reihenfolge. Vermeiden Sie es, duplizierte Indizes zu erzeugen, und löschen Sie sie, wenn Sie sie finden. Manchmal merkt man gar nicht, dass man duplizierte Indizes erzeugt. Schauen Sie sich z.B. den folgenden Code an: CREATE TABLE test ( ID INT NOT NULL PRIMARY KEY, UNIQUE(ID), INDEX(ID) );
Ein unerfahrener Benutzer könnte denken, dass dies die Rolle der Spalte als Primärschlüssel kennzeichnet, ein UNIQUE-Constraint hinzufügt und einen Index anlegt, den Abfragen benutzen können. Tatsächlich implementiert MySQL UNIQUE-Constraints und PRIMARY KEY-Constraints bereits mit Indizes, so dass hier in Wirklichkeit drei Indizes in derselben Spalte erzeugt werden! Dazu gibt es normalerweise keinen Grund, es sei denn, Sie wollen drei unterschiedliche Arten von Indizes in derselben Spalte haben, um unterschiedliche Arten von Abfragen zu bedienen.14 Redundante Indizes unterscheiden sich ein wenig von duplizierten Indizes. Wenn es einen Index in (A, B) gibt, dann wäre ein weiterer Index in (A) redundant, weil er ein Präfix des ersten Index wäre. Das heißt, der Index in (A, B) kann auch als Index in (A) allein benutzt werden. (Diese Art von Redundanz gilt nur für B-Baum-Indizes.) Ein Index in (B, A) wäre dagegen nicht redundant, genauso wenig wie ein Index in (B), weil B nicht das am weitesten links gelegene Präfix von (A, B) ist. Außerdem sind Indizes unterschiedlicher Typen (wie etwa Hash- oder Volltextindizes) nicht redundant zu B-Baum-Indizes, egal welche Spalten sie abdecken. Redundante Indizes erscheinen normalerweise, wenn Datenbankverwalter Indizes zu einer Tabelle hinzufügen. Beispielsweise könnte jemand einen Index in (A, B) hinzufügen, anstatt einen vorhandenen Index in (A) so zu erweitern, dass er (A, B) abdeckt.
14 Ein Index ist nicht unbedingt ein Duplikat, wenn er einer anderen Art von Index angehört; oft gibt es gute Gründe für KEY(col) und FULLTEXT KEY(col).
136 | Kapitel 3: Schema-Optimierung und Indizierung
Meist sind redundante Indizes unerwünscht. Um sie zu vermeiden, sollten Sie lieber vorhandene Indizes erweitern, als neue anzulegen. Manchmal allerdings benötigt man aus Leistungsgründen redundante Indizes. Das Erweitern eines vorhandenen Index könnte ihn zu sehr vergrößern und die Leistung bei manchen Abfragen herabsetzen. Falls Sie z.B. einen Index in einer Integer-Spalte haben und ihn mit einer langen VARCHARSpalte erweitern, könnte er deutlich langsamer werden. Das gilt vor allem, wenn Ihre Abfragen den Index als abdeckenden Index benutzen oder wenn es sich um eine MyISAM-Tabelle handelt und Sie viele Bereichsscans darauf durchführen (wegen der Präfixkomprimierung von MyISAM). Denken Sie an die Tabelle userinfo, die wir in »Mit InnoDB Zeilen in Primärschlüsselreihenfolge einfügen« auf Seite 125 beschrieben haben. Diese Tabelle enthält 1.000.000 Zeilen, und für jedes state_id gibt es etwa 20.000 Datensätze. Es gibt einen Index in state_id, der für die folgende Abfrage geeignet ist. Wir bezeichnen diese Abfrage als Q1: mysql> SELECT count(*) FROM userinfo WHERE state_id=5;
Ein einfacher Benchmark zeigt eine Ausführungsrate von fast 115 Abfragen pro Sekunde (Queries per Second; QPS) für diese Abfrage. Wir haben eine verwandte Abfrage, die mehrere Spalten holt, anstatt nur Zeilen zu zählen. Dies ist Q2: mysql> SELECT state_id, city, address FROM userinfo WHERE state_id=5;
Bei dieser Abfrage ist das Ergebnis kleiner als 10 QPS.15 Die einfache Lösung zum Steigern der Leistung besteht darin, den Index auf (state_id, city, address) zu erweitern, so dass er die Abfrage abdeckt: mysql> ALTER TABLE userinfo DROP KEY state_id, -> ADD KEY state_id_2 (state_id, city, address);
Nach dem Erweitern des Index läuft Q2 schneller, Q1 dagegen ist langsamer. Falls wir wirklich daran interessiert sind, dass beide Abfragen schnell ausgeführt werden, sollten wir beide Indizes so lassen, auch wenn der einspaltige Index redundant ist. Tabelle 3-4 zeigt ausführliche Ergebnisse für beide Abfragen und Indizierungsstrategien bei den Storage-Engines MyISAM und InnoDB. Sie können erkennen, dass die Leistung von InnoDB für Q1 mit dem state_id_2-Index nicht so stark abfällt, da InnoDB keine Schlüsselkomprimierung verwendet. Tabelle 3-4: Benchmark-Ergebnisse in QPS für SELECT-Abfragen mit verschiedenen Indizierungsstrategien Nur state_id
Nur state_id_2
Sowohl state_id als auch state_id_2
MyISAM, Q1
114,96
25,40
112,19
MyISAM, Q2
9,97
16,34
16,37
InnoDB, Q1
108,55
100,33
107,97
InnoDB, Q2
12,12
28,04
28,06
15 Wir haben hier ein Beispiel benutzt, das in den Speicher passt. Wenn die Tabelle größer ist und die Operationen viele Ein-/Ausgaben verwenden, wird der Unterschied zwischen diesen Werten noch größer.
Indizierungsstrategien für High Performance | 137
Leider bringen zwei Indizes auch einen erhöhten Wartungsaufwand mit sich. Tabelle 3-5 zeigt, wie lange es dauert, um eine Million Zeilen in die Tabelle einzufügen. Tabelle 3-5: Benötigte Zeit für das Einfügen von einer Million Zeilen mit verschiedenen Indizierungsstrategien Nur state_id
Sowohl state_id als auch state_id_2
InnoDB, genug Speicher für beide Indizes
80 Sekunden
136 Sekunden
MyISAM, Speicher reicht nur für einen Index
72 Sekunden
470 Sekunden
Wie Sie sehen, verläuft das Einfügen neuer Zeilen in die Tabelle mit mehr Indizes deutlich langsamer. Im Allgemeinen ist das tatsächlich so: Das Hinzufügen neuer Indizes kann starke Auswirkungen auf die Leistung von INSERT-, UPDATE- und DELETE-Operationen haben, vor allem, wenn Sie aufgrund eines neuen Index an die Grenzen des Speichers stoßen.
Indizes und Locking Indizes spielen für InnoDB eine sehr wichtige Rolle, weil sie dafür sorgen, dass Abfragen weniger Zeilen sperren. Das ist ein sehr wichtiger Fakt, da InnoDB in MySQL 5.0 eine Zeile erst dann wieder entsperrt, wenn die Transaktion bestätigt wird. Wenn Ihre Abfragen nicht benötigte Zeilen niemals anfassen, dann sperren sie weniger Zeilen, was aus zwei Gründen besser für die Leistung ist. Erstens: Obwohl Zeilen-Locks von InnoDB sehr effizient sind und wenig Speicher benötigen, verursachen sie dennoch einen gewissen Overhead. Zweitens: Das Sperren von mehr Zeilen als nötig erhöht die Konkurrenz um die Locks und vermindert die Nebenläufigkeit. InnoDB sperrt Zeilen nur, wenn es auf sie zugreift. Ein Index ist dazu in der Lage, die Anzahl der Zeilen, auf die InnoDB zugreift, und entsprechend die Locks zu verringern. Das funktioniert allerdings nur, wenn InnoDB die unerwünschten Zeilen auf StorageEngine-Ebene herausfiltern kann. Erlaubt der Index InnoDB dies nicht, muss der MySQL-Server eine WHERE-Klausel anwenden, nachdem InnoDB die Zeilen geholt und an die Serverebene zurückgegeben hat. An dieser Stelle ist es zu spät, das Sperren der Zeilen zu vermeiden: InnoDB hat sie bereits gesperrt, und der Server kann sie nicht entsperren. Das ist mit einem Beispiel einfacher zu verstehen. Wir nehmen wieder die Sakila-Beispieldatenbank: mysql> SET AUTOCOMMIT=0; mysql> BEGIN; mysql> SELECT actor_id FROM sakila.actor WHERE actor_id < 5 -> AND actor_id <> 1 FOR UPDATE; +----------+ | actor_id | +----------+ | 2 | | 3 | | 4 | +----------+
138 | Kapitel 3: Schema-Optimierung und Indizierung
Diese Abfrage liefert nur die Zeilen 2 bis 4 zurück, sperrt allerdings die Zeilen 1 bis 4 exklusiv. InnoDB hat Zeile 1 gesperrt, weil der Plan, den MySQL für diese Abfrage gewählt hat, ein Indexbereichszugriff war: mysql> EXPLAIN SELECT actor_id FROM sakila.actor -> WHERE actor_id < 5 AND actor_id <> 1 FOR UPDATE; +----+-------------+-------+-------+---------+--------------------------+ | id | select_type | table | type | key | Extra | +----+-------------+-------+-------+---------+--------------------------+ | 1 | SIMPLE | actor | range | PRIMARY | Using where; Using index | +----+-------------+-------+-------+---------+--------------------------+
Mit anderen Worten: Die einfache Storage-Engine-Operation lautete »beginne am Anfang des Index und hole alle Zeilen, bis actor_id < 5 falsch wird«. Der Server hat InnoDB nichts von der WHERE-Bedingung verraten, die Zeile 1 eliminiert hat. Beachten Sie das Vorhandensein von »Using where« in der Extra-Spalte in EXPLAIN. Dies zeigt an, dass der MySQL-Server einen WHERE-Filter anwendet, nachdem die Storage-Engine die Zeilen zurückliefert.
Zusammenfassung der Indizierungsstrategien Nachdem Sie mehr über die Indizierung erfahren haben, fragen Sie sich vielleicht, wo Sie in Ihren eigenen Tabellen anfangen sollen. Am wichtigsten ist es, die Abfragen zu untersuchen, die Sie am häufigsten ausführen. Denken Sie aber auch an die weniger häufigen Operationen, wie etwa das Einfügen und Aktualisieren von Daten. Versuchen Sie, den oft gemachten Fehler zu vermeiden, Indizes zu erzeugen, ohne zu wissen, welche Abfragen sie benutzen werden, und denken Sie darüber nach, ob all Ihre Indizes zusammen eine optimale Konfiguration formen. Manchmal reicht es, wenn man sich die Abfragen anschaut, feststellt, welche Indizes sie benötigen, und diese hinzufügt. Es kommt aber auch vor, dass man so viele verschiedene Arten von Abfragen hat, dass man nicht für alle perfekte Indizes hinzufügen kann und deshalb einen Kompromiss eingehen muss. Um das beste Maß zu finden, müssen Sie Benchmarks durchführen und Profile erstellen. Schauen Sie sich zuerst die Antwortzeit an. Möglicherweise müssen Sie für jede Abfrage, die zu lange dauert, einen Index hinzufügen. Untersuchen Sie dann die Abfragen, die die meiste Last verursachen (mehr dazu in Kapitel 2), und fügen Sie zu deren Unterstützung Indizes hinzu. Berücksichtigen Sie mögliche Speicher-, CPU oder Festplattenengpässe Ihres Systems. Falls Sie etwa viele lange Aggregatabfragen durchführen, um Zusammenfassungen zu erstellen, dann profitieren Ihre Festplatten möglicherweise von abdeckenden Indizes, die GROUP BY-Abfragen unterstützen. Versuchen Sie nach Möglichkeit, existierende Indizes zu erweitern, anstatt neue anzulegen. Normalerweise ist es effizienter, einen mehrspaltigen Index zu pflegen als mehrere einspaltige Indizes. Falls Sie Ihre Abfrageverteilung noch nicht kennen, dann machen Sie Ihre Indizes so selektiv, wie es geht, da stark selektive Indizes im Allgemeinen mehr Vorteile bieten.
Indizierungsstrategien für High Performance | 139
Hier ist eine zweite Abfrage, die beweist, dass Zeile 1 gesperrt ist, obwohl es in den Ergebnissen der ersten Abfrage nicht auftauchte. Lassen Sie die erste Verbindung offen, starten Sie eine zweite Verbindung, und führen Sie Folgendes aus: mysql> SET AUTOCOMMIT=0; mysql> BEGIN; mysql> SELECT actor_id FROM sakila.actor WHERE actor_id = 1 FOR UPDATE;
Die Abfrage bleibt hängen, da sie darauf wartet, dass die erste Transaktion die Sperre auf Zeile 1 wieder freigibt. Dieses Verhalten ist notwendig, damit die anweisungsbasierte Replikation (Statement-based Replication, SBR; wird in Kapitel 8 besprochen) richtig funktioniert. Wie dieses Beispiel zeigt, kann InnoDB Zeilen, die es eigentlich nicht braucht, sperren, wenn es einen Index benutzt. Das Problem verschlimmert sich sogar noch, wenn es keinen Index einsetzen kann, um die Zeilen zu suchen und zu sperren: Wenn es keinen Index für die Abfrage gibt, führt MySQL einen vollständigen Scan der Tabelle durch und sperrt alle Zeilen, ob es sie nun »braucht« oder nicht.16 Es gibt bezüglich InnoDB, Indizes und Locking noch ein wenig bekanntes Detail: InnoDB kann auf sekundäre Indizes Shared Locks (Lese-Locks) platzieren, exklusive Locks (Schreib-Locks) verlangen allerdings Zugriff auf den Primärschlüssel. Dies eliminiert die Möglichkeit, einen abdeckenden Index zu benutzen, und kann SELECT FOR UPDATE viel langsamer machen als LOCK IN SHARE MODE oder eine nichtsperrende Abfrage.
Indizierung – eine Fallstudie Zum besseren Verständnis der Indizierungskonzepte haben wir eine Fallstudie vorbereitet. Nehmen Sie an, wir müssen eine Online-Dating-Site entwerfen, auf der es Benutzerprofile mit vielen verschiedenen Spalten gibt, wie z.B. Land des Benutzers, Bundesland/Region, Stadt, Geschlecht, Alter, Augenfarbe usw. Die Site muss das Durchsuchen der Profile anhand verschiedener Kombinationen dieser Eigenschaften unterstützen. Darüber hinaus soll der Benutzer die Ergebnisse nach dem letzten Zeitpunkt seiner Anmeldung, nach Bewertungen anderer Benutzer usw. sortieren und einschränken können. Wie entwirft man Indizes für solche komplexen Anforderungen? Seltsamerweise müssen wir zuerst entscheiden, ob wir eine indexbasierte Sortierung benutzen müssen oder ob ein Filesort akzeptabel ist. Eine indexbasierte Sortierung verursacht Einschränkungen bezüglich des Aufbaus der Indizes und Abfragen. Beispielsweise können wir keinen Index für eine WHERE-Klausel wie WHERE age BETWEEN 18 AND 25 verwenden, wenn die gleiche Abfrage einen Index benutzt, um die Benutzer nach den Bewertungen zu sortieren, die die anderen Benutzer ihnen gegeben haben. Wenn MySQL einen 16 Dies sollte in MySQL 5.1 mit zeilenbasiertem Binär-Logging und der Transaktionsisolationsebene READ COMMITTED behoben werden, gilt aber für alle MySQL-Versionen, die wir getestet haben, bis einschließlich 5.1.22.
140 | Kapitel 3: Schema-Optimierung und Indizierung
Index für ein Bereichskriterium in einer Abfrage verwendet, kann es nicht auch noch einen anderen Index (oder ein Suffix des gleichen Index) für die Sortierung einsetzen. Unter der Annahme, dass dies eine der gebräuchlichsten WHERE-Klauseln ist, gehen wir davon aus, dass viele Abfragen ein Filesort brauchen.
Viele Arten der Filterung unterstützen Jetzt müssen wir uns anschauen, welche Spalten viele verschiedene Werte aufweisen und welche Spalten am häufigsten in den WHERE-Klauseln auftauchen. Indizes in Spalten mit vielen unterschiedlichen Werten sind sehr selektiv. Das ist im Allgemeinen ganz günstig, da es MySQL erlaubt, unerwünschte Zeilen effizienter auszufiltern. Die country-Spalte kann selektiv sein oder nicht, sie wird wahrscheinlich in den meisten Abfragen vorkommen. Die sex-Spalte ist sicher nicht selektiv, tritt aber wahrscheinlich in jeder Abfrage in Erscheinung. Mit diesem Wissen erzeugen wir eine Reihe von Indizes für viele verschiedene Kombinationen aus Spalten, denen (sex,country) als Präfix vorangestellt ist. Traditionell wird davon ausgegangen, dass es sinnlos ist, Spalten mit sehr niedriger Selektivität zu indizieren. Weshalb sollen wir dann eine nichtselektive Spalte an den Anfang jedes Index setzen? Haben wir den Verstand verloren? Es gibt dafür zwei Gründe. Der erste Grund besteht darin, dass, wie bereits angemerkt, fast jede Abfrage sex verwenden wird. Wir könnten die Site sogar so gestalten, dass die Benutzer immer nur nach einem Geschlecht gleichzeitig suchen können. Wichtiger ist aber, dass es eigentlich keinen großen Nachteil darstellt, die Spalte hinzuzufügen, weil wir noch ein Ass im Ärmel haben. Und so geht der Trick: Selbst wenn eine Abfrage ausgeführt wird, die die Ergebnisse nicht nach dem Geschlecht einschränkt, können wir sicherstellen, dass der Index benutzbar bleibt, indem wir zur WHERE-Klausel AND sex IN('m', 'f') hinzufügen. Damit werden keine Zeilen ausgefiltert, funktional ist es also das Gleiche, als würde man die sex-Spalte nicht in die WHERE-Klausel einfügen. Wir müssen jedoch diese Spalte einfügen, weil sie es MySQL erlaubt, ein größeres Präfix für den Index zu benutzen. Dieser Trick eignet sich für solche Situationen; wenn die Spalte jedoch viele verschiedene Werte aufweist, dann funktioniert er nicht so gut, weil die IN( )-Liste zu groß werden würde. Dieser Fall verdeutlicht ein allgemeines Prinzip: Spielen Sie alle Möglichkeiten durch. Wenn Sie Indizes entwerfen, dann denken Sie nicht nur an die Arten von Indizes, die Sie für existierende Abfragen benötigen, sondern ziehen Sie es auch in Betracht, die Abfragen zu optimieren. Falls Sie den Bedarf für einen Index sehen, aber der Meinung sind, dass einige Abfragen dadurch leiden könnten, dann fragen Sie sich, ob Sie die Abfragen ändern können. Optimieren Sie Abfragen und Indizes zusammen, um den besten Kompromiss zu finden; es ist nicht nötig, das perfekte Indizierungsschema im luftleeren Raum zu schaffen.
Indizierung – eine Fallstudie | 141
Als Nächstes überlegen wir, welche weiteren Kombinationen aus WHERE-Bedingungen uns wahrscheinlich begegnen werden und welche dieser Kombinationen ohne passende Indizes zu langsam sein werden. Ein Index in (sex, country, age) ist ganz offensichtlich, und vermutlich brauchen wir auch Indizes in (sex, country, region, age) und (sex, country, region, city, age). Das werden immer mehr Indizes. Falls wir Indizes wiederverwenden wollen und das nicht zu viele Kombinationen aus Bedingungen erzeugt, können wir den IN( )-Trick einsetzen und die (sex, country, age)- und (sex, country, region, age)-Indizes ausrangieren. Sind sie nicht im Suchformular angegeben, dann können wir dafür sorgen, dass das Indexpräfix Gleichheits-Constraints enthält, indem wir eine Liste aller Länder oder aller Regionen des Landes festlegen. (Kombinierte Listen aller Länder, aller Regionen und aller Geschlechter wären wahrscheinlich zu groß.) Diese Indizes bedienen die am häufigsten auftretenden Suchabfragen. Doch wie können wir Indizes für weniger häufig auftretende Möglichkeiten entwerfen, wie z.B. has_pictures, eye_color, hair_color und education? Wenn diese Spalten nicht sehr selektiv sind und nicht oft benutzt werden, dann können wir sie einfach überspringen und MySQL ein paar zusätzliche Zeilen scannen lassen. Alternativ können wir sie vor der ageSpalte einfügen und die bereits beschriebene IN( )-Technik einsetzen, um den Fall zu behandeln, in dem sie nicht angegeben sind. Sie haben vielleicht bemerkt, dass wir die age-Spalte am Ende des Index halten. Was ist das Besondere an dieser Spalte, und wieso sollte sie am Ende des Index stehen? Wir versuchen sicherzustellen, dass MySQL so viele Spalten des Index benutzt wie möglich, da es nur das am weitesten links gelegene Präfix einsetzt, bis zu und einschließlich der ersten Bedingung, die einen Bereich aus Werten festlegt. Alle anderen Spalten, die wir erwähnt haben, können Gleichheitsbedingungen in der WHERE-Klausel verwenden, age jedoch ist ziemlich sicher ein Bereich (z.B. age BETWEEN 18 AND 25). Wir könnten das in eine IN( )-Liste umwandeln, also etwa age IN(18, 19, 20, 21, 22, 23, 24, 25), das ist für diese Art von Abfrage aber nicht immer möglich. Das allgemeine Prinzip, das wir verdeutlichen wollen, besteht darin, das Bereichskriterium am Ende des Index zu behalten, damit der Optimierer so viel wie möglich des Index benutzt. Wir haben gesagt, dass Sie immer mehr Spalten zum Index hinzufügen und IN( )-Listen einsetzen können, um Fälle abzudecken, in denen diese Spalten nicht Teil der WHEREKlausel sind. Sie können es aber auch übertreiben und Probleme bekommen. Werden nämlich mehr als nur einige wenige Listen verwendet, dann explodiert förmlich die Anzahl der Kombinationen, die der Optimierer auswerten muss, und das kann die Abfragegeschwindigkeit unweigerlich herabsetzen. Betrachten Sie die folgende WHERE-Klausel: WHERE eye_color IN('brown','blue','hazel') AND hair_color IN('black','red','blonde','brown') AND sex IN('M','F')
Der Optimierer wandelt dies in 4*3*2 = 24 Kombinationen um, die WHERE-Klausel muss diese alle überprüfen. Vierundzwanzig ist noch keine übermäßig große Zahl, seien Sie
142 | Kapitel 3: Schema-Optimierung und Indizierung
aber vorsichtig, wenn dieser Wert in die Tausende geht. Ältere MySQL-Versionen hatten mehr Probleme mit vielen IN( )-Kombinationen: Die Optimierung der Abfragen konnte länger dauern als deren Ausführung und eine Menge Speicher belegen. Neuere MySQLVersionen stoppen die Bewertung von Kombinationen, wenn deren Anzahl zu groß wird, allerdings schränkt das dann ein, wie gut MySQL den Index benutzen kann.
Was ist eine Bereichsbedingung? Die Ausgabe von EXPLAIN lässt manchmal nur schwer erkennen, ob MySQL tatsächlich nach einem Bereich von Werten sucht oder nach einer Liste von Werten. EXPLAIN benutzt den gleichen Begriff, nämlich »range« (Bereich), um beide zu kennzeichnen. So nennt z.B. MySQL das Folgende eine »Bereichsabfrage«, wie Sie in der Spalte type erkennen können: mysql> EXPLAIN SELECT actor_id FROM sakila.actor -> WHERE actor_id > 45\G ************************* 1. row ************************* id: 1 select_type: SIMPLE table: actor type: range
Aber was ist hiermit? mysql> EXPLAIN SELECT actor_id FROM sakila.actor -> WHERE actor_id IN(1, 4, 99)\G ************************* 1. row ************************* id: 1 select_type: SIMPLE table: actor type: range
Es gibt keine Möglichkeit, den Unterschied festzustellen, indem man sich EXPLAIN anschaut, aber wir ziehen eine Trennlinie zwischen Bereichen von Werten und mehrfachen Gleichheitsbedingungen. Bei der zweiten Abfrage handelt es sich in unserer Terminologie um eine mehrfache Gleichheitsbedingung. Wir sind nicht einfach nur pingelig: Diese beiden Arten von Indexzugriffen funktionieren unterschiedlich. Die Bereichsbedingung sorgt dafür, dass MySQL alle weiteren Spalten in dem Index ignoriert, die mehrfache Gleichheitsbedingung dagegen unterliegt nicht dieser Einschränkung.
Mehrfache Bereichsbedingungen vermeiden Nehmen wir an, wir haben eine Spalte namens last_online und wollen den Benutzern zeigen können, wer in der letzten Woche online war: WHERE AND AND AND AND
eye_color hair_color sex last_online age
IN('brown','blue','hazel') IN('black','red','blonde','brown') IN('M','F') > DATE_SUB('2008-01-17', INTERVAL 7 DAY) BETWEEN 18 AND 25
Indizierung – eine Fallstudie | 143
Es gibt mit dieser Abfrage ein Problem: Sie enthält zwei Bereichsbedingungen. MySQL kann entweder das Kriterium last_online benutzen oder das Kriterium age, aber nicht beide. Wenn die Einschränkung last_online ohne die Einschränkung age erscheint oder wenn sich last_online selektiver verhält als age, dann möchten wir vielleicht noch eine weitere Gruppe von Indizes mit last_online am Ende hinzufügen. Aber was ist, wenn wir age nicht in eine IN( )-Liste umwandeln können und den Geschwindigkeitszuwachs brauchen, den das gleichzeitige Beschränken durch last_online und age bringt? Im Moment gibt es keine Möglichkeit, dies direkt zu erreichen, aber wir können einen der Bereiche in einen Gleichheitsvergleich umwandeln. Dazu fügen wir die vorberechnete Spalte active hinzu, die wir mithilfe eines periodisch ausgeführten Jobs pflegen. Wir setzen die Spalte auf 1, wenn sich der Benutzer anmeldet, der Job setzt sie wieder zurück auf 0, wenn sich der Benutzer in den folgenden sieben Tagen nicht anmeldet. Dieser Ansatz erlaubt es MySQL, Indizes wie (active, sex, country, age) zu benutzen. Die Spalte ist vielleicht nicht absolut akkurat, aber diese Art von Abfrage verlangt wahrscheinlich keinen so hohen Grad an Akkuratesse. Wenn wir Genauigkeit brauchen, können wir die Bedingung last_online in der WHERE-Klausel lassen, indizieren sie aber nicht. Diese Technik ähnelt derjenigen, die wir weiter vorn in diesem Kapitel benutzt haben, um HASH-Indizes für URL-Lookups zu simulieren. Die Bedingung benutzt keinen Index, aber da es unwahrscheinlich ist, dass man viele der Zeilen verwirft, die ein Index finden würde, wäre ein Index sowieso nicht so sinnvoll. Oder anders ausgedrückt, das Fehlen eines Index würde die Abfrage nicht merklich beeinträchtigen. Vermutlich erkennen Sie inzwischen das Muster: Wenn ein Benutzer sowohl aktive als auch inaktive Ergebnisse sehen möchte, können wir eine IN( )-Liste hinzufügen. Wir haben viele dieser Listen angelegt; die Alternative besteht darin, separate Indizes zu erzeugen, die jede Kombination aus Spalten bedienen können, die wir filtern müssen. Wir müssen wenigstens die folgenden Indizes einsetzen: (active, sex, country, age), (active, country, age), (sex, country, age) und (country, age). Solche Indizes wären zwar für jede spezielle Abfrage der optimale Fall, allerdings macht der zu erwartende Wartungsaufwand in Kombination mit dem erforderlichen Platz dies im Großen und Ganzen zu einer ziemlich schlechten Strategie. Dies ist ein Fall, in dem Änderungen des Optimierers die optimale Indizierungsstrategie wirklich beeinflussen können. Falls eine künftige Version von MySQL in der Lage ist, einen wirklich freien Indexscan durchzuführen, dann sollte es möglich sein, mehrere Bereichsbedingungen in einem einzigen Index einzusetzen, so dass die IN( )-Listen für die speziellen Arten von Abfragen, über die wir hier reden, nicht mehr erforderlich sind.
144 | Kapitel 3: Schema-Optimierung und Indizierung
Das Sortieren optimieren Der letzte Punkt, mit dem wir uns in dieser Fallstudie befassen wollen, ist das Sortieren. Das Sortieren kleiner Ergebnismengen mit Filesorts ist schnell, aber was ist, wenn Millionen von Zeilen einer Abfrage entsprechen? Was wäre z.B., wenn in der WHERE-Klausel nur sex angegeben ist? Wir können besondere Indizes zum Sortieren dieser wenig selektiven Fälle hinzufügen. So kann etwa ein Index in (sex, rating) für die folgende Abfrage benutzt werden: mysql> SELECT <spalten> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;
Diese Abfrage besitzt sowohl ORDER BY- als auch LIMIT-Klauseln und wäre ohne den Index sehr langsam. Selbst mit Index kann die Abfrage langsam sein, wenn die Benutzeroberfläche in Seiten unterteilt ist und jemand eine Seite anfordert, die nicht am Anfang liegt. Dieser Fall erzeugt eine schlechte Kombination aus ORDER BY und LIMIT mit einem Offset: mysql> SELECT <spalten> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 100000, 10;
Solche Abfragen können – unabhängig davon, wie sie indiziert werden – zu einem ernsthaften Problem werden, da der hohe Offset dafür sorgt, dass sie die meiste Zeit damit verbringen, eine Menge Daten zu scannen, die sie dann wegwerfen. Denormalisieren, Vorberechnen und Caching sind wahrscheinlich die einzigen Strategien, die bei solchen Abfragen funktionieren. Noch besser ist es, die Anzahl der Seiten zu beschränken, die Sie den Benutzer anschauen lassen. Die Arbeit des Benutzers wird dadurch mit sehr großer Wahrscheinlichkeit nicht beeinflusst, da sich vermutlich niemand wirklich um die 10.000. Seite mit Suchergebnissen kümmert. Eine weitere gute Strategie zum Optimieren solcher Abfragen besteht darin, einen abdeckenden Index zu verwenden, um nur die Primärschlüsselspalten der Zeilen zu beziehen, die Sie schließlich abrufen werden. Sie können dies dann wieder mit der Tabelle verbinden, um alle gewünschten Spalten zu erhalten. Damit wird die Arbeit verringert, die MySQL verrichten muss, um Daten zu sammeln, die es dann doch nur wegwirft. Hier ist ein Beispiel, das einen Index in (sex, rating) verlangt, um effizient zu funktionieren: mysql> SELECT <Spalten> FROM profiles INNER JOIN ( -> SELECT FROM profiles -> WHERE x.sex='M' ORDER BY rating LIMIT 100000, 10 -> ) AS x USING();
Index- und Tabellenpflege Wenn Sie Tabellen mit richtigen Datentypen erzeugt und Indizes hinzugefügt haben, ist Ihre Arbeit noch nicht beendet: Sie müssen die Tabellen und Indizes pflegen, damit diese auch gut funktionieren. Die drei Hauptziele der Tabellenpflege sind das Suchen und Reparieren von Schäden, das Pflegen der exakten Indexstatistiken und das Reduzieren der Fragmentierung.
Index- und Tabellenpflege | 145
Schäden an der Tabelle suchen und reparieren Das Schlimmste, was einer Tabelle passieren kann, ist Beschädigung. Bei der MyISAMStorage-Engine kommen solche Schäden oft aufgrund von Abstürzen vor. Allerdings können bei allen Storage-Engines in Folge von Hardware-Problemen oder internen Bugs an MySQL oder dem Betriebssystem Beschädigungen des Index auftreten. Beschädigte Indizes sind manchmal die Ursache dafür, dass Abfragen falsche Ergebnisse zurückliefern, dass fälschlicherweise duplizierte Schlüssel auftreten, obwohl es gar keine duplizierten Werte gibt, oder dass es sogar zu Sperren und Abstürzen kommt. Wenn Ihnen etwas am Verhalten des Systems seltsam vorkommt – wenn etwa ein Fehler auftritt, der Ihrer Meinung nach nicht geschehen sollte –, dann sollten Sie CHECK TABLE ausführen, um festzustellen, ob die Tabelle beschädigt ist. (Beachten Sie, dass manche Storage-Engines diesen Befehl nicht unterstützen, andere wiederum unterstützen verschiedene Optionen, mit denen man festlegen kann, wie gründlich die Tabelle überprüft werden soll.) CHECK TABLE erkennt normalerweise die meisten Tabellen- und Indexfehler. Sie können beschädigte Tabellen mit dem Befehl REPAIR TABLE reparieren, allerdings unterstützen nicht alle Storage-Engines diesen Befehl. In diesen Fällen führen Sie einen »leeren« ALTER-Befehl aus, »ändern« also die Tabelle derart, dass sie die gleiche StorageEngine wie zuvor benutzt. Hier ist ein Beispiel für eine InnoDB-Tabelle: mysql> ALTER TABLE innodb_tbl ENGINE=INNODB;
Alternativ können Sie entweder ein offline ausgeführtes, engine-spezifisches Reparaturprogramm wie myisamchk einsetzen oder einen Dump der Daten erzeugen und die Daten dann neu laden. Falls der Schaden sich jedoch im Systembereich befindet oder im »Zeilendaten«-Bereich der Tabelle anstatt im Index, dann helfen Ihnen diese Möglichkeiten nicht weiter. Sie müssten dann versuchen, die Tabelle aus Ihren Backups wiederherzustellen oder zumindest die Daten aus den beschädigten Dateien zu retten (siehe Kapitel 11).
Indexstatistiken aktualisieren Der MySQL-Abfrageoptimierer fragt die Storage-Engines mithilfe von zwei API-Aufrufen, wie die Indexwerte verteilt sind, wenn er entscheidet, wie die Indizes benutzt werden sollen. Der erste Aufruf lautet records_in_range( ). Er akzeptiert Bereichsendpunkte und liefert die (möglicherweise geschätzte) Anzahl an Datensätzen in diesem Bereich zurück. Der zweite Aufruf ist info( ), der verschiedene Arten von Daten zurückliefern kann, einschließlich der Indexkardinalität (also wie viele Datensätze es für jeden Schlüsselwert gibt). Wenn die Storage-Engine dem Optimierer keine exakten Informationen über die Anzahl der Zeilen übermittelt, die eine Abfrage untersuchen wird, verwendet der Optimierer die Indexstatistiken, die Sie mittels ANALYZE TABLE erneuern, um die Anzahl der Zeilen abzuschätzen. Der MySQL-Optimierer ist kostenbasiert, und das wichtigste Maß für die Kosten ist, auf wie viele Daten die Abfrage zugreifen wird. Wenn die Statistiken noch nie
146 | Kapitel 3: Schema-Optimierung und Indizierung
erzeugt wurden oder wenn sie nicht mehr auf dem neuesten Stand sind, trifft der Optimierer möglicherweise schlechte Entscheidungen. Die Lösung besteht darin, ANALYZE TABLE auszuführen. Jede Storage-Engine implementiert die Indexstatistiken anders, so dass Sie im Einzelfall entscheiden müssen, wie oft Sie ANALYZE TABLE ausführen. Auch die Kosten für das Ausführen der Anweisung unterscheiden sich von Storage-Engine zu Storage-Engine: • Die Memory-Storage-Engine speichert Indexstatistiken überhaupt nicht. • MyISAM legt die Statistiken auf der Festplatte ab, und ANALYZE TABLE führt einen vollständigen Indexscan durch, um die Kardinalität zu berechnen. Die gesamte Tabelle ist während dieses Vorgangs gesperrt. • InnoDB speichert keine Statistiken auf der Festplatte, sondern schätzt sie mithilfe von zufälligen Indexzugriffen, wenn die Tabelle das erste Mal geöffnet wird. ANALYZE TABLE benutzt für InnoDB zufällige Indexzugriffe. InnoDB-Statistiken sind daher weniger genau, müssen allerdings wahrscheinlich nicht manuell aktualisiert werden, es sei denn, Sie lassen den Server über einen langen Zeitraum laufen. ANALYZE TABLE ist außerdem in InnoDB nichtblockierend und relativ unaufwendig, so dass Sie die Statistiken online aktualisieren können, ohne dass der Server stark beeinträchtigt werden würde. Sie können die Kardinalität Ihrer Indizes mit dem Befehl SHOW INDEX FROM untersuchen. Zum Beispiel: mysql> SHOW INDEX FROM sakila.actor\G *************************** 1. row *************************** Table: actor Non_unique: 0 Key_name: PRIMARY Seq_in_index: 1 Column_name: actor_id Collation: A Cardinality: 200 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment: *************************** 2. row *************************** Table: actor Non_unique: 1 Key_name: idx_actor_last_name Seq_in_index: 1 Column_name: last_name Collation: A Cardinality: 200 Sub_part: NULL Packed: NULL Null: Index_type: BTREE Comment:
Index- und Tabellenpflege | 147
Dieser Befehl liefert Ihnen eine Menge Indexinformationen, die das MySQL-Handbuch genauer erklärt. Wenden Sie Ihre geschätzte Aufmerksamkeit bitte der Spalte Cardinality zu. Diese zeigt, wie viele Einzelwerte die Storage-Engine im Index vermutet. In MySQL 5.0 und neueren Versionen erhalten Sie diese Daten auch aus der Tabelle INFORMATION_ SCHEMA.STATISTICS, was sehr praktisch sein kann. So könnten Sie z.B. Abfragen an die INFORMATION_SCHEMA-Tabellen schreiben, um Indizes mit sehr niedriger Selektivität zu ermitteln.
Index- und Datenfragmentierung verringern B-Baum-Indizes können fragmentiert sein, wodurch sich die Leistung vermindert. Fragmentierte Indizes sind möglicherweise schlecht gefüllt und/oder nichtsequenziell auf der Festplatte. Aufgrund ihres Designs erfordern B-Baum-Indizes zufällige Festplattenzugriffe, um zu den Blattseiten zu gelangen. Zufällige Zugriffe sind also die Regel und nicht die Ausnahme. Die Blattseiten funktionieren jedoch besser, wenn sie physisch sequenziell angeordnet und dicht gepackt sind. Sind sie es nicht, sprechen wir davon, dass sie fragmentiert sind. Bereichsscans oder vollständige Indexscans können in diesem Fall deutlich langsamer vonstatten gehen. Das gilt im Besonderen für indexabdeckende Abfragen. Auch die Datenspeicherung der Tabelle kann fragmentiert werden. Diese Art der Fragmentierung ist jedoch komplexer als die Indexfragmentierung. Es gibt zwei Arten von Datenfragmentierung: Zeilenfragmentierung Diese Art der Fragmentierung tritt auf, wenn die Zeile an mehreren Stellen in mehreren Teilen gespeichert ist. Zeilenfragmentierung vermindert die Performance, selbst wenn die Abfrage nur eine einzige Zeile aus dem Index benötigt. Interne Zeilenfragmentierung Diese Art der Fragmentierung tritt auf, wenn logisch sequenziell angeordnete Seiten oder Zeilen nicht sequenziell auf der Festplatte gespeichert werden. Sie beeinträchtigt Operationen wie vollständige Tabellenscans und Bereichsscans in Cluster-Indizes, die normalerweise von einer sequenziellen Anordnung der Daten auf der Festplatte profitieren. In MyISAM-Tabellen können beide Arten der Fragmentierung auftreten, InnoDB dagegen fragmentiert kurze Zeilen nie. Um die Daten zu defragmentieren, führen Sie entweder OPTIMIZE TABLE aus oder erzeugen einen Dump der Daten und laden die Daten dann wieder. Diese Ansätze funktionieren bei den meisten Storage-Engines. Bei einigen, wie etwa MyISAM, werden dabei gleich die Indizes defragmentiert, indem sie mit einem Sortieralgorithmus neu aufgebaut, das heißt, in der richtigen Reihenfolge neu erstellt werden. Momentan gibt es keine Möglichkeit, InnoDB-Indizes zu defragmentieren, da InnoDB in
148 | Kapitel 3: Schema-Optimierung und Indizierung
MySQL Indizes nicht durch Sortieren aufbauen kann.17 Selbst das Verwerfen und Neuerzeugen von InnoDB-Indizes kann je nach den Daten zu fragmentierten Indizes führen. Bei Storage-Engines, die OPTIMIZE TABLE nicht unterstützen, können Sie die Tabelle mit einem leeren ALTER TABLE-Befehl neu aufbauen. Damit veranlassen Sie, dass die Tabelle dieselbe Engine wie zuvor verwendet: mysql> ALTER TABLE ENGINE=<Engine>;
Normalisierung und Denormalisierung Es gibt normalerweise viele Möglichkeiten, bestimmte Daten darzustellen: von vollständig normalisiert bis vollständig denormalisiert und in beliebigen Abstufungen dazwischen. In einer normalisierten Datenbank wird jeder Fakt einmal, und zwar nur einmal abgebildet. Im Gegenzug werden Informationen in einer denormalisierten Datenbank dupliziert oder an mehreren Stellen gespeichert. Befassen Sie sich mit Normalisierung, falls Sie damit nicht vertraut sind. Es gibt viele gute Bücher sowie online verfügbare Ressourcen zu dem Thema; wir bieten Ihnen hier nur eine kurze Einführung zu den Aspekten, die Sie für dieses Kapitel kennen sollten. Beginnen wir mit dem klassischen Beispiel von Angestellten, Abteilungen und Abteilungsleitern: ANGESTELLTER
ABTEILUNG
LEITER
Jones
Buchhaltung
Jones
Smith
Entwicklung
Smith
Brown
Buchhaltung
Jones
Green
Entwicklung
Smith
Das Problem mit diesem Schema besteht darin, dass Anomalien auftreten können, wenn die Daten modifiziert werden. Nehmen wir an, Brown wird zum Chef der Buchhaltung. Wir müssen mehrere Zeilen aktualisieren, um diese Änderung zu übernehmen. Während dieser Aktualisierungen befinden sich die Daten in einem inkonsistenten Zustand. Wenn sich der Abteilungsleiter in der »Jones«-Zeile von dem in der »Brown«-Zeile unterscheidet, gibt es keine Möglichkeit festzustellen, was richtig ist. Es ist wie in dem Spruch: »Jemand mit zwei Uhren weiß nicht, wie spät es wirklich ist«. Darüber hinaus können wir eine Abteilung nie ohne Angestellte darstellen – wenn wir alle Angestellten in der Buchhaltungsabteilung löschen, verlieren wir alle Datensätze über die Abteilung selbst. Um diese Probleme zu umgehen, müssen wir die Tabelle normalisieren, indem wir die Angestellten- und die Abteilungseinheiten trennen. Das führt zu den folgenden zwei Tabellen für die Angestellten:
17 Die InnoDB-Entwickler arbeiten momentan daran, dieses Problem zu lösen.
Normalisierung und Denormalisierung | 149
ANGESTELLTENNAME
ABTEILUNG
Jones
Buchhaltung
Smith
Entwicklung
Brown
Buchhaltung
Green
Entwicklung
und folgenden Abteilungen: ABTEILUNG
LEITER
Buchhaltung
Jones
Entwicklung
Smith
Diese Tabellen sind nun in der zweiten Normalform, was für viele Zwecke ausreichend ist. Allerdings stellt die zweite Normalform nur eine von vielen möglichen Normalformen dar. Wir verwenden hier zur Verdeutlichung den Nachnamen als Primärschlüssel, da er der »natürliche Identifikator« der Daten ist. In der Praxis würden Sie das natürlich nicht tun. Er ist nicht unbedingt eindeutig, und außerdem sollte man normalerweise keinen langen String als Primärschlüssel benutzen.
Vor- und Nachteile eines normalisierten Schemas Leute, die aufgrund von Performance-Problemen um Hilfe bitten, werden oft aufgefordert, ihre Schemata zu normalisieren, vor allem, wenn die Operationen sehr schreiblastig sind. Das ist meist ein guter Rat, und zwar aus folgenden Gründen: • Normalisierte Updates sind üblicherweise schneller als denormalisierte Updates. • Wenn die Daten gut normalisiert sind, gibt es kaum oder keine duplizierten Daten, so dass weniger Daten zu ändern sind. • Normalisierte Tabellen sind meist kleiner, passen also besser in den Speicher und funktionieren besser. • Das Fehlen redundanter Daten bedeutet, dass weniger Bedarf an DISTINCT- oder GROUP BY-Abfragen besteht, wenn Listen mit Werten bezogen werden müssen. Denken Sie an das vorgestellte Beispiel: Es ist nicht möglich, ohne DISTINCT oder GROUP BY eine eindeutige Liste der Abteilungen aus einem denormalisierten Schema zu erhalten. Wenn ABTEILUNG dagegen eine eigene Tabelle ist, dann ist die Abfrage trivial. Die Nachteile eines normalisierten Schemas haben üblicherweise mit der Abfrage zu tun. Jede nichttriviale Abfrage in einem gut normalisierten Schema verlangt wahrscheinlich wenigstens einen, vielleicht sogar mehrere Joins. Das ist nicht nur teuer, sondern kann
150 | Kapitel 3: Schema-Optimierung und Indizierung
einige Indizierungsstrategien sogar vereiteln. Beispielsweise könnten durch eine Normalisierung Spalten in unterschiedliche Tabellen gelangen, für die es besser wäre, wenn sie zum gleichen Index gehörten.
Vor- und Nachteile eines denormalisierten Schemas Ein denormalisiertes Schema funktioniert, weil alles sich in derselben Tabelle befindet, wodurch Joins vermieden werden. Wenn Sie Tabellen nicht zusammenführen müssen, dann ist das Schlimmste für die meisten Abfragen – selbst für die, die keine Indizes verwenden – ein vollständiger Tabellenscan. Dieser kann viel schneller sein als ein Join, wenn die Daten nicht in den Speicher passen, weil er willkürliche Ein-/Ausgaben vermeidet. Eine einzelne Tabelle kann außerdem effizientere Indizierungsstrategien zulassen. Nehmen Sie an, Sie haben eine Website, auf der Benutzer ihre Nachrichten veröffentlichen. Einige Benutzer sind Premium-Benutzer. Nehmen Sie weiterhin an, Sie wollen die letzten zehn Nachrichten der jeweiligen Premium-Benutzer anzeigen lassen. Wenn Sie dieses Schema normalisiert und die Veröffentlichungsdaten der Nachrichten indiziert hätten, dann könnte die Abfrage so aussehen: mysql> -> -> -> ->
SELECT message_text, user_name FROM message INNER JOIN user ON message.user_id=user.id WHERE user.account_type='premium' ORDER BY message.published DESC LIMIT 10;
Um diese Abfrage effizient auszuführen, muss MySQL den Index published in der Tabelle message durchsuchen. Für jede Zeile, die es findet, muss es in die Tabelle user schauen und überprüfen, ob der Benutzer ein Premium-Benutzer ist. Das ist ineffizient, wenn nur ein Bruchteil der Benutzer einen Premium-Zugang besitzt. Der andere mögliche Abfrageplan sieht vor, mit der user-Tabelle zu beginnen, alle Premium-Benutzer auszuwählen, alle Nachrichten für sie zu holen und einen Filesort durchzuführen. Das ist wahrscheinlich sogar noch schlimmer. Das Problem ist der Join, der Sie davon abhält, gleichzeitig mit einem einzigen Index zu sortieren und zu filtern. Wenn Sie die Daten denormalisieren, indem Sie die Tabellen miteinander kombinieren und einen Index in (account_type, published) hinzufügen, können Sie die Abfrage ohne Join schreiben. Das ist dann wiederum sehr effizient: mysql> -> -> -> ->
SELECT message_text,user_name FROM user_messages WHERE account_type='premium' ORDER BY published DESC LIMIT 10;
Normalisierung und Denormalisierung | 151
Ein Mix aus normalisiert und denormalisiert Wie sollen Sie nun das beste Design wählen, wenn sowohl normalisierte als auch denormalisierte Schemata Vor- und Nachteile aufweisen? Um ehrlich zu sein, sind vollständig normalisierte und vollständig denormalisierte Schemata wie Laborratten: Sie haben üblicherweise mit dem richtigen Leben wenig zu tun. Im richtigen Leben muss man oft die verschiedenen Ansätze mischen, d.h., ein teilweise normalisiertes Schema, Cache-Tabellen und andere Techniken verwenden. Die gebräuchlichste Methode, um Daten zu denormalisieren, besteht darin, ausgewählte Spalten von einer Tabelle in eine andere zu duplizieren oder im Cache zu speichern. Seit MySQL 5.0 können Sie Trigger verwenden, um die im Cache abgelegten Werte zu aktualisieren, was die Implementierung vereinfacht. Anstatt z.B. in unserem Website-Beispiel vollständig zu denormalisieren, können Sie account_type sowohl in der user- als auch in der message-Tabelle speichern. Dadurch vermeiden Sie Probleme beim Einfügen und Löschen, die bei einer vollständigen Denormalisierung auftreten, weil Sie niemals die Informationen über den Benutzer verlieren, selbst wenn es keine Nachrichten gibt. Die Tabelle user_message wird dadurch nicht viel größer, Sie können aber die Daten effizient auswählen. Es ist nun jedoch aufwendiger, den Zugangstyp eines Benutzers zu aktualisieren, da Sie diesen in beiden Tabellen ändern müssen. Um herauszufinden, ob das ein Problem darstellt, müssen Sie überlegen, wie oft Sie solche Änderungen vornehmen und wie lange sie dauern, und dies damit vergleichen, wie oft Sie eine SELECT-Abfrage ausführen. Ein weiterer guter Grund dafür, einige der Daten von der Elterntabelle in die Kindtabelle zu verschieben, ist die Sortierung. Es wäre z.B. außerordentlich teuer, die Nachrichten in einem normalisierten Schema nach dem Namen des Autors zu sortieren. Sie können eine solche Sortierung dagegen sehr effizient vornehmen, wenn Sie den author_name in der message-Tabelle zwischenspeichern und indizieren. Auch das Speichern von abgeleiteten Werten im Cache kann sehr nützlich sein. Falls Sie anzeigen müssen, wie viele Nachrichten die jeweiligen Benutzer veröffentlicht haben (wie es viele Foren tun), können Sie entweder eine aufwendige Unterabfrage ausführen, um die Daten jedes Mal zu zählen, wenn Sie sie anzeigen, oder Sie legen eine num_messagesSpalte in der user-Tabelle an, die Sie immer dann aktualisieren, wenn ein Benutzer eine neue Nachricht veröffentlicht.
Cache- und Summary-Tabellen Manchmal besteht die beste Methode, um die Leistung zu verbessern, darin, redundante Daten in der gleichen Tabelle abzulegen wie die Daten, aus denen sie abgeleitet wurden. Dann wiederum müssen Sie manchmal aber auch völlig getrennte Summary- oder CacheTabellen erstellen, die speziell an Ihre Anforderungen angepasst sind. Dieser Ansatz funktioniert am besten, wenn Sie es tolerieren können, dass die Daten nicht mehr ganz
152 | Kapitel 3: Schema-Optimierung und Indizierung
taufrisch sind, aber manchmal haben Sie wirklich keine andere Wahl (z.B. wenn Sie komplexe und teure Echtzeit-Updates vermeiden müssen). Die Begriffe »Cache-Tabelle« und »Summary-Tabelle« haben keine standardisierten Bedeutungen. Wir verwenden den Begriff »Cache-Tabelle« für die Bezeichnung von Tabellen, die Daten enthalten, die leicht, wenn auch langsam, aus dem Schema bezogen werden können (d.h. Daten, die logisch redundant sind). Wenn wir »Summary-Tabelle« sagen, dann meinen wir Tabellen, die gesammelte Daten aus GROUP BY-Abfragen enthalten (d.h. Daten, die nicht logisch redundant sind). Manche Leute verwenden auch den Begriff »Roll-up«-Tabellen für diese Tabellen. Um bei dem Website-Beispiel zu bleiben: Stellen Sie sich vor, Sie müssten die Anzahl der Nachrichten zählen, die während der vorangegangenen 24 Stunden veröffentlicht worden sind. Bei einer ausgelasteten Site wäre es unmöglich, einen exakten Echtzeitzähler anzubieten. Stattdessen würden Sie einmal in der Stunde eine Summary-Tabelle generieren. Oft können Sie dies mit einer einzigen Abfrage erledigen, und das ist effizienter, als Zähler in Echtzeit zu unterhalten. Nachteilig ist, dass die Zahlen nicht 100 % exakt sind. Um die exakte Zahl der Nachrichten zu erhalten, die im Laufe der vergangenen 24 Stunden veröffentlicht wurden, gibt es eine andere Option. Beginnen Sie mit einer SummaryTabelle auf Stundenbasis. Sie können dann die exakte Anzahl der Nachrichten ermitteln, die in einem bestimmten 24-Stunden-Zeitraum veröffentlicht wurden, indem Sie die Anzahl der Nachrichten der 23 ganzen Stunden, die in dem Zeitraum enthalten sind, zu der Teilstunde am Anfang und der Teilstunde am Ende addieren. Nehmen Sie an, Ihre Summary-Tabelle heißt msg_per_hr und ist folgendermaßen definiert: CREATE TABLE msg_per_hr ( hr DATETIME NOT NULL, cnt INT UNSIGNED NOT NULL, PRIMARY KEY(hr) );
Sie können die Anzahl der Nachrichten der vorangegangenen 24 Stunden ermitteln, indem Sie die Ergebnisse der folgenden drei Abfragen addieren:18 mysql> -> -> -> mysql> -> -> mysql> ->
SELECT SUM(cnt) FROM msg_per_hr WHERE hr BETWEEN CONCAT(LEFT(NOW( ), 14), '00:00') - INTERVAL 23 HOUR AND CONCAT(LEFT(NOW( ), 14), '00:00') - INTERVAL 1 HOUR; SELECT COUNT(*) FROM message WHERE posted >= NOW( ) - INTERVAL 24 HOUR AND posted < CONCAT(LEFT(NOW( ), 14), '00:00') - INTERVAL 23 HOUR; SELECT COUNT(*) FROM message WHERE posted >= CONCAT(LEFT(NOW( ), 14), '00:00');
Beide Ansätze – ein ungenaues Zählen oder ein exaktes Zählen mit kleinen Bereichsabfragen, um die Lücken zu füllen – sind effizienter, als alle Zeilen in der message-Tabelle zu zählen. Das ist der wesentliche Grund für das Erzeugen von Summary-Tabellen. Diese 18 Wir benutzen LEFT(NOW( ), 14), um das aktuelle Datum und die Uhrzeit auf die nächste Stunde zu runden.
Normalisierung und Denormalisierung | 153
Statistiken sind in Echtzeit aufwendig zu berechnen, da sie das Scannen vieler Daten erfordern oder Abfragen verlangen, die nur mit bestimmten Indizes effizient laufen, die Sie aber aufgrund der Auswirkungen, die diese auf die Updates haben, nicht hinzufügen wollen. Das Ermitteln der aktivsten Benutzer oder der am häufigsten verwendeten »Tags« sind typische Beispiele für solche Operationen. Cache-Tabellen sind wiederum sinnvoll zum Optimieren von Such- und Retrieval-Abfragen. Diese Abfragen verlangen oft eine Tabellen- und Indexstruktur, die sich von der für allgemeine OLTP-Operationen (Online Transaction Processing) unterscheidet. So brauchen Sie z.B. viele unterschiedliche Indexkombinationen, um die verschiedenen Arten von Abfragen zu beschleunigen. Diese widersprüchlichen Anforderungen erfordern es manchmal, dass Sie eine Cache-Tabelle erzeugen, die nur einige der Spalten aus der Haupttabelle enthält. Eine sinnvolle Technik ist es, eine andere Storage-Engine für die Cache-Tabelle zu benutzen. Wenn die Haupttabelle z.B. InnoDB benutzt, dann bekommen Sie, wenn Sie MyISAM für die Cache-Tabelle benutzen, eine kleinere Indexbasis sowie die Fähigkeit, Volltextsuchabfragen durchzuführen. Manchmal wollen Sie vielleicht sogar eine Tabelle vollständig aus MySQL herausnehmen und in ein spezialisiertes System bringen, das effizienter suchen kann, wie etwa die Lucene- oder SphinxSuchmaschinen. Wenn Sie Cache- und Summary-Tabellen benutzen, müssen Sie entscheiden, ob Sie deren Daten in Echtzeit oder mit periodisch wiederkehrenden Neuaufbauten pflegen. Welche Wahl besser ist, hängt von Ihrer Anwendung ab. Ein periodisch wiederkehrender Neuaufbau kann nicht nur Ressourcen sparen, sondern auch zu einer effizienteren Tabelle führen, die nicht fragmentiert ist und vollständig sortierte Indizes besitzt. Beim Neuaufbauen von Summary- und Cache-Tabellen müssen Sie oft dafür sorgen, dass deren Daten während der Operation verfügbar bleiben. Dies erreichen Sie z.B. mithilfe einer »Schattentabelle«, also einer Tabelle, die Sie »hinter« der echten Tabelle aufbauen. Wenn Sie mit dem Aufbau fertig sind, können Sie die Tabellen mit einem atomaren Umbenennen wechseln. Falls Sie etwa my_summary neu aufbauen müssen, erzeugen Sie my_summary_new, füllen die Tabelle mit Daten und vertauschen sie mit der echten Tabelle: mysql> DROP TABLE IF EXISTS my_summary_new, my_summary_old; mysql> CREATE TABLE my_summary_new LIKE my_summary; -- populate my_summary_new as desired mysql> RENAME TABLE my_summary TO my_summary_old, my_summary_new TO my_summary;
Falls Sie, wie wir es hier getan haben, die Original-my_summary-Tabelle my_summary_old nennen, bevor Sie den Namen my_summary der neu aufgebauten Tabelle zuweisen, können Sie die alte Version behalten, bis Sie sie beim nächsten Neuaufbau überschreiben. Das kann sich als praktisch erweisen, falls es mit der neuen Tabelle ein Problem gibt und Sie zurückrudern müssen.
154 | Kapitel 3: Schema-Optimierung und Indizierung
Zählertabellen Eine Anwendung, die Zähler in einer Tabelle führt, kann beim Aktualisieren der Zähler Probleme mit der Nebenläufigkeit bekommen. Solche Tabellen sind in Webanwendungen sehr gebräuchlich. Damit können Sie die Anzahl der Freunde eines Benutzers, die Anzahl der Downloads einer Datei usw. speichern. Oft bietet es sich an, eine eigene Tabelle für die Zähler anzulegen, um sie klein und schnell zu halten. Die Verwendung einer getrennten Tabelle hilft Ihnen, das Ungültigwerden des Abfrage-Cache zu vermeiden und erlaubt es Ihnen, einige der ausgefeilteren Techniken einzusetzen, die wir Ihnen in diesem Abschnitt zeigen. Der Einfachheit halber nehmen Sie an, Sie haben eine Zählertabelle mit einer einzigen Zeile, die lediglich die Zugriffe (Hits) auf Ihre Website zählt: mysql> CREATE TABLE hit_counter ( -> cnt int unsigned not null -> ) ENGINE=InnoDB;
Jeder Zugriff auf die Website aktualisiert den Zähler: mysql> UPDATE hit_counter SET cnt = cnt + 1;
Das Problem ist, dass diese einzelne Zeile im Prinzip einen globalen »Mutex« für jede Transaktion bildet, die den Zähler aktualisiert. Dadurch werden diese Transaktionen serialisiert. Sie erreichen eine größere Nebenläufigkeit, indem Sie mehr als eine Zeile vorhalten und eine zufällige Zeile aktualisieren. Dies erfordert die folgende Änderung an der Tabelle: mysql> CREATE TABLE hit_counter ( -> slot tinyint unsigned not null primary key, -> cnt int unsigned not null -> ) ENGINE=InnoDB;
Fügen Sie der Tabelle 100 Zeilen hinzu. Jetzt kann die Abfrage einen beliebigen Slot wählen und aktualisieren: mysql> UPDATE hit_counter SET cnt = cnt + 1 WHERE slot = RAND( ) * 100;
Um eine Statistik zu erhalten, benutzen Sie Aggregat-Abfragen: mysql> SELECT SUM(cnt) FROM hit_counter;
Oft wird verlangt, dass gelegentlich neue Zähler gestartet werden müssen (z.B. einmal am Tag). Falls dies der Fall ist, können Sie das Schema leicht ändern: mysql> CREATE TABLE daily_hit_counter ( -> day date not null, -> slot tinyint unsigned not null, -> cnt int unsigned not null, -> primary key(day, slot) -> ) ENGINE=InnoDB;
Für dieses Szenario wollen Sie nicht bereits vorab Zeilen generieren. Stattdessen können Sie ON DUPLICATE KEY UPDATE einsetzen:
Normalisierung und Denormalisierung | 155
mysql> INSERT INTO daily_hit_counter(day, slot, cnt) -> VALUES(CURRENT_DATE, RAND( ) * 100, 1) -> ON DUPLICATE KEY UPDATE cnt = cnt + 1;
Falls Sie die Anzahl der Zeilen verringern wollen, um eine kleinere Tabelle zu erhalten, können Sie einen periodisch auszuführenden Job schreiben, der alle Ergebnisse in Slot 0 zusammenfasst und alle anderen Slots löscht: mysql> UPDATE daily_hit_counter as c -> INNER JOIN ( -> SELECT day, SUM(cnt) AS cnt, MIN(slot) AS mslot -> FROM daily_hit_counter -> GROUP BY day -> ) AS x USING(day) -> SET c.cnt = IF(c.slot = x.mslot, x.cnt, 0), -> c.slot = IF(c.slot = x.mslot, 0, c.slot); mysql> DELETE FROM daily_hit_counter WHERE slot <> 0 AND cnt = 0;
Schnelleres Lesen, langsameres Schreiben Oft braucht man zusätzliche Indizes, redundante Felder oder sogar Cache- und SummaryTabellen, um Leseabfragen zu beschleunigen. Das bringt zwar mehr Arbeit für Schreibabfragen und Wartungsaufgaben mit sich, aber dennoch findet man diese Technik häufig, wenn es um hohe Leistung geht: Die Kosten für die langsameren Schreiboperationen werden durch die deutlichen Geschwindigkeitszuwächse beim Lesen mehr als ausgeglichen. Das ist jedoch nicht der einzige Preis, den Sie für schnellere Leseabfragen zahlen. Auch die Komplexität der Entwicklung vergrößert sich für Lese- und Schreiboperationen.
ALTER TABLE beschleunigen Die Leistung des MySQL-Befehls ALTER TABLE kann bei sehr großen Tabellen zu einem Problem werden. MySQL führt die meisten Umbauten aus, indem es eine leere Tabelle mit der gewünschten neuen Struktur anlegt, alle Daten aus der alten Tabelle in die neue Tabelle einfügt und die alte Tabelle dann löscht. Das kann sehr lange dauern, vor allem, wenn der Speicher knapp bemessen ist und die Tabelle sehr groß ist und viele Indizes besitzt. Viele Leute haben ALTER TABLE-Operationen »durchlitten«, die Stunden oder Tage gebraucht haben. MySQL AB arbeitet daran, das zu verbessern. Einige der kommenden Verbesserungen umfassen eine Unterstützung für »Online«-Operationen, bei denen die Tabelle nicht während der gesamten Operation gesperrt ist. Die InnoDB-Entwickler arbeiten ebenfalls an einer Unterstützung für das Aufbauen von Indizes durch Sortierung. MyISAM unterstützt diese Technik bereits, die das Erstellen von Indizes deutlich beschleunigt und einen kompakten Aufbau des Index bietet. (InnoDB baut seine Indizes momentan zeilenweise in Primärschlüsselreihenfolge auf, was bedeutet, dass die Indexbäume nicht optimal sortiert werden und fragmentiert sind.)
156 | Kapitel 3: Schema-Optimierung und Indizierung
Nicht alle ALTER TABLE-Operationen verursachen Umbauten an der Tabelle. Sie können z.B. den Standardwert einer Spalte auf zwei Arten ändern oder verwerfen (einmal schnell und einmal langsam). Nehmen Sie einmal an, Sie wollen die vorgegebene Ausleihdauer eines Films von 3 auf 5 Tage ändern. Dies ist die aufwendige Methode: mysql> ALTER TABLE sakila.film -> MODIFY COLUMN rental_duration TINYINT(3) NOT NULL DEFAULT 5;
Das Profiling dieser Anweisung mit SHOW STATUS zeigt, dass sie 1.000 Handler-Lese- und 1.000 Einfügevorgänge ausführt. Mit anderen Worten: Sie kopierte die Tabelle in eine neue Tabelle, obwohl sich Art, Größe und »Nullability« der Spalte nicht geändert haben. Theoretisch hätte MySQL das Aufbauen einer neuen Tabelle überspringen können. Der Vorgabewert für die Spalte ist eigentlich in der .frm-Datei der Tabelle gespeichert, so dass man ihn ändern können müsste, ohne die Tabelle selbst anzurühren. MySQL benutzt diese Optimierung noch nicht; dafür sorgt jedoch jedes MODIFY COLUMN für einen Tabellenumbau. Sie können allerdings den Vorgabewert einer Spalte mit ALTER COLUMN19 ändern: mysql> ALTER TABLE sakila.film -> ALTER COLUMN rental_duration SET DEFAULT 5;
Diese Anweisung modifiziert die .frm-Datei und lässt die Tabelle in Ruhe. Daher ist sie sehr schnell.
Nur die .frm-Datei modifizieren Wir haben gesehen, dass das Ändern der .frm-Datei einer Tabelle schnell geht und dass MySQL manchmal eine Tabelle umbaut, obwohl es das nicht muss. Wenn Sie bereit sind, einige Risiken auf sich zu nehmen, können Sie MySQL davon überzeugen, einige andere Arten von Modifikationen auszuführen, ohne die Tabelle umzubauen. Die Technik, die wir gleich demonstrieren wollen, wird nicht unterstützt, ist nicht dokumentiert und funktioniert möglicherweise nicht. Sie benutzen sie auf eigenes Risiko. Wir raten Ihnen, Ihre Daten vorher zu sichern!
Sie können potenziell die folgenden Arten von Operationen ausführen, ohne dass die Tabelle neu aufgebaut wird: • Entfernen (aber nicht Hinzufügen) des AUTO_INCREMENT-Attributs einer Spalte • Hinzufügen, Entfernen oder Ändern von ENUM- und SET-Konstanten. Wenn Sie eine Konstante entfernen und einige Zeilen diesen Wert enthalten, dann liefern Abfragen den Wert als leeren String zurück.
19 ALTER TABLE erlaubt es Ihnen, Spalten mit ALTER COLUMN, MODIFY COLUMN und CHANGE COLUMN zu modifizieren. Alle drei führen unterschiedliche Dinge aus.
ALTER TABLE beschleunigen | 157
Die zugrunde liegende Technik besteht darin, eine .frm-Datei für die gewünschte Tabellenstruktur zu erzeugen und sie an die Stelle der .frm-Datei der existierenden Tabelle zu kopieren: 1. Erzeugen Sie eine leere Tabelle mit exakt demselben Layout, abgesehen von der
gewünschten Änderung (wie etwa hinzugefügten ENUM-Konstanten). 2. Führen Sie FLUSH TABLES WITH READ LOCK aus. Damit werden alle in Benutzung befind-
lichen Tabellen geschlossen, und gleichzeitig wird verhindert, dass Tabellen geöffnet werden. 3. Tauschen Sie die .frm-Dateien. 4. Führen Sie UNLOCK TABLES aus, um den Lese-Lock aufzuheben.
Als Beispiel fügen wir zu der rating-Spalte in sakila.film eine Konstante hinzu. Die aktuelle Spalte sieht so aus: mysql> SHOW COLUMNS FROM sakila.film LIKE 'rating'; +--------+------------------------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+------------------------------------+------+-----+---------+-------+ | rating | enum('G','PG','PG-13','R','NC-17') | YES | | G | | +--------+------------------------------------+------+-----+---------+-------+
Wir fügen eine PG-14-Bewertung für besorgte Eltern hinzu: mysql> mysql> -> -> mysql>
CREATE TABLE sakila.film_new LIKE sakila.film; ALTER TABLE sakila.film_new MODIFY COLUMN rating ENUM('G','PG','PG-13','R','NC-17', 'PG-14') DEFAULT 'G'; FLUSH TABLES WITH READ LOCK;
Beachten Sie, dass wir den neuen Wert am Ende der Liste mit den Konstanten hinzufügen. Hätten wir ihn in die Mitte gesetzt, also hinter PG-13, würden wir die Bedeutung der existierenden Daten ändern: Vorhandene R-Werte würden zu PG-14 werden, NC-17 zu R usw. Jetzt tauschen wir auf der Kommandozeile des Betriebssystems die .frm-Dateien aus: root:/var/lib/mysql/sakila# mv film.frm film_tmp.frm root:/var/lib/mysql/sakila# mv film_new.frm film.frm root:/var/lib/mysql/sakila# mv film_tmp.frm film_new.frm
Wieder zurück am MySQL-Prompt, können wir die Tabelle entsperren und sehen, dass die Änderungen wirksam werden: mysql> UNLOCK TABLES; mysql> SHOW COLUMNS FROM sakila.film LIKE 'rating'\G *************************** 1. row *************************** Field: rating Type: enum('G','PG','PG-13','R','NC-17','PG-14')
Jetzt müssen wir nur noch die Tabelle verwerfen, die wir zur Unterstützung der Operation angelegt haben: mysql> DROP TABLE sakila.film_new;
158 | Kapitel 3: Schema-Optimierung und Indizierung
Schnell MyISAM-Indizes aufbauen Der übliche Trick zum effizienten Laden von MyISAM-Tabellen besteht darin, die Schlüssel zu deaktivieren, die Daten zu laden und die Schlüssel wieder zu reaktivieren: mysql> ALTER TABLE test.load_data DISABLE KEYS; -- load the data mysql> ALTER TABLE test.load_data ENABLE KEYS;
Das funktioniert, weil es MyISAM erlaubt, das Erstellen der Schlüssel zu verzögern, bis alle Daten geladen sind. An dieser Stelle kann es dann die Indizes durch Sortierung erzeugen. Das ist viel schneller und ergibt einen defragmentierten, kompakten Indexbaum.20 Leider funktioniert das bei eindeutigen (unique) Indizes nicht, weil DISABLE KEYS nur für nicht eindeutige Indizes gilt. MyISAM baut eindeutige Indizes im Speicher auf und prüft die Eindeutigkeit, wenn es die einzelnen Zeilen lädt. Das Laden wird dann außerordentlich langsam, sobald die Größe des Index die Größe des verfügbaren Speichers überschreitet. Wie bei den ALTER TABLE-Hacks im vorhergehenden Abschnitt können Sie diesen Vorgang beschleunigen, wenn Sie etwas mehr Aufwand betreiben und bereit sind, ein gewisses Risiko einzugehen. Das kann sich z.B. beim Laden von Daten aus Backups als nützlich erweisen, wenn Sie bereits wissen, dass alle Daten gültig sind und die Eindeutigkeit nicht mehr überprüft werden muss. Auch dies ist eine nicht dokumentierte, nicht unterstützte Technik. Benutzen Sie sie auf eigenes Risiko, und sichern Sie auf jeden Fall vorher Ihre Daten.
Folgende Schritte müssen Sie unternehmen: 1. Erzeugen Sie eine Tabelle mit der gewünschten Struktur, aber ohne Indizes. 2. Laden Sie die Daten in die Tabelle, um die .MYD-Datei anzulegen. 3. Erzeugen Sie eine weitere leere Tabelle mit der gewünschten Struktur, dieses Mal
einschließlich der Indizes. Dadurch werden die benötigten .frm- und .MYI-Dateien angelegt. 4. Setzen Sie in den Tabellen einen Lese-Lock. 5. Benennen Sie die .frm- und .MYI-Dateien der zweiten Tabelle um, damit MySQL sie
für die erste Tabelle benutzt. 6. Heben Sie den Lese-Lock auf. 7. Erzeugen Sie mit REPAIR TABLE die Indizes der Tabelle. Dadurch werden alle Indizes
durch Sortierung erzeugt, auch die eindeutigen Indizes. Diese Prozedur kann bei sehr großen Tabellen viel schneller vonstatten gehen. 20 MyISAM erstellt Indizes auch durch Sortierung, wenn Sie LOAD DATA INFILE benutzen und die Tabelle leer ist.
ALTER TABLE beschleunigen | 159
Hinweise zu Storage-Engines Wir beschließen dieses Kapitel mit einigen Storage-Engine-spezifischen Schema-Designhinweisen, die Sie beherzigen sollten. Das soll hier keine umfassende Liste werden, wir wollen nur einige wesentliche Faktoren präsentieren, die für das Schema-Design relevant sind.
Die MyISAM-Storage-Engine Tabellen-Locks MyISAM-Tabellen haben Locks auf Tabellenebene. Achten Sie darauf, dass diese nicht zu Engstellen werden. Keine automatisierte Datenwiederherstellung Wenn der MySQL-Server abstürzt oder der Strom ausfällt, müssen Sie Ihre MyISAM-Tabellen überprüfen und möglicherweise reparieren, bevor Sie sie wieder benutzen. Bei großen Tabellen kann das mehrere Stunden dauern. Keine Transaktionen MyISAM-Tabellen unterstützen keine Transaktionen. Um genau zu sein, garantiert MyISAM nicht einmal, dass eine einzelne Anweisung abgeschlossen wird. Wenn z.B. bei einem mehrzeiligen UPDATE auf halbem Wege ein Fehler auftritt, werden einige Zeilen aktualisiert und andere nicht. Nur Indizes werden im Speicher zwischengespeichert MyISAM speichert nur den Index innerhalb des MySQL-Prozesses im Schlüsselpuffer-Cache. Das Betriebssystem speichert die Daten der Tabelle in einem Cache, so dass in MySQL 5.0 ein teurer Betriebssystem-Systemaufruf erforderlich ist, um diese Daten zu holen. Kompakte Speicherung Die Zeilen werden sehr eng nacheinander gespeichert, so dass sie auf einer Festplatte wenig Platz einnehmen und vollständige Tabellenscans bei Daten, die auf der Platte liegen, sehr schnell erfolgen.
Die Memory-Storage-Engine Tabellen-Locks Ebenso wie MyISAM-Tabellen besitzen Memory-Tabellen Tabellen-Locks. Das stellt normalerweise aber kein Problem dar, da Abfragen an Memory-Tabellen üblicherweise sehr schnell sind. Keine dynamischen Zeilen Memory-Tabellen unterstützen keine dynamischen Zeilen (d.h. Zeilen variabler Länge), sie unterstützen also auch keine BLOB- und TEXT-Felder. Selbst ein VARCHAR(5000) wird in ein CHAR(5000) umgewandelt – eine ziemliche Speicherverschwendung, wenn die meisten Werte klein sind.
160 | Kapitel 3: Schema-Optimierung und Indizierung
Hash-Indizes sind der vorgegebene Indextyp Im Gegensatz zu anderen Storage-Engines ist der vorgegebene Indextyp Hash, wenn Sie nicht explizit etwas anderes angeben. Keine Indexstatistiken Memory-Tabellen unterstützen keine Indexstatistiken, weshalb Sie für komplexe Abfragen unter Umständen schlechte Ausführungspläne erhalten. Beim Neustart geht der Inhalt verloren Memory-Tabellen behalten keine Daten auf der Festplatte. Die Daten gehen also verloren, wenn der Server neu gestartet wird, obwohl die Definitionen der Tabellen bleiben.
Die InnoDB-Storage-Engine Transaktionsfähig InnoDB unterstützt Transaktionen sowie vier Transaktionsisolationsebenen. Fremdschlüssel Seit MySQL 5.0 ist InnoDB die einzige mitgelieferte Storage-Engine, die Fremdschlüssel unterstützt. Andere Storage-Engines akzeptieren sie in CREATE TABLE-Anweisungen, erzwingen sie jedoch nicht. Manche Engines von Drittherstellern, wie etwa solidDB für MySQL und PBXT, unterstützen sie ebenfalls auf Storage-EngineEbene. MySQL AB plant, in Zukunft Unterstützung für Fremdschlüssel auf Serverebene hinzuzufügen. Sperren auf Zeilenebene Sperren werden auf Zeilenebene gesetzt, ohne die Notwendigkeit von Lock Escalation und mit nichtblockierenden Select-Zugriffen – normale Select-Zugriffe setzen überhaupt keine Sperren, so dass eine sehr gute Nebenläufigkeit erreicht wird. Multiversionierung InnoDB benutzt eine Multiversion Concurrency Control, so dass die Select-Zugriffe standardmäßig veraltete Daten lesen können. Um genau zu sein, führt die MVCCArchitektur zu einer großen Komplexität und möglicherweise unerwarteten Verhaltensweisen. Sie müssen das InnoDB-Handbuch gründlich studieren, wenn Sie InnoDB einsetzen. Cluster-Bildung nach Primärschlüssel Alle Cluster in InnoDB-Tabellen werden nach dem Primärschlüssel gebildet, was Sie beim Schemaentwurf zu Ihrem Vorteil ausnutzen können. Alle Indizes enthalten die Primärschlüsselspalten Indizes verweisen über den Primärschlüssel auf die Zeilen. Wenn Sie also keinen kurzen Primärschlüssel wählen, werden die Indizes schnell sehr groß. Optimiertes Caching InnoDB legt sowohl Daten als auch Speicher im Puffer-Pool ab. Es baut außerdem automatisch Hash-Indizes auf, um die Zeilenabfrage zu beschleunigen.
Hinweise zu Storage-Engines | 161
Ungepackte Indizes Die Indizes werden nicht mit Präfixkomprimierung gepackt, so dass sie viel größer sein können als für MyISAM-Tabellen. Langsames Laden von Daten Seit MySQL 5.0 optimiert InnoDB die Operationen zum Laden von Daten nicht speziell. Es baut die Indizes zeilenweise auf, anstatt sie nach der Sortierung zu erstellen. Dadurch kann es passieren, dass das Laden der Daten deutlich langsamer erfolgt. Blockierendes AUTO_INCREMENT In Versionen vor MySQL 5.1verwendet InnoDB Sperren auf Tabellenebene, um einen neuen AUTO_INCREMENT-Wert zu generieren. Kein im Cache gespeicherter COUNT(*)-Wert Im Gegensatz zu MyISAM- oder Memory-Tabellen speichern InnoDB-Tabellen die Anzahl der Zeilen in der Tabelle nicht, was bedeutet, dass COUNT(*)-Abfragen ohne WHERE-Klausel nicht optimiert werden können und vollständige Tabellen- oder Indexscans erfordern. Mehr Informationen zu diesem Thema finden Sie in »COUNT( )-Abfragen optimieren« auf Seite 202.
162 | Kapitel 3: Schema-Optimierung und Indizierung
KAPITEL 4
Optimierung der Abfrageleistung
Im vorangegangenen Kapitel haben wir erläutert, wie ein Schema optimiert wird, was eine der notwendigen Bedingungen für High Performance darstellt. Es reicht aber nicht, mit dem Schema zu arbeiten – Sie müssen auch Ihre Abfragen gut entwerfen. Wenn Ihre Abfragen schlecht sind, nützt Ihnen das beste Schema nichts. Abfrageoptimierung, Indexoptimierung und Schema-Optimierung gehen Hand in Hand. Mit zunehmender Erfahrung beim Schreiben von Abfragen in MySQL werden Sie verstehen, wie Sie die Schemata zu gestalten haben, damit diese effiziente Abfragen unterstützen. Und das, was Sie über optimales Schemadesign lernen, beeinflusst wiederum die Art der Abfragen, die Sie schreiben. Dieser Prozess kostet Zeit, so dass wir Sie ermuntern wollen, auch später immer wieder zu diesem und den vorangegangenen Kapiteln zurückzukehren. Dieses Kapitel beginnt mit allgemeinen Überlegungen zum Abfrageentwurf – Dingen, die Sie in Betracht ziehen sollten, wenn eine Abfrage nicht gut funktioniert. Wir gehen anschließend genauer auf die Abfrageoptimierung und die Server-Interna ein. Sie erfahren, wie Sie feststellen können, wie MySQL eine bestimmte Abfrage ausführt, und lernen, wie Sie den Ausführungsplan für eine Abfrage ändern. Schließlich schauen wir uns Stellen an, an denen MySQL Abfragen nicht gut optimiert hat, und untersuchen Optimierungsmuster, die es MySQL ermöglichen, Abfragen effizienter auszuführen. Sie müssen verstehen, wie MySQL Abfragen wirklich ausführt, damit Sie selbst entscheiden können, was effizient oder ineffizient ist, die Stärken von MySQL erkennen und seine Schwächen vermeiden.
Grundlagen langsamer Abfragen: Datenzugriff optimieren Im einfachsten Fall funktioniert eine Abfrage nicht gut, weil sie mit zu vielen Daten konfrontiert wird. Manche Abfragen müssen einfach sehr viele Daten durchsuchen und kommen nicht voran. Allerdings ist das ungewöhnlich – man kann die meisten schlechten Abfragen so ändern, dass sie auf weniger Daten zugreifen. Wir haben festgestellt, dass es
163 |
ganz günstig ist, wenn man eine schlecht funktionierende Abfrage in zwei Schritten analysiert: 1. Stellen Sie fest, ob Ihre Anwendung mehr Daten bezieht, als Sie brauchen. Normaler-
weise bedeutet dies, dass sie auf zu viele Zeilen zugreift. Es kann aber auch heißen, dass sie auf zu viele Spalten zugreift. 2. Stellen Sie fest, ob der MySQL-Server mehr Zeilen analysiert als nötig.
Fragen Sie die Datenbank nach Daten, die Sie nicht brauchen? Manche Abfragen fragen nach mehr Daten, als sie brauchen, und werfen einen Teil davon dann weg. Das bedeutet einen erhöhten Aufwand für den MySQL-Server, vergrößert die Last im Netzwerk1 und verbraucht Speicher sowie CPU-Ressourcen auf dem Anwendungsserver. Hier sind einige typische Fehler: Es werden mehr Zeilen als nötig geholt Oft wird fälschlicherweise angenommen, dass MySQL die Ergebnisse auf Anforderung bereitstellt, anstatt die vollständige Ergebnismenge zu berechnen und zurückzuliefern. Uns begegnet das oft in Anwendungen von Leuten, die mit anderen Datenbanksystemen vertraut sind. Diese Entwickler sind an Techniken gewöhnt, bei denen z.B. eine SELECT-Anweisung ausgeführt wird, die viele Zeilen zurückliefert, dann die ersten N Zeilen geholt werden und die Ergebnismenge anschließend geschlossen wird (wenn etwa auf einer Nachrichten-Site die 100 neuesten Artikel geholt werden, auf der ersten Seite aber nur 10 von ihnen angezeigt werden sollen). Sie glauben, dass MySQL diese 10 Zeilen liefert, und beenden das Ausführen der Abfrage. In Wirklichkeit generiert MySQL die komplette Ergebnismenge. Die Client-Bibliothek holt dann alle Daten und verwirft die meisten. Am besten ist es, der Abfrage eine LIMIT-Klausel hinzuzufügen. Es werden aus einem Mehrtabellen-Join alle Spalten geholt Wenn Sie alle Schauspieler abfragen wollen, die in Academy Dinosaur mitspielen, dann schreiben Sie die Abfrage nicht so: mysql> -> -> ->
SELECT * FROM sakila.actor INNER JOIN sakila.film_actor USING(actor_id) INNER JOIN sakila.film USING(film_id) WHERE sakila.film.title = 'Academy Dinosaur';
Damit werden nämlich alle Spalten aus allen drei Tabellen zurückgeliefert. Schreiben Sie die Abfrage stattdessen folgendermaßen: mysql> SELECT sakila.actor.* FROM sakila.actor...;
1 Der Aufwand im Netzwerk ist dann am größten, wenn sich die Anwendung auf einem anderen Host befindet als der Server. Aber auch, wenn sich MySQL und die Anwendung auf demselben Server befinden, ist die Datenübertragung nicht ganz kostenlos.
164 | Kapitel 4: Optimierung der Abfrageleistung
Es werden alle Spalten geholt Sie sollten immer misstrauisch werden, wenn Sie SELECT * sehen. Brauchen Sie wirklich alle Spalten? Vermutlich nicht. Das Abfragen aller Spalten kann Optimierungen wie abdeckende Indizes vereiteln und verursacht außerdem meist zusätzlichen Aufwand für die Ein-/Ausgabe, den Speicher und die CPU. Manche Datenbankadministratoren verbieten SELECT * aufgrund dieser Tatsache ganz allgemein, um das Risiko von Problemen zu verringern, wenn jemand die Spaltenliste einer Tabelle verändert. Es ist natürlich nicht immer schlecht, wenn man mehr Daten abfragt, als man eigentlich benötigt. Bei vielen Fällen, die wir untersucht haben, wurde uns gesagt, dass dieser verschwenderische Ansatz die Entwicklung vereinfacht, da er es den Entwicklern erlaubt, den gleichen Code an mehreren Stellen einzusetzen. Das ist eine ganz vernünftige Überlegung, solange Ihnen klar ist, welche Auswirkungen sie auf die Ausführungsleistung hat. Es kann auch sinnvoll sein, mehr Daten als nötig zu holen, wenn man in der Anwendung eine Art Caching einsetzt oder irgendeinen anderen Vorteil im Sinn hat. Das Abfragen und Speichern vollständiger Objekte ist manchmal besser, als viele Abfragen durchzuführen, die nur Teile des Objekts holen.
Untersucht MySQL zu viele Daten? Sobald Sie sich sicher sind, dass Ihre Abfragen nur die Daten beziehen, die Sie benötigen, können Sie nach Abfragen suchen, die zu viele Daten untersuchen, wenn sie die Ergebnisse generieren. In MySQL sind dies die einfachsten Maße für die Kosten von Abfragen: • Ausführungszeit • Anzahl der untersuchten Zeilen • Anzahl der zurückgelieferten Zeilen Keines dieser Maße ist wirklich perfekt, um die Kosten einer Abfrage zu messen, allerdings spiegeln sie ungefähr wider, auf wie viele Daten MySQL intern zugreifen muss, um eine Abfrage auszuführen, und lassen Rückschlüsse darauf zu, wie schnell die Abfrage ausgeführt wird. Alle drei Maße werden im Slow-Query-Log protokolliert, weshalb ein Blick in dieses Protokoll eine der besten Möglichkeiten darstellt, Abfragen zu finden, die zu viele Daten analysieren.
Ausführungszeit Wie in Kapitel 2 besprochen wurde, unterliegt die normale Slow-Query-Log-Funktion in MySQL 5.0 und früheren Versionen ernsten Beschränkungen, wie z.B. fehlender Unterstützung für eine ausführliche Protokollierung. Glücklicherweise gibt es Patches, die es Ihnen erlauben, langsame Abfragen mit Mikrosekundenauflösung zu messen und zu protokollieren. Diese Patches sind im MySQL 5.1-Server enthalten, Sie können aber auch frühere Versionen bei Bedarf damit ausstatten. Hüten Sie sich jedoch davor, zu viel Augenmerk auf die Ausführungszeit von Abfragen zu richten. Sie ist ganz hübsch anzuse-
Grundlagen langsamer Abfragen: Datenzugriff optimieren | 165
hen, da es sich um ein objektives Maß handelt, allerdings ist sie unter wechselnden Lastbedingungen nicht konsistent. Andere Faktoren – wie etwa Storage-Engine-Sperren (Tabellen-Locks und Row-Locks), hohe Nebenläufigkeit und die Hardware – können die Ausführungszeiten von Abfragen ebenfalls merklich beeinflussen. Dieses Maß ist nützlich, um die Abfragen zu finden, die die Antwortzeit der Anwendung am meisten beeinträchtigen oder den Server am stärksten belasten, es verrät Ihnen aber nicht, ob die tatsächliche Ausführungszeit für eine Abfrage mit einer bestimmten Komplexität vernünftig ist. (Die Ausführungszeit kann sowohl das Symptom als auch die Ursache von Problemen sein, und es ist nicht unbedingt klar, welcher Fall zutrifft.)
Untersuchte Zeilen und zurückgelieferte Zeilen Es bietet sich an, beim Analysieren von Abfragen an die Anzahl der untersuchten Zeilen zu denken, weil man anhand dessen feststellen kann, wie effizient die Abfrage die Daten ermittelt, die Sie benötigen. Doch genau wie bei der Ausführungszeit handelt es sich auch hier nicht um ein perfektes Maß, um schlechte Abfragen zu finden. Nicht alle Zeilenzugriffe sind gleich. Kürzere Zeilen lassen sich schneller abfragen, und das Beziehen von Zeilen aus dem Speicher geht viel schneller als das Lesen von der Festplatte. Idealerweise wäre die Anzahl der untersuchten Zeilen identisch mit der Anzahl der zurückgelieferten Zeilen. In der Praxis ist das aber kaum möglich. Wenn z.B. Zeilen mithilfe von Joins konstruiert werden, dann muss zum Generieren einer Zeile in der Ergebnismenge jeweils auf mehrere Zeilen zugegriffen werden. Das Verhältnis von untersuchten Zeilen zu zurückgelieferten Zeilen ist normalerweise klein – es liegt etwa zwischen 1:1 und 10:1 –, kann manchmal aber auch um Größenordnungen größer sein.
Untersuchte Zeilen und Zugriffsarten Wenn Sie über die Kosten für eine Abfrage nachdenken, dann sollten Sie auch die Kosten zum Suchen einer einzelnen Zeile in einer Tabelle in Betracht ziehen. MySQL kennt verschiedene Zugriffsmethoden, um eine Zeile zuzugreifen und zurückzuliefern. Manche erfordern das Untersuchen vieler Zeilen, andere wiederum können das Ergebnis generieren, ohne überhaupt eine Zeile zu untersuchen. Die Zugriffsmethode(n) finden Sie in der type-Spalte der EXPLAIN-Ausgabe. Die Zugriffsarten reichen von vollständigen Tabellenscans bis zu Indexscans, Bereichsscans, UniqueIndex-Suchen und Konstanten. Jede Art ist schneller als die jeweils vorhergehende, da entsprechend weniger Daten gelesen werden müssen. Sie müssen sich die Zugriffsarten nicht merken, sollten jedoch die allgemeinen Konzepte verstehen, die sich hinter dem Scannen einer Tabelle, dem Scannen eines Index, Bereichszugriffen und den Zugriffen auf einzelne Werte verbergen. Wenn Sie keine gute Zugriffsart bekommen, dann lösen Sie dieses Problem am besten, indem Sie einen passenden Index hinzufügen. Wir sind im vorangegangenen Kapitel ausführlich auf die Indizierung eingegangen, jetzt erkennen Sie vermutlich auch, weshalb Indizes für die Abfrageoptimierung so wichtig sind. Indizes erlauben es MySQL, Zeilen mit einer effizienteren Zugriffsart zu finden, die weniger Daten untersucht. 166 | Kapitel 4: Optimierung der Abfrageleistung
Schauen wir uns z.B. eine einfache Abfrage in der Sakila-Beispieldatenbank an: mysql> SELECT * FROM sakila.film_actor WHERE film_id = 1;
Diese Abfrage liefert 10 Zeilen zurück. EXPLAIN zeigt, dass MySQL die Zugriffsart ref im idx_fk_film_id-Index einsetzt, um die Abfrage auszuführen: mysql> EXPLAIN SELECT * FROM sakila.film_actor WHERE film_id = 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ref possible_keys: idx_fk_film_id key: idx_fk_film_id key_len: 2 ref: const rows: 10 Extra:
EXPLAIN gibt an, dass MySQL geschätzt hat, es müsste nur auf 10 Zeilen zugreifen. Mit anderen Worten: Der Abfrageoptimierer wusste, dass die gewählte Abfrageart die Abfrage effizient beantworten konnte. Was würde passieren, wenn es keinen passenden Index für die Abfrage gäbe? MySQL müsste eine weniger optimale Abfrageart einsetzen, wie wir leicht sehen können, wenn wir den Index weglassen und die Abfrage erneut ausführen: mysql> ALTER TABLE sakila.film_actor DROP FOREIGN KEY fk_film_actor_film; mysql> ALTER TABLE sakila.film_actor DROP KEY idx_fk_film_id; mysql> EXPLAIN SELECT * FROM sakila.film_actor WHERE film_id = 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 5073 Extra: Using where
Wie vorherzusehen war, wird als Zugriffsart nun ein vollständiger Tabellenscan (ALL) angegeben, und MySQL schätzt, dass es 5.073 Zeilen untersuchen muss, um die Abfrage zu bedienen. Das »Using where« in der Extra-Spalte zeigt, dass der MySQL-Server die WHERE-Klausel benutzt, um Zeilen zu verwerfen, nachdem die Storage-Engine sie gelesen hat. Im Allgemeinen kann MySQL eine WHERE-Klausel auf drei Arten anwenden (von der besten zur schlechten): • Es wendet die Bedingungen auf die Index-Lookup-Operation an, um nicht passende Zeilen zu eliminieren. Dies geschieht auf der Storage-Engine-Ebene.
Grundlagen langsamer Abfragen: Datenzugriff optimieren | 167
• Es benutzt einen abdeckenden Index (»Using index« in der Extra-Spalte), um Zeilenzugriffe zu vermeiden, und filtert nicht passende Zeilen aus, nachdem es die einzelnen Ergebnisse aus dem Index bekommen hat. Dies geschieht auf der Serverebene, verlangt aber nicht das Lesen von Zeilen aus der Tabelle. • Es holt Zeilen aus der Tabelle und filtert dann nicht passende Zeilen aus (»Using where« in der Extra-Spalte). Dies geschieht auf der Serverebene und verlangt, dass der Server Zeilen aus der Tabelle liest, bevor er sie ausfiltern kann. Dieses Beispiel verdeutlicht, wie wichtig es ist, gute Indizes zu haben. Gute Indizes helfen Ihren Abfragen, eine gute Zugriffsart zu wählen und nur die benötigten Zeilen zu untersuchen. Allerdings bedeutet das Hinzufügen eines Index nicht immer, dass MySQL auf die gleiche Anzahl an Zeilen zugreift und diese zurückliefert. Hier ist z.B. eine Abfrage, die die Aggregatfunktion COUNT( ) verwendet:2 mysql> SELECT actor_id, COUNT(*) FROM sakila.film_actor GROUP BY actor_id;
Hier werden nur 200 Zeilen zurückgeliefert, allerdings müssen Tausende von Zeilen gelesen werden, um die Ergebnismenge zusammenzustellen. Ein Index kann die Anzahl der zu untersuchenden Zeilen für eine solche Abfrage nicht verringern. Leider verrät MySQL Ihnen nicht, wie viele Zeilen, auf die es zugegriffen hat, zum Erstellen der Ergebnismenge benutzt wurden; es gibt lediglich die Gesamtzahl der zugegriffenen Zeilen an. Viele dieser Zeilen könnten mit einer WHERE-Klausel eliminiert werden und würden nichts zur Ergebnismenge beitragen. Im gezeigten Beispiel griff die Abfrage nach dem Entfernen des Index in sakila.film_actor auf jede Zeile in der Tabelle zu, und die WHERE-Klausel verwarf alle bis auf 10 von ihnen. Die Ergebnismenge wurde nur aus den zehn verbleibenden Zeilen generiert. Um zu verstehen, auf wie viele Zeilen der Server zugreift und wie viele er tatsächlich benutzt, muss man über die Abfrage nachdenken. Falls Sie feststellen, dass eine große Anzahl von Zeilen untersucht wurde, um relativ wenige Zeilen im Ergebnis auszuliefern, dann testen Sie diese raffinierteren Ansätze: • Benutzen Sie abdeckende Indizes, die die Daten speichern, damit die Storage-Engine nicht vollständige Zeilen holen muss. (Wir haben das im vorherigen Kapitel besprochen.) • Ändern Sie das Schema. Benutzen Sie z.B. Summary-Tabellen (das wurde im vorherigen Kapitel beschrieben). • Schreiben Sie eine komplizierte Abfrage so um, dass der MySQL-Optimierer sie optimal ausführen kann. (Darauf kommen wir weiter hinten in diesem Kapitel zu sprechen.)
2 Siehe »COUNT( )-Abfragen optimieren« auf Seite 202 für mehr Informationen zu diesem Thema.
168 | Kapitel 4: Optimierung der Abfrageleistung
Methoden zum Umstrukturieren von Abfragen Wenn Sie problematische Abfragen optimieren, sollte Ihr Ziel darin bestehen, alternative Möglichkeiten zu finden, um das gewünschte Ergebnis zu erhalten – das bedeutet aber nicht unbedingt, dass Sie von MySQL die gleiche Ergebnismenge zurückbekommen. Manchmal kann man Abfragen in äquivalente Formen umwandeln und eine bessere Leistung erzielen. Sie sollten allerdings auch darüber nachdenken, die Abfrage so umzuschreiben, dass Sie andere Ergebnisse bekommen, falls das einen Effizienzvorteil bietet. Sie sollten schließlich auch in der Lage sein, die gleiche Arbeit zu verrichten, indem Sie sowohl den Anwendungscode als auch die Abfrage ändern. In diesem Abschnitt erläutern wir Techniken, die Ihnen dabei helfen können, einen großen Bereich an Abfragen umzustrukturieren, und zeigen Ihnen, wann Sie die einzelnen Techniken einsetzen.
Komplexe Abfragen oder viele Abfragen? Eine wichtige Frage beim Entwurf von Abfragen lautet, ob es besser ist, eine komplexe Abfrage in mehrere einfachere Abfragen aufzuteilen. Der traditionelle Ansatz beim Datenbankentwurf verfolgt eine Philosophie, nach der so viel Arbeit wie möglich mit möglichst wenigen Abfragen erledigt werden sollte. Historisch gesehen war dieser Ansatz wegen der Kosten für die Netzwerkkommunikation und dem Overhead bei der Analyse der Abfragen und der verschiedenen Stadien der Optimierung besser. Dieser Rat hat allerdings für MySQL keine allzu große Bedeutung, da es bereits von sich aus den Auf- und Abbau von Verbindungen sehr effizient erledigt und schnell auf kleine und einfache Abfragen reagiert. Moderne Netzwerke sind darüber hinaus deutlich schneller als früher, wodurch sich die Netzwerklatenz verringert. MySQL kann auf ganz normaler Serverhardware mehr als 50.000 einfache Abfragen pro Sekunde ausführen. In einem Gigabit-Netzwerk kommt es auf mehr als 2.000 Abfragen pro Sekunde von einem einzelnen Partner, so dass es im Allgemeinen kein großes Problem mehr darstellt, mehrere Abfragen gleichzeitig abzufertigen. Die Verbindungsantwort ist allerdings im Vergleich zu der Anzahl an Zeilen, die MySQL intern pro Sekunde verschieben kann (die bei Daten im Speicher in Millionen pro Sekunde gerechnet werden), immer noch verhältnismäßig langsam. Es ist also im Großen und Ganzen eine gute Idee, so wenige Abfragen wie möglich zu verwenden. Manchmal allerdings können Sie eine Abfrage effizienter gestalten, indem Sie sie auseinandernehmen und einige einfachere Abfragen anstelle einer komplexen Abfrage ausführen. Haben Sie keine Angst davor; wägen Sie den Aufwand ab, und wählen Sie die Strategie, die weniger Arbeit verursacht. Wir zeigen etwas weiter hinten in diesem Kapitel einige Beispiele für diese Technik. Das heißt also, ein verbreiteter Fehler beim Anwendungsentwurf ist die Verwendung zu vieler Abfragen. So führen z.B. manche Anwendungen 10 einzeilige Abfragen aus, um Daten aus einer Tabelle zu beziehen, wenn sie auch eine einzige zehnzeilige Abfrage benutzen könnten. Uns sind sogar schon Anwendungen begegnet, die jede Spalte einzeln geholt haben, wobei jede Zeile viele Male abgefragt wurde! Methoden zum Umstrukturieren von Abfragen | 169
Eine Abfrage zerlegen Eine andere Möglichkeit, eine Abfrage zu zerlegen, geht nach dem Prinzip »teile und herrsche« vor, wobei die Abfrage prinzipiell gleich bleibt, aber in kleineren »Bröckchen« ausgeführt wird, die weniger Zeilen gleichzeitig beeinflussen. Ein großartiges Beispiel ist das Beseitigen alter Daten. Periodisch durchgeführte Löschjobs müssen möglicherweise eine Menge Daten entfernen. Wird dies innerhalb einer umfangreichen Abfrage erledigt, werden vielleicht viele Zeilen über einen langen Zeitraum gesperrt, Transaktions-Logs gefüllt, Ressourcen verschwendet und kleine Abfragen blockiert, die nicht unterbrochen werden dürfen. Nimmt man die DELETE-Anweisung aus der Abfrage heraus und verwendet man mittelgroße Abfragen, kann die Leistung deutlich verbessert werden. Außerdem reduziert sich die Verzögerung bei einer Replikation der Abfrage. Anstatt z.B. diesen Koloss von Abfrage auszuführen: mysql> DELETE FROM messages WHERE created < DATE_SUB(NOW( ),INTERVAL 3 MONTH);
könnten Sie etwa den folgenden Pseudocode einsetzen: rows_affected = 0 do { rows_affected = do_query( "DELETE FROM messages WHERE created < DATE_SUB(NOW( ),INTERVAL 3 MONTH) LIMIT 10000") } while rows_affected > 0
Das gleichzeitige Löschen von 10.000 Zeilen ist als Aufgabe typischerweise groß genug, um jede Abfrage effizient zu machen, und gleichzeitig kurz genug, um die Auswirkungen auf den Server zu minimieren3 (transaktionsfähige Storage-Engines könnten von kleineren Transaktionen profitieren). Es ist vielleicht außerdem ganz günstig, wenn man zwischen den DELETE-Anweisungen eine gewisse Sleep-Zeit einfügt, damit die Last über die Zeit verteilt wird und die Haltedauer für die Sperren reduziert wird.
Join-Zerlegung Viele Hochleistungswebsites setzen Join-Zerlegung ein. Sie können einen Join zerlegen, indem Sie mehrere Eintabellenabfragen anstelle eines Mehrtabellen-Joins ausführen und dann den Join in der Anwendung erledigen. Anstatt dieser einzelnen Abfrage: mysql> SELECT * FROM tag -> JOIN tag_post ON tag_post.tag_id=tag.id -> JOIN post ON tag_post.post_id=post.id -> WHERE tag.tag='mysql';
könnten Sie z.B. diese Abfragen ausführen: mysql> SELECT * FROM mysql> SELECT * FROM mysql> SELECT * FROM
tag WHERE tag='mysql'; tag_post WHERE tag_id=1234; post WHERE post.id in (123,456,567,9098,8904);
3 Maatkits mk-archiver-Programm erleichtert diese Art von Jobs.
170 | Kapitel 4: Optimierung der Abfrageleistung
Auf den ersten Blick sieht das vielleicht verschwenderisch aus, da Sie die Anzahl der Abfragen erhöht haben, ohne dass Sie etwas zurückbekommen. Eine solche Restrukturierung kann allerdings beträchtliche Leistungsvorteile bringen: • Das Caching kann effizienter sein. Viele Anwendungen legen »Objekte« im Cache ab, die direkt auf Tabellen abgebildet werden. Falls in diesem Beispiel das Objekt mit dem Tag mysql bereits im Cache vorliegt, kann die Anwendung die erste Abfrage überspringen. Finden Sie Nachrichten mit der id 123, 567 oder 9098 im Cache, können Sie sie von der IN( )-Liste entfernen. Auch der Abfrage-Cache könnte etwas von dieser Strategie haben. Falls sich nur eine der Tabellen häufig ändert, kann das Zerlegen eines Join die Anzahl der Cache-Ausfälle verringern. • Bei MyISAM-Tabellen werden Tabellen-Locks effizienter eingesetzt, wenn nur eine Abfrage pro Tabelle ausgeführt wird: Die Abfragen sperren die Tabellen individuell und relativ kurz, anstatt sie alle über einen längeren Zeitraum zu sperren. • Das Ausführen von Joins in der Anwendung macht es leichter, die Datenbank zu skalieren, indem man Tabellen auf unterschiedliche Server legt. • Die Abfragen selbst können effizienter sein. In diesem Beispiel erlaubt der Einsatz einer IN( )-Liste anstelle eines Joins es MySQL, Zeilen-IDs zu sortieren und Zeilen optimaler abzurufen, als es mit einem Join möglich wäre. Später mehr dazu. • Sie können die redundanten Zeilenzugriffe reduzieren. Das Ausführen eines Joins in der Anwendung bedeutet, dass Sie jede Zeile nur einmal abrufen, während ein Join in der Abfrage im Prinzip eine Denormalisierung ist, die wiederholt auf die gleichen Daten zugreifen könnte. Aus dem gleichen Grund könnte eine solche Umstrukturierung den Gesamtnetzwerkverkehr und die Speicherbelegung verringern. • Bis zu einem gewissen Grad können Sie diese Technik als eine manuelle Implementierung eines Hash-Joins betrachten statt als den geschachtelten Schleifenalgorithmus, den MySQL einsetzt, um einen Join auszuführen. Ein Hash-Join ist möglicherweise effizienter. (Wir kommen weiter hinten in diesem Kapitel auf die Join-Strategie von MySQL zurück.)
Zusammenfassung: Wann Anwendungs-Joins effizienter sind Das Ausführen von Joins in der Anwendung könnte effizienter sein, wenn:
• • • • •
Sie viele Daten aus früheren Abfragen im Cache speichern und wiederverwenden Sie mehrere MyISAM-Tabellen benutzen Sie Daten über mehrere Server verteilen Sie Joins in großen Tabellen durch IN( )-Listen ersetzen Ein Join sich mehrmals auf die gleiche Tabelle bezieht
Methoden zum Umstrukturieren von Abfragen | 171
Grundlagen der Abfrageverarbeitung Falls Sie von Ihrem MySQL-Server Spitzenleistungen erwarten, sollten Sie sich am besten darauf konzentrieren, zu lernen, wie MySQL Abfragen optimiert und ausführt. Sobald Sie das verstanden haben, besteht die Abfrageoptimierung zum Großteil darin, sie von den Prinzipien abzuleiten. Sie wird dadurch zu einem sehr logischen Vorgang. Dieser Abschnitt geht davon aus, dass Sie Kapitel 1 gelesen haben, wo die Grundlagen für das Verständnis der Abfrageverarbeitung bei MySQL gelegt werden.
Abbildung 4-1 zeigt, wie MySQL im Allgemeinen Abfragen ausführt. MySQL-Server Client/ServerProtokoll
SQL Ergebnis
AbfrageCache
Parser
Präprozessor
Client
Parser-Baum
Abfrageoptimierer Ergebnis Abfrageausführungsplan
Sprint-Review Abfrageausführungs-Engine API-Aufrufe Storage-Engines MyISAM InnoDB usw....
Abbildung 4-1: Ausführungspfad einer Abfrage
172 | Kapitel 4: Optimierung der Abfrageleistung
Daten
Anhand der Illustration können Sie erkennen, was passiert, wenn Sie MySQL eine Abfrage schicken: 1. Der Client sendet die SQL-Anweisung an den Server. 2. Der Server prüft den Abfrage-Cache. Wenn es einen Treffer gibt, liefert er das
gespeicherte Ergebnis aus dem Cache; ansonsten übergibt er die SQL-Anweisung an den nächsten Schritt. 3. Der Server analysiert, verarbeitet und optimiert das SQL in einem Abfrageausfüh-
rungsplan. 4. Die Engine zur Abfrageausführung führt den Plan aus, indem sie Aufrufe an die API
der Storage-Engine startet. 5. Der Server sendet das Ergebnis an den Client zurück.
Es handelt sich hier um jeweils recht komplexe Schritte, auf die wir in den folgenden Abschnitten eingehen. Wir erläutern außerdem, in welchen Zuständen sich die Abfrage während der einzelnen Schritte befinden wird. Der Vorgang der Abfrageoptimierung ist besonders komplex, und es ist wichtig, dass Sie ihn verstehen.
Das MySQL-Client/Server-Protokoll Es ist zwar nicht notwendig, dass Sie die Interna des Client/Server-Protokolls von MySQL verstehen, Sie sollten aber wissen, wie es auf einer höheren Ebene funktioniert. Das Protokoll arbeitet im Halbduplex-Modus. Das bedeutet, dass der MySQL-Server zu einem bestimmten Zeitpunkt Nachrichten entweder senden oder empfangen kann, aber nicht beides. Es bedeutet außerdem, dass es keine Möglichkeit gibt, eine Nachricht abzukürzen. Dieses Protokoll macht die MySQL-Kommunikation einfach und schnell, beschränkt sie aber auch auf gewisse Weise. Zum einen gibt es keine Flusskontrolle; sobald die eine Seite eine Nachricht sendet, muss die andere Seite die gesamte Nachricht entgegennehmen, bevor sie antworten kann. Es ist, als würde man einen Ball hin- und herwerfen: Nur eine Seite hat zu einem Zeitpunkt den Ball, und Sie können den Ball nur dann werfen (eine Nachricht senden), wenn Sie ihn haben. Der Client sendet eine Abfrage als einzelnes Datenpaket an den Server. Aus diesem Grund ist bei großen Abfragen die Konfigurationsvariable max_packet_size so wichtig.4 Nachdem der Client die Abfrage gesendet hat, besitzt er quasi den Ball nicht mehr, er kann jetzt nur noch auf Ergebnisse warten. Im Gegensatz dazu besteht die Antwort vom Server normalerweise aus vielen Datenpaketen. Wenn der Server antwortet, muss der Client die gesamte Ergebnismenge entgegennehmen. Er kann nicht einfach ein paar Zeilen abrufen und dann den Server auffordern, ihn mit dem Rest nicht mehr zu belästigen. Benötigt der Client nur die ersten Zeilen, die zurückgeliefert werden, muss er entweder darauf warten, bis alle Pakete des Servers ange4 Wenn die Abfrage zu groß ist, lehnt der Server es ab, weitere Daten zu empfangen, und löst einen Fehler aus.
Grundlagen der Abfrageverarbeitung | 173
kommen sind, und dann die nicht mehr benötigten Zeilen verwerfen, oder er muss die Verbindung unsanft unterbrechen. Keine der beiden Varianten ist besonders gut, weshalb passende LIMIT-Klauseln so wichtig sind. Sie können es sich auch so vorstellen: Wenn ein Client Zeilen vom Server abruft, dann glaubt er, dass er sie zieht. In Wirklichkeit schiebt der MySQL-Server die Zeilen zum Client, nachdem er sie generiert hat. Der Client empfängt lediglich die geschobenen Zeilen; er hat keine Möglichkeit, den Server aufzufordern, das Senden der Zeilen zu stoppen. Der Client wird förmlich überschwemmt. Die meisten Bibliotheken, die eine Verbindung zu MySQL herstellen, erlauben es Ihnen entweder, die gesamte Ergebnismenge zu holen und im Speicher zu puffern, oder holen die einzelnen Zeilen, wenn Sie sie brauchen. Standardmäßig wird im Allgemeinen das gesamte Ergebnis geholt und im Speicher gepuffert. Das ist wichtig, weil der MySQL-Server erst dann alle Sperren und weiteren Ressourcen, die von der Abfrage benötigt wurden, freigibt, wenn er alle Zeilen geholt hat. Die Abfrage bleibt im »Sending data«Zustand (erläutert in »Abfragezustände« auf Seite 175). Wenn die Client-Bibliothek die Ergebnisse alle auf einmal holt, verringert sie die Arbeitsmenge, die der Server verrichten muss: Der Server kann die Abfrage so schnell wie möglich beenden und hinter sich aufräumen. Die meisten Client-Bibliotheken erlauben es Ihnen, die Ergebnismenge so zu behandeln, als würden Sie sie vom Server holen, obwohl Sie sie eigentlich aus dem Puffer im Speicher der Bibliothek beziehen. Meist funktioniert das auch ganz gut, es eignet sich aber nicht besonders gut für riesige Ergebnismengen, bei denen das Holen schon lange dauert und die entsprechend viel Speicher beanspruchen. Sie brauchen weniger Speicher und können schneller mit dem Ergebnis arbeiten, wenn Sie die Bibliothek anweisen, das Ergebnis nicht zu puffern. Der Nachteil dieses Vorgehens besteht darin, dass die Sperren und die anderen Ressourcen auf dem Server offen bleiben, während die Anwendung mit der Bibliothek interagiert.5 Schauen wir uns ein Beispiel mit PHP an. So würde man normalerweise MySQL aus PHP heraus abfragen:
Der Code scheint anzudeuten, dass Sie die Zeilen nur dann holen, wenn Sie sie benötigen, nämlich in der while-Schleife. Tatsächlich packt der Code das gesamte Ergebnis mit dem Funktionsaufruf mysql_query( ) in einen Puffer. Die while-Schleife durchläuft ein-
5 Sie können das mit SQL_BUFFER_RESULT umgehen, wie Sie gleich sehen werden.
174 | Kapitel 4: Optimierung der Abfrageleistung
fach den Puffer. Im Gegensatz dazu puffert der folgende Code die Ergebnisse nicht, da er mysql_unbuffered_query( ) anstelle von mysql_query( ) verwendet:
Programmiersprachen haben verschiedene Methoden, um die Pufferung außer Kraft zu setzen. So verlangt z.B. der Perl-Treiber DBD::mysql, dass Sie das mysql_use_result-Attribut der C-Client-Bibliothek angeben (Vorgabe ist mysql_buffer_result). Hier ein Beispiel: #!/usr/bin/perl use DBI; my $dbh = DBI->connect('DBI:mysql:;host=localhost', 'user', 'p4ssword'); my $sth = $dbh->prepare('SELECT * FROM HUGE_TABLE', { mysql_use_result => 1 }); $sth->execute( ); while ( my $row = $sth->fetchrow_array( ) ) { # Tun Sie etwas mit dem Ergebnis }
Beachten Sie, dass der Aufruf zu prepare( ) angegeben hat, das Ergebnis zu »benutzen« (use) anstatt es zu »puffern« (buffer). Sie können das auch beim Verbindungsaufbau angeben, wodurch jede Anweisung ungepuffert bleibt: my $dbh = DBI->connect('DBI:mysql:;mysql_use_result=1', 'user', 'p4ssword');
Abfragezustände Jede MySQL-Verbindung oder jeder Thread besitzt einen Zustand, der anzeigt, was sie bzw. er zu einem bestimmten Zeitpunkt tut. Es gibt verschiedene Möglichkeiten, diese Zustände anzuschauen, am einfachsten geht es mit dem Befehl SHOW FULL PROCESSLIST (die Zustände stehen in der Command-Spalte). Während der Lebensdauer einer Abfrage ändert sich ihr Zustand viele Male, und es gibt Dutzende von Zuständen. Das MySQL-Handbuch ist die verbindliche Informationsquelle für alle Zustände, wir wollen aber trotzdem hier einige aufführen und erläutern, was sie bedeuten: Sleep
Der Thread wartet auf eine neue Abfrage vom Client. Query
Der Thread führt entweder die Abfrage aus oder schickt das Ergebnis zurück an den Client. Locked
Der Thread wartet darauf, dass ein Tabellen-Lock auf Serverebene gewährt wird. Sperren, die durch die Storage-Engine implementiert werden, wie etwa Row-Locks von InnoDB, veranlassen den Thread nicht dazu, in den Zustand Locked überzugehen.
Grundlagen der Abfrageverarbeitung | 175
Analyzing und statistics
Der Thread prüft die Storage-Engine-Statistiken und optimiert die Abfrage. Copying to tmp table [on disk]
Der Thread verarbeitet die Abfrage und kopiert die Ergebnisse in eine temporäre Tabelle, wahrscheinlich für ein GROUP BY, ein Filesort oder um ein UNION zu bedienen. Wenn der Zustand mit »on disk« endet, konvertiert MySQL eine im Speicher befindliche Tabelle in eine auf der Festplatte befindliche Tabelle. Sorting result
Der Thread sortiert eine Ergebnismenge. Sending data
Dies kann mehrere Dinge bedeuten: Der Thread könnte Daten zwischen Stadien der Abfrage verschicken, eine Ergebnismenge generieren oder die Ergebnismenge an den Client zurücksenden. Es ist ganz hilfreich, wenigstens die grundlegenden Zustände zu kennen, damit Sie eine Vorstellung davon bekommen können, »wer den Ball für die Abfrage hat«. Bei sehr ausgelasteten Servern könnte ein ungewöhnlicher oder normalerweise kurzer Zustand, wie etwa statistics, anfangen, einen wesentlichen Anteil der Zeit zu beanspruchen. Das deutet üblicherweise darauf hin, dass etwas nicht in Ordnung ist.
Der Abfrage-Cache Bevor MySQL überhaupt eine Abfrage analysiert, sucht es im Abfrage-Cache nach ihr, falls der Cache aktiviert ist. Bei dieser Operation handelt es sich um eine Hash-Suche unter Beachtung der Groß- und Kleinschreibung. Wenn die Abfrage sich auch nur um ein Byte von einer ähnlichen Abfrage im Cache unterscheidet, gilt dies nicht als Treffer, und die Abfrageverarbeitung geht ins nächste Stadium über. Findet MySQL dagegen eine passende Abfrage im Abfrage-Cache, muss es die Rechte überprüfen, bevor es die im Cache gespeicherte Abfrage zurückliefert. Das ist möglich, ohne die Abfrage zu analysieren, weil MySQL zusammen mit der Abfrage Tabelleninformationen im Cache speichert. Wenn die Rechte in Ordnung sind, fragt MySQL das gespeicherte Ergebnis aus dem Abfrage-Cache ab und sendet es an den Client. Dadurch wird jedes weitere Stadium in der Abfrageausführung übergangen. Die Abfrage wird niemals analysiert, optimiert oder ausgeführt. Sie erfahren in Kapitel 5 mehr über den Abfrage-Cache.
Die Abfrageoptimierung Der nächste Schritt im Leben einer Abfrage wandelt eine SQL-Abfrage in einen Ausführungsplan für die Abfrageausführungs-Engine um. Dieser Schritt besteht aus mehreren Teilschritten: Analyse, Vorverarbeitung und Optimierung. Fehler (z.B. Syntaxfehler) können an jeder Stelle in diesem Vorgang aufgedeckt werden. Wir versuchen hier nicht, die MySQL-Interna zu dokumentieren, weshalb wir uns hier einige Freiheiten nehmen.
176 | Kapitel 4: Optimierung der Abfrageleistung
So beschreiben wir etwa die Schritte einzeln, obwohl sie aus Gründen der Effizienz oftmals ganz oder teilweise kombiniert werden. Wir wollen ganz einfach, dass Sie verstehen, wie MySQL Abfragen ausführt, damit Sie in der Lage sind, bessere Abfragen zu schreiben.
Der Parser und der Präprozessor Zuerst unterteilt der MySQL-Parser die Abfrage in Token und erzeugt aus ihnen einen »Parse-Baum«. Der Parser verwendet die SQL-Grammatik von MySQL, um die Abfrage zu interpretieren und zu validieren. Er stellt z.B. sicher, dass die Abfrage gültig (valide) ist und in der richtigen Reihenfolge vorliegt, und prüft auf Fehler wie etwa quotierte Strings, die nicht terminiert wurden. Der Präprozessor überprüft dann den resultierenden Parse-Baum auf zusätzliche Semantiken, die der Parser nicht auflösen kann. So schaut er z.B. nach, ob Tabellen und Spalten existieren, und löst Namen und Aliase auf, um sicherzugehen, dass die Spaltenreferenzen nicht mehrdeutig sind. Als Nächstes prüft der Präprozessor die Rechte. Das geht normalerweise sehr schnell, es sei denn, Ihr Server verfügt über sehr viele Rechte. (In Kapitel 12 erfahren Sie mehr über Rechte und Sicherheit.)
Der Abfrageoptimierer Der Parse-Baum ist jetzt gültig und wartet darauf, vom Optimierer in einen Ausführungsplan umgewandelt zu werden. Eine Abfrage kann oft auf verschiedene Weisen ausgeführt werden und immer das gleiche Ergebnis erzeugen. Die Aufgabe des Optimierers besteht darin, die beste Möglichkeit zu finden. MySQL verwendet einen kostenbasierten Optimierer. Dieser versucht, die Kosten für die verschiedenen Ausführungspläne vorherzusagen und den am wenigsten teuren Ausführungsplan zu wählen. Die Einheit für die Kosten ist ein einzelner zufälliger Lesevorgang für eine vier Kilobyte große Datenseite. Sie können feststellen, wie teuer der Optimierer eine Abfrage einschätzt, indem Sie die Abfrage ausführen und dann die Session-Variable Last_query_cost auswerten: mysql> SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor; +----------+ | count(*) | +----------+ | 5462 | +----------+ mysql> SHOW STATUS LIKE 'last_query_cost'; +-----------------+-------------+ | Variable_name | Value | +-----------------+-------------+ | Last_query_cost | 1040.599000 | +-----------------+-------------+
Grundlagen der Abfrageverarbeitung | 177
Dieses Ergebnis bedeutet, dass der Optimierer geschätzt hat, es würde etwa 1.040 zufällige Lesezugriffe auf Datenseiten erfordern, um die Abfrage auszuführen. Er baut seine Schätzung auf Statistiken auf: der Anzahl der Seiten pro Tabelle oder Index, der Kardinalität (Anzahl der Einzelwerte) der Indizes, der Länge der Zeilen und Schlüssel sowie der Schlüsselverteilung. Der Optimierer bezieht die Auswirkungen irgendwelcher Caches nicht in seine Schätzung ein, sondern geht davon aus, dass jeder Lesevorgang zu einem Festplattenzugriff führt. Möglicherweise wählt der Optimierer nicht immer den besten Plan. Das kann viele Gründe haben: • Die Statistiken könnten falsch sein. Der Server verlässt sich für die Statistiken auf die Storage-Engines. Sie können von außerordentlich korrekt bis zu völlig falsch reichen. So liefert z.B. die InnoDB-Storage-Engine aufgrund ihrer MVCC-Architektur keine exakten Statistiken über die Anzahl der Zeilen in einer Tabelle. • Das Kostenmaß ist nicht genau äquivalent zu den wahren Kosten für das Ausführen einer Abfrage. Das heißt, selbst wenn die Statistiken exakt sind, kann die Abfrage teurer oder weniger teuer sein, als die Schätzung von MySQL angibt. Ein Plan, der mehr Seiten liest, könnte in einigen Fällen dennoch billiger ausfallen, etwa wenn die Lesevorgänge sequenziell erfolgen, die Plattenzugriffe also schneller sind, oder wenn die Seiten bereits im Speicher vorliegen. • Die Ansichten von MySQL über ein Optimum entsprechen möglicherweise nicht Ihren Vorstellungen. Wahrscheinlich wollen Sie die schnellste Ausführungszeit haben; MySQL dagegen versteht »schnell« nicht, es meint »Kosten«. Und wie wir gesehen haben, ist das Feststellen der Kosten keine exakte Wissenschaft. • MySQL zieht keine anderen Abfragen in Betracht, die parallel ablaufen und damit beeinflussen können, wie schnell die Abfrage ausgeführt wird. • MySQL führt nicht immer eine kostenbasierte Optimierung durch. Manchmal hält es sich einfach nur an die Regeln, wie etwa: »Wenn es eine Volltext-MATCH( )-Klausel gibt, verwende einen FULLTEXT-Index, so es einen gibt.« Das tut es auch dann, wenn es eigentlich schneller wäre, einen anderen Index einzusetzen und eine Nicht-FULLTEXT-Abfrage mit einer WHERE-Klausel durchzuführen. • Der Optimierer zieht die Kosten von Operationen, die nicht seiner Kontrolle unterliegen, nicht in Betracht, wie etwa das Ausführen gespeicherter benutzerdefinierter Funktionen. • Wie wir später sehen werden, kann der Optimierer nicht immer jeden möglichen Ausführungsplan abschätzen und verpasst deswegen manchmal einen optimalen Plan. Der Abfrageoptimierer von MySQL ist ein ausgesprochen komplexes Stück Software und benutzt viele Optimierungen, um die Abfrage in einen Ausführungsplan umzuwandeln. Es gibt zwei grundlegende Arten von Optimierungen, die wir als statisch und dynamisch bezeichnen. Statische Optimierungen können ausgeführt werden, indem einfach der
178 | Kapitel 4: Optimierung der Abfrageleistung
Parse-Baum untersucht wird. So kann z.B. der Optimierer die WHERE-Klausel in eine äquivalente Form umwandeln, indem er algebraische Regeln anwendet. Statische Optimierungen sind unabhängig von Werten, wie etwa dem Wert einer Konstanten in einer WHERE-Klausel. Sie können einmal durchgeführt werden und sind immer gültig, selbst wenn die Abfrage mit anderen Werten noch einmal ausgeführt wird. Stellen Sie sie sich einfach als »Optimierungen zur Compile-Zeit« vor. Im Gegensatz dazu beruhen dynamische Optimierungen auf dem Kontext und können von vielen Faktoren abhängen, wie etwa vom Wert in einer WHERE-Klausel oder der Anzahl der Zeilen in einem Index. Sie müssen jedes Mal neu bewertet werden, wenn die Abfrage ausgeführt wird. Stellen Sie sie sich als »Laufzeitoptimierungen« vor. Der Unterschied ist wichtig, wenn vorbereitete Anweisungen oder gespeicherte Prozeduren ausgeführt werden. MySQL kann statische Optimierungen einmal durchführen, muss aber dynamische Optimierungen jedes Mal neu bewerten, wenn es eine Abfrage ausführt. Manchmal optimiert es sogar die Abfrage neu, wenn es sie ausführt.6 Dies sind einige Arten von Optimierungen, die MySQL kennt: Umsortieren von Joins Tabellen müssen nicht immer in der Reihenfolge zusammengeführt werden, die Sie in der Abfrage angeben. Das Festlegen der besten Join-Reihenfolge ist eine wichtige Optimierung. Wir erläutern sie in »Der Join-Optimierer« auf Seite 186 genauer. OUTER JOINs in INNER JOINs umwandeln Ein OUTER JOIN muss nicht unbedingt als OUTER JOIN ausgeführt werden. Manche Faktoren, wie etwa die WHERE-Klausel und das Tabellenschema können dafür sorgen, dass ein OUTER JOIN eigentlich äquivalent zu einem INNER JOIN ist. MySQL kann das
erkennen und den Join umschreiben, so dass er für eine Umsortierung geeignet ist. Anwenden algebraischer Äquivalenzregeln MySQL wendet algebraische Transformationen an, um Ausdrücke zu vereinfachen und zu kanonisieren. Es kann auch Konstanten zusammenlegen und reduzieren, wodurch unmögliche Constraints und Konstantenbedingungen eliminiert werden. So wird z.B. der Term (5=5 AND a>5) auf a>5 reduziert. Aus (a5 AND b=c AND a=5. Diese Regeln helfen beim Schreiben bedingter Abfragen, auf die wir weiter hinten in diesem Kapitel eingehen. COUNT( )-, MIN( )- und MAX( )-Optimierungen
Indizes und Spalten-Nullability können MySQL oft dabei unterstützen, diese Ausdrücke wegzurationalisieren. Um z.B. den Minimalwert einer Spalte zu ermitteln, die am weitesten links in einem B-Baum-Index steht, kann MySQL einfach die erste Zeile im Index anfordern. Das kann es sogar während der Abfrageoptimierung tun; anschließend behandelt es den Wert für den Rest der Abfrage als Konstante. Um 6 Zum Beispiel bewertet der Bereichstest-Abfrageplan Indizes für jede Zeile in einem JOIN neu. Sie können diesen Abfrageplan sehen, wenn Sie nach »range checked for each record« in der Extra-Spalte in EXPLAIN suchen. Dieser Abfrageplan inkrementiert außerdem die Servervariable Select_full_range_join.
Grundlagen der Abfrageverarbeitung | 179
entsprechend den Maximalwert in einem B-Baum-Index zu finden, liest der Server die letzte Zeile. Wenn der Server diese Optimierung einsetzt, steht »Select tables optimized away« im EXPLAIN-Plan. Das bedeutet tatsächlich, dass der Optimierer die Tabelle aus dem Abfrageplan entfernt und durch eine Konstante ersetzt hat. Ebenso können COUNT(*)-Abfragen ohne WHERE-Klausel in manchen Storage-Engines »wegoptimiert« werden (wie etwa bei MyISAM, wo es in der Tabelle immer einen exakten Zeilenzähler gibt). Näheres erfahren Sie in »COUNT( )-Abfragen optimieren« auf Seite 202. Konstantenausdrücke evaluieren und reduzieren Wenn MySQL entdeckt, dass ein Ausdruck auf eine Konstante reduziert werden kann, dann erledigt es das während der Optimierung. Beispielsweise kann eine benutzerdefinierte Variable in eine Konstante umgewandelt werden, wenn sie nicht in der Abfrage verändert wird. Arithmetische Ausdrücke bilden ein weiteres Beispiel. Vielleicht überraschenderweise kann sogar etwas, das Sie als Abfrage betrachten, während der Optimierungsphase auf eine Konstante reduziert werden. Ein Beispiel ist ein MIN( ) in einem Index. Das kann sogar zu einer Konstantensuche in einem Primärschlüssel oder einem eindeutigen Index erweitert werden. Wenn eine WHEREKlausel eine Konstantenbedingung auf einen solchen Index anwendet, weiß der Optimierer, dass MySQL den Wert am Anfang der Abfrage nachschauen kann. Es behandelt den Wert dann im weiteren Verlauf der Abfrage als Konstante. Hier ist ein Beispiel: mysql> EXPLAIN SELECT film.film_id, film_actor.actor_id -> FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id) -> WHERE film.film_id = 1; +----+-------------+------------+-------+----------------+-------+------+ | id | select_type | table | type | key | ref | rows | +----+-------------+------------+-------+----------------+-------+------+ | 1 | SIMPLE | film | const | PRIMARY | const | 1 | | 1 | SIMPLE | film_actor | ref | idx_fk_film_id | const | 10 | +----+-------------+------------+-------+----------------+-------+------+
MySQL führt diese Abfrage in zwei Schritten aus, die den zwei Zeilen in der Ausgabe entsprechen. Der erste Schritt besteht darin, die gewünschte Zeile in der filmTabelle zu suchen. Der MySQL-Optimierer weiß, dass es nur eine Zeile gibt, weil sich ein Primärschlüssel in der film_id-Spalte befindet, und hat bereits während der Abfrageoptimierung den Index konsultiert, um festzustellen, wie viele Zeilen er vorfinden wird. Da der Abfrageoptimierer bei der Suche eine bekannte Größe (den Wert in der WHERE-Klausel) verwendet, ist der ref-Typ dieser Tabelle const. Im zweiten Schritt behandelt MySQL die film_id-Spalte aus der Zeile, die im ersten Schritt gefunden wurde, als bekannte Größe. Das geht, weil der Optimierer weiß, dass er zu dem Zeitpunkt, in dem die Abfrage den zweiten Schritt erreicht, alle Werte aus dem ersten Schritt kennt. Der ref-Typ der film_actor-Tabelle ist ebenfalls const, genau wie bei der film-Tabelle.
180 | Kapitel 4: Optimierung der Abfrageleistung
Eine weitere Möglichkeit, bei der Sie die Anwendung von Konstantenbedingungen sehen können, ist die Verbreitung der Konstantheit eines Wertes von einer Stelle zu einer anderen bei WHERE-, USING- oder ON-Klauseln, die verlangen, dass der Wert gleich ist. In diesem Beispiel weiß der Optimierer, dass die USING-Klausel film_id zwingt, überall in der Abfrage den gleichen Wert anzunehmen – er muss gleich dem konstanten Wert sein, der in der WHERE-Klausel übergeben wurde. Abdeckende Indizes MySQL kann manchmal einen Index einsetzen, um das Lesen von Zeilendaten zu vermeiden, wenn der Index alle Spalten enthält, die die Abfrage benötigt. Wir stellen abdeckende Indizes in Kapitel 3 ausführlich vor. Unterabfrageoptimierung MySQL kann manche Arten von Unterabfragen in effizientere alternative Formen verwandeln, wodurch sie auf Indexsuchen reduziert werden, anstatt separate Abfragen zu bilden. Frühe Terminierung MySQL kann die Verarbeitung einer Abfrage (oder eines Schrittes in einer Abfrage) stoppen, sobald die Abfrage oder der Schritt erfüllt ist. Der offensichtliche Fall ist eine LIMIT-Klausel, es gibt aber noch weitere Arten von frühzeitiger Terminierung. Falls z.B. MySQL eine unmögliche Bedingung entdeckt, kann es die gesamte Abfrage abbrechen. Schauen Sie sich das folgende Beispiel an: mysql> EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1; +----+...+-----------------------------------------------------+ | id |...| Extra | +----+...+-----------------------------------------------------+ | 1 |...| Impossible WHERE noticed after reading const tables | +----+...+-----------------------------------------------------+
Diese Abfrage stoppte während des Optimierungsschrittes, MySQL kann die Ausführung in manchen Fällen aber auch früher beenden. Der Server kann diese Optimierung benutzen, wenn die Abfrageausführungs-Engine merkt, dass sie einzelne Werte abrufen oder dass sie stoppen muss, wenn ein Wert nicht existiert. So findet z.B. die folgende Abfrage alle Filme ohne Schauspieler:7 mysql> SELECT film.film_id -> FROM sakila.film -> LEFT OUTER JOIN sakila.film_actor USING(film_id) -> WHERE film_actor.film_id IS NULL;
Bei dieser Abfrage werden alle Filme eliminiert, die Schauspieler enthalten. Jeder Film könnte viele Schauspieler haben, sobald aber ein Schauspieler gefunden wird, wird die Verarbeitung des aktuellen Films gestoppt und zum nächsten Film weitergegangen, weil bekannt ist, dass die WHERE-Klausel das Ausgeben dieses Films verbietet. Eine ähnliche »Einzelwerte/nicht existent«-Optimierung kann auf bestimmte Arten von DISTINCT-, NOT EXISTS( )- und LEFT JOIN-Abfragen angewandt werden. 7 Es stimmt schon, ein Film ohne Schauspieler scheint seltsam zu sein, allerdings gibt die Sakila-Beispieldatenbank keine Schauspieler für »SLACKER LIAISONS« an. Sie beschreibt diesen Film als »Eine hektische Geschichte von einem Hai und einem Schüler, der im Alten China ein Krokodil treffen muss«.
Grundlagen der Abfrageverarbeitung | 181
Verbreitung von Gleichheit MySQL erkennt, wenn eine Abfrage zwei gleiche Spalten enthält – z.B. in einer JOINBedingung –, und verteilt WHERE-Klauseln über äquivalente Spalten. So etwa in der folgenden Abfrage: mysql> SELECT film.film_id -> FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id) -> WHERE film.film_id > 500;
MySQL weiß, dass die WHERE-Klausel nicht nur für die film-Tabelle, sondern auch für die film_actor-Tabelle gilt, weil die USING-Klausel die beiden Spalten zwingt zusammenzugehen. Falls Sie an einen anderen Datenbankserver gewöhnt sind, der so etwas nicht tut, wird Ihnen vielleicht geraten, »dem Optimierer zu helfen«, indem Sie die WHEREKlausel manuell für beide Tabellen angeben: ... WHERE film.film_id > 500 AND film_actor.film_id > 500
In MySQL ist das unnötig. Die Abfragen werden dadurch nur schwerer zu pflegen. IN( )-Listenvergleiche
Bei vielen Datenbankservern ist IN( ) nur ein Synonym für mehrere OR-Klauseln, da die beiden logisch äquivalent sind. Nicht so in MySQL, das die Werte in der IN( )Liste sortiert und mithilfe einer schnellen Binärsuche feststellt, ob sich ein Wert in der Liste befindet. Das ist O(log n) in der Größe der Liste, während eine äquivalente Folge von OR-Klauseln O(n) in der Größe der Liste ist (d.h., bei großen Listen viel langsamer abläuft). Die vorstehende Liste ist beklagenswert unvollständig, da MySQL mehr Optimierungen durchführt, als wir in dieses eine Kapitel pressen konnten. Sie sollte Ihnen aber einen Eindruck von der Komplexität und Intelligenz des Optimierers vermitteln. Wenn es eine Sache gibt, die Sie in diesem Abschnitt lernen sollten, dann ist es diese: Versuchen Sie nicht, den Optimierer zu überlisten. Sie könnten seine Bemühungen damit einfach nur vereiteln oder Ihre Abfragen und deren Pflege unnötig verkomplizieren, ohne dass Ihnen daraus ein Nutzen erwächst. Im Allgemeinen sollten Sie den Optimierer in Ruhe sein Werk verrichten lassen. Trotz aller Schlauheit gibt es natürlich auch Gelegenheiten, bei denen der Optimierer nicht das beste Ergebnis liefert. Manchmal wissen Sie vielleicht etwas über die Daten, was der Optimierer nicht weiß, wie etwa eine Tatsache, die aufgrund der Logik der Anwendung garantiert zutrifft. Manchmal verfügt der Optimierer auch nicht über die notwendige Funktionalität (wie etwa Hash-Indizes), oder seine Kostenschätzungen bevorzugen einen Abfrageplan, der teurer zu werden droht als eine Alternative. Wenn Sie wissen, dass der Optimierer kein gutes Ergebnis liefert, und Sie die Ursache kennen, können Sie etwas dagegen tun. Es wäre z.B. möglich, einen Hinweis zur Abfrage hinzuzufügen, die Abfrage umzuschreiben, das Schema umzugestalten oder Indizes hinzuzufügen.
182 | Kapitel 4: Optimierung der Abfrageleistung
Tabellen- und Indexstatistiken Rufen Sie sich die verschiedenen Ebenen der MySQL-Serverarchitektur wieder ins Gedächtnis, die wir in Abbildung 1-1 dargestellt haben. Die Serverebene, die den Abfrageoptimierer enthält, speichert keine Statistiken über Daten und Indizes. Das ist eine Aufgabe für die Storage-Engines, da jede Storage-Engine unterschiedliche Arten von Statistiken erheben (oder sie auf unterschiedliche Weise aufheben) könnte. Manche Engines, wie etwa Archive, besitzen überhaupt keine Statistiken! Da der Server keine Statistiken speichert, muss der MySQL-Abfrageoptimierer die Engines in einer Abfrage nach Statistiken über die Tabellen fragen. Die Engines könnten dem Optimierer Statistiken liefern, wie die Anzahl der Seiten pro Tabelle oder Index, die Kardinalität der Tabellen und Indizes, die Länge der Zeilen und Schlüssel und Informationen über die Schlüsselverteilung. Der Optimierer kann mithilfe dieser Informationen über den besten Ausführungsplan entscheiden. Wir werden später noch sehen, wie diese Statistiken die Entscheidungen des Optimierers beeinflussen.
Die Join-Ausführungsstrategie von MySQL MySQL verwendet den Begriff »Join« allgemeiner, als Sie es möglicherweise gewöhnt sind. Im Prinzip betrachtet es jede Abfrage als Join – nicht nur jede Abfrage, die Zeilen aus zwei Tabellen miteinander vergleicht, sondern jede Abfrage, Punkt (einschließlich Unterabfragen und sogar einem SELECT an einer einzigen Tabelle). Daher ist es sehr wichtig, dass Sie verstehen, wie MySQL Joins ausführt. Nehmen Sie das Beispiel einer UNION-Abfrage. MySQL führt ein UNION als eine Reihe einzelner Abfragen aus, deren Ergebnisse in eine temporäre Tabelle geschrieben und dann wieder ausgelesen werden. Jede der einzelnen Abfragen ist in der MySQL-Terminologie ein Join – genau wie der Akt des Lesens aus der resultierenden temporären Tabelle. Im Moment ist die MySQL-Join-Ausführungsstrategie einfach: Es behandelt alle Joins als Nested-Loop-Join (Geschachtelte-Schleife-Join). Das bedeutet, dass MySQL eine Schleife ausführt, um eine Zeile in einer Tabelle zu suchen, und dann eine geschachtelte Schleife ausführt, um eine passende Zeile in der nächsten Tabelle zu finden. Es fährt damit fort, bis es in jeder Tabelle des Joins eine passende Zeile gefunden hat. Es baut dann eine Zeile aus den Spalten zusammen, die in der SELECT-Liste genannt sind, und liefert sie zurück. Es versucht, die nächste Zeile zu erzeugen, indem es nach weiteren passenden Zeilen in der letzten Tabelle sucht. Findet es keine, geht es eine Tabelle zurück und sucht dort nach weiteren Zeilen. Mit diesem Zurückgehen macht es weiter, bis es eine weitere Zeile in irgendeiner Tabelle findet, von wo es dann wieder in der nächsten Tabelle nach einer passenden Zeile sucht, usw.8
8 Wie wir später zeigen werden, ist die Ausführung von Abfragen mit MySQL nicht ganz so einfach, da es viele Optimierungen gibt, die sie verkomplizieren.
Grundlagen der Abfrageverarbeitung | 183
Dieser Vorgang des Suchens von Zeilen, des Weitersuchens in der nächsten Tabelle und des Zurückgehens kann im Ausführungsplan in Form von geschachtelten Schleifen beschrieben werden – daher der Name. Betrachten Sie beispielsweise diese einfache Abfrage: mysql> SELECT tbl1.col1, tbl2.col2 -> FROM tbl1 INNER JOIN tbl2 USING(col3) -> WHERE tbl1.col1 IN(5,6);
Angenommen, MySQL beschließt, die Tabellen in der gezeigten Reihenfolge zusammenzuführen. Der folgende Pseudocode zeigt, wie MySQL die Abfrage ausführen könnte: outer_iter = iterator over tbl1 where col1 IN(5,6) outer_row = outer_iter.next while outer_row inner_iter = iterator over tbl2 where col3 = outer_row.col3 inner_row = inner_iter.next while inner_row output [ outer_row.col1, inner_row.col2 ] inner_row = inner_iter.next end outer_row = outer_iter.next end
Dieser Abfrageausführungsplan kann auf eine Eintabellenabfrage genauso leicht angewandt werden wie auf eine Mehrtabellenabfrage, weshalb sogar eine Eintabellenabfrage als Join angesehen wird – die Eintabellenabfrage ist schließlich die grundlegende Operation, aus der komplexere Joins zusammengesetzt werden. Er unterstützt auch OUTER JOINs. Ändern wir z.B. die Beispielabfrage wie folgt: mysql> SELECT tbl1.col1, tbl2.col2 -> FROM tbl1 LEFT OUTER JOIN tbl2 USING(col3) -> WHERE tbl1.col1 IN(5,6);
Hier ist der entsprechende Pseudocode; die geänderten Teile sind fettgedruckt: outer_iter = iterator over tbl1 where col1 IN(5,6) outer_row = outer_iter.next while outer_row inner_iter = iterator over tbl2 where col3 = outer_row.col3 inner_row = inner_iter.next if inner_row while inner_row output [ outer_row.col1, inner_row.col2 ] inner_row = inner_iter.next end else output [ outer_row.col1, NULL ] end outer_row = outer_iter.next end
184 | Kapitel 4: Optimierung der Abfrageleistung
Eine weitere Möglichkeit, einen Abfrageausführungsplan zu visualisieren, besteht darin, ein sogenanntes »Schwimmbahn-Diagramm« zu benutzen. Abbildung 4-2 enthält ein Schwimmbahn-Diagramm unserer ersten INNER JOIN-Abfrage. Lesen Sie es von links nach rechts und von oben nach unten. Tabellen tbl1
tbl2
Ergebniszeilen
Spl1=5, Spl3=1
Spl3=1, Spl2=1
Spl1=5, Spl2=1
Spl3=1, Spl2=2
Spl1=5, Spl2=2
Spl3=1, Spl2=3
Spl1=5, Spl2=3
Spl3=1, Spl2=1
Spl1=6, Spl2=1
Spl3=1, Spl2=2
Spl1=6, Spl2=2
Spl3=1, Spl2=3
Spl1=6, Spl2=3
Spl1=6, Spl3=1
Abbildung 4-2: Ein Schwimmbahn-Diagramm, das verdeutlicht, wie Zeilen mittels eines Joins abgefragt werden
MySQL führt im Prinzip alle Arten von Abfragen auf die gleiche Weise aus. Zum Beispiel verarbeitet es eine Unterabfrage in der FROM-Klausel, indem es sie zuerst ausführt, die Ergebnisse in eine temporäre Tabelle legt9 und dann diese Tabelle wie eine ganz normale Tabelle behandelt (daher der Name »abgeleitete Tabelle«). MySQL führt UNION-Abfragen ebenfalls mit temporären Tabellen aus und schreibt alle RIGHT OUTER JOIN-Abfragen in äquivalente LEFT OUTER JOIN-Abfragen um. Kurz gesagt zwingt MySQL jede Art von Abfrage in diesen Ausführungsplan. Es ist allerdings nicht möglich, jede zulässige SQL-Abfrage auf diese Weise auszuführen. So kann z.B. ein FULL OUTER JOIN nicht mit geschachtelten Schleifen und durch Zurückgehen ausgeführt werden, sobald eine Tabelle gefunden wurde, deren Zeilen nicht passen, da diese Operation mit einer Tabelle beginnen könnte, die keine passenden Zeilen enthält. Das erklärt, wieso MySQL FULL OUTER JOIN nicht unterstützt. Andere Abfragen können zwar mit geschachtelten Schleifen ausgeführt werden, laufen aber sehr schlecht. Wir schauen uns einige dieser Abfragen später noch an.
Der Ausführungsplan MySQL generiert im Gegensatz zu vielen anderen Datenbankprodukten keinen Bytecode, um eine Abfrage auszuführen. Stattdessen ist der Abfrageausführungsplan eigentlich ein Anweisungsbaum, den die Ausführungs-Engine durchläuft, um die Abfrageergebnisse zu 9 Es gibt keine Indizes in der temporären Tabelle. Das sollten Sie bedenken, wenn Sie komplexe Joins an Unterabfragen in der FROM-Klausel schreiben. Das gilt auch für UNION-Abfragen.
Grundlagen der Abfrageverarbeitung | 185
erzeugen. Der fertige Plan enthält genügend Informationen, um die Originalabfrage zu rekonstruieren. Wenn Sie EXPLAIN EXTENDED, gefolgt von SHOW WARNINGS, auf einer Abfrage ausführen, erhalten Sie die rekonstruierte Abfrage.10 Jede Mehrtabellenabfrage kann konzeptuell als Baum dargestellt werden. Es wäre z.B. möglich, einen Viertabellen-Join auszuführen, wie in Abbildung 4-3 gezeigt. Join
Join
tbl1
Join
tbl2
tbl3
tbl4
Abbildung 4-3: Eine Möglichkeit, mehrere Tabellen zusammenzuführen
Informatiker bezeichnen dies als balancierten Baum. Allerdings ist dies nicht die Art, wie MySQL die Abfrage ausführt. Wie wir im vorherigen Abschnitt beschrieben haben, beginnt MySQL immer mit einer Tabelle und sucht in der nächsten Tabelle nach passenden Zeilen. Abfrageausführungspläne von MySQL nehmen deshalb immer die Form eines Left-Deep Tree an wie in Abbildung 4-4. Join
Join
Join
tbl1
tbl4
tbl3
tbl2
Abbildung 4-4: Wie MySQL mehrere Tabellen zusammenführt
Der Join-Optimierer Der wichtigste Teil des MySQL-Abfrageoptimierers ist der Join-Optimierer, der die beste Ausführungsreihenfolge für Mehrtabellenabfragen festlegt. Oft ist es möglich, Tabellen in mehreren unterschiedlichen Anordnungen zusammenzuführen und dennoch die glei-
10 Der Server generiert die Ausgabe aus dem Ausführungsplan. Sie weist daher die gleiche Semantik wie die Originalabfrage auf, aber nicht unbedingt den gleichen Text.
186 | Kapitel 4: Optimierung der Abfrageleistung
chen Ergebnisse zu erhalten. Der Join-Optimierer schätzt die Kosten für die verschiedenen Pläne ab und versucht, den preiswertesten Plan auszuwählen, der das gewünschte Ergebnis liefert. Hier ist eine Abfrage, deren Tabellen in verschiedenen Anordnungen zusammengeführt werden können, ohne dass sich die Ergebnisse ändern: mysql> SELECT film.film_id, film.title, film.release_year, actor.actor_id, -> actor.first_name, actor.last_name -> FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id) -> INNER JOIN sakila.actor USING(actor_id);
Sie können sich wahrscheinlich noch ein paar andere Abfragepläne ausdenken. Zum Beispiel könnte MySQL mit der film-Tabelle beginnen, den Index in film_id in der film_actor-Tabelle verwenden, um actor_id-Werte zu finden, und dann Zeilen im Primärschlüssel der actor-Tabelle nachschlagen. Das sollte effizient sein, oder? Wir wollen nun mithilfe von EXPLAIN feststellen, wie MySQL die Abfrage ausführen möchte: *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: ALL possible_keys: PRIMARY key: NULL key_len: NULL ref: NULL rows: 200 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ref possible_keys: PRIMARY,idx_fk_film_id key: PRIMARY key_len: 2 ref: sakila.actor.actor_id rows: 1 Extra: Using index *************************** 3. row *************************** id: 1 select_type: SIMPLE table: film type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 2 ref: sakila.film_actor.film_id rows: 1 Extra:
Grundlagen der Abfrageverarbeitung | 187
Dieser Plan unterscheidet sich von demjenigen aus dem vorhergehenden Absatz. MySQL möchte mit der actor-Tabelle beginnen (wir wissen das, weil sie in der EXPLAIN-Ausgabe als Erstes auftaucht) und in der umgekehrten Reihenfolge vorgehen. Ist das wirklich effizienter? Wir wollen es herausfinden. Das Schlüsselwort STRAIGHT_JOIN zwingt den Join, in der Reihenfolge fortzufahren, die in der Abfrage angegeben wurde. Hier ist die EXPLAINAusgabe für die überarbeitete Abfrage: mysql> EXPLAIN SELECT STRAIGHT_JOIN film.film_id...\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film type: ALL possible_keys: PRIMARY key: NULL key_len: NULL ref: NULL rows: 951 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ref possible_keys: PRIMARY,idx_fk_film_id key: idx_fk_film_id key_len: 2 ref: sakila.film.film_id rows: 1 Extra: Using index *************************** 3. row *************************** id: 1 select_type: SIMPLE table: actor type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 2 ref: sakila.film_actor.actor_id rows: 1 Extra:
Dies zeigt, weshalb MySQL die Join-Reihenfolge umkehren möchte: Auf diese Weise müsste es weniger Zeilen in der ersten Tabelle untersuchen.11 In beiden Fällen wäre es in der Lage, schnelle indizierte Lookups in der zweiten und dritten Tabelle durchzuführen. Der Unterschied besteht in der Anzahl dieser indizierten Lookups:
11 Genau genommen versucht MySQL nicht, die Anzahl der Zeilen, die es liest, zu reduzieren. Stattdessen versucht es, eine Optimierung in Richtung auf weniger Seitenleseoperationen durchzuführen. Die Zeilenzahl vermittelt Ihnen jedoch oft eine ungefähre Vorstellung von den Kosten der Abfrage.
188 | Kapitel 4: Optimierung der Abfrageleistung
• Würde film an die erste Stelle gesetzt, dann müssten 951 Sondierungen in film_ actor und actor durchgeführt werden, und zwar je eine für jede Zeile in der ersten Tabelle. • Scannt der Server die actor-Tabelle zuerst, muss er nur 200 Index-Lookups in später folgenden Tabellen durchführen. Mit anderen Worten: Die umgekehrte Join-Reihenfolge erfordert weniger Zurückgehen und erneutes Lesen. Um die Wahl des Optimierers noch einmal zu überprüfen, führten wir zwei Abfrageversionen durch und schauten uns jeweils die Variable Last_query_cost an. Die umsortierte Abfrage hatte geschätzte Kosten von 241, während die geschätzten Kosten zum Erzwingen der Join-Reihenfolge 1.154 betrugen. Dies ist ein einfaches Beispiel dafür, wie der Join-Optimierer von MySQL umsortieren kann, um die Ausführung preiswerter zu machen. Das Umsortieren von Joins ist normalerweise eine sehr effektive Optimierung. Manchmal jedoch ergibt sich kein optimaler Plan. In diesen Fällen können Sie STRAIGHT_JOIN verwenden und die Abfrage in der Anordnung schreiben, die Ihnen am besten vorkommt – solche Fälle sind aber selten. Meist ist der Join-Optimierer besser als ein Mensch. Der Join-Optimierer versucht, einen Abfrageausführungsplan mit den niedrigsten erreichbaren Kosten zu erzeugen. Wenn möglich, untersucht er alle potenziellen Kombinationen aus Teilbäumen, wobei er mit allen Eintabellenplänen beginnt. Leider ergibt ein Join über n Tabellen n-Fakultät Kombinationen aus Join-Anordnungen für die Untersuchung. Man bezeichnet dies als den Suchraum aller möglichen Abfragepläne; dieser wird sehr groß – ein 10-Tabellen-Join kann auf 3.628.800 unterschiedliche Arten ausgeführt werden! Wenn der Suchraum zu groß wird, dauert es möglicherweise viel zu lange, um die Abfrage zu optimieren, so dass der Server keine vollständige Analyse durchführt. Stattdessen greift er auf Abkürzungen zurück, wie etwa auf »Greedy«Suchen, wenn die Anzahl der Tabellen die Grenze optimizer_search_depth überschreitet. MySQL verfügt über viele im Laufe vieler Jahre des Forschens und Experimentierens gesammelte Heuristiken, die es anwendet, um das Optimierungsstadium zu beschleunigen. Das kann vorteilhaft sein, kann aber auch bedeuten, dass MySQL möglicherweise (obwohl es selten vorkommt) einen optimalen Plan verpasst und einen weniger optimalen Plan wählt, weil es versucht, nicht jeden möglichen Abfrageplan zu untersuchen. Manchmal können Abfragen nicht neu angeordnet werden. Der Join-Optimierer nutzt diese Tatsache, um den Suchraum zu verkleinern, indem er Möglichkeiten eliminiert. Ein LEFT JOIN ist ein gutes Beispiel, genau wie miteinander verwandte Unterabfragen (später mehr zu Unterabfragen). Das liegt daran, dass die Ergebnisse für eine Tabelle von Daten abhängen, die aus einer anderen Tabelle abgefragt werden. Diese Abhängigkeiten helfen dem Join-Optimierer, den Suchraum zu verringern, indem Möglichkeiten eliminiert werden.
Grundlagen der Abfrageverarbeitung | 189
Sortieroptimierungen Das Sortieren von Ergebnissen ist unter Umständen sehr kostenintensiv. Sie können deshalb die Leistung oft verbessern, indem Sie das Sortieren vermeiden oder es mit weniger Zeilen durchführen. Wir haben Ihnen in Kapitel 3 gezeigt, wie Sie Indizes zum Sortieren verwenden. Wenn MySQL keinen Index benutzen kann, um ein sortiertes Ergebnis zu erzeugen, muss es die Zeilen selbst sortieren. Dieser Vorgang wird, unabhängig davon, ob er im Speicher oder auf der Festplatte erfolgt, als Filesort bezeichnet, obwohl eigentlich überhaupt keine Datei benutzt wird. Wenn die Werte, die sortiert werden sollen, in den Sortierpuffer passen, kann MySQL das Sortieren mit einem Quicksort komplett im Speicher erledigen. Ist das Sortieren im Speicher nicht möglich, führt MySQL es auf der Festplatte aus, indem es die Werte gruppenweise sortiert. Das Sortieren der einzelnen Gruppen erfolgt mit einem Quicksort, anschließend werden die sortierten Gruppen zu den Ergebnissen zusammengefasst. Es gibt zwei Filesort-Algorithmen: Mit zwei Durchläufen (Two passes), alt Liest Zeilenzeiger und ORDER BY-Spalten, sortiert sie und scannt dann die sortierten Listen und liest die Zeilen erneut für die Ausgabe. Der Two-Pass-Algorithmus kann sehr aufwendig sein, weil er die Zeilen zweimal aus der Tabelle liest und der zweite Lesevorgang viele zufällige Ein-/Ausgaben verursacht. Vor allem für MyISAM ist dies teuer, da MyISAM einen Systemaufruf verwendet, um die einzelnen Zeilen abzurufen. (MyISAM greift nämlich auf den Cache des Betriebssystems zurück, um die Daten abzulegen.) Andererseits speichert er während der Sortierung nur eine minimale Menge an Daten. Wenn also die Zeilen komplett im Speicher sortiert werden können, kann es billiger sein, weniger Daten zu speichern und die Zeilen erneut zu lesen, um das endgültige Ergebnis zu generieren. Mit einem Durchlauf (Single Pass), neu Liest alle Spalten, die für die Abfrage benötigt werden, sortiert sie nach den ORDER BYSpalten, durchsucht dann die sortierte Liste und gibt die festgelegten Spalten aus. Diesen Algorithmus gibt es erst ab MySQL 4.1. Er kann viel effizienter sein, vor allem bei großen ein-/ausgabebasierten Datenmengen, da er die Zeilen aus der Tabelle nicht zweimal liest und statt zufälliger Ein-/Ausgaben sequenzielle Ein-/Ausgaben nutzt. Allerdings ist er möglicherweise sehr raumgreifend, da er alle gewünschten Spalten aus jeder einzelnen Zeile aufnimmt, nicht nur die Spalten, die zum Sortieren der Zeilen erforderlich sind. Das bedeutet, dass in den Sortierpuffer weniger Tupel passen und der Filesort-Algorithmus mehr Durchläufe zum Zusammenfassen der Sortierergebnisse benötigt. MySQL kann für einen Filesort viel mehr temporären Speicherplatz einsetzen, als Sie erwarten würden, da es für jedes Tupel, das es sortiert, einen Datensatz fester Größe reserviert. Diese Datensätze sind groß genug für das größtmögliche Tupel, einschließlich
190 | Kapitel 4: Optimierung der Abfrageleistung
der vollständigen Länge der einzelnen VARCHAR-Spalten. Falls Sie UTF-8 verwenden, stellt MySQL außerdem drei Byte für jedes Zeichen bereit. Wir haben aus diesem Grund schon Fälle gesehen, in denen schlecht optimierte Schemata dafür gesorgt haben, dass der temporäre Platz, der für das Sortieren verwendet wurde, viele Male größer war als die Größe der kompletten Tabelle auf der Festplatte. Beim Sortieren eines Joins kann MySQL den Filesort in zwei Stufen während der Abfrageausführung erledigen. Bezieht sich die ORDER BY-Klausel nur auf Spalten aus der ersten Tabelle in der Join-Reihenfolge, kann MySQL diese Tabelle mit Filesort sortieren und dann mit dem Join fortfahren. Wenn dies geschieht, zeigt EXPLAIN »Using filesort« in der Extra-Spalte. Ansonsten muss MySQL die Ergebnisse der Abfrage in einer temporären Tabelle speichern und diese Tabelle dann mit Filesort sortieren, wenn der Join abgeschlossen ist. In diesem Fall zeigt EXPLAIN »Using temporary; Using filesort« in der ExtraSpalte. Ein eventuell vorhandenes LIMIT wird nach dem Filesort angewendet, die temporäre Tabelle und der Filesort können also sehr groß werden. In »Für Filesorts optimieren« auf Seite 325 finden Sie weitere Informationen darüber, wie Sie den Server für Filesorts einstellen und wie Sie beeinflussen können, welchen Algorithmus der Server verwendet.
Die Abfrageausführungs-Engine Nach der Stufe des Parsens und Optimierens hat man einen Abfrageausführungsplan, den die Abfrageausführungs-Engine von MySQL benutzt, um die Abfrage zu verarbeiten. Bei dem Plan handelt es sich um eine Datenstruktur, nicht um ausführbaren Bytecode wie bei vielen anderen Datenbanken. Im Gegensatz zur Optimierung ist das Ausführungsstadium nicht besonders komplex: MySQL folgt einfach den Anweisungen des Abfrageausführungsplans. Viele der Operationen in dem Plan rufen Methoden auf, die im Interface der Storage-Engine, der sogenannten Handler API, implementiert sind. Jede Tabelle in der Abfrage wird durch eine Instanz eines Handlers repräsentiert. Wenn eine Tabelle z.B. dreimal in der Abfrage auftaucht, erzeugt der Server drei Handler-Instanzen. Wir haben diese Tatsache zwar ausgelassen, aber eigentlich erzeugt MySQL die Handler-Instanzen bereits früh im Optimierungsstadium. Der Optimierer holt mit ihnen Informationen über die Tabellen, wie etwa ihre Spaltennamen und die Indexstatistiken. Das Interface der Storage-Engine umfasst viele Funktionen, benötigt aber zum Ausführen der meisten Abfragen nur etwa ein Dutzend »Basis«-Operationen. So gibt es z.B. eine Operation zum Lesen der ersten Zeile in einem Index und eine zum Lesen der nächsten Zeile in einem Index. Das reicht für eine Abfrage, die einen Indexscan durchführt. Diese grob vereinfachende Ausführungsmethode ermöglicht erst die Storage-Engine-Architektur von MySQL, ist aber auch für einige der Optimiererbeschränkungen verantwortlich, die wir besprochen haben.
Grundlagen der Abfrageverarbeitung | 191
Nicht alles ist eine Handler-Operation. So verwaltet der Server z.B. Tabellen-Locks. Der Handler könnte auf niedrigerer Ebene sein eigenes Locking implementieren, wie etwa InnoDB mit seinen Row-Level-Locks. Das ersetzt aber nicht die servereigene Locking-Implementierung. Wie in Kapitel 1 erläutert wurde, ist alles, was allen Storage-Engines gemein ist, im Server implementiert, wie etwa Datums- und Uhrzeitfunktionen, Sichten und Trigger.
Um die Abfrage auszuführen, wiederholt der Server einfach die Anweisungen, bis keine Zeilen mehr übrig sind, die untersucht werden müssten.
Zurückliefern der Ergebnisse an den Client Der letzte Schritt beim Ausführen einer Abfrage ist die Antwort an den Server. Selbst Abfragen, die keine Ergebnismenge zurückliefern, geben an die Clientverbindung Informationen über die Abfrage zurück, wie etwa die Anzahl der Zeilen, die sie bearbeitet haben. Wenn die Abfrage cache-fähig ist, setzt MySQL die Ergebnisse in dieser Stufe außerdem in den Abfrage-Cache. Der Server generiert und sendet die Ergebnisse inkrementell. Denken Sie an die Multijoin-Methode in einem Durchgang, die wir erwähnt haben. Sobald MySQL die letzte Tabelle verarbeitet und eine Zeile erfolgreich generiert, kann und sollte es diese Zeile an den Client senden. Das hat zwei Vorteile: Der Server muss die Zeile nicht im Speicher ablegen, und der Client bekommt die Ergebnisse so früh wie möglich.12
Grenzen des MySQL-Abfrageoptimierers Der MySQL-Ansatz des »alles ist ein Join mit geschachtelter Schleife« für die Abfrageausführung ist nicht unbedingt am besten zum Optimieren aller Arten von Abfragen geeignet. Glücklicherweise kommt es relativ selten vor, dass der MySQL-Abfrageoptimierer nicht so gut funktioniert, und normalerweise ist es möglich, solche Abfragen effizienter umzuschreiben. Die Informationen in diesem Abschnitt gelten für MySQL-Serverversionen, auf die wir zum Zeitpunkt der Entstehung dieses Buches Zugriff hatten, d.h. bis zu MySQL 5.1. Einige dieser Beschränkungen werden wahrscheinlich in künftigen Versionen gelockert oder aufgehoben, und einige von ihnen sind bereits aus Versionen verschwunden, die noch nicht als GA (generally available, d.h. allgemein verfügbar) veröffentlicht wurden. So gibt es im MySQL 6-Quellcode speziell einige Optimierungen für Unterabfragen, und weitere werden folgen. 12 Sie können dieses Verhalten bei Bedarf beeinflussen, z.B. mit dem Hinweis SQL_BUFFER_RESULT (siehe »Hinweise für den Abfrageoptimierer« auf Seite 210).
192 | Kapitel 4: Optimierung der Abfrageleistung
Korrelierte Unterabfragen Manchmal optimiert MySQL Unterabfragen nur sehr schlecht. Die schlimmsten Missetäter sind IN( )-Unterabfragen in der WHERE-Klausel. Wir wollen als Beispiel alle Filme in der sakila.film-Tabelle der Sakila-Beispieldatenbank finden, zu deren Besetzung die Schauspielerin Penelope Guiness (actor_id=1) gehört. Das scheint mit einer solchen Unterabfrage schnell erledigt zu sein: mysql> SELECT * FROM sakila.film -> WHERE film_id IN( -> SELECT film_id FROM sakila.film_actor WHERE actor_id = 1);
Es ist verlockend, sich vorzustellen, dass MySQL diese Abfrage von innen nach außen ausführt, indem es eine Liste mit actor_id-Werten sucht und diese in die IN( )-Liste einsetzt. Wir haben gesagt, dass eine IN( )-Liste im Allgemeinen sehr schnell ist, so dass Sie vermutlich erwarten, dass die Abfrage etwa so optimiert werden würde: -- SELECT GROUP_CONCAT(film_id) FROM sakila.film_actor WHERE actor_id = 1; -- Result: 1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980 SELECT * FROM sakila.film WHERE film_id IN(1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980) ;
Leider tritt genau der entgegengesetzte Fall ein. MySQL versucht, der Unterabfrage zu »helfen«, indem es eine Beziehung aus der äußeren Tabelle in die Abfrage herstellt, von der es glaubt, dass sie die Unterabfrage effizienter die Zeilen finden lässt. Es schreibt die Abfrage folgendermaßen um: SELECT * FROM sakila.film WHERE EXISTS ( SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id);
Jetzt verlangt die Unterabfrage die film_id aus der äußeren film-Tabelle und kann nicht mehr zuerst ausgeführt werden. EXPLAIN zeigt das Ergebnis als DEPENDENT SUBQUERY (mit EXPLAIN EXTENDED können Sie genau feststellen, wie die Abfrage umgeschrieben wurde): mysql> EXPLAIN SELECT * FROM sakila.film ...; +----+--------------------+------------+--------+------------------------+ | id | select_type | table | type | possible_keys | +----+--------------------+------------+--------+------------------------+ | 1 | PRIMARY | film | ALL | NULL | | 2 | DEPENDENT SUBQUERY | film_actor | eq_ref | PRIMARY,idx_fk_film_id | +----+--------------------+------------+--------+------------------------+
Entsprechend der EXPLAIN-Ausgabe durchsucht MySQL die film-Tabelle und führt die Unterabfrage für jede Zeile aus, die es findet. Bei kleinen Tabellen hat dies noch keine merklichen Auswirkungen auf die Leistung; wenn hingegen die äußere Tabelle sehr groß ist, fällt die Leistung außerordentlich stark ab. Zum Glück ist es einfach, eine solche Abfrage in einen JOIN umzuschreiben: mysql> SELECT film.* FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id) -> WHERE actor_id = 1;
Grenzen des MySQL-Abfrageoptimierers | 193
Eine weitere gute Optimierung besteht darin, die IN( )-Liste manuell zu generieren, indem man die Unterabfrage als separate Abfrage mit GROUP_CONCAT( ) ausführt. Manchmal kann das schneller sein als ein JOIN. MySQL ist für diese spezielle Art von Ausführungsplan für Unterabfragen gründlich gescholten worden. Obwohl hier sicher Verbesserungsbedarf besteht, bringt die Kritik oft zwei Probleme durcheinander: Ausführungsreihenfolge und Caching. Das Ausführen der Abfrage von innen nach außen ist eine Möglichkeit, sie zu optimieren; das Speichern des Ergebnisses der inneren Abfrage im Cache eine andere. Indem Sie die Abfrage selbst umschreiben, können Sie beide Aspekte kontrollieren. Künftige Versionen von MySQL sollten in der Lage sein, diese Art von Abfrage besser zu optimieren, auch wenn dies keine einfache Aufgabe darstellt. Es gibt für jeden Ausführungsplan ziemlich ungünstige Fälle. Dazu gehört auch der Von-innen-nach-außen-Ausführungsplan, von dem manche Leute glauben, er wäre leicht zu optimieren.
Wann eine korrelierte Unterabfrage gut ist Manchmal ist MySQL beim Optimieren von Unterabfragen gar nicht so schlecht. Hören Sie nicht zu, wenn man Ihnen rät, sie immer zu vermeiden! Testen Sie sie stattdessen selbst, und treffen Sie Ihre eigenen Entscheidungen. Manchmal ist eine korrelierte Unterabfrage absolut vernünftig oder sogar optimal, um zum Ergebnis zu kommen. Schauen wir uns ein Beispiel an: mysql> EXPLAIN SELECT film_id, language_id FROM sakila.film -> WHERE NOT EXISTS( -> SELECT * FROM sakila.film_actor -> WHERE film_actor.film_id = film.film_id -> )\G *************************** 1. row *************************** id: 1 select_type: PRIMARY table: film type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 951 Extra: Using where *************************** 2. row *************************** id: 2 select_type: DEPENDENT SUBQUERY table: film_actor type: ref possible_keys: idx_fk_film_id key: idx_fk_film_id key_len: 2 ref: film.film_id rows: 2 Extra: Using where; Using index
194 | Kapitel 4: Optimierung der Abfrageleistung
Der normale Ratschlag für diese Abfrage lautet, sie als LEFT OUTER JOIN zu schreiben, anstatt eine Unterabfrage zu verwenden. Theoretisch sieht der MySQL-Ausführungsplan beide Male gleich aus. Schauen Sie: mysql> EXPLAIN SELECT film.film_id, film.language_id -> FROM sakila.film -> LEFT OUTER JOIN sakila.film_actor USING(film_id) -> WHERE film_actor.film_id IS NULL\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 951 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ref possible_keys: idx_fk_film_id key: idx_fk_film_id key_len: 2 ref: sakila.film.film_id rows: 2 Extra: Using where; Using index; Not exists
Die Pläne sind fast identisch, es gibt aber einige Unterschiede: • Der SELECT-Typ an film_actor ist DEPENDENT SUBQUERY in einer Abfrage und SIMPLE in der anderen. Dieser Unterschied spiegelt einfach die Syntax wider, weil die erste Abfrage eine Unterabfrage verwendet und die zweite nicht. In Bezug auf die Handler-Operationen gibt es keinen großen Unterschied. • Bei der zweiten Abfrage steht nicht »Using where« in der Extra-Spalte für die filmTabelle. Das macht allerdings nichts: Die USING-Klausel der zweiten Abfrage ist sowieso das Gleiche wie eine WHERE-Klausel. • Die zweite Abfrage sagt »Not exists« in der Extra-Spalte der film_actor-Tabelle. Dies ist ein Beispiel für den frühzeitig terminierten Algorithmus, den wir bereits weiter vorn in diesem Kapitel erwähnt haben. Es bedeutet, dass MySQL eine NotExists-Optimierung verwendet, um zu vermeiden, dass es mehr als eine Zeile im idx_fk_film_id-Index der film_actor-Tabelle lesen muss. Das ist äquivalent zu einer korrelierten NOT EXISTS( )-Unterabfrage, weil es die Verarbeitung der aktuellen Zeile stoppt, sobald ein Treffer gefunden wird.
Grenzen des MySQL-Abfrageoptimierers | 195
Theoretisch führt MySQL also die Abfragen fast gleich aus. In Wirklichkeit können Sie nur mithilfe eines Benchmark-Tests feststellen, welcher Ansatz schneller ist. Wir haben beide Abfragen in unserer Standardeinstellung getestet. Die Ergebnisse sehen Sie in Tabelle 4-1. Tabelle 4-1: NOT EXISTS im Vergleich mit LEFT OUTER JOIN Abfrage
Ergebnisse in Abfragen pro Sekunde (Queries per Second; QPS)
NOT EXISTS-Unterabfrage
360 QPS
LEFT OUTER JOIN
425 QPS
Unser Benchmark hat ergeben, dass die Unterabfrage etwas langsamer ist! Das ist jedoch nicht immer der Fall. Manchmal kann eine Unterabfrage auch schneller sein. Sie kann z.B. gut funktionieren, wenn Sie einfach nur Zeilen aus einer Tabelle sehen wollen, die Zeilen aus einer anderen Tabelle entsprechen. Das klingt zwar so, als würde es perfekt einen Join beschreiben, ist aber nicht immer das Gleiche. Der folgende Join, der dazu gedacht ist, jeden Film zu finden, der einen Schauspieler besitzt, liefert Duplikate zurück, weil manche Filme mehrere Schauspieler haben: mysql> SELECT film.film_id FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id);
Wir müssen DISTINCT oder GROUP BY verwenden, um die Duplikate zu eliminieren: mysql> SELECT DISTINCT film.film_id FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id);
Aber was wollen wir wirklich mit dieser Abfrage ausdrücken, und wird dies aus dem SQL deutlich? Der EXISTS-Operator drückt das logische Konzept von »hat einen Treffer« aus, ohne dass duplizierte Zeilen erzeugt werden, und vermeidet eine GROUP BY- oder DISTINCTOperation, die eine temporäre Tabelle erfordern könnte. Dies ist die Abfrage, geschrieben als Unterabfrage und nicht als Join: mysql> SELECT film_id FROM sakila.film -> WHERE EXISTS(SELECT * FROM sakila.film_actor -> WHERE film.film_id = film_actor.film_id);
Wir haben wieder mit einem Benchmark festgestellt, welche Strategie schneller ist. Die Ergebnisse finden Sie in Tabelle 4-2. Tabelle 4-2: EXISTS im Vergleich mit INNER JOIN Abfrage
Ergebnis in Abfragen pro Sekunde (Queries per Second; QPS)
INNER JOIN
185 QPS
EXISTS-Unterabfrage
325 QPS
In diesem Beispiel wird die Unterabfrage viel schneller ausgeführt als der Join.
196 | Kapitel 4: Optimierung der Abfrageleistung
Wir wollten mit diesem ausführlichen Beispiel zwei Punkte verdeutlichen: Sie sollten grundsätzlichen Ratschlägen in Bezug auf Unterabfragen nicht kritiklos folgen, und Sie sollten Benchmarks einsetzen, um Ihre Annahmen über Abfragepläne und Ausführungsgeschwindigkeiten zu bestätigen.
UNION-Beschränkungen Manchmal kann MySQL Bedingungen von außerhalb einer UNION nicht nach innen »durchreichen«, wo sie Ergebnisse begrenzen oder zusätzliche Optimierungen erlauben könnten. Falls Sie glauben, dass eine der jeweiligen Abfragen innerhalb einer UNION von einem LIMIT profitieren könnte, oder wenn Sie wissen, dass sie von einer ORDER BY-Klausel beansprucht wird, sobald man sie mit anderen Abfragen kombiniert, dann müssen Sie diese Klauseln in jeden Teil der UNION setzen. Wenn Sie z.B. zwei riesige Tabellen mit UNION vereinigen und das Ergebnis mit LIMIT auf die ersten 20 Zeilen beschränken, speichert MySQL die beiden riesigen Tabellen in einer temporären Tabelle und holt dann nur die 20 Zeilen aus ihr. Sie können das vermeiden, indem Sie in jede Abfrage innerhalb der UNION ein LIMIT 20 setzen.
Index-Merge-Optimierungen Index-Merge-Algorithmen, die in MySQL 5.0 eingeführt wurden, erlauben es MySQL, in einer Abfrage mehr als einen Index pro Tabelle zu verwenden. Frühere Versionen von MySQL konnten nur einen einzigen Index benutzen; wenn also ein einzelner Index nicht ausreichte, um mit all den Beschränkungen in der WHERE-Klausel zurechtzukommen, wählte MySQL oft einen Tabellenscan. So hat z.B. die film_actor-Tabelle einen Index in film_id und einen Index in actor_id, aber keiner von beiden ist eine gute Wahl für die beiden WHERE-Bedingungen in dieser Abfrage: mysql> SELECT film_id, actor_id FROM sakila.film_actor -> WHERE actor_id = 1 OR film_id = 1;
In älteren MySQL-Versionen würde diese Abfrage einen Tabellenscan verursachen, es sei denn, Sie hätten sie als UNION der beiden Abfragen geschrieben: mysql> SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1 -> UNION ALL -> SELECT film_id, actor_id FROM sakila.film_actor WHERE film_id = 1 -> AND actor_id <> 1;
Seit MySQL 5.0 jedoch kann diese Abfrage beide Indizes benutzen, sie gleichzeitig scannen und die Ergebnisse zusammenfassen. Es gibt drei Variationen des Algorithmus: Vereinigung für OR-Bedingungen, Durchschnitte für AND-Bedingungen und Vereinigungen aus Durchschnitten für Kombinationen aus den beiden. Die folgende Abfrage nutzt eine Vereinigung aus zwei Indexscans, wie Sie erkennen, wenn Sie sich die Extra-Spalte anschauen:
Grenzen des MySQL-Abfrageoptimierers | 197
mysql> EXPLAIN SELECT film_id, actor_id FROM sakila.film_actor -> WHERE actor_id = 1 OR film_id = 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film_actor type: index_merge possible_keys: PRIMARY,idx_fk_film_id key: PRIMARY,idx_fk_film_id key_len: 2,2 ref: NULL rows: 29 Extra: Using union(PRIMARY,idx_fk_film_id); Using where
MySQL kann diese Technik bei komplexen WHERE-Klauseln einsetzen, so dass Sie bei manchen Abfragen geschachtelte Operationen in der Extra-Spalte finden. Das funktioniert oft sehr gut, manchmal allerdings belegen die Puffer-, Sortier- und Merge-Operationen des Algorithmus viele CPU- und Speicherressourcen. Das trifft vor allem dann zu, wenn nicht alle Indizes sehr selektiv sind, so dass die parallelen Scans viele Zeilen an die Merge-Operation zurückliefern. Erinnern Sie sich daran, dass der Optimierer diese Kosten nicht berücksichtigt – er optimiert nur die Anzahl der zufälligen Seitenlesevorgänge. Das kann dazu führen, dass er die Abfrage »unterbewertet«, die tatsächlich viel langsamer läuft als ein einfacher Tabellenscan. Die intensive Speicher- und CPU-Benutzung beeinflusst darüber hinaus oft auch nebenläufige Abfragen, was Ihnen aber nicht auffallen wird, wenn Sie die Abfrage isoliert ausführen. Das ist ein weiterer Grund für realistische Benchmarks. Falls Ihre Abfragen aufgrund dieser Optimiererbeschränkung langsamer laufen, könnten Sie entweder einige Indizes mit IGNORE INDEX deaktivieren oder einfach auf die alte UNIONTaktik zurückgreifen.
Verbreitung von Gleichheit Die Verbreitung von Gleichheit kann manchmal unerwartete Kosten mit sich bringen. Nehmen Sie z.B. eine riesige IN( )-Liste in einer Spalte, die der Optimierer aufgrund einer WHERE-, ON- oder USING-Klausel, die die Spalten einander gleichsetzt, als gleich mit anderen Spalten in anderen Tabellen betrachtet. Der Optimierer »verteilt« die Liste, indem er sie in die entsprechenden Spalten in allen verwandten Tabellen kopiert. Normalerweise ist das hilfreich, weil es dem Abfrageoptimierer und der Ausführungs-Engine mehr Möglichkeiten dafür bietet, wo diese tatsächlich den IN( )-Test ausführen sollen. Wenn aber eine Liste sehr groß ist, kann dies zu langsamerer Optimierung und Ausführung führen. Momentan gibt es für dieses Problem keine bereits integrierte Lösung – Sie müssen den Quellcode ändern, wenn es für Sie ein Problem darstellt. (Für die meisten Leute ist es kein Problem.)
198 | Kapitel 4: Optimierung der Abfrageleistung
Parallele Ausführung MySQL kann auf vielen CPUs eine einzelne Abfrage nicht parallel ausführen. Das ist eine Eigenschaft, die von einigen anderen Datenbankservern angeboten wird, nicht jedoch von MySQL. Wir erwähnen das hier, damit Sie keine Zeit damit verschwenden, eine parallele Abfrageausführung unter MySQL zum Laufen zu bringen!
Hash-Joins MySQL kann momentan keine echten Hash-Joins ausführen – alles ist ein Join mit geschachtelter Schleife. Sie können allerdings Hash-Joins mit Hash-Indizes emulieren. Wenn Sie nicht gerade die Memory-Storage-Engine einsetzen, müssen Sie auch die HashIndizes emulieren. Wir haben Ihnen dies in »Eigene Hash-Indizes aufbauen« auf Seite 111 gezeigt.
Lockere Indexscans MySQL war noch nie in der Lage, lockere Indexscans durchzuführen, bei denen unzusammenhängende Bereiche eines Index gescannt werden. Die MySQL-Indexscans erfordern im Allgemeinen einen definierten Anfangspunkt und einen definierten Endpunkt im Index, selbst wenn nur einige unzusammenhängende Zeilen in der Mitte wirklich für die Abfrage von Interesse sind. MySQL scannt den gesamten Zeilenbereich zwischen diesen Endpunkten. Ein Beispiel soll das verdeutlichen. Nehmen Sie an, wir haben eine Tabelle mit einem Index in den Spalten (a, b) und wollen folgende Abfrage durchführen: mysql> SELECT ... FROM tbl WHERE b BETWEEN 2 AND 3;
Da der Index mit Spalte a beginnt, die WHERE-Klausel der Abfrage Spalte a aber nicht angibt, führt MySQL einen Tabellenscan durch und eliminiert die nicht passenden Zeilen mit einer WHERE-Klausel, wie in Abbildung 4-5 zu sehen ist. Es ist leicht zu erkennen, dass es eine schnellere Methode gibt, um diese Abfrage auszuführen. Die Struktur des Index (aber nicht die API der MySQL-Storage-Engine) erlaubt es Ihnen, zum Beginn des jeweiligen Wertebereichs zu gehen, bis zum Ende des Bereichs zu scannen und dann zurückzugehen und zum Anfang des nächsten Bereichs zu springen. Abbildung 4-6 zeigt, wie diese Strategie aussehen würde, wenn MySQL in der Lage wäre, sie zu benutzen. Beachten Sie das Fehlen einer WHERE-Klausel, die nicht erforderlich ist, weil der Index allein es uns erlaubt, die unerwünschten Zeilen auszulassen. (Noch einmal, MySQL kann das noch nicht.) Das ist zugegebenermaßen ein vereinfachtes Beispiel, und wir könnten die gezeigte Abfrage leicht optimieren, indem wir einen anderen Index hinzufügten. Allerdings kommt es oft vor, dass ein anderer Index das Problem nicht löst. Ein Beispiel ist eine Abfrage, die eine Bereichsbedingung in der ersten Spalte des Index aufweist und eine Gleichheitsbedingung in der zweiten Spalte.
Grenzen des MySQL-Abfrageoptimierers | 199
Abbildung 4-5: MySQL scannt die gesamte Tabelle, um Zeilen zu suchen.
Abbildung 4-6: Ein lockerer Indexscan, zu dem MySQL momentan nicht in der Lage ist, wäre viel effizienter.
Seit MySQL 5.0 sind lockere Indexscans unter bestimmten, stark eingeschränkten Umständen möglich, etwa bei Abfragen, die maximale und minimale Werte in einer gruppierten Abfrage suchen:
200 | Kapitel 4: Optimierung der Abfrageleistung
mysql> EXPLAIN SELECT actor_id, MAX(film_id) -> FROM sakila.film_actor -> GROUP BY actor_id\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film_actor type: range possible_keys: NULL key: PRIMARY key_len: 2 ref: NULL rows: 396 Extra: Using index for group-by
Die »Using index for group-by«-Information in diesem EXPLAIN-Plan kennzeichnet einen lockeren Indexscan. Für diesen speziellen Zweck ist das eine gute Optimierung, allerdings lässt sich dieser lockere Indexscan nicht verallgemeinern. Vermutlich wäre es besser, dies als »lockere Indexuntersuchung« zu bezeichnen. Bis MySQL allgemeingültige lockere Indexscans unterstützt, muss man eine Konstante oder eine Liste aus Konstanten für die führenden Spalten des Index liefern. Wir haben in unserer Indizierungsfallstudie im vorangegangenen Kapitel mehrere Beispiele dafür gezeigt, wie man eine gute Leistung mit solchen Abfragen erreicht.
MIN( ) und MAX( ) MySQL optimiert bestimmte MIN( )- und MAX( )-Abfragen nicht sehr gut. Hier ist ein Beispiel: mysql> SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';
Da es keinen Index in first_name gibt, führt diese Abfrage einen Tabellenscan durch. Wenn MySQL den Primärschlüssel scannt, kann es theoretisch nach dem Lesen der ersten passenden Zeile stoppen, weil der Primärschlüssel streng ansteigend ist und jede nachfolgende Zeile eine größere actor_id besitzt. In diesem Fall jedoch scannt MySQL die gesamte Tabelle. Sie können das durch ein Profiling der Abfrage überprüfen. Eine Möglichkeit, das zu umgehen, besteht darin, das MIN( ) zu entfernen und die Abfrage mit einem LIMIT umzuschreiben: mysql> SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) -> WHERE first_name = 'PENELOPE' LIMIT 1;
Diese allgemeine Strategie funktioniert oft ganz gut, wenn MySQL ansonsten beschließen würde, mehr Zeilen als notwendig zu scannen. Wenn Sie ein Purist sind, könnten Sie einwenden, dass diese Abfrage den Zweck von SQL verfehlt. Wir sollten dazu in der Lage sein, dem Server mitzuteilen, was wir wollen, und er soll herausfinden können, wie er an diese Daten herankommt, während wir in diesem Fall MySQL sagen, wie es die Abfrage ausführen soll, und es daher aus der Abfrage nicht deutlich wird, dass das, was wir suchen, ein Minimalwert ist. Manchmal müssen Sie eben mit Blick auf Ihre Prinzipien Kompromisse eingehen, um eine hohe Leistung zu erzielen. Grenzen des MySQL-Abfrageoptimierers | 201
SELECT und UPDATE in derselben Tabelle MySQL erlaubt es Ihnen nicht, ein SELECT in einer Tabelle auszuführen, während gleichzeitig ein UPDATE darin läuft. Eigentlich ist das keine Beschränkung des Optimierers, allerdings hilft es Ihnen, damit zurechtzukommen, wenn Sie wissen, wie MySQL Abfragen ausführt. Hier ist ein Beispiel für eine Abfrage, die nicht zulässig ist, obwohl es sich dabei um normales SQL handelt. Die Abfrage aktualisiert jede Zeile mit der Anzahl ähnlicher Zeilen in der Tabelle: mysql> UPDATE tbl AS outer_tbl -> SET cnt = ( -> SELECT count(*) FROM tbl AS inner_tbl -> WHERE inner_tbl.type = outer_tbl.type -> ); ERROR 1093 (HY000): You can't specify target table 'outer_tbl' for update in FROM clause
Um diese Beschränkung zu umgehen, können Sie eine abgeleitete Tabelle einsetzen, da MySQL diese als temporäre Tabelle ausführt. Damit werden im Prinzip zwei Abfragen ausgeführt: ein SELECT innerhalb der Unterabfrage und ein Mehrtabellen-UPDATE mit den zusammengeführten Ergebnissen der Tabelle und der Unterabfrage. Die Unterabfrage öffnet und schließt die Tabelle, bevor das äußere UPDATE die Tabelle öffnet, so dass die Abfrage erfolgreich verläuft: mysql> UPDATE tbl -> INNER JOIN( -> SELECT type, count(*) AS cnt -> FROM tbl -> GROUP BY type -> ) AS der USING(type) -> SET tbl.cnt = der.cnt;
Bestimmte Arten von Abfragen optimieren In diesem Abschnitt zeigen wir Ihnen, wie Sie bestimmte Arten von Abfragen optimieren. Wir haben die meisten dieser Themen an anderen Stellen in diesem Buch ausführlicher behandelt, wollten aber für einen leichteren Zugang eine Liste von häufiger auftretenden Optimierungsproblemen aufstellen. Die meisten der Hinweise in diesem Abschnitt sind versionsabhängig und gelten möglicherweise für künftige Versionen von MySQL nicht. Es gibt keinen Grund, weshalb der Server nicht irgendwann in der Lage sein sollte, einige oder alle Optimierungen selbst zu erledigen.
COUNT( )-Abfragen optimieren Die Aggregatfunktion COUNT( ) und ihre Verwendung zum Optimieren von Abfragen, die sie verwenden, gehört wahrscheinlich zu den 10 am meisten missverstandenen Themen in MySQL. Sie können eine Websuche durchführen und werden mehr Fehlinformationen zu diesem Thema finden, als wir uns vorzustellen wagen. Bevor wir zur Optimierung kommen, sollten Sie verstehen lernen, was COUNT( ) wirklich tut. 202 | Kapitel 4: Optimierung der Abfrageleistung
Was COUNT( ) macht COUNT( ) ist eine Spezialfunktion, die auf zwei unterschiedliche Arten funktioniert: Sie zählt Werte und Zeilen. Ein Wert ist ein Nicht-NULL-Ausdruck (NULL bezeichnet die Abwe-
senheit eines Wertes). Wenn Sie einen Spaltennamen oder einen anderen Ausdruck in den Klammern angeben, zählt COUNT( ), wie oft dieser Ausdruck einen Wert besitzt. Das ist für viele Leute verwirrend, zum Teil, weil Werte und NULL verwirrend sind. Falls Sie lernen müssen, wie das in SQL funktioniert, raten wir Ihnen zu einem guten Buch über die SQL-Grundlagen. (Das Internet ist in diesem Fall nicht unbedingt eine zuverlässige Informationsquelle.) Die andere Form von COUNT( ) zählt einfach die Anzahl der Zeilen im Ergebnis. Das ist genau, was MySQL tut, wenn es weiß, dass der Ausdruck in den Klammern niemals NULL sein kann. Das bekannteste Beispiel ist COUNT(*), eine Sonderform von COUNT( ), die das *-Wildcard nicht zur vollständigen Liste der Spalten in der Tabelle erweitert, wie Sie vielleicht vermuten würden, sondern die Spalten völlig ignoriert und die Zeilen zählt. Einer der Fehler, die uns am häufigsten auffallen, besteht darin, dass Spaltennamen in den Klammern angegeben werden, wenn man Zeilen zählen möchte. Wenn Sie die Anzahl der Zeilen im Ergebnis wissen möchten, sollten Sie immer COUNT(*) benutzen. Dies macht Ihre Wünsche deutlich und verhindert Leistungseinbrüche.
Mythen über MyISAM Ein weiteres Missverständnis besagt, dass MyISAM außergewöhnlich schnell bei COUNT( )-Abfragen ist. Es ist schnell, aber nur in einem Sonderfall: COUNT(*) ohne WHEREKlausel, das nur die Anzahl der Zeilen in der gesamten Tabelle ermittelt. MySQL kann das wegoptimieren, da die Storage-Engine immer weiß, wie viele Zeilen in der Tabelle stehen. Wenn MySQL weiß, dass col nie NULL sein kann, kann es auch einen COUNT(col)Ausdruck optimieren, indem es ihn intern in COUNT(*) umwandelt. MyISAM besitzt keine magischen Geschwindigkeitsoptimierungen zum Zählen von Zeilen, wenn die Abfrage eine WHERE-Klausel enthält, oder für den allgemeineren Fall, dass Werte anstelle von Zeilen gezählt werden. Es kann bei einer bestimmten Abfrage schneller sein als andere Storage-Engines, ist es aber vielleicht auch nicht. Das hängt von vielen Faktoren ab.
Einfache Optimierungen Manchmal können Sie die COUNT(*)-Optimierung von MyISAM zu Ihrem Vorteil einsetzen, wenn Sie alle bis auf eine kleine Anzahl von Zeilen zählen wollen, die gut indiziert sind. Das folgende Beispiel nutzt die normale World-Datenbank, um Ihnen zu zeigen, wie Sie effizient die Anzahl der Städte ermitteln, deren ID größer als 5 ist. Sie könnten diese Abfrage folgendermaßen schreiben: mysql> SELECT COUNT(*) FROM world.City WHERE ID > 5;
Bestimmte Arten von Abfragen optimieren | 203
Wenn Sie diese Abfrage mit SHOW STATUS profilieren, dann sehen Sie, dass sie 4.079 Zeilen scannt. Falls Sie die Bedingungen negieren und die Anzahl der Städte, deren IDs kleiner oder gleich 5 sind, von der Gesamtanzahl der Städte abziehen, können Sie das auf fünf Zeilen reduzieren: mysql> SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*) -> FROM world.City WHERE ID <= 5;
Diese Version liest weniger Zeilen, weil die Unterabfrage während der Optimierungsphase in eine Konstante umgewandelt wurde, wie Sie mit EXPLAIN erkennen können: +----+-------------+-------+...+------+------------------------------+ | id | select_type | table |...| rows | Extra | +----+-------------+-------+...+------+------------------------------+ | 1 | PRIMARY | City |...| 6 | Using where; Using index | | 2 | SUBQUERY | NULL |...| NULL | Select tables optimized away | +----+-------------+-------+...+------+------------------------------+
Eine häufig gestellte Frage auf Mailinglisten und in IRC-Kanälen lautet, wie man mit nur einer Abfrage die Zahlen für mehrere verschiedene Werte in derselben Spalte ermittelt, um die Anzahl der erforderlichen Abfragen zu verringern. Nehmen Sie z.B. an, dass Sie eine einzige Abfrage erzeugen wollen, die zählen soll, wie viele Objekte je eine von mehreren Farben haben. Sie können kein OR verwenden (wie in SELECT COUNT(color = 'blue' OR color = 'red') FROM items;), weil dies die verschiedenen Zähler für die verschiedenen Farben nicht trennt. Und Sie können die Farben nicht in die WHERE-Klausel setzen (z.B. SELECT COUNT(*) FROM items WHERE color = 'blue' AND color = 'red';), weil die Farben einander ausschließen. Hier ist eine Abfrage, die dieses Problem löst: mysql> SELECT SUM(IF(color = 'blue', 1, 0)) AS blue, SUM(IF(color = 'red', 1, 0)) -> AS red FROM items;
Dies ist eine weitere Abfrage, die äquivalent zu der gezeigten ist. Anstelle von SUM( ) wird allerdings COUNT( ) verwendet, und außerdem wird sichergestellt, dass die Ausdrücke keine Werte haben, wenn die Kriterien falsch sind: mysql> SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULL) -> AS red FROM items;
Komplexere Optimierungen Im Allgemeinen sind COUNT( )-Abfragen schwer zu optimieren, weil sie normalerweise viele Zeilen zählen (d.h. auf viele Daten zugreifen) müssen. Ihre einzige andere Möglichkeit zum Optimieren in MySQL selbst besteht darin, einen abdeckenden Index einzusetzen. Abdeckende Indizes haben wir in Kapitel 3 vorgestellt. Falls das nicht ausreicht, müssen Sie Ihre Anwendungsarchitektur ändern. Ziehen Sie Summary-Tabellen (diese wurden ebenfalls in Kapitel 3 behandelt) und vielleicht ein externes Cache-System wie memcached in Betracht. Wahrscheinlich werden Sie mit dem vertrauten Dilemma konfrontiert: »Schnell, akkurat und einfach: Wählen Sie zwei!«
204 | Kapitel 4: Optimierung der Abfrageleistung
JOIN-Abfragen optimieren Dieses Thema finden Sie an vielen Stellen in diesem Buch, wir wollen aber zumindest einige Kernpunkte erwähnen: • Stellen Sie sicher, dass es Indizes in den Spalten in den ON- oder USING-Klauseln gibt. Mehr über Indizierung finden Sie in »Grundlagen der Indizierung« auf Seite 103. Beachten Sie die Join-Reihenfolge, wenn Sie Indizes hinzufügen. Wenn Sie die Tabellen A und B in Spalte c zusammenführen und der Abfrageoptimierer beschließt, den Join der Tabellen in der Reihenfolge B, A durchzuführen, müssen Sie die Spalten in Tabelle B nicht indizieren. Nicht genutzte Indizes bedeuten zusätzlichen Overhead. Im Allgemeinen müssen Sie nur der zweiten Tabelle in der Join-Reihenfolge Indizes hinzufügen, es sei denn, die Indizes werden aus anderen Gründen benötigt. • Versuchen Sie sicherzustellen, dass sich alle GROUP BY- oder ORDER BY-Ausdrücke nur auf Spalten aus einer einzigen Tabelle beziehen, damit MySQL versuchen kann, einen Index für diese Operation zu benutzen. • Seien Sie vorsichtig, wenn Sie MySQL aufrüsten, da sich die Join-Syntax, die Rangordnung der Operatoren und andere Verhaltensweisen öfter einmal geändert haben. Was einmal ein normaler Join war, wird manchmal zu einem Kreuzprodukt (einer anderen Art von Join, die andere Ergebnisse zurückliefert) oder sogar zu ungültiger Syntax.
Unterabfragen optimieren Der wichtigste Rat, den wir Ihnen zu Unterabfragen geben können, ist der, dass Sie nach Möglichkeit lieber einen Join einsetzen sollten, zumindest in aktuellen Versionen von MySQL. Wir haben dieses Thema weiter vorn in diesem Kapitel ausführlich behandelt. Unterabfragen werden vom Optimiererteam intensiv bearbeitet, so dass kommende MySQL-Versionen möglicherweise mehr Unterabfragenoptimierungen enthalten werden. Es bleibt abzwarten, welche der Optimierungen, die wir gesehen haben, es bis in den endgültigen Code schaffen und welche Auswirkungen sie haben werden. Wir wollen Ihnen nur klarmachen, dass »nehmen Sie lieber einen Join« kein ewig gültiger Ratschlag ist. Der Server wird mit der Zeit immer besser werden und Sie werden ihm immer seltener sagen müssen, was er tun soll, anstatt ihm mitzuteilen, welche Ergebnisse Sie sehen wollen.
GROUP BY und DISTINCT optimieren MySQL optimiert diese beiden Arten von Abfragen in vielen Fällen ähnlich. Um genau zu sein, konvertiert es sie während der Optimierung sogar bei Bedarf intern ineinander. Beide Arten von Abfragen profitieren üblicherweise von Indizes, was auch die wichtigste Methode ist, um sie zu optimieren.
Bestimmte Arten von Abfragen optimieren | 205
MySQL kennt zwei Arten von GROUP BY-Strategien, wenn es keinen Index benutzen kann: Es kann eine temporäre Tabelle oder einen Filesort verwenden, um die Gruppierung durchzuführen. Jeweils beide können sich bei bestimmten Abfragen als effizienter erweisen. Sie können den Optimierer zwingen, mithilfe der Optimiererhinweise SQL_BIG_ RESULT und SQL_SMALL_RESULT die eine oder andere Methode zu wählen. Falls Sie einen Join nach einem Wert gruppieren müssen, der aus einer Lookup-Tabelle stammt, dann ist es meist effizienter, die Gruppierung anhand des Identifikators der Lookup-Tabelle vorzunehmen als anhand des Wertes. So ist etwa die folgende Abfrage nicht so effizient, wie sie sein könnte: mysql> SELECT actor.first_name, actor.last_name, COUNT(*) -> FROM sakila.film_actor -> INNER JOIN sakila.actor USING(actor_id) -> GROUP BY actor.first_name, actor.last_name;
Die Abfrage ist effizienter, wenn sie so geschrieben wird: mysql> SELECT actor.first_name, actor.last_name, COUNT(*) -> FROM sakila.film_actor -> INNER JOIN sakila.actor USING(actor_id) -> GROUP BY film_actor.actor_id;
Das Gruppieren nach actor.actor_id könnte effizienter sein als das Gruppieren nach film_actor.actor_id. Sie sollten dies mit Ihren speziellen Daten profilieren oder testen. Diese Abfrage nutzt die Tatsache aus, dass Vor- und Nachname des Schauspielers von der actor_id abhängen, liefert also die gleichen Ergebnisse. Es ist aber nicht immer der Fall, dass Sie munter nichtgruppierte Spalten auswählen können und das gleiche Ergebnis erhalten. Möglicherweise ist sogar der SQL_MODE des Servers so konfiguriert, dass dies verboten ist. Sie können notfalls MIN( ) oder MAX( ) einsetzen, wenn Sie wissen, dass die Werte innerhalb der Gruppe verschieden sind, weil sie von der grouped-by-Spalte abhängen oder weil es Ihnen egal ist, welchen Wert Sie erhalten: mysql> SELECT MIN(actor.first_name), MAX(actor.last_name), ...;
Puristen werden einwenden, dass Sie nach der falschen Sache gruppieren, und sie haben Recht. Ein unberechtigtes MIN( ) oder MAX( ) ist das Zeichen dafür, dass die Abfrage nicht richtig strukturiert ist. Manchmal allerdings ist es einfach Ihr einziges Bestreben, dass MySQL die Abfrage so schnell wie möglich ausführt. Die Puristen werden mit der folgenden Art, die Abfrage zu schreiben, zufrieden sein: mysql> SELECT actor.first_name, actor.last_name, c.cnt -> FROM sakila.actor -> INNER JOIN ( -> SELECT actor_id, COUNT(*) AS cnt -> FROM sakila.film_actor -> GROUP BY actor_id -> ) AS c USING(actor_id) ;
Manchmal sind allerdings die Kosten zum Erzeugen und Füllen der temporären Tabelle, die für die Unterabfrage benötigt wird, hoch im Vergleich mit den Kosten für das Verfäl-
206 | Kapitel 4: Optimierung der Abfrageleistung
schen der reinen relationalen Theorie. Denken Sie daran, dass die temporäre Tabelle, die von der Unterabfrage erzeugt wird, keine Indizes hat. Es ist im Allgemeinen keine gute Idee, nichtgruppierte Spalten in einer gruppierten Abfrage auszuwählen, weil die Ergebnisse nichtdeterministisch sind und sich leicht ändern können, wenn Sie einen Index ändern oder der Optimierer beschließt, eine andere Strategie einzusetzen. Die meisten dieser Abfragen, die uns begegnen, sind Unfälle (weil der Server sich nicht beschwert) oder das Ergebnis von Faulheit und wurden nicht zum Zwecke der Optimierung so geschaffen. Es ist besser, explizit zu sein. Wir empfehlen Ihnen sogar, die Konfigurationsvariable SQL_MODE des Servers so einzustellen, dass sie ONLY_FULL_GROUP_BY einschließt, damit sie einen Fehler ausgibt, anstatt Sie eine schlechte Abfrage schreiben zu lassen. MySQL ordnet gruppierte Abfragen automatisch nach den Spalten in der GROUP BY-Klausel, wenn Sie keine ORDER BY-Klausel explizit angeben. Falls Sie sich nicht um die Reihenfolge kümmern und feststellen, dass dies einen Filesort verursacht, können Sie ORDER BY NULL verwenden, um das automatische Sortieren zu überspringen. Sie können direkt hinter die GROUP BY-Klausel auch ein optionales DESC- oder ASC-Schlüsselwort setzen, um die Ergebnisse in der gewünschten Richtung anzuordnen.
GROUP BY WITH ROLLUP optimieren Eine Variante der gruppierten Abfragen ist, wenn Sie MySQL anweisen, innerhalb der Ergebnisse eine Superaggregation durchzuführen. Dies erreichen Sie mit einer WITH ROLLUPKlausel, es ist aber möglicherweise nicht so gut optimiert, wie Sie es brauchen. Prüfen Sie die Ausführungsmethode mit EXPLAIN, wobei Sie besonders darauf achten, ob die Gruppierung über einen Filesort oder über eine temporäre Tabelle erfolgt; versuchen Sie, das WITH ROLLUP zu entfernen und festzustellen, ob Sie die gleiche Gruppierungsmethode erhalten. Vielleicht können Sie die Gruppierungsmethode auch mit den Hinweisen erzwingen, die wir weiter vorn in diesem Abschnitt erwähnt haben. Manchmal ist es effizienter, die Superaggregation in Ihrer Anwendung durchzuführen, selbst wenn das bedeutet, dass Sie viel mehr Zeilen vom Server holen müssen. Sie können eine Teilabfrage auch in die FROM-Klausel schachteln oder eine temporäre Tabelle für die Zwischenergebnisse verwenden. Der beste Ansatz ist es möglicherweise, die WITH ROLLUP-Funktionalität in Ihren Anwendungscode zu verschieben.
LIMIT und OFFSET optimieren Abfragen mit LIMITs und OFFSETs sind in Systemen gebräuchlich, die Paginierungen durchführen, und zwar fast immer in Verbindung mit einer ORDER BY-Klausel. Es hilft, einen Index zu haben, der die Sortierung unterstützt, da der Server ansonsten viele Filesorts durchführen muss.
Bestimmte Arten von Abfragen optimieren | 207
Ein häufig auftretendes Problem ist ein hoher Wert für den Offset. Wenn Ihre Abfrage wie LIMIT 10000, 20 aussieht, dann generiert sie 10.020 Zeilen und wirft die ersten 10.000 von ihnen weg, was sehr teuer ist. Wenn Sie einmal annehmen, dass auf alle Seiten gleich häufig zugegriffen wird, dann scannen solche Abfragen durchschnittlich die halbe Tabelle. Um sie zu optimieren, können Sie entweder die Anzahl der Seiten beschränken, die in einer Paginierungsansicht erlaubt sind, oder versuchen, die hohen Offsets effizienter zu machen. Eine einfache Technik zum Verbessern der Effizienz besteht darin, den Offset in einem abdeckenden Index einzustellen und nicht in den vollständigen Zeilen. Sie können das Ergebnis dann zur kompletten Zeile zusammenfügen und die zusätzlichen Zeilen bei Bedarf holen. Betrachten Sie die folgende Abfrage: mysql> SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
Wenn die Tabelle sehr groß ist, dann sollte die Abfrage besser so geschrieben werden: mysql> SELECT film.film_id, film.description -> FROM sakila.film -> INNER JOIN ( -> SELECT film_id FROM sakila.film -> ORDER BY title LIMIT 50, 5 -> ) AS lim USING(film_id);
Das funktioniert, weil dadurch der Server so wenige Daten wie möglich in einem Index untersucht, ohne auf Zeilen zuzugreifen. Sobald dann die gewünschten Zeilen gefunden wurden, führt er sie mit der vollständigen Tabelle zusammen, um die anderen Spalten aus der Zeile zu holen. Eine ähnliche Technik gilt für Joins mit LIMIT-Klauseln. Manchmal kann man das Limit auch in eine Positionsabfrage umwandeln, die der Server als Indexbereichsscan ausführen kann. Falls Sie z.B. eine Positionsspalte vorberechnen und indizieren, können Sie die Abfrage folgendermaßen umschreiben: mysql> SELECT film_id, description FROM sakila.film -> WHERE position BETWEEN 50 AND 54 ORDER BY position;
Klassifizierte Daten werfen ein ähnliches Problem auf, bringen allerdings normalerweise GROUP BY auf den Plan. Sie müssen Ränge fast immer vorberechnen und speichern. Falls Sie wirklich Paginierungssysteme optimieren müssen, sollten Sie wahrscheinlich auf vorberechnete Zusammenfassungen zurückgreifen. Alternativ können Sie Joins mit redundanten Tabellen vornehmen, die nur den Primärschlüssel und die Spalten enthalten, die Sie für das ORDER BY benötigen. Sie können auch Sphinx verwenden; Näheres dazu finden Sie in Anhang C.
SQL_CALC_FOUND_ROWS optimieren Eine weitere verbreitete Technik für die seitenweise Anzeige besteht darin, den Hinweis SQL_CALC_FOUND_ROWS zu einer Abfrage mit einem LIMIT hinzuzufügen, damit Sie wissen, wie viele Zeilen ohne das LIMIT zurückgeliefert worden wären. Es scheint so auszusehen,
208 | Kapitel 4: Optimierung der Abfrageleistung
als wäre irgendeine Art von »Magie« in Gang, mit der der Server vorhersagt, wie viele Zeilen er gefunden haben würde. Leider aber tut der Server nichts dergleichen; er kann Zeilen, die er eigentlich nicht findet, nicht zählen. Diese Option weist den Server lediglich an, den Rest der Ergebnismenge zu generieren und wegzuwerfen, anstatt anzuhalten, wenn er die gewünschte Anzahl an Zeilen erreicht. Das ist sehr teuer. Ein besserer Entwurf sieht so aus, dass der Pager in einen »Nächste«-Link umgewandelt wird. Angenommen, es gibt 20 Ergebnisse pro Seite. Die Abfrage sollte in diesem Fall ein LIMIT von 21 Zeilen benutzen und nur 20 anzeigen. Wenn die 21. Zeile existiert, dann gibt es eine nächste Seite, und Sie können den »Nächste«-Link rendern. Eine andere Möglichkeit besteht darin, mehr Zeilen, als Sie brauchen – sagen wir, 1.000 – zu holen und im Cache zu speichern, und sie dann für nachfolgende Seiten aus dem Cache zu beziehen. Diese Strategie lässt Ihre Anwendung wissen, wie groß die vollständige Ergebnismenge ist. Wenn es weniger als 1.000 Zeilen sind, dann weiß die Anwendung, wie viele Seitenlinks erstellt werden müssen. Sind es mehr als 1.000 Zeilen, dann kann sie einfach »Es wurden mehr als 1.000 Ergebnisse gefunden« anzeigen. Beide Strategien sind effizienter, als wenn wiederholt das gesamte Ergebnis generiert und der größte Teil davon verworfen wird. Selbst wenn Sie diese Taktiken nicht einsetzen, kann eine separate COUNT(*)-Abfrage zum Ermitteln der Zeilenzahl schneller sein als SQL_CALC_FOUND_ROWS, wenn sie einen abdeckenden Index benutzen kann.
UNION optimieren MySQL führt UNION-Abfragen immer aus, indem es eine temporäre Tabelle anlegt und diese mit den UNION-Ergebnissen füllt. MySQL kann auf UNION-Abfragen nicht so viele Optimierungen anwenden, wie Sie vielleicht gewöhnt sind. Möglicherweise müssen Sie dem Optimierer helfen, indem Sie ihm manuell WHERE-, LIMIT-, ORDER BY- und andere Bedingungen übergeben (d.h., sie bei Bedarf aus der äußeren Abfrage in jedes SELECT in dem UNION kopieren). Es ist wichtig, immer UNION ALL zu verwenden, es sei denn, der Server soll duplizierte Zeilen eliminieren. Wenn Sie das Schlüsselwort ALL weglassen, fügt MySQL der temporären Tabelle die Option distinct hinzu, die die vollständige Zeile verwendet, um Eindeutigkeit festzustellen. Das ist ziemlich teuer. Seien Sie sich allerdings bewusst, dass das Schlüsselwort ALL nicht die temporäre Tabelle eliminiert. MySQL setzt Ergebnisse immer in eine temporäre Tabelle und liest sie dann wieder aus, selbst wenn das nicht unbedingt notwendig ist (wenn etwa die Ergebnisse direkt an den Client zurückgeliefert werden könnten).
Bestimmte Arten von Abfragen optimieren | 209
Hinweise für den Abfrageoptimierer MySQL besitzt einige Optimiererhinweise, mit deren Hilfe Sie den Abfrageplan steuern können, wenn Sie mit demjenigen nicht zufrieden sind, den der MySQL-Optimierer auswählt. Die folgende Liste zeigt diese Hinweise und gibt an, wann man sie günstigerweise benutzen sollte. Sie setzen den entsprechenden Hinweis in die Abfrage, deren Plan Sie modifizieren wollen. Er ist dann nur für diese Abfrage wirksam. Im MySQL-Handbuch finden Sie die exakte Syntax der einzelnen Hinweise. Einige von ihnen sind versionsabhängig. Die Optionen sind: HIGH_PRIORITY und LOW_PRIORITY
Diese Hinweise teilen MySQL mit, wie es die Anweisung in Bezug auf andere Anweisungen priorisieren soll, die versuchen, auf die gleichen Tabellen zuzugreifen. HIGH_PRIORITY teilt MySQL mit, dass es eine SELECT-Anweisung vor den anderen
Anweisungen eintakten soll, die vielleicht auf Locks warten, damit sie die Daten modifizieren können. Im Prinzip kommt das SELECT an den Anfang der Warteschlange, anstatt warten zu müssen, bis es dran ist. Sie können diesen Modifikator auch auf INSERT anwenden, wo er einfach die Wirkung einer globalen LOW_PRIORITYServereinstellung aufhebt. LOW_PRIORITY ist genau das Gegenteil: Es sorgt dafür, dass die Anweisung am Ende
der Warteschlange wartet, falls es andere Anweisungen gibt, die auf die Tabelle zugreifen wollen – selbst wenn die anderen Anweisungen erst später aufgerufen wurden. Stellen Sie sich einfach eine übertrieben höfliche Person vor, die anderen im Restaurant die Tür aufhält: Solange noch irgendjemand kommt, bekommt diese Person nichts zu essen! Sie können diesen Hinweis auf SELECT-, INSERT-, UPDATE-, REPLACE- und DELETE-Anweisungen anwenden. Diese Hinweise sind bei Storage-Engines mit Locking auf Tabellenebene wirksam, bei InnoDB oder anderen Storage-Engines mit einem feinkörnigen Locking und Nebenläufigkeitskontrolle sollten Sie sie dagegen niemals benötigen. Passen Sie auf, wenn Sie sie bei MyISAM verwenden, da sie nebenläufige Einfügeoperationen außer Kraft setzen und die Leistungsfähigkeit stark beeinträchtigen können. Die Hinweise HIGH_PRIORITY und LOW_PRIORITY sind oft Grund für Verwirrung. Sie belegen nicht einfach mehr oder weniger Ressourcen für Abfragen, um sie »schwerer arbeiten« oder »nicht so schwer arbeiten« zu lassen; stattdessen beeinflussen sie, wie der Server Anweisungen in die Warteschlange einreiht, die auf den Zugriff auf eine Tabelle warten. DELAYED
Dieser Hinweis soll mit INSERT und REPLACE benutzt werden. Er ermöglicht es der Anwendung, auf die er angewandt wird, sofort zurückzukehren, und legt die eingefügten Zeilen in einen Puffer. Diese Zeilen werden dann zusammen eingefügt, wenn die Tabelle frei ist. Das eignet sich besonders für die Protokollierung und ähnliche Anwendungen, bei denen Sie viele Zeilen einfügen wollen, ohne dass der Client war-
210 | Kapitel 4: Optimierung der Abfrageleistung
ten muss und ohne dass für jede Anweisung eine Ein-/Ausgabe verursacht wird. Es gibt hier viele Beschränkungen; so sind z.B. verzögerte Einfügeoperationen nicht in allen Storage-Engines implementiert, und LAST_INSERT_ID( ) funktioniert nicht mit ihnen. STRAIGHT_JOIN
Dieser Hinweis kann entweder direkt hinter dem SELECT-Schlüsselwort in einer SELECT-Anweisung oder in einer beliebigen Anweisung zwischen zwei mit einem Join zusammengefügten Tabellen auftauchen. Die erste Anwendung sorgt dafür, dass alle Tabellen in der Abfrage in der Reihenfolge zusammengefügt werden, in der sie in der Anweisung aufgeführt sind. Die zweite Anwendung erzwingt eine Join-Reihenfolge in den beiden Tabellen, zwischen denen der Hinweis steht. Der Hinweis STRAIGHT_JOIN wird sinnvollerweise dann eingesetzt, wenn MySQL keine gute Join-Reihenfolge gewählt hat oder wenn der Optimierer sehr lange braucht, um eine Join-Reihenfolge festzulegen. Im zweiten Fall verbringt der Thread viel Zeit im »Statistics«-Zustand; durch das Hinzufügen dieses Hinweises wird der Suchraum für den Optimierer reduziert. Sie können mithilfe von EXPLAIN feststellen, welche Reihenfolge der Optimierer wählen würde, dann die Abfrage in dieser Reihenfolge umschreiben und STRAIGHT_JOIN hinzufügen. Das ist so lange eine gute Idee, wie Sie nicht glauben, dass die festgelegte Reihenfolge bei einigen WHERE-Klauseln zu einer schlechten Leistung führt. Denken Sie daran, solche Abfragen erneut anzuschauen, wenn Sie auf eine höhere MySQL-Version umsteigen, da es neue Optimierungen geben könnte, die von STRAIGHT_JOIN vereitelt werden. SQL_SMALL_RESULT und SQL_BIG_RESULT Diese Hinweise sind für SELECT-Anweisungen. Sie teilen dem Optimierer mit, wie und wann er temporäre Tabellen und Sortierungen in GROUP BY- oder DISTINCT-Abfragen benutzen soll. SQL_SMALL_RESULT sagt dem Optimierer, dass die Ergebnismenge
klein sein wird und in indizierte temporäre Tabellen gelegt werden kann, um das Sortieren für das Gruppieren zu vermeiden, während SQL_BIG_RESULT anzeigt, dass das Ergebnis groß sein wird, so dass es besser ist, temporäre Tabellen auf der Festplatte mit Sortierung zu benutzen. SQL_BUFFER_RESULT
Dieser Hinweis weist den Optimierer an, die Ergebnisse in eine temporäre Tabelle zu legen und Tabellen-Locks so schnell wie möglich wieder aufzuheben. Dies unterscheidet sich von der clientseitigen Pufferung, die wir in »Das MySQL-Client/ServerProtokoll« auf Seite 173 beschrieben haben. Serverseitiges Puffern kann ganz sinnvoll sein, wenn Sie keine Pufferung auf dem Client einsetzen, da Sie damit vermeiden, auf dem Client viel Speicher zu belegen, und Sperren ebenfalls schnell aufgehoben werden. Nachteilig ist, dass anstelle des Client-Speichers der Speicher des Servers belegt wird.
Hinweise für den Abfrageoptimierer
| 211
SQL_CACHE und SQL_NO_CACHE
Diese Hinweise teilen dem Server mit, dass die Abfrage entweder ein Kandidat für die Ablage im Abfrage-Cache ist oder nicht. Im nächsten Kapitel erfahren Sie genauer, wie Sie das benutzen. SQL_CALC_FOUND_ROWS
Dieser Hinweis weist MySQL an, eine vollständige Ergebnismenge zu berechnen, wenn es eine LIMIT-Klausel gibt, auch wenn nur LIMIT Zeilen zurückgeliefert werden. Sie können die Gesamtzahl der gefundenen Zeilen über FOUND_ROWS( ) erfahren. (Lesen Sie allerdings »SQL_CALC_FOUND_ROWS optimieren« auf Seite 208; dort erfahren Sie, weshalb Sie diesen Hinweis nicht benutzen sollten.) FOR UPDATE und LOCK IN SHARE MODE
Diese Hinweise kontrollieren das Locking bei SELECT-Anweisungen, allerdings nur bei Storage-Engines, die Row-Level-Locks besitzen. Sie erlauben es Ihnen, auf den gefundenen Zeilen Sperren anzulegen, was ganz gut sein kann, wenn Sie Zeilen sperren wollen, die Sie später aktualisieren werden, oder wenn Sie eine Lock-Eskalation vermeiden und daher so bald wie möglich exklusive Locks auslösen wollen. Diese Hinweise sind für INSERT ... SELECT-Abfragen nicht nötig, die standardmäßig in MySQL 5.0 Lese-Locks auf den Quellzeilen setzen. (Sie können dieses Verhalten deaktivieren, allerdings raten wir davon ab – die Gründe dafür erläutern wir in den Kapiteln 8 und 11.) MySQL 5.1 kann diese Beschränkung unter bestimmten Bedingungen aufheben. Momentan unterstützt nur InnoDB diese Hinweise. Es ist auch noch zu früh zu sagen, welche anderen Storage-Engines mit Row-Level-Locks sie künftig unterstützen werden. Wenn Sie diese Hinweise mit InnoDB benutzen, dann sollten Sie sich bewusst sein, dass sie einige Optimierungen, wie etwa abdeckende Indizes, deaktivieren könnten. InnoDB kann Zeilen nicht exklusiv sperren, ohne auf den Primärschlüssel zuzugreifen, wo die Versionsinformationen für die Zeilen gespeichert werden. USE INDEX, IGNORE INDEX und FORCE INDEX
Diese Hinweise sagen dem Optimierer, welche Indizes er zum Suchen von Zeilen in einer Tabelle benutzen oder ignorieren soll (z.B. wenn er eine Join-Reihenfolge festlegt). Bis MySQL 5.0 haben sie keinen Einfluss darauf, welche Indizes der Server zum Sortieren und Gruppieren verwendet, in MySQL 5.1 kann die Syntax eine optionale FOR ORDER BY- oder FOR GROUP BY-Klausel übernehmen. FORCE INDEX ist das Gleiche wie USE INDEX, sagt dem Optimierer aber, dass ein Tabel-
lenscan im Vergleich zum Index außerordentlich teuer ausfällt, selbst wenn der Index nicht besonders nützlich ist. Sie können diese Hinweise benutzen, wenn Sie nicht davon überzeugt sind, dass der Optimierer den richtigen Index wählt, oder wenn Sie aus irgendeinem Grund einen ganz bestimmten Index haben wollen, etwa weil Sie eine implizite Sortierung ohne ORDER BY durchführen wollen. Ein Beispiel dafür lieferten wir Ihnen in »LIMIT und OFFSET optimieren« auf Seite 207, wo wir Ihnen zeigten, wie Sie mit LIMIT effizient einen Minimalwert erhielten.
212 | Kapitel 4: Optimierung der Abfrageleistung
Seit MySQL 5.0 gibt es darüber hinaus noch Systemvariablen, die den Optimierer beeinflussen: optimizer_search_depth
Diese Variable teilt dem Optimierer mit, wie gründlich er Teilpläne untersuchen soll. Falls Ihre Abfragen sehr lange im Zustand »Statistics« verbleiben, sollten Sie versuchen, diesen Wert zu verringern. optimizer_prune_level
Diese Variable, die standardmäßig aktiviert ist, erlaubt es dem Optimierer, basierend auf der Anzahl der untersuchten Zeilen, bestimmte Pläne zu überspringen. Beide Optionen kontrollieren Optimiererabkürzungen. Diese Abkürzungen sind sehr wertvoll, um eine gute Leistung bei komplexen Abfragen zu erreichen, können aber auch dafür sorgen, dass der Server um der Effizienz willen optimale Pläne verfehlt. Daher ist es manchmal sinnvoll, sie zu ändern.
Benutzerdefinierte Variablen Man verliert die benutzerdefinierten Variablen von MySQL leicht aus den Augen, allerdings können sie sehr nützlich beim Schreiben effizienter Abfragen sein. Sie funktionieren besonders gut für Abfragen, die von einer Mischung aus prozeduraler und relationaler Logik profitieren. Rein relationale Abfragen behandeln alles als ungeordnete Mengen, die der Server irgendwie alle auf einmal manipuliert. MySQL geht einen pragmatischeren Weg. Dieser kann eine Schwäche sein, kann aber auch zu einer Stärke werden, wenn Sie wissen, wie Sie ihn richtig nutzen. Benutzerdefinierte Variablen helfen Ihnen dabei. Benutzerdefinierte Variablen sind temporäre Container für Werte, die so lange bestehen wie Ihre Verbindung zum Server. Sie definieren Sie, indem Sie sie einfach mit einer SEToder SELECT-Anweisung zuweisen:13 mysql> SET @one := 1; mysql> SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor); mysql> SET @last_week := CURRENT_DATE-INTERVAL 1 WEEK;
Sie können die Variablen dann an den meisten Stellen einsetzen, an denen auch ein Ausdruck stehen könnte: mysql> SELECT ... WHERE col <= @last_week;
Bevor wir uns mit den Stärken der benutzerdefinierten Variablen befassen, wollen wir uns einige ihrer Eigenarten und Nachteile anschauen und feststellen, wofür Sie sie nicht benutzen können: • Benutzerdefinierte Variablen verhindern Abfrage-Caching. • Sie können sie nicht benutzen, wenn ein Literal oder ein Identifikator benötigt wird, wie etwa bei Tabellen- oder Spaltennamen, oder in einer LIMIT-Klausel. 13 In manchen Kontexten können Sie die Zuweisung mit einem einfachen =-Zeichen vornehmen. Wir glauben allerdings, dass es besser ist, die Zweideutigkeit zu vermeiden und immer := zu benutzen.
Benutzerdefinierte Variablen | 213
• Benutzerdefinierte Variablen sind verbindungsspezifisch, eignen sich daher nicht für verbindungsübergreifende Kommunikation. • Wenn Sie Verbindungs-Pooling oder dauerhafte Verbindungen einsetzen, können sie dafür sorgen, dass scheinbar isolierte Teile Ihres Codes miteinander interagieren. • In MySQL-Versionen vor 5.0 musste für sie die Groß- und Kleinschreibung beachtet werden. Achten Sie also auf Kompatibilitätsprobleme. • Sie können die Typen dieser Variablen nicht explizit deklarieren, und die Stelle, an der die Typen für undefinierte Variablen festgelegt werden, liegt bei jeder MySQLVersion woanders. Am besten weisen Sie zu Anfang den Wert 0 für Variablen zu, die Sie für Integer-Werte einsetzen wollen, 0.0 für Gleitkommazahlen oder '' (den leeren String) für Strings. Der Typ einer Variablen ändert sich, wenn sie zugewiesen wird; die Typzuweisung für benutzerdefinierte Variablen in MySQL ist dynamisch. • Der Optimierer könnte in bestimmten Situationen diese Variablen wegoptimieren, wodurch er ihren Einsatz verhindern würde. • Die Reihenfolge der Zuweisung und sogar der Zeitpunkt der Zuweisung können nichtdeterministisch sein und hängen von dem Abfrageplan ab, den der Optimierer gewählt hat. Wie Sie später sehen werden, können die Ergebnisse sehr verwirrend sein. • Der :=-Zuweisungsoperator hat eine geringere Priorität als jeder andere Operator, Sie müssen ihn also sorgfältig einklammern. • Undefinierte Werte generieren keinen Syntaxfehler, so dass man leicht Fehler macht, ohne es zu merken. Eine der wichtigsten Eigenschaften von Variablen besteht darin, dass Sie einer Variablen einen Wert zuweisen und gleichzeitig den resultierenden Wert benutzen können. Mit anderen Worten: Eine Zuweisung ist ein L-Wert. Hier ist ein Beispiel, dass gleichzeitig eine »Zeilennummer« für eine Abfrage berechnet und ausgibt: mysql> SET @rownum := 0; mysql> SELECT actor_id, @rownum := @rownum + 1 AS rownum -> FROM sakila.actor LIMIT 3; +----------+--------+ | actor_id | rownum | +----------+--------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | +----------+--------+
Dieses Beispiel ist nicht so wahnsinnig aufregend, da es einfach nur zeigt, dass wir den Primärschlüssel der Tabelle duplizieren können. Es hat aber dennoch einen Nutzen – etwa beim Ranking. Wir wollen eine Abfrage schreiben, die 10 Schauspieler zurückliefert, die in den meisten Filmen mitgespielt haben – mit einer Rangspalte, die den Schauspielern den gleichen Rang zuweist, wenn sie das gleiche Ergebnis aufweisen. Wir beginnen mit einer Abfrage, die die Schauspieler und die Anzahl der Filme ermittelt:
214 | Kapitel 4: Optimierung der Abfrageleistung
mysql> SELECT actor_id, COUNT(*) as cnt -> FROM sakila.film_actor -> GROUP BY actor_id -> ORDER BY cnt DESC -> LIMIT 10; +----------+-----+ | actor_id | cnt | +----------+-----+ | 107 | 42 | | 102 | 41 | | 198 | 40 | | 181 | 39 | | 23 | 37 | | 81 | 36 | | 106 | 35 | | 60 | 35 | | 13 | 35 | | 158 | 35 | +----------+-----+
Jetzt wollen wir den Rang hinzufügen. Alle Schauspieler, die in 35 Filmen mitgewirkt haben, sollen den gleichen Rang erhalten. Dazu verwenden wir drei Variablen: eine, um den aktuellen Rang zu verfolgen, eine, um die Filmanzahl des vorhergehenden Schauspielers aufzunehmen, und eine für die Filmanzahl des aktuellen Schauspielers. Wir ändern den Rang, wenn sich der Filmzähler ändert. Hier ist ein erster Versuch: mysql> SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0; mysql> SELECT actor_id, -> @curr_cnt := COUNT(*) AS cnt, -> @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank, -> @prev_cnt := @curr_cnt AS dummy -> FROM sakila.film_actor -> GROUP BY actor_id -> ORDER BY cnt DESC -> LIMIT 10; +----------+-----+------+-------+ | actor_id | cnt | rank | dummy | +----------+-----+------+-------+ | 107 | 42 | 0 | 0 | | 102 | 41 | 0 | 0 | ...
Hoppla – Rang und Zähler stehen am Ende immer noch auf null. Wieso das denn? Es ist hier nicht möglich, eine allumfassende Antwort zu geben. Das Problem könnte ganz simpel bei einem falsch geschriebenen Variablennamen liegen (nicht in diesem Beispiel) oder etwas komplizierter sein. In diesem Fall zeigt EXPLAIN, dass es eine temporäre Tabelle und einen Filesort gibt, so dass die Variablen zu einem anderen Zeitpunkt ausgewertet werden, als wir erwartet haben. Das ist genau die Art von unergründlichem Verhalten, das Ihnen oft bei den benutzerdefinierten Variablen von MySQL begegnen wird. Das Lösen solcher Probleme kann schwer sein, zahlt sich aber wahrscheinlich wirklich aus. Das Festlegen von Rangfolgen
Benutzerdefinierte Variablen | 215
in SQL erfordert normalerweise quadratische Algorithmen, wie etwa das Zählen der Schauspieler, die in einer größeren Anzahl von Filmen mitgespielt haben. Eine Lösung mit einer benutzerdefinierten Variablen kann ein linearer Algorithmus sein – eine ziemliche Verbesserung. Eine einfache Lösung besteht in diesem Fall darin, der Abfrage eine weitere Ebene temporärer Tabellen hinzuzufügen, wobei in der FROM-Klausel eine Teilabfrage verwendet wird: mysql> SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0; -> SELECT actor_id, -> @curr_cnt := cnt AS cnt, -> @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank, -> @prev_cnt := @curr_cnt AS dummy -> FROM ( -> SELECT actor_id, COUNT(*) AS cnt -> FROM sakila.film_actor -> GROUP BY actor_id -> ORDER BY cnt DESC -> LIMIT 10 -> ) as der; +----------+-----+------+-------+ | actor_id | cnt | rank | dummy | +----------+-----+------+-------+ | 107 | 42 | 1 | 42 | | 102 | 41 | 2 | 41 | | 198 | 40 | 3 | 40 | | 181 | 39 | 4 | 39 | | 23 | 37 | 5 | 37 | | 81 | 36 | 6 | 36 | | 106 | 35 | 7 | 35 | | 60 | 35 | 7 | 35 | | 13 | 35 | 7 | 35 | | 158 | 35 | 7 | 35 | +----------+-----+------+-------+
Die meisten Probleme mit benutzerdefinierten Variablen resultieren aus ihrer Zuweisung und dem Lesen der Variablen in verschiedenen Stadien der Abfrage. Zum Beispiel ist das Verhalten nicht vorhersagbar, wenn sie in der SELECT-Anweisung zugewiesen und in der WHERE-Klausel ausgelesen werden. Die folgende Abfrage scheint einfach nur eine Zeile zurückzuliefern, tut es aber nicht: mysql> SET @rownum := 0; mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt -> FROM sakila.actor -> WHERE @rownum <= 1; +----------+------+ | actor_id | cnt | +----------+------+ | 1 | 1 | | 2 | 2 | +----------+------+
216 | Kapitel 4: Optimierung der Abfrageleistung
Dies liegt daran, dass WHERE und SELECT unterschiedliche Stadien in der Abfrageausführung darstellen. Es wird noch offensichtlicher, wenn Sie mit einem ORDER BY eine weitere Stufe hinzufügen: mysql> mysql> -> -> ->
SET @rownum := 0; SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum <= 1 ORDER BY first_name;
Diese Abfrage liefert alle Zeilen in der Tabelle zurück, weil das ORDER BY ein Filesort hinzugefügt hat und das WHERE vor dem Filesort ausgewertet wird. Die Lösung für dieses Problem besteht darin, im gleichen Stadium der Abfrageausführung zuzuweisen und zu lesen: mysql> SET @rownum := 0; mysql> SELECT actor_id, @rownum AS rownum -> FROM sakila.actor -> WHERE (@rownum := @rownum + 1) <= 1; +----------+--------+ | actor_id | rownum | +----------+--------+ | 1 | 1 | +----------+--------+
Preisfrage: Was passiert, wenn Sie das ORDER BY wieder zu dieser Abfrage hinzufügen? Probieren Sie es aus. Woran liegt es, dass Sie nicht die erwarteten Ergebnisse erhielten? Was ist mit der folgenden Abfrage, bei der das ORDER BY den Wert der Variablen ändert und die WHERE-Klausel sie auswertet? mysql> mysql> -> -> ->
SET @rownum := 0; SELECT actor_id, first_name, @rownum AS rownum FROM sakila.actor WHERE @rownum <= 1 ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);
Die Antwort auf die meisten unerwarteten Verhaltensweisen von benutzerdefinierten Variablen kann man erhalten, indem man EXPLAIN ausführt und nach »Using where«, »Using temporary« oder »Using filesort« in der Extra-Spalte sucht. Das letzte Beispiel führte einen weiteren nützlichen Hack ein: Wir setzten die Zuweisung in die LEAST( )-Funktion, so dass ihr Wert gewissermaßen maskiert ist und die Ergebnisse des ORDER BY nicht stört (wie wir geschrieben haben, liefert die LEAST( )-Funktion immer 0). Dieser Trick ist sehr hilfreich, wenn Sie Variablenzuweisungen einzig wegen ihrer Nebenwirkungen durchführen wollen: Sie können damit den Rückgabewert verbergen und vermeiden das Anlegen zusätzlicher Spalten, wie der dummy-Spalte aus einem vorhergehenden Beispiel. Die Funktionen GREATEST( ), LENGTH( ), ISNULL( ), NULLIF( ), COALESCE( ) und IF( ) eignen sich ebenfalls für diesen Zweck, entweder allein oder in Kombination, da sie besondere Verhaltensweisen mitbringen. So stoppt z.B. COALESCE( ) das Auswerten seiner Argumente, sobald es einen definierten Wert annimmt.
Benutzerdefinierte Variablen | 217
Sie können Variablenzuweisungen in alle Arten von Anweisungen legen, nicht nur in SELECT-Anweisungen. Das ist sogar eine der besten Anwendungen für benutzerdefinierte Variablen. So können Sie etwa teure Abfragen, wie z.B. Rangberechnungen mit Teilabfragen, in billige UPDATE-Anweisungen umschreiben. Möglicherweise allerdings ist es nicht ganz so einfach, das gewünschte Verhalten zu erreichen. Manchmal beschließt der Optimierer, die Variablen als Konstanten zum CompileZeitpunkt zu betrachten, und lehnt es ab, Zuweisungen vorzunehmen. Meist hilft, es, die Zuweisungen in eine Funktion wie LEAST( ) zu legen. Ein anderer Tipp besteht darin, zu überprüfen, ob Ihre Variable einen definierten Wert hat, bevor die übergeordnete Anweisung ausgeführt wird. Manchmal soll sie ihn haben, dann wieder nicht. Mit ein wenig Erfahrungen können Sie alle Arten interessanter Dinge mit benutzerdefinierten Variablen erreichen. Hier sind einige Anregungen: • laufendes Berechnen von Gesamt- und Durchschnittswerten • Emulieren von FIRST( )- und LAST( )-Funktionen für gruppierte Abfragen • Berechnungen an außerordentlich großen Zahlen • Reduzieren einer ganzen Tabelle auf einen einzigen MD5-Hash-Wert • »Auspacken« eines abgefragten Wertes, der eingepackt wird, wenn er über eine bestimmte Grenze steigt • Emulieren eines Lese/Schreib-Cursors
Seien Sie vorsichtig mit MySQL-Upgrades Wir haben schon gesagt, dass es normalerweise keine gute Idee ist, wenn man versucht, den MySQL-Optimierer zu überlisten. Im Allgemeinen macht man sich damit mehr Arbeit und erhöht den Pflegeaufwand, hat aber kaum einen Nutzen davon. Das gilt vor allem, wenn Sie MySQL aufrüsten, da Optimiererhinweise, die in Ihren Abfragen verwendet werden, verhindern könnten, dass neue Optimiererstrategien zum Einsatz kommen. Die Art und Weise, wie der MySQL-Optimierer Indizes einsetzt, ist sehr unterschiedlich. Neue MySQL-Versionen ändern die Verwendung existierender Indizes, und Sie müssen Ihre Indizierungsgewohnheiten an diese neuen Versionen anpassen. Wir haben z.B. erwähnt, dass MySQL 4.0 und ältere Versionen nur einen Index pro Tabelle pro Abfrage benutzen konnten, Versionen ab MySQL 5.0 können dagegen auf Index-Merge-Strategien zurückgreifen. Abgesehen von den großen Änderungen am Abfrageoptimierer, die MySQL gelegentlich mit sich bringt, haben kleinere Versionswechsel oft viele winzige Änderungen zur Folge. Diese Änderungen betreffen üblicherweise kleine Dinge, wie etwa die Bedingungen, unter denen ein Index von der weiteren Berücksichtigung ausgeschlossen wird, und erlauben es MySQL, mehr Sonderfälle zu optimieren.
218 | Kapitel 4: Optimierung der Abfrageleistung
Das klingt zwar in der Theorie ganz gut, in der Praxis funktionieren dagegen manche Optimierungen nach einem Versionswechsel schlechter. Wenn Sie sich über einen langen Zeitraum an eine bestimmte Version gewöhnt haben, dann haben Sie wahrscheinlich – ob bewusst oder unbewusst – Ihre Abfragen genau an diese Version angepasst. Diese Optimierungen gelten möglicherweise nicht mehr für die neueren Versionen oder vermindern unter Umständen die Leistung. Falls Sie eine hohe Leistung aus Ihrem System herausholen wollen, dann sollten Sie eine Benchmark-Suite einsetzen, die Ihre spezielle Arbeitslast repräsentiert und mit der Sie die neue Version auf einem Entwicklungsserver testen können, bevor Sie Ihre Produktionsserver umstellen. Lesen Sie vor dem Aufrüsten außerdem die »Release Notes« sowie die Liste der bekannten Fehler in der neuen Version. Das MySQL-Handbuch enthält eine Liste der bekannten ernsthaften Bugs. Die meisten SQL-Upgrades bringen dennoch eine bessere Leistung mit sich, wir wollen gar nichts anderes behaupten. Seien Sie dennoch vorsichtig.
Benutzerdefinierte Variablen | 219
KAPITEL 5
Erweiterte MySQL-Funktionen
MySQL 5.0 und 5.1 führten viele Funktionen ein, wie etwa gespeicherte Prozeduren, Sichten und Trigger, die Benutzern vertraut sind, die bereits mit anderen Datenbankservern gearbeitet haben. Das Auftauchen dieser Funktionen hat MySQL viele neue Benutzer gebracht. Ihre Auswirkungen auf die Performance wurden allerdings erst deutlich, als viele Leute begannen, sie umfassend einzusetzen. Dieses Kapitel behandelt die aktuellen Ergänzungen und andere fortgeschrittene Themen, darunter einige Funktionen, die es schon in MySQL 4.1 und auch in einigen früheren Versionen gab. Wir konzentrieren uns hier auf die Performance, zeigen Ihnen aber auch, wie Sie das meiste aus diesen erweiterten Funktionen herausholen.
Der MySQL-Abfrage-Cache Viele Datenbankprodukte können Abfrageausführungspläne im Cache speichern, so dass der Server die Möglichkeit erhält, bei wiederholten Abfragen die SQL-Parsing- und Optimierungsstadien zu überspringen. MySQL kann das unter bestimmten Umständen auch, besitzt aber auch eine andere Art von Cache (den sogenannten Abfrage-Cache), der komplette Ergebnismengen für SELECT-Anweisungen speichert. Dieser Abschnitt befasst sich speziell mit diesem Cache. Der MySQL-Abfrage-Cache enthält die exakten Bits, die eine abgeschlossene Abfrage an den Client zurückgeliefert hat. Wenn ein Treffer im Abfrage-Cache auftritt, kann der Server einfach sofort die gespeicherten Ergebnisse zurückliefern und die Schritte zum Parsen, Optimieren und Ausführen überspringen. Der Abfrage-Cache behält den Überblick darüber, welche Tabellen eine Abfrage verwendet, und wenn sich eine dieser Tabellen ändert, macht er den Cache-Eintrag ungültig. Dieses Vorgehen scheint ineffektiv zu sein, da die Änderungen an den Tabellen nicht unbedingt die Ergebnisse im Cache beeinflussen müssen, es ist aber ein einfacher, relativ unaufwendiger Ansatz, was auf einem ausgelasteten System nicht ohne Bedeutung ist.
220 |
Der Abfrage-Cache ist für die Anwendung völlig transparent. Die Anwendung muss nicht wissen, ob MySQL Daten aus dem Cache zurückgeliefert oder tatsächlich die Abfrage ausgeführt hat. Das Ergebnis müsste auf jeden Fall gleich sein. Mit anderen Worten: Der Abfrage-Cache ändert die Semantik nicht; der Server scheint sich immer gleich zu verhalten, ob der Cache nun aktiviert ist oder nicht.1
Wie MySQL prüft, ob es einen Cache-Treffer gibt Die Art und Weise, wie MySQL prüft, ob es einen Treffer im Cache gibt, ist einfach und ziemlich schnell: Der Cache ist eine Lookup-Tabelle. Der Lookup-Schlüssel ist ein Hash aus dem Abfragetext selbst, der aktuellen Datenbank, der Clientprotokollversion und einer Handvoll weiterer Dinge, die die tatsächlichen Bytes im Ergebnis der Abfrage beeinflussen könnten. MySQL parst, »normalisiert« oder parametrisiert eine Anweisung nicht, wenn es nach einem Cache-Treffer sucht; stattdessen benutzt es die Anweisung und die anderen Datenbits genau so, wie der Client sie sendet. Jeder Unterschied in der Groß-/Kleinschreibung, bei den Leerzeichen oder Kommentaren – also praktisch jeder Unterschied verhindert einen erfolgreichen Vergleich mit einer zuvor im Cache gespeicherten Version. Das sollte man immer im Hinterkopf haben, wenn man Abfragen schreibt. Es ist sowieso eine gute Angewohnheit, eine konsistente Formatierung und einen einheitlichen Stil zu benutzen, in diesem Fall jedoch machen Sie damit sogar Ihr System schneller. Weiterhin speichert der Abfrage-Cache ein Ergebnis nur dann, wenn die Abfrage, die es generiert hat, deterministisch war. Das heißt, alle Abfragen, die nichtdeterministische Funktionen wie NOW( ) oder CURRENT_DATE( ) enthalten, werden nicht im Cache gespeichert. Darüber hinaus können sich Funktionen wie CURRENT_USER( ) oder CONNECTION_ID( ) unterscheiden, wenn sie von unterschiedlichen Benutzern ausgeführt werden, und verhindern auf diese Weise einen Treffer im Cache. Der Abfrage-Cache funktioniert bei Abfragen nicht, die sich auf benutzerdefinierte Funktionen, gespeicherte Funktionen, Benutzervariablen, temporäre Tabellen, Tabellen in der mysql-Datenbank oder Tabellen mit Berechtigungen auf Spaltenebene beziehen. (Im MySQL-Handbuch finden Sie eine ausführliche Liste der Dinge, die einer Cache-Speicherung im Wege stehen.) Wir hören oft Aussagen wie »MySQL prüft den Cache nicht, wenn die Abfrage eine nichtdeterministische Funktion enthält«. Das ist falsch. MySQL kann bis zum Parsen einer Abfrage nicht wissen, ob eine Abfrage eine nichtdeterministische Funktion enthält, und die Cache-Suche erfolgt vor dem Parsen. Der Server führt einen unabhängig von der Schreibweise erfolgenden Test aus, um festzustellen, dass die Abfrage mit den Buchstaben SEL beginnt, aber das ist alles.
1 Um genau zu sein, ändert der Abfrage-Cache die Semantik auf subtile Weise: Eine Abfrage kann standardmäßig aus dem Cache bedient werden, wenn eine der Tabellen, auf die sie sich bezieht, mit LOCK TABLES gesperrt ist. Sie können dies mit der Variablen query_cache_wlock_invalidate deaktivieren.
Der MySQL-Abfrage-Cache | 221
Es ist jedoch richtig zu sagen »Der Server findet keine Ergebnisse im Cache, wenn die Abfrage eine Funktion wie NOW( ) enthält«, weil der Server die Ergebnisse nicht im Cache vorliegen hat, selbst wenn er diese Abfrage schon früher ausgeführt hatte. MySQL kennzeichnet eine Abfrage als nicht-cache-fähig, sobald es ein Konstrukt entdeckt, das eine Cache-Speicherung verbietet; die Ergebnisse einer solchen Abfrage werden nicht gespeichert. Eine nützliche Technik zum Aktivieren der Cache-Speicherung von Abfragen, die sich auf das aktuelle Datum beziehen, besteht darin, das Datum als Literal einzufügen, anstatt eine Funktion zu benutzen. Zum Beispiel: ... DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY) -- Nicht-cache-fähig! ... DATE_SUB('2007-07-14', INTERVAL 1 DAY) -- Cache-fähig
Da der Abfrage-Cache auf der Ebene einer vollständigen SELECT-Anweisung arbeitet, wenn der Server sie das erste Mal von der Clientverbindung entgegennimmt, können identische Abfragen, die innerhalb einer Unterabfrage oder Sicht gemacht werden, den Abfrage-Cache nicht benutzen – ebenso wenig wie Abfragen in gespeicherten Prozeduren. Vorbereitete Anweisungen können in Versionen vor MySQL 5.1 den Abfrage-Cache ebenfalls nicht verwenden. Der MySQL-Abfrage-Cache kann die Leistung verbessern, allerdings gibt es einige Punkte, die Sie beachten sollten, wenn Sie ihn benutzen. Erstens sorgt das Aktivieren des Abfrage-Cache für zusätzlichen Aufwand beim Lesen und Schreiben: • Leseabfragen müssen vor dem Beginn den Cache überprüfen. • Wenn die Abfrage cache-fähig ist und noch nicht im Cache vorliegt, kommt es aufgrund der Speicherung des Ergebnisses nach dem Generieren zu einem gewissen Overhead. • Schließlich verursachen Schreibabfragen, die die Cache-Einträge für Abfragen ungültig machen müssen, die Tabellen benutzen, die beim Schreiben verändert werden, ebenfalls Aufwand. Dieser Overhead ist relativ gesehen minimal, der Abfrage-Cache bringt also trotzdem einen Gewinn. Allerdings: Kleinvieh macht auch Mist, wie wir später sehen werden. Für InnoDB-Benutzer besteht ein weiteres Problem darin, dass Transaktionen den Nutzen des Abfrage-Cache mindern. Wenn eine Anweisung in einer Transaktion eine Tabelle modifiziert, dann macht der Server alle im Cache gespeicherten Abfragen ungültig, die sich auf diese Tabelle beziehen, selbst wenn die InnoDB-eigene Multiversionierung die Änderungen der Transaktion vor anderen Anweisungen verbirgt. Die Tabelle ist global gesehen erst dann wieder cache-fähig, wenn die Transaktion bestätigt wird, so dass also keine weiteren Abfragen an diese Tabelle – ob außerhalb oder innerhalb dieser Transaktion – im Cache gespeichert werden können, solange die Transaktion nicht bestätigt wurde. Lange laufende Transaktionen erhöhen daher die Anzahl der fehlgeschlagenen Cache-Zugriffe.
222 | Kapitel 5: Erweiterte MySQL-Funktionen
Die zunehmende Entwertung kann bei einem großen Abfrage-Cache ebenfalls zu einem Problem werden. Wenn sich im Cache viele Abfragen befinden, kann die Entwertung lange dauern und dafür sorgen, dass das gesamte System ausgebremst wird. Das liegt daran, dass eine globale Sperre auf dem Abfrage-Cache liegt, die alle Abfragen blockiert, die auf den Cache zugreifen müssen. Der Zugriff geschieht sowohl dann, wenn auf einen Treffer geprüft wird, als auch dann, wenn geprüft wird, ob weitere Abfragen ungültig gemacht werden müssen.
Wie der Cache den Speicher benutzt MySQL legt den Abfrage-Cache vollständig im Speicher ab, Sie müssen also verstehen, wie es den Speicher benutzt, bevor Sie es richtig anpassen können. Der Cache legt in seinem Speicher mehr als nur die Abfrageergebnisse ab. Auf gewisse Weise können Sie sich das wie ein Dateisystem vorstellen: Der Cache hält Strukturen bereit, mit deren Hilfe Sie feststellen können, wie viel Speicher in seinem Pool frei ist, und er enthält Zuordnungen zwischen Tabellen und Abfrageergebnissen sowie den Abfragetext und die Abfrageergebnisse. Abgesehen von grundlegenden Verwaltungsstrukturen, die etwa 40 KByte erfordern, kann der Speicher-Pool des Abfrage-Cache in Blöcken variabler Größe benutzt werden. Jeder Block weiß, von welchem Typ er ist, welche Größe er hat und wie viele Daten er enthält. Außerdem enthält er Zeiger auf den jeweils nächsten und vorhergehenden logischen und physischen Block. Es gibt verschiedene Typen von Blöcken: Sie können Cache-Ergebnisse, Listen mit Tabellen, die von einer Abfrage benötigt werden, Abfragetext usw. speichern. Allerdings werden unterschiedliche Arten von Blöcken fast gleich behandelt, es besteht also beim Einstellen des Abfrage-Cache keine Notwendigkeit, zwischen ihnen zu unterscheiden. Wenn der Server startet, initialisiert er den Speicher für den Abfrage-Cache. Der Speicher-Pool ist anfangs ein einziger freier Block. Dieser Block ist so groß wie der gesamte Speicher, den der Cache aufgrund seiner Konfiguration belegen darf, abzüglich der Verwaltungsstrukturen. Wenn der Server die Ergebnisse einer Abfrage im Cache ablegt, reserviert er einen Block, um die Ergebnisse zu speichern. Dieser Block muss mindestens query_cache_min_res_unit Bytes groß sein, kann aber auch größer sein, wenn der Server weiß, dass er ein größeres Ergebnis speichert. Leider ist der Server nicht in der Lage, einen Block in exakt der richtigen Größe zu reservieren, da er diese anfängliche Reservierung durchführt, bevor die Ergebnismenge komplett ist. Der Server setzt die Ergebnismenge nicht im Speicher zusammen und sendet sie dann; es ist effizienter, jede Zeile dann zu senden, wenn sie generiert wird. Daraus folgt, dass der Server zu Beginn der Cache-Speicherung nicht wissen kann, wie groß die Ergebnismenge schließlich sein wird. Das Reservieren der Blöcke ist ein relativ langsamer Vorgang, weil es verlangt, dass der Server in seine Liste der freien Blöcke schaut, um einen Block zu finden, der groß genug ist. Daher versucht der Server, die Anzahl der Reservierungen, die er durchführen muss, Der MySQL-Abfrage-Cache | 223
zu minimieren. Wenn er eine Ergebnismenge im Cache speichern muss, reserviert er einen Block, der wenigstens die Minimalgröße aufweist, und beginnt damit, die Ergebnisse in diesen Block zu legen. Wird der Block voll, obwohl noch Daten vorhanden sind, die gespeichert werden müssen, belegt der Server einen neuen Block – wieder mit wenigstens der Mindestgröße – und fährt mit dem Ablegen der Daten fort. Ist das Ergebnis abgeschlossen und ist noch Platz in dem Block, dann kürzt der Server ihn auf die passende Größe und fügt den übrig gebliebenen Platz in den nachfolgenden freien Block ein. Abbildung 5-1 verdeutlicht diesen Vorgang.2 Verwaltung
Verwaltung
Verwaltung
Verwaltung
Frei Frei Frei
Anfangszustand
Speichern der Ergebnisse
Ergebnisse vollständig
Frei
Nach dem Kürzen
Cache-Block Gespeicherte Daten
Abbildung 5-1: So reserviert der Abfrage-Cache Blöcke, um ein Ergebnis zu speichern
Wenn wir sagen, dass der Server »einen Block belegt«, dann meinen wir damit nicht, dass er das Betriebssystem bittet, Speicher mit malloc( ) oder einem ähnlichen Aufruf zu reservieren. Er tut das nur einmal, wenn er den Abfrage-Cache anlegt. Wir meinen, dass der Server seine Liste der Blöcke untersucht und entweder die beste Stelle auswählt, um einen neuen Block abzulegen, oder nötigenfalls die älteste im Cache befindliche Abfrage entfernt, um Platz zu schaffen. Mit anderen Worten: Der MySQL-Server verwaltet seinen eigenen Speicher und greift dazu nicht auf das Betriebssystem zurück. Bisher ist das alles noch recht einfach. Das Bild kann allerdings deutlich komplizierter werden, als es in Abbildung 5-1 aussah. Nehmen wir an, das durchschnittliche Ergebnis ist recht klein, und der Server sendet gleichzeitig Ergebnisse an zwei Clientverbindungen. 2 Wir haben die Diagramme in diesem Abschnitt für die Darstellung vereinfacht. Die Belegung der Blöcke für den Abfrage-Cache durch den Server ist in Wirklichkeit viel komplizierter, als wir hier gezeigt haben. Falls es Sie interessiert, wie das funktioniert, schauen Sie sich die Kommentare am Anfang von sql/sql_cache.cc im Quellcode des Servers an, die das sehr gründlich erklären.
224 | Kapitel 5: Erweiterte MySQL-Funktionen
Durch das Kürzen der Ergebnisse kann ein freier Block entstehen, der kleiner ist als query_cache_min_res_unit und zum Speichern künftiger Cache-Ergebnisse nicht zu gebrauchen ist. Die Belegung der Blöcke könnte dann so aussehen wie in Abbildung 5-2. Verwaltung
Verwaltung
Verwaltung
Verwaltung Abfrage 1
Abfrage 1
Abfrage 1 Abfrage 2
Abfrage 2
Frei
Frei
Frei
Speichern der Ergebnisse
Ergebnisse vollständig
Nach dem Kürzen
Abfrage 2 Frei
Anfangszustand
Abbildung 5-2: Fragmentierung, verursacht durch das Speichern der Ergebnisse im Abfrage-Cache
Das Anpassen des ersten Ergebnisses auf die Größe hinterließ eine Lücke zwischen den beiden Ergebnissen – einen Block, der zu klein zum Ablegen eines anderen Abfrageergebnisses ist. Das Auftauchen solcher Lücken bezeichnet man als Fragmentierung. Diese bildet ein klassisches Problem bei der Speicher- und Dateisystembelegung. Fragmentierung kann aus verschiedenen Gründen auftreten, unter anderem aufgrund von Cache-Entwertungen, die Blöcke verursachen können, die für eine spätere Wiederverwendung zu klein sind.
Wann der Abfrage-Cache hilfreich ist Das Speichern von Abfragen im Cache ist nicht automatisch effizienter als das Nichtspeichern im Cache. Die Cache-Speicherung macht Arbeit, und die Ergebnisse aus dem Abfrage-Cache rentieren sich nur dann, wenn die Ersparnisse größer sind als der Aufwand. Im Prinzip hängt es von der Last auf Ihrem Server ab. Theoretisch können Sie feststellen, ob der Cache sinnvoll ist, indem Sie vergleichen, wie viel Arbeit der Server jeweils bei aktiviertem und deaktiviertem Cache verrichten muss. Bei deaktiviertem Cache muss jede Leseabfrage ausgeführt werden und ihre Ergebnisse zurückliefern, und auch alle Schreibabfragen müssen ausgeführt werden. Bei aktiviertem Cache muss jede Leseabfrage zuerst den Cache überprüfen und dann entweder das gespeicherte Ergebnis zurückliefern oder, falls es keins gibt, das Ergebnis generieren, speichern und zurückliefern. Jede Schreibabfrage muss ausgeführt werden, und anschließend muss sie überprüfen, ob im Cache Abfragen vorliegen, die entwertet werden müssen.
Der MySQL-Abfrage-Cache | 225
Das klingt zwar einfach, ist es aber nicht, da sich nicht so leicht berechnen oder vorhersagen lässt, welchen Nutzen der Abfrage-Cache abwerfen wird. Auch externe Faktoren müssen in Betracht gezogen werden. So kann z.B. der Abfrage-Cache die Zeit verringern, die erforderlich ist, bis das Ergebnis der Abfrage vorliegt, nicht jedoch die Zeit, die nötig ist, um das Ergebnis an das Clientprogramm zu senden, was der wesentliche Faktor sein könnte. Am meisten profitieren solche Abfragen von der Cache-Speicherung, die teuer in der Generierung sind, aber nicht viel Platz im Cache wegnehmen, so dass die Speicherung, die Auslieferung an den Client und die Entwertung unaufwendig sind. In diese Kategorie passen Aggregatabfragen wie kleine COUNT( )-Ergebnisse aus großen Tabellen. Es lohnt sich aber auch, andere Arten von Abfragen im Cache abzulegen. Um den Nutzen des Abfrage-Cache zu ermitteln, kann man die Trefferrate für den Abfrage-Cache untersuchen. Dabei handelt es sich um die Anzahl der Abfragen, die, anstatt vom Server ausgeführt zu werden, aus dem Cache geliefert werden. Wenn der Server eine SELECT-Anweisung empfängt, erhöht er entweder Qcache_hits oder Com_select (beides Statusvariablen), je nachdem, ob die Abfrage im Cache gelandet ist oder nicht. Die Trefferrate des Abfrage-Cache ergibt sich daher aus der Formel Qcache_hits / (Qcache_hits+Com_select). Wie sieht eine gute Cache-Trefferrate aus? Es kommt drauf an. Selbst eine Trefferrate von 30% kann hilfreich sein, da nicht ausgeführte Abfragen typischerweise (pro Abfrage) mehr Arbeit sparen als Abfragen, deren Einträge entwertet und die dann neu im Cache gespeichert werden müssen. Man sollte außerdem wissen, welche Abfragen im Cache landen. Wenn die Cache-Treffer die teuersten Abfragen repräsentieren, dann kann sogar eine niedrige Trefferrate für den Server eine Ersparnis bedeuten. Eine SELECT-Abfrage, die MySQL nicht aus dem Cache bedient, ist ein Cache-Miss (Cache-Verfehlen). Ein Cache-Miss kann aus verschiedenen Gründen auftreten: • Die Abfrage ist nicht cache-fähig, weil sie entweder ein nichtdeterministisches Konstrukt (wie etwa CURRENT_DATE) enthält oder weil ihre Ergebnismenge zu groß für die Speicherung ist. Beide Arten von nicht-cache-fähigen Abfragen erhöhen die Statusvariable Qcache_not_cached. • Der Server hat die Abfrage zuvor noch nie gesehen, hatte also keine Möglichkeit, ihr Ergebnis im Cache zu speichern. • Das Ergebnis der Abfrage wurde zuvor im Cache gespeichert, der Server hat es aber entfernt. Das kann passieren, weil der Speicher für das Ergebnis nicht ausgereicht hat, weil jemand den Server angewiesen hat, es zu entfernen, oder weil es entwertet wurde (zu Entwertungen kommen wir gleich). Wenn Ihr Server viele Cache-Misses hat, aber nur wenige nicht-cache-fähige Abfragen, dann kann das einen der folgenden Gründe haben: • Der Abfrage-Cache ist noch nicht warm. Das bedeutet, dass der Server noch keine Gelegenheit hatte, den Cache mit Ergebnismengen zu füllen.
226 | Kapitel 5: Erweiterte MySQL-Funktionen
• Der Server sieht Abfragen, die ihm vorher noch nicht begegnet sind. Falls sich kaum Abfragen bei Ihnen wiederholen, kann das passieren, obwohl der Cache warm ist. • Es gibt viele Cache-Entwertungen. Zu Cache-Entwertungen kommt es aufgrund von Fragmentierung, unzulänglichem Speicher oder Datenveränderungen. Wenn Sie für den Cache genügend Speicher belegt und den Wert query_cache_min_res_unit richtig eingestellt haben, resultieren die meisten Cache-Entwertungen aus Veränderungen an den Daten. Die Com_*-Statusvariablen (Com_update, Com_delete usw.) zeigen Ihnen, wie viele Abfragen Daten modifiziert haben, und wenn Sie die Statusvariable Qcache_lowmem_prunes überprüfen, erfahren Sie, wie viele Abfragen aufgrund mangelnden Speichers ungültig gemacht wurden. Es ist günstig, den Aufwand für die Entwertung getrennt von der Trefferrate zu betrachten. Stellen Sie sich ein extremes Beispiel vor: Sie haben eine Tabelle, die alle Leseoperationen bekommt und im Abfrage-Cache eine Trefferrate von 100 % hat, und eine weitere Tabelle, die nur Updates bekommt. Falls Sie die Trefferrate einfach aus der Statusvariablen berechnen, erhalten Sie einen Wert von 100 %. Dennoch kann der Abfrage-Cache ineffizient sein, weil er die Update-Abfragen verlangsamt. Alle Update-Abfragen müssen überprüfen, ob eine der Abfragen im Cache aufgrund ihrer Änderungen ungültig gemacht werden muss, doch da die Antwort immer »nein« lautet, ist das vergebene Liebesmüh. Sie werden ein solches Problem möglicherweise nur bemerken, wenn Sie sowohl die Anzahl der nicht-cache-fähigen Abfragen als auch die Trefferrate überprüfen. Ein Server, der einen ausgewogenen Mix aus Schreib- und cache-fähigen Lesevorgängen in den gleichen Tabellen verarbeitet, hat vermutlich auch nicht viel von einem AbfrageCache. Die Schreiboperationen entwerten laufend Ergebnisse, die im Cache vorliegen, während die cache-fähigen Leseoperationen gleichzeitig neue Ergebnisse in den Cache legen. Diese nützen nur dann etwas, wenn sie gleich im Anschluss wieder aus dem Cache ausgeliefert werden. Wenn ein im Cache gespeichertes Ergebnis entwertet wird, bevor der Server die gleiche SELECT-Anweisung noch einmal erhält, dann war das Speichern eine Zeit- und Speicherverschwendung. Schauen Sie sich die relativen Größen von Com_select und Qcache_inserts an, um festzustellen, ob dies passiert. Wenn fast jedes SELECT ein CacheMiss ist (und deshalb Com_select erhöht) und anschließend sein Ergebnis im Cache ablegt, dann ist Qcache_inserts fast so groß wie Com_select. Das heißt, Qcache_inserts sollte viel kleiner sein als Com_select, zumindest wenn der Cache richtig angewärmt ist. Jede Anwendung hat eine begrenzte potenzielle Cache-Größe, auch wenn es gar keine Schreibabfragen gibt. Die potenzielle Cache-Größe ist die Menge an Speicher, die erforderlich ist, um jede mögliche cache-fähige Abfrage zu speichern, die die Anwendung jemals ausführt. Theoretisch ist dies bei den meisten Anwendungen eine außerordentlich große Zahl. In der Praxis haben dagegen viele Anwendungen aufgrund der Anzahl der Entwertungen eine viel kleinere nutzbare Cache-Größe, als Sie vermuten würden. Selbst wenn Sie den Abfrage-Cache sehr groß machen, wird er kaum über die potenzielle Cache-Größe hinaus gefüllt werden.
Der MySQL-Abfrage-Cache | 227
Sie sollten überwachen, welchen Anteil des Abfrage-Cache Ihr Server tatsächlich benutzt. Falls er weniger Speicher verwendet, als Sie ihm zur Verfügung gestellt haben, dann verkleinern Sie ihn; falls Speicherbeschränkungen dagegen massive Entwertungen verursachen, dann machen Sie ihn größer. Machen Sie sich jedoch nicht zu große Sorgen wegen der Cache-Größe; wenn Sie den Cache ein wenig kleiner oder größer machen, als er Ihrer Meinung nach sein sollte, sind die Auswirkungen auf die Leistung nicht allzu groß. Es wird nur dann zu einem Problem, wenn viel Speicher verschwendet wird oder so viele Cache-Entwertungen auftreten, dass die Cache-Speicherung im Prinzip nutzlos wird. Sie müssen den Abfrage-Cache darüber hinaus mit den anderen Server-Caches abgleichen, wie etwa dem InnoDB-Puffer-Pool oder dem MyISAM-Schlüssel-Cache. Man kann hierfür nicht einfach ein Verhältnis oder eine einfache Formel angeben, weil die richtige Balance von der Anwendung abhängt.
Wie man den Abfrage-Cache einstellt und pflegt Wenn Sie einmal verstanden haben, wie der Abfrage-Cache funktioniert, dann ist es ganz leicht, ihn einzustellen. Er besitzt nur wenige »bewegliche Teile«: query_cache_type
Gibt an, ob der Abfrage-Cache aktiviert ist. Mögliche Werte sind OFF, ON oder DEMAND, wobei der letzte Wert bedeutet, dass nur Abfragen, die den SQL_CACHE-Modifikator enthalten, für eine Cache-Speicherung geeignet sind. Es ist sowohl eine globale Variable als auch eine Variable auf Sitzungsebene. (In Kapitel 6 erfahren Sie mehr über Sitzungs- und globale Variablen.) query_cache_size
Der Gesamtspeicher, der für den Abfrage-Cache belegt wird, in Byte. Dieser Wert muss ein Vielfaches von 1.024 Byte sein; MySQL kann hier also einen etwas anderen Wert benutzen, als Sie angegeben haben. query_cache_min_res_unit
Die minimale Größe beim Belegen eines Blocks. Wir erläuterten diese Einstellung bereits in »Wie der Cache den Speicher benutzt« auf Seite 223; sie wird im nächsten Abschnitt genauer besprochen. query_cache_limit
Die größte Ergebnismenge, die MySQL im Cache speichert. Abfragen, deren Ergebnis diese Einstellung überschreiten, gelangen nicht in den Cache. Denken Sie daran, dass der Server Ergebnisse beim Generieren im Cache ablegt, so dass er nicht im Voraus weiß, ob ein Ergebnis zu groß für den Cache wird. Wenn das Ergebnis die angegebene Grenze überschreitet, erhöht MySQL die Statusvariable Qcache_not_ cached und verwirft die Ergebnisse, die bisher in den Cache geschrieben wurden. Falls Sie wissen, dass dies oft vorkommt, können Sie den Hinweis SQL_NO_CACHE zu solchen Abfragen hinzufügen, die diesen Aufwand nicht verursachen sollen.
228 | Kapitel 5: Erweiterte MySQL-Funktionen
query_cache_wlock_invalidate
Gibt an, ob im Cache gespeicherte Ergebnisse ausgeliefert werden sollen, die sich auf Tabellen beziehen, die andere Verbindungen gesperrt haben. Der Vorgabewert lautet OFF, wodurch der Abfrage-Cache die Semantik des Servers ändert, weil er es Ihnen erlaubt, Daten aus dem Cache zu lesen, deren Tabelle von einer anderen Verbindung gesperrt wurde, was normalerweise nicht der Fall wäre. Wenn Sie diesen Wert auf ON ändern, lesen Sie zwar diese Daten nicht mehr, erhöhen aber möglicherweise die Wartezeiten auf die Sperren. Bei den meisten Anwendungen spielt das allerdings keine Rolle, so dass der Standardwert im Allgemeinen in Ordnung ist. Im Prinzip ist das Einstellen des Cache recht einfach. Komplizierter indes ist es, die Wirkungen Ihrer Änderungen zu verstehen. In den folgenden Abschnitten zeigen wir Ihnen, wie Sie den Abfrage-Cache beurteilen, um zu einer guten Entscheidung zu kommen.
Fragmentierung reduzieren Es gibt keine Möglichkeit, die Fragmentierung komplett zu vermeiden; wenn Sie jedoch den query_cache_min_res_unit-Wert sorgfältig wählen, dann tragen Sie zumindest dazu bei, dass kein Speicher im Abfrage-Cache verschwendet wird. Der Trick besteht darin, die Größe eines neuen Blocks gegen die Anzahl der Belegungen abzuwägen, die der Server durchführen muss, während er die Ergebnisse speichert. Wenn Sie diesen Wert zu klein wählen, dann verschwendet der Server weniger Speicher, muss aber die Blöcke öfter belegen, was wiederum mehr Arbeit für den Server bedeutet. Wenn Sie ihn zu groß wählen, verstärkt sich auch die Fragmentierung. Die Frage heißt also: Speicherverschwendung oder mehr CPU-Zyklen während der Zuordnung? Die beste Einstellung hängt von der Größe Ihres typischen Abfrageergebnisses ab. Sie können die durchschnittliche Größe der Abfragen im Cache ermitteln, indem Sie den verwendeten Speicher (ungefähr query_cache_size – Qcache_free_memory) durch den Wert der Statusvariablen Qcache_queries_in_cache teilen. Bei einem Mix aus großen und kleinen Ergebnissen sind Sie unter Umständen nicht in der Lage, eine Größe zu wählen, die sowohl Fragmentierung als auch zu viele Belegungen vermeidet. Sie werden aber wahrscheinlich Grund haben anzunehmen, dass es nichts bringt, die größeren Ergebnisse im Cache abzulegen. Sie können verhindern, dass große Ergebnisse im Cache gespeichert werden, indem Sie die Variable query_cache_limit verkleinern. Auf diese Weise erreichen Sie vermutlich einen besseren Ausgleich zwischen Fragmentierung und dem Aufwand, der nötig ist, um die Ergebnisse im Cache zu speichern. Eine Fragmentierung des Abfrage-Cache erkennen Sie, indem Sie die Statusvariable Qcache_free_blocks untersuchen, die Ihnen zeigt, wie viele Blöcke im Abfrage-Cache vom Typ FREE sind. In der letzten Konfiguration in Abbildung 5-2 gibt es zwei freie Blöcke. Die schlechtestmögliche Fragmentierung wäre diejenige, bei der es zwischen jedem Blockpaar, das zum Speichern von Daten verwendet wird, einen nur etwas zu kleinen freien Block gäbe, so dass jeder zweite Block ein freier Block wäre. Falls daher Qcache_free_blocks den Wert Qcache_total_blocks / 2 erreicht, ist Ihr Cache stark frag-
Der MySQL-Abfrage-Cache | 229
mentiert. Erhöht sich die Statusvariable Qcache_lowmem_prunes und haben Sie viele freie Blöcke, dann sorgt die Fragmentierung dafür, dass Abfragen vorzeitig aus dem Cache gelöscht werden. Sie können den Abfrage-Cache mit FLUSH QUERY CACHE defragmentieren. Dieser Befehl verdichtet den Abfrage-Cache, indem er alle Blöcke »nach oben« verschiebt und den freien Platz zwischen ihnen entfernt, wodurch am Ende ein einzelner freier Block übrig bleibt. Während der Ausführung blockiert der Befehl den Zugriff auf den Abfrage-Cache, wodurch praktisch der gesamte Server gesperrt wird. Wenn der Cache nicht wirklich sehr groß ist, läuft der Befehl allerdings recht schnell. Entgegen seinem Namen entfernt er keine Abfragen aus dem Cache; das erledigt RESET QUERY CACHE.
Die Verwendung des Abfrage-Cache verbessern Falls Ihr Abfrage-Cache nicht fragmentiert ist, Sie aber immer noch keine gute Trefferrate erreichen, dann haben Sie ihm möglicherweise zu wenig Speicher zugewiesen. Kann der Server keine freien Blöcke finden, die groß genug für einen neuen Block sind, muss er einige Abfragen aus dem Cache »herausschneiden«. Wenn der Server Cache-Einträge herausschneidet, erhöht er die Statusvariable Qcache_ lowmem_prunes. Steigt dieser Wert stark an, kann das einen dieser Gründe haben: • Gibt es viele freie Blöcke, dann ist wahrscheinlich die Fragmentierung schuld (siehe den vorhergehenden Abschnitt). • Gibt es weniger freie Blöcke, dann kann das bedeuten, dass bei Ihrer Arbeitslast ein größerer Cache benötigt wird, als Sie eingestellt haben. Sie ermitteln die Größe des nichtbenutzten Speichers im Cache mithilfe der Statusvariablen Qcache_free_memory. Wenn es viele freie Blöcke gibt, die Fragmentierung gering ist, es zu wenigen Kürzungen aufgrund geringen Speichers kommt und die Trefferrate immer noch niedrig ist, dann profitiert Ihre Arbeitslast möglicherweise kaum vom Cache. Irgendetwas verhindert, dass er benutzt wird. Wenn Sie viele Aktualisierungen haben, liegt es möglicherweise daran; vielleicht sind auch einfach Ihre Abfragen nicht cache-fähig. Haben Sie die Trefferrate gemessen und sind sich immer noch nicht sicher, ob der Server vom Abfrage-Cache profitiert, können Sie ihn deaktivieren und die Leistung untersuchen, ihn dann erneut aktivieren und überprüfen, ob sich die Leistung geändert hat. Um den Abfrage-Cache zu deaktivieren, setzen Sie query_cache_size auf 0. (Das globale Ändern von query_cache_type hat keinen Einfluss auf Verbindungen, die bereits geöffnet sind, und gibt dem Server den Speicher nicht zurück.) Sie können auch einen Benchmark-Test durchführen, allerdings ist es manchmal kompliziert, eine realistische Kombination aus im Cache gespeicherten Abfragen, nicht im Cache gespeicherten Abfragen und Aktualisierungen zu erhalten. Abbildung 5-3 zeigt ein Flussdiagramm mit einem einfachen Beispiel für das Vorgehen beim Analysieren und Einstellen des Abfrage-Cache Ihres Servers.
230 | Kapitel 5: Erweiterte MySQL-Funktionen
Start
Ja
Ist die Trefferrate akzeptabel?
Fertig
Nein Sind die meisten Abfragen nicht Cachefähig?
Ja
Ist query_cache_limit groß genug?
Ja
Nein
Erhöhe query_cache_limit
Nein
Gibt es viele Entwertungen?
Ja
Nein
Ist der Cache bereits aufgewärmt?
Nein
Ist der Cache fragmentiert?
Fertig. Abfragen können nicht im Cache gespeichert werden.
Ja
Verkleinere query_cache_min_res_unit oder defragmentiere mit FLUSH QUERY CACHE
Nein
Ja
Fertig. Abfragen wurden nie gesehen.
Wärmen wir den Cache auf
Wird wegen Speichermangels viel herausgeschnitten?
Ja
Vergrößere query_cache_size
Ja
Fertig. Arbeitslast eignet sich nicht für den Cache.
Nein
Gibt es viele Updates?
Nein Irgendetwas anderes ist falsch konfiguriert
Abbildung 5-3: Wie man den Abfrage-Cache analysiert und einstellt
InnoDB und der Abfrage-Cache InnoDB arbeitet aufgrund seiner MVCC-Implementierung mit dem Abfrage-Cache auf viel komplexere Art und Weise zusammen als andere Storage-Engines. In MySQL 4.0 wird der Abfrage-Cache in Transaktionen ganz und gar deaktiviert, seit MySQL 4.1 dagegen gibt InnoDB dem Server tabellenweise zu verstehen, ob eine Transaktion auf den
Der MySQL-Abfrage-Cache | 231
Abfrage-Cache zugreifen kann. Es steuert den Zugriff auf den Abfrage-Cache sowohl für Lesevorgänge (d.h. für Beziehen von Ergebnissen aus dem Cache) als auch für Schreibvorgänge (d.h. für Speichern von Ergebnissen im Cache). Faktoren, die den Zugriff bestimmen, sind die Transaktions-ID und die Frage, ob es Sperren auf der Tabelle gibt. Jede Tabelle im InnoDB-eigenen Data-Dictionary besitzt einen Transaktions-ID-Zähler. Transaktionen, deren IDs kleiner sind als der Zählerwert, dürfen bei Abfragen, die diese Tabelle betreffen, nicht aus dem Abfrage-Cache lesen oder in ihn schreiben. Auch Sperren auf einer Tabelle sorgen dafür, dass Abfragen, die auf sie zugreifen, nicht cache-fähig sind. Falls z.B. eine Transaktion eine SELECT FOR UPDATEAbfrage in einer Tabelle ausführt, dann darf für Abfragen, die diese Tabelle betreffen, keine andere Transaktion aus dem Abfrage-Cache lesen oder in ihn schreiben. Das geht erst wieder, wenn die Sperren aufgehoben wurden. Wenn die Transaktion bestätigt wird, aktualisiert InnoDB die Zähler für die Tabellen, auf die die Transaktion Sperren gelegt hat. Eine Sperre ist ein grober Anhaltspunkt dafür, ob die Transaktion die Tabelle modifiziert hat. Eine Transaktion kann Zeilen in einer Tabelle auch sperren, aber nicht aktualisieren. Es ist aber nicht möglich, dass sie den Inhalt der Tabelle modifiziert, ohne dass sie Sperren dafür anfordert. InnoDB setzt den Zähler jeder Tabelle auf die Transaktions-ID des Systems, die maximal mögliche Transaktions-ID. Das hat folgende Konsequenzen: • Der Zähler der Tabelle ist eine absolute untere Grenze, bis zu der Transaktionen den Abfrage-Cache benutzen können. Wenn die Transaktions-ID des Systems 5 ist, eine Transaktion Locks für die Zeilen in einer Tabelle beschafft und dann bestätigt wird, können die Transaktionen 1 bis 4 für Abfragen, die wieder diese Tabelle betreffen, niemals aus dem Abfrage-Cache lesen oder in ihn schreiben. • Der Zähler der Tabelle wird nicht auf die Transaktions-ID der Transaktion aktualisiert, in der die Zeilen gesperrt wurden, sondern auf die Transaktions-ID des Systems. Transaktionen, die Zeilen in Tabellen sperren, werden deshalb möglicherweise in der Zukunft bei Abfragen, die diese Tabelle betreffen, am Lesen oder Schreiben des Abschreibe-Cache gehindert. Speicherung, Abfrage und Entwertung des Abfrage-Cache geschehen auf der Serverebene. InnoDB kann das nicht umgehen oder verzögern. Allerdings kann InnoDB den Server ausdrücklich anweisen, Abfragen ungültig zu machen, die bestimmte Tabellen betreffen. Das ist notwendig, wenn eine Fremdschlüsselbeschränkung, wie etwa ON DELETE CASCADE, den Inhalt einer Tabelle verändert, die in einer Abfrage nicht erwähnt wird. Im Prinzip könnte InnoDBs MVCC-Architektur es erlauben, Abfragen aus dem Cache zu bedienen, wenn Änderungen an einer Tabelle die konsistente Lesesicht nicht beeinflussen, die andere Transaktionen sehen. Es wäre allerdings sehr kompliziert, so etwas zu implementieren. Der InnoDB-Algorithmus nimmt aus Gründen der Einfachheit Abkürzungen, auch wenn damit Transaktionen aus dem Abfrage-Cache ausgesperrt werden, obwohl das nicht unbedingt nötig wäre.
232 | Kapitel 5: Erweiterte MySQL-Funktionen
Allgemeine Abfrage-Cache-Optimierungen Viele Entscheidungen für den Schema-, Abfrage- und Anwendungsentwurf haben Einfluss auf den Abfrage-Cache. Neben den Dingen, die wir im vorhergehenden Abschnitt besprochen haben, sollten Sie Folgendes bedenken: • Es kann den Abfrage-Cache unterstützen, wenn man mehrere kleinere Tabellen anstelle einer großen Tabelle hat. Mit einem solchen Design kann man seine Entwertungsstrategie feiner ausarbeiten. Lassen Sie sich dadurch jedoch nicht übermäßig bei Ihrem Schemaentwurf beeinflussen, da andere Faktoren diesen Vorteil leicht aufwiegen können. • Es ist effizienter, Schreibvorgänge im Bündel auszuführen, da mit dieser Methode Cache-Einträge nur einmal entwertet werden. • Wir haben bemerkt, dass der Server für eine lange Zeit blockiert sein kann, während er Einträge im Abfrage-Cache entwertet oder einen sehr großen Abfrage-Cache beschneidet. Das war zumindest bis MySQL 5.1 der Fall. Eine einfache Lösung wäre es, query_cache_size nicht zu groß zu machen; etwa 256 MByte sollten mehr als genug sein. • Sie können den Abfrage-Cache nicht datenbank- oder tabellenweise kontrollieren, es ist aber möglich, einzelne Abfragen mit den Modifikatoren SQL_CACHE und SQL_NO_CACHE in die SELECT-Anweisung einzubeziehen oder aus ihr auszuschließen. Sie können den Abfrage-Cache auch für einzelne Verbindungen aktivieren oder deaktivieren, indem Sie die auf Sitzungsebene agierende Servervariable query_cache_ type auf den entsprechenden Wert setzen. • Bei einer schreibintensiven Anwendung könnten Sie eine bessere Leistung erzielen, wenn Sie den Abfrage-Cache vollständig ausschalten. Auf diese Weise werden Abfragen, die sowieso bald wieder entwertet werden, nicht erst im Cache abgelegt. Denken Sie daran, query_cache_size auf 0 zu setzen, wenn Sie ihn deaktivieren, damit der Cache keinen Speicher wegnimmt. Falls Sie den Abfrage-Cache für die meisten Abfragen weglassen wollen, dabei aber wissen, dass einige Abfragen deutlich vom Cache profitieren werden, können Sie die globale Variable query_cache_type auf DEMAND setzen und dann den Abfragen, die in den Cache gelangen sollen, den Hinweis SQL_CACHE hinzufügen. Sie haben dadurch zwar mehr Arbeit, aber auch eine exaktere Kontrolle über den Cache. Falls Sie andererseits die meisten Abfragen im Cache speichern und nur einige ausschließen wollen, können Sie ihnen SQL_NO_CACHE hinzufügen.
Alternativen zum Abfrage-Cache Der MySQL-Abfrage-Cache funktioniert nach dem Prinzip, dass die schnellste Abfrage diejenige ist, die Sie nicht ausführen müssen. Allerdings müssen Sie trotzdem die Abfrage aufrufen, und der Server bekommt auch etwas zu tun. Was wäre, wenn Sie für bestimmte Abfragen den Datenbankserver wirklich nicht ansprechen müssten? Clientseitige Cache-
Der MySQL-Abfrage-Cache | 233
Speicherung kann Ihnen helfen, die Arbeitslast auf Ihrem MySQL-Server weiter zu mindern. Mehr dazu erfahren Sie in Kapitel 10.
Code in MySQL speichern MySQL erlaubt es Ihnen, Code in Form von Triggern, gespeicherten Prozeduren und gespeicherten Funktionen innerhalb des Servers abzulegen. In MySQL 5.1 können Sie Code auch in regelmäßig wiederkehrenden Jobs, sogenannten Events, speichern. Gespeicherte Prozeduren und gespeicherte Funktionen werden zusammenfassend als »gespeicherte Routinen« bezeichnet. Alle vier Arten gespeicherten Codes benutzen eine besondere, erweiterte SQL-Sprache, die prozedurale Strukturen wie Schleifen und Bedingungen enthält.3 Der größte Unterschied zwischen den Arten gespeicherten Codes besteht in dem Kontext, in dem sie operieren – d.h. ihren Ein- und Ausgaben. Gespeicherte Prozeduren und gespeicherte Funktionen können Parameter akzeptieren und Ergebnisse zurückliefern, Trigger und Events dagegen nicht. Im Prinzip eignet sich gespeicherter Code sehr gut dazu, Code weiterzugeben und wiederzuverwenden. Giuseppe Maxia und andere haben eine Bibliothek mit allgemein einsetzbaren gespeicherten Routinen geschaffen, die Sie unter http://mysql-sr-lib.sourceforge .net finden. Es ist allerdings schwierig, gespeicherte Routinen aus anderen Datenbanksystemen wiederzuverwenden, weil die meisten ihre eigene Sprache haben (die Ausnahme ist DB2, das eine sehr ähnliche Sprache verwendet, die auf dem gleichen Standard beruht).4 Wir wollen gespeicherten Code hier nicht schreiben, sondern uns mehr auf seine Auswirkungen auf die Performance konzentrieren. Das O’Reilly-Buch MySQL Stored Procedure Programming (von Guy Harrison und Steven Feuerstein) könnte Ihnen weiterhelfen, falls Sie vorhaben, gespeicherte Prozeduren in MySQL zu schreiben. Es ist nicht schwer, sowohl Befürworter als auch Gegner gespeicherten Codes zu finden. Ohne irgendeine Partei zu ergreifen, zeigen wir hier einige der Vor- und Nachteile seines Einsatzes in MySQL. Zuerst die Vorteile: • Gespeicherter Code läuft dort, wo die Daten sind, Sie können also Bandbreite sparen und die Latenz verringern, indem Sie Aufgaben auf dem Datenbankserver ausführen. • Die Arbeit mit gespeichertem Code ist eine Form von Code-Recycling. Sie kann helfen, Geschäftsregeln zu zentralisieren, womit sich ein konsistentes Verhalten erreichen und Sicherheit und Ordnung durchsetzen lassen. 3 Die Sprache ist eine Teilmenge von SQL/PSM, dem Persistent-Stored-Modules-Teil des SQL-Standards. Sie ist in ISO/IEC 9075-4:2003 (E) definiert. 4 Es gibt außerdem einige Portierungsdienstprogramme wie etwa das tsql2mysql-Projekt (http://sourceforge.net/ projects/tsql2mysql) zum Portieren vom Microsoft SQL-Server.
234 | Kapitel 5: Erweiterte MySQL-Funktionen
• Gespeicherter Code kann Versionsstrategien und die Versionspflege erleichtern. • Er kann Sicherheitsvorteile bieten und eine Methode bereitstellen, um Rechte exakter zu kontrollieren. Ein verbreitetes Beispiel ist eine gespeicherte Prozedur für den Zahlungsverkehr in einer Bank: Die Prozedur überträgt das Geld innerhalb einer Transaktion und protokolliert die gesamte Operation für die Revision. Sie können es Anwendungen erlauben, die gespeicherte Prozedur aufzurufen, ohne Zugriff auf die zugrunde liegenden Tabellen zu gewähren. • Der Server speichert die Ausführungspläne der gespeicherten Prozedur im Cache, wodurch sich der Aufwand für wiederholte Aufrufe verringert. • Da der gespeicherte Code sich auf dem Server befindet und zusammen mit dem Server verteilt, gesichert und gewartet werden kann, ist er für Wartungsjobs geeignet. Er besitzt keine externen Abhängigkeiten, wie etwa Perl-Bibliotheken oder andere Software, die Sie lieber nicht auf den Server legen würden. • Er erlaubt die Arbeitsteilung zwischen Anwendungsentwicklern und Datenbankprogrammierern. Es könnte besser sein, wenn ein Datenbankexperte die gespeicherten Prozeduren schreibt, da nicht jeder Anwendungsprogrammierer die Fähigkeit besitzt, effiziente SQL-Abfragen zu schreiben. Zu den Nachteilen: • MySQL bietet keine guten Werkzeuge zum Entwickeln und Debuggen, weshalb es in MySQL schwieriger als in einigen anderen Datenbankservern ist, gespeicherten Code zu schreiben. • Die Sprache ist im Vergleich zu Anwendungssprachen langsam und primitiv. Die Anzahl der benutzbaren Funktionen ist eingeschränkt, und es ist schwer, komplexe Stringmanipulationen durchzuführen und eine komplexe Logik zu schreiben. • Gespeicherter Code kann das Verbreiten Ihrer Anwendung verkomplizieren. Zusätzlich zum Anwendungscode und zu den Änderungen am Datenbankschema müssen Sie auch noch Code weitergeben, der im Server gespeichert ist. • Da gespeicherte Routinen mit der Datenbank gespeichert werden, können Sie eine Sicherheitslücke schaffen. Nichtstandardisierte kryptografische Funktionen innerhalb einer gespeicherten Routine schützen z.B. Ihre Daten nicht, wenn die Datenbank kompromittiert wird. Wären die kryptografischen Funktionen im Code, müsste der Angreifer sowohl den Code als auch die Datenbank kompromittieren. • Das Speichern von Routinen verschiebt die Last auf den Datenbankserver, der typischerweise schwieriger zu skalieren ist und teurer ist als Anwendungs- oder Webserver. • MySQL bietet Ihnen nicht viel Kontrolle über die Ressourcen, die der gespeicherte Code belegen kann, so dass ein Fehler den Server zum Absturz bringen könnte. • Die MySQL-Implementierung gespeicherten Codes ist ziemlich eingeschränkt – Cache-Einträge für Ausführungspläne sind verbindungsbasiert, Cursor werden als temporäre Tabellen ausgeführt usw. (Wir gehen auf die Beschränkungen bestimmter Funktionen dann ein, wenn wir diese Funktionen beschreiben.)
Code in MySQL speichern | 235
• Es ist schwierig, Code mit gespeicherten Prozeduren in MySQL zu profilieren. Es ist schwierig, das Slow-Query-Log zu analysieren, wenn es nur CALL XYZ('A') anzeigt, weil Sie dann erst diese Prozedur suchen und sich die Anweisungen darin anschauen müssen. • Gespeicherter Code ist eine Methode, um Komplexität zu verbergen, wodurch sich die Entwicklung vereinfacht. Oft ist das aber schlecht für die Performance. Wenn Sie darüber nachdenken, gespeicherten Code zu verwenden, dann sollten Sie sich fragen, wo Sie Ihre Geschäftslogik unterbringen wollen: im Anwendungscode oder in der Datenbank? Beide Ansätze sind beliebt. Sie müssen sich nur bewusst sein, dass Sie Logik in die Datenbank setzen, wenn Sie gespeicherten Code verwenden.
Gespeicherte Prozeduren und Funktionen Die MySQL-Architektur und der Abfrageoptimierer erlegen der Art und Weise, wie Sie gespeicherte Routinen benutzen können, sowie ihrer Effizienz einige Beschränkungen auf. Momentan gelten folgende Einschränkungen: • Der Optimierer verwendet in gespeicherten Funktionen nicht den Modifikator DETERMINISTIC, um mehrfache Aufrufe innerhalb einer einzigen Abfrage wegzuoptimieren. • Der Optimierer kann momentan nicht abschätzen, wie viel es kosten wird, eine gespeicherte Funktion auszuführen. • Jede Verbindung hat ihren eigenen Ausführungsplan-Cache für gespeicherte Prozeduren. Wenn viele Verbindungen die gleiche Prozedur aufrufen, dann verschwenden sie Ressourcen, indem sie den gleichen Ausführungsplan immer wieder im Cache speichern. (Mit Verbindungs-Pooling oder persistenten Verbindungen kann der Ausführungsplan-Cache ein viel längeres nützliches Leben führen.) • Gespeicherte Routinen und Replikation sind eine verzwickte Kombination. Vermutlich wollen Sie den Aufruf der Routine nicht replizieren. Stattdessen wollen Sie die exakten Änderungen replizieren, die an Ihrer Datenmenge vorgenommen wurden. Die zeilenbasierte Replikation, die in MySQL 5.1 eingeführt wurde, hilft dabei, dieses Problem zu mildern. Wenn in MySQL 5.0 Binär-Logging aktiviert ist, wird der Server darauf bestehen, dass Sie entweder alle gespeicherten Prozeduren als DETERMINISTIC definieren oder dass Sie die mit dem komplizierten Namen log_bin_trust_ function_creators bezeichnete Serveroption aktivieren. Normalerweise halten wir gespeicherte Routinen klein und einfach. Wir führen die komplexe Logik gern außerhalb der Datenbank in einer prozeduralen Sprache aus, die vielseitig ist und größere Ausdrucksmöglichkeiten bietet. Sie kann Ihnen außerdem den Zugriff auf weitere Rechnerressourcen und potenziell sogar auf andere Formen der Cache-Speicherung bieten.
236 | Kapitel 5: Erweiterte MySQL-Funktionen
Allerdings können gespeicherte Prozeduren bei bestimmten Arten von Operationen – vor allem bei kleinen Abfragen – viel schneller sein. Wenn eine Abfrage klein genug ist, dann wird der Aufwand für das Parsen und die Kommunikation über das Netzwerk schnell zu einem wesentlichen Anteil der gesamten Arbeit, die erforderlich ist, um sie auszuführen. Um dies zu verdeutlichen, haben wir eine einfache gespeicherte Prozedur erzeugt, die eine bestimmte Anzahl von Zeilen in eine Tabelle einfügt. Hier ist der Code der Prozedur: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
DROP PROCEDURE IF EXISTS insert_many_rows; delimiter // CREATE PROCEDURE insert_many_rows (IN loops INT) BEGIN DECLARE v1 INT; SET v1=loops; WHILE v1 > 0 DO INSERT INTO test_table values(NULL,0, 'qqqqqqqqqqwwwwwwwwwweeeeeeeeeerrrrrrrrrrtttttttttt', 'qqqqqqqqqqwwwwwwwwwweeeeeeeeeerrrrrrrrrrtttttttttt'); SET v1 = v1 - 1; END WHILE; END; // delimiter ;
Mit einem Benchmark haben wir dann getestet, wie schnell diese gespeicherte Prozedur eine Million Zeilen in eine Tabelle einfügen könnte, verglichen mit dem Einfügen über eine Clientanwendung. Tabellenstruktur und Hardware sind eigentlich egal – wichtig ist die relative Geschwindigkeit der unterschiedlichen Ansätze. Nur so aus Spaß haben wir noch gemessen, wie schnell diese Abfragen ausgeführt wurden, wenn wir eine Verbindung über einen MySQL-Proxy verwendeten. Aus Gründen der Einfachheit führten wir den gesamten Benchmark auf einem einzigen Server aus, der sowohl die Clientanwendung als auch die MySQL-Proxy-Instanz enthielt. Tabelle 5-1 zeigt die Ergebnisse. Tabelle 5-1: Gesamtzeit für das Nacheinandereinfügen von einer Million Zeilen Methode
Gesamtzeit
Gespeicherte Prozedur
101 sek.
Clientanwendung
279 sek.
Clientanwendung mit MySQL-Proxy
307 sek.
Die gespeicherte Prozedur ist viel schneller. Das liegt hauptsächlich daran, dass sie den Aufwand für die Netzwerkkommunikation, das Parsen, Optimieren usw. vermeidet. Wir zeigen in »Die SQL-Schnittstelle für vorbereitete Anweisungen« auf Seite 245 eine typische gespeicherte Prozedur für Wartungsjobs.
Code in MySQL speichern | 237
Trigger Trigger erlauben es Ihnen, Code auszuführen, wenn eine INSERT-, UPDATE- oder DELETEAnweisung vorhanden ist. Sie können MySQL anweisen, sie auszuführen, bevor und/oder nachdem die auslösende Anweisung ausgeführt wurde. Sie können keine Werte zurückliefern, lesen und/oder ändern, wohl aber die Daten, die die triggernde Anweisung ändert. Aus diesem Grund können Sie Trigger einsetzen, um Randbedingungen oder eine Geschäftslogik durchzusetzen, die Sie ansonsten in den Clientcode schreiben müssten. Ein gutes Beispiel ist das Emulieren von Fremdschlüsseln in einer StorageEngine, die diese nicht unterstützt, wie z.B. MyISAM. Trigger können die Anwendungslogik vereinfachen und die Performance verbessern, da sie den Wechsel zwischen Client und Server sparen. Sie können außerdem beim automatischen Aktualisieren von denormalisierten Tabellen und Summary-Tabellen hilfreich sein. So verwendet sie z.B. die Sakila-Beispieldatenbank zum Pflegen der Tabelle film_text. Die MySQL-Trigger-Implementierung ist momentan noch nicht sehr umfangreich. Gehen Sie nicht davon aus, dass Sie Trigger genauso umfassend einsetzen können, wie Sie es möglicherweise von einem anderen Datenbankprodukt her gewöhnt sind: • Sie können für jedes Event nur einen Trigger pro Tabelle haben. (Mit anderen Worten: Sie können nicht zwei Trigger haben, die AFTER INSERT auslösen.) • MySQL unterstützt nur Trigger auf Zeilenebene – d.h., Trigger operieren immer FOR EACH ROW (sozusagen für jede Zeile einzeln) anstatt für die Anweisung als Ganzes. Für die Verarbeitung großer Datenmengen ist das nicht besonders effizient. Beachten Sie außerdem die folgenden allgemeinen Warnungen für Trigger in MySQL: • Trigger können verschleiern, was Ihr Server wirklich macht, da eine einzige Anweisung dafür sorgen kann, dass der Server eine Menge »unsichtbarer Arbeit« verrichtet. Falls z.B. ein Trigger eine verwandte Tabelle aktualisiert, kann es passieren, dass er die Anzahl der Zeilen verdoppelt, die eine Anweisung beeinflusst. • Es kann schwierig sein, Fehler in Triggern zu finden, und oft ist es schwer, Leistungsengpässe zu analysieren, wenn Trigger beteiligt sind. • Trigger können Deadlocks und Wartezeiten auf Sperren verursachen, die nicht offensichtlich sind. Wenn ein Trigger fehlschlägt, dann wird auch aus der ursprünglichen Abfrage nichts, und falls Ihnen gar nicht bewusst ist, dass ein Trigger existiert, werden Sie vermutlich Probleme haben, den Fehlercode zu entziffern. Bezüglich der Leistung ist das FOR EACH ROW-Design die stärkste Einschränkung der MySQL-Trigger-Implementierung. Es ist dafür verantwortlich, dass es manchmal recht unpraktisch ist, Trigger zum Pflegen von Summary- und Cache-Tabellen einzusetzen, weil sie zu langsam sein könnten. Der Hauptgrund für die Verwendung von Triggern anstelle einer regelmäßig wiederkehrenden Massenaktualisierung ist, dass mit Triggern Ihre Daten immer konsistent sind.
238 | Kapitel 5: Erweiterte MySQL-Funktionen
Trigger garantieren außerdem keine Atomizität. So kann z.B. ein Trigger, der eine MyISAM-Tabelle aktualisiert, nicht zurückgenommen werden, wenn in der Anweisung, die den Trigger auslöst, ein Fehler auftritt. Auch ein Trigger könnte einen Fehler verursachen. Nehmen Sie an, dass Sie einen AFTER UPDATE-Trigger an eine MyISAM-Tabelle anhängen und ihn verwenden, um eine andere MyISAM-Tabelle zu aktualisieren. Wenn der Trigger einen Fehler hat, der das Update der zweiten Tabelle scheitern lässt, wird das Update der ersten Tabelle nicht rückgängig gemacht. Trigger in InnoDB-Tabellen operieren innerhalb derselben Transaktion, so dass die Aktionen, die sie unternehmen, zusammen mit der Anweisung, die sie ausgelöst hat, atomar sind. Falls Sie jedoch mit InnoDB einen Trigger verwenden, um die Daten einer anderen Tabelle zu überprüfen, wenn Sie eine Beschränkung validieren, dann denken Sie an MVCC, da Sie ansonsten falsche Ergebnisse erhalten könnten. Nehmen Sie z.B. an, Sie wollen Fremdschlüssel emulieren, wollen jedoch nicht die InnoDB-Fremdschlüssel benutzen. Sie können einen BEFORE INSERT-Trigger schreiben, der die Existenz eines passenden Datensatzes in einer anderen Tabelle überprüft. Falls Sie allerdings beim Lesen aus der anderen Tabelle nicht SELECT FOR UPDATE in dem Trigger wählen, können nebenläufige Updates an dieser Tabelle unkorrekte Ergebnisse verursachen. Wir wollen Ihnen die Benutzung von Triggern jedoch nicht vermiesen. Im Gegenteil, sie können sehr nützlich sein, speziell für Beschränkungen, Systempflegeaufgaben und zum Synchronisieren von denormalisierten Daten. Sie können Trigger außerdem benutzen, um Änderungen an Zeilen zu protokollieren. Das ist z.B. praktisch für eigene Replikationseinrichtungen, bei denen Sie Systeme trennen, Daten ändern und die Änderungen dann wieder zusammenführen wollen. Betrachten Sie etwa eine Gruppe von Benutzern, die ihre Laptops mit zur Arbeit nehmen. Deren Änderungen müssen mit einer Master-Datenbank synchronisiert werden. Anschließend müssen die Master-Daten wieder zurück auf die einzelnen Laptops kopiert werden. Um dies zu erreichen, ist eine Zweiwegesynchronisation erforderlich. Trigger bieten eine gute Möglichkeit, solche Systeme aufzubauen. Jeder Laptop kann Trigger verwenden, um alle Datenmodifikationen in Tabellen zu protokollieren, die anzeigen, welche Zeilen geändert worden sind. Das eigene Synchronisationswerkzeug kann dann diese Änderungen auf die Master-Datenbank anwenden. Schließlich kann dann eine ganz normale MySQL-Replikation die Laptops mit dem Master synchronisieren, der die Änderungen von all den Laptops hat. Manchmal ist es sogar möglich, die FOR EACH ROW-Einschränkung zu umgehen. Roland Bouman hat festgestellt, dass ROW_COUNT( ) innerhalb eines Triggers immer 1 zurückgibt, mit Ausnahme der ersten Zeile eines BEFORE-Triggers. Diese Tatsache können Sie nutzen, um zu verhindern, dass der Code des Triggers bei jeder betroffenen Zeile ausgeführt wird, und um ihn nur einmal pro Anweisung auszuführen. Das ist nicht das Gleiche wie ein anweisungsbezogener Trigger, bietet aber die Möglichkeit, in einigen Fällen einen anweisungsbezogenen BEFORE-Trigger zu emulieren. Dieses Verhalten ist vielleicht ein Bug, den es irgendwann einmal nicht mehr geben wird. Seien Sie also vorsichtig, wenn
Code in MySQL speichern | 239
Sie darauf zurückgreifen, und vergewissern Sie sich, dass es nach einem Aufrüsten des Servers noch vorhanden ist. Setzen Sie diesen Hack z.B. so ein: CREATE TRIGGER fake_statement_trigger BEFORE INSERT ON sometable FOR EACH ROW BEGIN DECLARE v_row_count INT DEFAULT ROW_COUNT( ); IF v_row_count <> 1 THEN -- Ihr Code END IF; END;
Events Events sind eine neue Form gespeicherten Codes in MySQL 5.1. Sie gleichen cron-Jobs, befinden sich aber vollkommen im MySQL-Server. Man kann Events erzeugen, die SQLCode einmal zu einem angegebenen Zeitpunkt oder öfter in einem angegebenen Intervall ausführen. Das übliche Vorgehen sieht so aus, dass das komplexe SQL in einer gespeicherten Prozedur verpackt wird, so dass das Event lediglich einen CALL ausführen muss. Events werden in einem separaten Event-Scheduler-Thread ausgeführt, weil sie nichts mit den Verbindungen zu tun haben. Sie nehmen keine Eingaben entgegen und geben keine Werte zurück – es gibt für sie keine Verbindungen, über die sie Eingaben erhalten oder Werte zurückliefern könnten. Sie können im Server-Log – falls es aktiviert ist – die Befehle sehen, die sie ausführen, es lässt sich aber oft nur schwer feststellen, dass diese Befehle von einem Event ausgeführt wurden. Mithilfe der INFORMATION_SCHEMA.EVENTSTabelle ermitteln Sie den Status eines Events, wie etwa den letzten Zeitpunkt, zu dem es ausgeführt wurde. Für Events gelten ähnliche Überlegungen wie für gespeicherte Prozeduren: Sie verursachen dem Server zusätzliche Arbeit. Der Aufwand für das Event selbst ist minimal, allerdings kann das SQL, das es aufruft, ernste Auswirkungen auf die Leistung haben. Gute Anwendungen für Events sind unter anderem regelmäßig ausgeführte Wartungsarbeiten, der Neuaufbau von Cache- und Summary-Tabellen zum Emulieren von materialisierten Sichten oder das Speichern von Statuswerten für Überwachung und Diagnose. Das folgende Beispiel erzeugt ein Event, das einmal in der Woche eine gespeicherte Prozedur für eine bestimmte Datenbank ausführt:5 CREATE EVENT optimize_somedb ON SCHEDULE EVERY 1 WEEK DO CALL optimize_tables('somedb');
Sie können festlegen, ob Events auf Slave-Server repliziert werden sollen. Manchmal ist das angebracht, in anderen Fällen wiederum nicht. Nehmen Sie etwa das gezeigte Beispiel: Wahrscheinlich wollen Sie die Operation OPTIMIZE TABLE auf allen Slaves ausführen. 5 Wir zeigen Ihnen später, wie Sie diese gespeicherte Prozedur erzeugen.
240 | Kapitel 5: Erweiterte MySQL-Funktionen
Denken Sie allerdings daran, dass dies die Gesamtleistung des Servers beeinträchtigen könnte (z.B. mit Tabellen-Locks), wenn alle Slaves diese Operation gleichzeitig ausführen würden. Und falls schließlich ein periodisch wiederkehrendes Event lange braucht, bis es abgeschlossen ist, könnte es passieren, dass das Event schon wieder ausgelöst wird, obwohl seine vorhergehende Instanz noch läuft. MySQL bietet davor keinen Schutz, so dass Sie einen eigenen Code schreiben müssen, der so etwas ausschließt. Sie könnten z.B. GET_LOCK( ) einsetzen, um sicherzustellen, dass immer nur ein Event ausgeführt wird: CREATE EVENT optimize_somedb ON SCHEDULE EVERY 1 WEEK DO BEGIN DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; IF GET_LOCK('somedb', 0) THEN DO CALL optimize_tables('somedb'); END IF; DO RELEASE_LOCK('somedb'); END
Der »Dummy«-Continue-Handler stellt sicher, dass das Event die Sperre sogar dann freigibt, wenn die gespeicherte Prozedur eine Ausnahme auslöst. Events haben zwar nichts mit Verbindungen zu tun, sind aber mit Threads verknüpft. Es gibt einen Haupt-Event-Scheduler-Thread, den Sie in der Konfigurationsdatei Ihres Servers oder mit einem SET-Befehl aktivieren müssen: mysql> SET GLOBAL event_scheduler := 1;
Wenn dieser Thread aktiviert ist, erzeugt er zum Ausführen jedes Events einen neuen Thread. Innerhalb des Event-Codes liefert ein Aufruf von CONNECTION_ID( ) wie üblich einen eindeutigen Wert zurück – auch wenn es eigentlich keine »Verbindung« gibt. (Der Rückgabewert von CONNECTION_ID( ) ist tatsächlich nur die Thread-ID.) Sie können im Fehler-Log des Servers nach Informationen über die Ausführung von Events suchen.
Kommentare in gespeichertem Code schützen Gespeicherte Prozeduren, gespeicherte Funktionen, Trigger und Events können jeweils große Mengen Code enthalten, so dass es praktisch unerlässlich ist, diesen mit Kommentaren zu versehen. Allerdings sollten die Kommentare nicht innerhalb des Servers gespeichert werden, da der Kommandozeilenclient sie möglicherweise entfernt. (Diese Eigenart des Kommandozeilenclients ist wirklich ein Ärgernis, aber hey, c’est la vie.) Ein hilfreicher Trick, um die Kommentare in Ihrem gespeicherten Code zu schützen, ist die Verwendung von versionsspezifischen Kommentaren, die der Server als potenziell ausführbaren Code betrachtet (d.h. als Code, der nur dann ausgeführt wird, wenn die Versionsnummer des Servers dieser oder einer höheren Version entspricht). Server- und Clientprogramme wissen, dass dies keine einfachen Kommentare sind, und verwerfen sie
Code in MySQL speichern | 241
deshalb nicht. Um zu verhindern, dass der »Code« ausgeführt wird, benutzen Sie einfach eine sehr hohe Versionsnummer, wie etwa 99999. Wir wollen z.B. hier unser Trigger-Beispiel ein wenig dokumentieren, um den Schleier etwas zu lüften: CREATE TRIGGER fake_statement_trigger BEFORE INSERT ON irgendeine_tabelle FOR EACH ROW BEGIN DECLARE v_row_count INT DEFAULT ROW_COUNT( ); /*!99999 ROW_COUNT( ) ist 1 ausser fuer die erste Zeile, so dass dies nur einmal pro Anweisung ausgefuehrt wird. */ IF v_row_count <> 1 THEN -- Hier kommt Ihr Code hin END IF; END;
Cursor MySQL bietet momentan schreibgeschützte, vorwärtsgerichtete, serverseitige Cursor, die Sie nur aus einer gespeicherten MySQL-Prozedur heraus benutzen können. Sie erlauben es Ihnen, Abfrageergebnisse Zeile für Zeile durchzugehen und alle Zeilen zur weiteren Verarbeitung in Variablen zu bringen. Eine gespeicherte Prozedur kann gleichzeitig mehrere Cursor offen halten. Außerdem können Sie Cursor in Schleifen »schachteln«. In Zukunft könnte MySQL Cursor anbieten, die sich aktualisieren lassen, in aktuellen Versionen gibt es sie jedoch noch nicht. Cursor sind schreibgeschützt, d.h., lassen sich nur lesen, weil sie über temporäre Tabellen iterieren und nicht über die Tabellen, aus denen die Daten eigentlich stammen. Das Cursordesign von MySQL hält für unachtsame Anwender einige Fallstricke bereit. Da Cursor mit temporären Tabellen implementiert werden, bieten sie den Entwicklern ein falsches Gefühl von Effizienz. Das Wichtigste, was Sie wissen müssen, ist, dass ein Cursor die gesamte Abfrage ausführt, wenn Sie ihn öffnen. Betrachten Sie folgende Prozedur: 1 CREATE PROCEDURE bad_cursor( ) 2 BEGIN 3 DECLARE film_id INT; 4 DECLARE f CURSOR FOR SELECT film_id FROM sakila.film; 5 OPEN f; 6 FETCH f INTO film_id; 7 CLOSE f; 8 END
Dieses Beispiel zeigt, dass Sie einen Cursor schließen können, bevor Sie alle seine Ergebnisse durchlaufen. Ein Entwickler, der an Oracle oder Microsoft SQL Server gewöhnt ist, sieht möglicherweise nichts Falsches in dieser Prozedur, in MySQL verursacht sie hingegen eine Menge unnützer Arbeit. Wenn Sie diese Prozedur mit SHOW STATUS profilieren,
242 | Kapitel 5: Erweiterte MySQL-Funktionen
dann sehen Sie, dass sie 1.000 Indexlesevorgänge und 1.000 Einfügeoperationen durchführt. Das liegt daran, dass 1.000 Zeilen in sakila.film stehen. Alle 1.000 Lese- und Schreibvorgänge geschehen, wenn Zeile 5 und bevor Zeile 6 ausgeführt wird. Die Moral von der Geschicht’ lautet, dass Sie eigentlich keine Arbeit sparen, wenn Sie einen Cursor, der Daten aus einer großen Ergebnismenge holt, frühzeitig schließen. Falls Sie wirklich nur einige Zeilen benötigen, dann verwenden Sie LIMIT. Cursor können außerdem dafür sorgen, dass MySQL zusätzliche Ein-/Ausgabeoperationen ausführt, und sie können sehr langsam sein. Da temporäre Tabellen, die im Speicher liegen, die Typen BLOB und TEXT nicht unterstützen, muss MySQL für Cursor über Ergebnisse mit diesen Typen eine temporäre Tabelle auf der Festplatte anlegen. Und auch wenn diese Typen nicht enthalten sind, erzeugt MySQL die temporäre Tabelle auf der Festplatte, nämlich dann, wenn diese Tabelle größer als tmp_table_size ist. MySQL unterstützt keine clientseitigen Cursor, allerdings enthält die Client-API Funktionen, die clientseitige Cursor emulieren, indem sie das gesamte Ergebnis in den Speicher holen. Das ist eigentlich nichts anderes, als wenn Sie das Ergebnis in ein Array in Ihrer Anwendung setzen und es dort manipulieren. In »Das MySQL-Client/Server-Protokoll« auf Seite 173 erfahren Sie mehr darüber, welche Auswirkungen es auf die Leistung hat, wenn Sie das gesamte Ergebnis in den clientseitigen Speicher holen.
Vorbereitete Anweisungen MySQL 4.1 und neuere Versionen unterstützen serverseitige vorbereitete Anweisungen, die ein verbessertes binäres Client/Server-Protokoll verwenden, um Daten effizient zwischen Client und Server hin- und herzuschicken. Sie greifen auf die Funktionalität der vorbereiteten Anweisungen über eine Programmierbibliothek zu, die das neue Protokoll unterstützt, wie etwa die MySQL-C-API. Die Bibliotheken MySQL Connector/J und MySQL Connector/NET bieten die gleichen Fähigkeiten wie Java bzw. .NET. Es gibt außerdem eine SQL-Schnittstelle für vorbereitete Anweisungen, auf die wir später eingehen. Wenn Sie eine vorbereitete Anweisung anlegen, sendet die Clientbibliothek dem Server einen Prototyp der eigentlichen Abfrage, die Sie benutzen wollen. Der Server analysiert und verarbeitet dieses Abfrage-»Skelett«, speichert eine Struktur, die die teilweise optimierte Abfrage repräsentiert, und gibt ein Anweisungs-Handle an den Client zurück. Die Clientbibliothek kann die Abfrage dann wiederholt ausführen, indem sie das Anweisungs-Handle angibt. Vorbereitete Anweisungen können Parameter haben, bei denen es sich um Platzhalter in Form von Fragezeichen für Werte handelt, die Sie angeben, wenn Sie die Anweisung ausführen. So könnten Sie die folgende Abfrage vorbereiten: mysql> INSERT INTO tbl(col1, col2, col3) VALUES (?, ?, ?) ;
Vorbereitete Anweisungen | 243
Sie führen diese Abfrage dann aus, indem Sie das Anweisungs-Handle an den Server senden und dabei Werte für die einzelnen Platzhalter mitliefern. Das können Sie dann so oft wiederholen, wie Sie wollen. Wie genau Sie das Anweisungs-Handle an den Server senden, hängt von Ihrer Programmiersprache ab. Eine Möglichkeit besteht darin, die MySQL-Konnektoren für Java und .NET zu benutzen. Viele Clientbibliotheken, die mit den MySQL-C-Bibliotheken verknüpft sind, bieten ebenfalls irgendeine Schnittstelle zum Binärprotokoll. Lesen Sie einfach die Dokumentation Ihrer MySQL-API. Aus verschiedenen Gründen kann es effizienter sein, vorbereitete Anweisungen zu verwenden, als eine Abfrage wiederholt auszuführen: • Der Server muss die Abfrage nur einmal analysieren, was das Parsen und einige andere Dinge spart. • Der Server muss einige Schritte zur Abfrageoptimierung nur einmal durchführen, da er einen teilweisen Abfrageausführungsplan im Cache ablegt. • Die Parameter über das Binärprotokoll zu senden ist effizienter als das Senden als ASCII-Text. So kann z.B. ein DATE-Wert in nur 3 Bytes gesendet werden und benötigt nicht 10 Bytes wie in ASCII. Die größten Ersparnisse gibt es für BLOB- und TEXTWerte, die in Stücken anstatt in einem riesigen Datenbatzen gesendet werden können. Das Binärprotokoll spart daher auch Speicher auf dem Client, reduziert den Netzwerkverkehr und den Aufwand beim Konvertieren zwischen dem dateneigenen Speicherformat und dem Format des nichtbinären Protokolls. • Zur jeweiligen Ausführung müssen nur die Parameter und nicht der gesamte Abfragetext gesendet werden, was den Netzwerkverkehr ebenfalls verringert. • MySQL speichert die Parameter direkt in Puffern auf dem Server. Der Server muss deshalb die Werte nicht im Speicher herumkopieren. Vorbereitete Anweisungen sind darüber hinaus gut für die Sicherheit. Es ist nicht notwendig, Werte in der Anwendung zu schützen oder zu quotieren. Das ist bequem und verkleinert die Gefahr durch SQL-Injection und andere Angriffe. (Sie dürfen niemals einer Benutzereingabe vertrauen, selbst wenn Sie vorbereitete Anweisungen verwenden.) Sie können das Binärprotokoll nur mit vorbereiteten Anweisungen einsetzen. Werden Abfragen über die normale mysql_query( )-API-Funktion aufgerufen, kommt nicht das Binärprotokoll zum Einsatz. Viele Clientbibliotheken erlauben es Ihnen, Anweisungen mit Fragezeichenplatzhaltern »vorzubereiten« und geben dann die Werte für jede Ausführung an. Allerdings emulieren diese Bibliotheken häufig nur den Vorbereiten-Ausführen-Zyklus im clientseitigen Code und senden eigentlich jede Abfrage mit mysql_query( ) an den Server.
Optimierung von vorbereiteten Anweisungen MySQL legt teilweise Abfrageausführungspläne für vorbereitete Anweisungen im Cache ab. Allerdings sind manche Optimierungen von den tatsächlichen Werten abhängig, die an die einzelnen Parameter gebunden sind, und können deshalb nicht vorab berechnet
244 | Kapitel 5: Erweiterte MySQL-Funktionen
und im Cache gespeichert werden. Man kann die Optimierungen in drei Typen unterteilen, die jeweils darauf basieren, wann sie ausgeführt werden müssen. Die folgende Liste ist zurzeit gültig, kann sich aber künftig ändern: Zum Vorbereitungszeitpunkt Der Server analysiert den Abfragetext, eliminiert Negationen und schreibt Unterabfragen um. Bei der ersten Ausführung Der Server vereinfacht geschachtelte Joins und wandelt OUTER JOIN in INNER JOIN um, wo das möglich ist. Bei jeder Ausführung Der Server tut Folgendes: • Er kürzt Partitionen. • Er eliminiert COUNT( ), MIN( ) und MAX( ), wo das möglich ist. • Er entfernt konstante Unterausdrücke. • Er sucht konstante Tabellen. • Er propagiert Gleichheiten. • Er analysiert und optimiert die Zugriffsmethoden ref, range und index_merge. • Er optimiert die Join-Reihenfolge. In Kapitel 4 finden Sie mehr Informationen über diese Optimierungen.
Die SQL-Schnittstelle für vorbereitete Anweisungen Es gibt seit MySQL 4.1 eine SQL-Schnittstelle für vorbereitete Anweisungen. Hier ist ein Beispiel dafür, wie Sie eine vorbereitete Anweisung über SQL ansprechen: mysql> SET @sql := 'SELECT actor_id, first_name, last_name -> FROM sakila.actor WHERE first_name = ?'; mysql> PREPARE stmt_fetch_actor FROM @sql; mysql> SET @actor_name := 'Penelope'; mysql> EXECUTE stmt_fetch_actor USING @actor_name; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 1 | PENELOPE | GUINESS | | 54 | PENELOPE | PINKETT | | 104 | PENELOPE | CRONYN | | 120 | PENELOPE | MONROE | +----------+------------+-----------+ mysql> DEALLOCATE PREPARE stmt_fetch_actor;
Wenn der Server diese Anweisungen empfängt, übersetzt er sie in die gleichen Operationen, die auch von der Clientbibliothek aufgerufen worden wären. Das bedeutet, dass Sie nicht das spezielle Binärprotokoll benutzen müssen, um vorbereitete Anweisungen zu erzeugen und auszuführen.
Vorbereitete Anweisungen | 245
Wie Sie sehen können, ist die Syntax etwas seltsamer, verglichen mit einer einfachen, direkten SELECT-Anweisung. Worin besteht also der Vorteil, wenn Sie eine vorbereitete Anweisung auf diese Weise einsetzen? Der wichtigste Anwendungsfall sind gespeicherte Prozeduren. In MySQL 5.0 können Sie vorbereitete Anweisungen in gespeicherten Prozeduren benutzen; die Syntax ähnelt derjenigen der SQL-Schnittstelle. Das bedeutet, dass Sie »dynamisches SQL« in gespeicherten Prozeduren aufbauen und ausführen können, indem Sie Strings aneinanderhängen. Die gespeicherten Prozeduren werden dadurch viel flexibler. Hier sehen Sie z.B. eine gespeicherte Prozedur, die OPTIMIZE TABLE in jeder Tabelle in einer angegebenen Datenbank aufrufen kann: DROP PROCEDURE IF EXISTS optimize_tables; DELIMITER // CREATE PROCEDURE optimize_tables(db_name VARCHAR(64)) BEGIN DECLARE t VARCHAR(64); DECLARE done INT DEFAULT 0; DECLARE c CURSOR FOR SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = db_name AND TABLE_TYPE = 'BASE TABLE'; DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done = 1; OPEN c; tables_loop: LOOP FETCH c INTO t; IF done THEN CLOSE c; LEAVE tables_loop; END IF; SET @stmt_text := CONCAT("OPTIMIZE TABLE ", db_name, ".", t); PREPARE stmt FROM @stmt_text; EXECUTE stmt; DEALLOCATE PREPARE stmt; END LOOP; CLOSE c; END// DELIMITER ;
Sie können diese gespeicherte Prozedur folgendermaßen anwenden: mysql> CALL optimize_tables('sakila');
Hier ist eine andere Möglichkeit, die Schleife in der Prozedur zu schreiben: REPEAT FETCH c INTO t; IF NOT done THEN SET @stmt_text := CONCAT("OPTIMIZE TABLE ", db_name, ".", t); PREPARE stmt FROM @stmt_text; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; UNTIL done END REPEAT;
246 | Kapitel 5: Erweiterte MySQL-Funktionen
Es gibt einen wichtigen Unterschied zwischen den beiden Schleifenkonstrukten: REPEAT prüft für jede Schleife zweimal die Schleifenbedingung. Das verursacht in diesem Beispiel wahrscheinlich kein großes Performance-Problem, da wir hier lediglich den Wert eines Integers überprüfen. Es könnte bei komplexeren Tests allerdings kostenintensiver werden. Das Verketten von Strings, um auf Tabellen und Datenbanken zu verweisen, ist eine gute Anwendung für die SQL-Schnittstelle in vorbereiteten Anweisungen, da Sie damit Anweisungen schreiben können, die nicht mit Parametern arbeiten. Man kann Datenbank- und Tabellennamen nicht parametrisieren, weil es sich hierbei um Identifikatoren handelt. Ein anderes Szenario ist das dynamische Einstellen einer LIMIT-Klausel, die Sie ebenfalls nicht mit einem Parameter angeben können. Die SQL-Schnittstelle eignet sich zum manuellen Testen einer vorbereiteten Anweisung, außerhalb von gespeicherten Prozeduren ist sie dagegen nicht so nützlich. Da die Schnittstelle über SQL agiert, benutzt sie nicht das Binärprotokoll. Außerdem sorgt sie kaum für eine Verringerung des Netzwerkverkehrs, weil man zusätzliche Abfragen aufrufen muss, um die Variablen einzustellen, wenn es Parameter gibt. In besonderen Fällen können Sie davon profitieren, diese Schnittstelle zu benutzen, etwa wenn Sie einen riesigen SQLString vorbereiten, den Sie sehr oft ohne Parameter ausführen werden. Allerdings sollten Sie auf jeden Fall einen Benchmark-Test durchführen, wenn Sie der Meinung sind, dass der Einsatz der SQL-Schnittstelle für vorbereitete Anweisungen Ihnen Arbeit erspart.
Die Grenzen vorbereiteter Anweisungen Für vorbereitete Anweisungen gelten einige Grenzen und Warnungen: • Vorbereitete Anweisungen gehören lokal zu einer Verbindung. Eine andere Verbindung kann also nicht das gleiche Handle benutzen. Aus dem gleichen Grund verliert ein Client, der eine Verbindung schließt und anschließend neu etabliert, die Anweisungen. (Verbindungs-Pooling oder persistente Verbindungen können dieses Problem mildern.) • In MySQL-Versionen vor 5.1 können vorbereitete Anweisungen den MySQLAbfrage-Cache nicht benutzen. • Es ist nicht immer effizienter, vorbereitete Anweisungen zu verwenden. Wenn Sie eine vorbereitete Anweisung nur einmal benutzen, dann benötigen Sie wahrscheinlich mehr Zeit für die Vorbereitung, als wenn Sie sie einfach als normales SQL einsetzen würden. Das Vorbereiten einer Anweisung erfordert darüber hinaus eine zusätzliche Reise zum Server. • Momentan können Sie eine vorbereitete Anweisung nicht innerhalb einer gespeicherten Funktion anwenden (allerdings können Sie vorbereitete Anweisungen in gespeicherten Prozeduren benutzen). • Es ist möglich, versehentlich eine vorbereitete Anweisung »entweichen« zu lassen, wenn Sie vergessen, sie freizugeben. Das kann auf dem Server eine Menge Ressour-
Vorbereitete Anweisungen | 247
cen belegen. Und da es eine einzige globale Grenze für die Anzahl der vorbereiteten Anweisungen gibt, könnte ein solcher Fehler dazu führen, dass andere Verbindungen bei ihrem Einsatz von vorbereiteten Anweisungen gestört werden.
Benutzerdefinierte Funktionen MySQL unterstützt bereits lange benutzerdefinierte Funktionen (User-Defined Functions; UDFs). Im Gegensatz zu gespeicherten Funktionen, die in SQL geschrieben sind, können Sie UDFs in allen Programmiersprachen schreiben, die C-Aufrufkonventionen unterstützen. UDFs müssen kompiliert und dynamisch mit dem Server verknüpft werden, wodurch sie plattformspezifisch werden und Ihnen viel Macht verleihen. UDFs können sehr schnell sein und auf einen Großteil der Funktionalität des Betriebssystems und der verfügbaren Bibliotheken zugreifen. Gespeicherte SQL-Funktionen eignen sich gut für einfache Operationen wie das Berechnen der Orthodrome6 zwischen zwei Punkten auf dem Globus. Falls Sie dagegen Netzwerkpakete verschicken wollen, benötigen Sie eine benutzerdefinierte Funktion. Auch Aggregatfunktionen, die Sie momentan in SQL nicht herstellen können, sind mit einer UDF möglich. Große Macht bringt aber auch große Verantwortung mit sich. Ein Fehler in Ihrer UDF kann Ihren gesamten Server zum Absturz bringen, den Speicher des Servers und/oder Ihre Daten beschädigen und ganz allgemein den gleichen Schaden anrichten wie potenziell jeder wildgewordene C-Code. Im Gegensatz zu gespeicherten Funktionen, die in SQL geschrieben sind, können UDFs momentan keine Tabellen lesen und schreiben – zumindest nicht im gleichen transaktionellen Kontext wie die Anweisung, die sie aufgerufen hat. Das bedeutet, dass sie eher für reine Berechnungen oder die Interaktion mit der Außenwelt von Nutzen sind. MySQL erhält immer mehr Möglichkeiten für die Interaktion mit Ressourcen außerhalb des Servers. Die Funktionen, die Brian Aker und Patrick Galbraith erzeugt haben, um mit memcached (http://tangent.org/586/Memcached_Functions_for_ MySQL.html) zu kommunizieren, sind ein gutes Beispiel dafür, wie das mit UDFs erreicht werden kann.
Falls Sie UDFs benutzen, überprüfen Sie sorgfältig die Änderungen zwischen den MySQL-Versionen, wenn Sie aufrüsten, da sie möglicherweise neu kompiliert oder sogar geändert werden müssen, um korrekt mit dem neuen MySQL-Server zu funktionieren. Sorgen Sie außerdem dafür, dass Ihre UDFs absolut Thread-sicher sind, da sie innerhalb des MySQL-Serverprozesses, einer reinen Multithread-Umgebung, ausgeführt werden.
6 Eine Orthodrome ist die kürzeste Verbindung zwischen zwei Punkten auf einer Kugeloberfläche (siehe http://de.wikipedia.org/wiki/Orthodrome).
248 | Kapitel 5: Erweiterte MySQL-Funktionen
Es gibt gute Bibliotheken mit vorgefertigten UDFs für MySQL sowie viele gute Beispiele dafür, wie man eigene implementiert. Die größte Sammlung von UDFs finden Sie unter http://www.mysqludf.org. Hier folgt der Code für die UDF NOW_USEC( ), mit der wir die Replikationsgeschwindigkeit messen (siehe »Wie schnell ist die Replikation?« auf Seite 441): #include <my_global.h> #include <my_sys.h> #include <mysql.h> #include #include #include #include
<stdio.h> <sys/time.h>
extern "C" { my_bool now_usec_init(UDF_INIT *initid, UDF_ARGS *args, char *message); char *now_usec( UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error); } my_bool now_usec_init(UDF_INIT *initid, UDF_ARGS *args, char *message) { return 0; } char *now_usec(UDF_INIT *initid, UDF_ARGS *args, char *result, unsigned long *length, char *is_null, char *error) { struct timeval tv; struct tm* ptm; char time_string[20]; /* e.g. "2006-04-27 17:10:52" */ char *usec_time_string = result; time_t t; /* Tageszeit abfragen und in eine tm-Struktur konvertieren. */ gettimeofday (&tv, NULL); t = (time_t)tv.tv_sec; ptm = localtime (&t); /* Datum und Zeit bis auf die Sekunde runterformatieren. */ strftime (time_string, sizeof (time_string), "%Y-%m-%d %H:%M:%S", ptm); /* Formatierte Zeit in Sekunden mit Dezimalpunkt und Mikrosekunden ausgeben. */ sprintf(usec_time_string, "%s.%06ld\n", time_string, tv.tv_usec); *length = 26; return(usec_time_string); }
Benutzerdefinierte Funktionen | 249
Sichten Sichten sind eine beliebte Datenbankfunktion, die in MySQL 5.0 hinzugefügt wurde. Eine Sicht (View) in MySQL ist eine Tabelle, die selbst keine Daten speichert. Stattdessen werden die Daten »in« der Tabelle aus einer SQL-Abfrage abgeleitet. In diesem Buch wird nicht erläutert, wie man Sichten erzeugt oder anwendet; lesen Sie dazu den entsprechenden Abschnitt im MySQL-Handbuch, bzw. suchen Sie Beschreibungen für die Benutzung von Sichten in anderen Dokumentationen. MySQL behandelt eine Sicht für viele Zwecke genau wie eine Tabelle, außerdem teilen sich Sichten und Tabellen in MySQL den gleichen Namensraum; dennoch behandelt MySQL sie nicht identisch. So kann man z.B. keine Trigger in Sichten haben und kann eine Sicht auch nicht mit dem Befehl DROP TABLE verwerfen. Es ist wichtig, die interne Implementierung von Sichten und ihre Interaktion mit dem Abfrageoptimierer zu verstehen, da man sonst möglicherweise keine gute Performance mit ihnen erreicht. Wir demonstrieren anhand der Beispieldatenbank world, wie Sichten funktionieren: mysql> CREATE VIEW Oceania AS -> SELECT * FROM Country WHERE Continent = 'Oceania' -> WITH CHECK OPTION;
Am einfachsten implementiert der Server eine Sicht, indem er seine SELECT-Anweisung ausführt und das Ergebnis in eine temporäre Tabelle legt. Er kann sich dann auf die temporäre Tabelle beziehen, wenn der Name der Sicht in der Abfrage auftaucht. Damit Sie sehen, wie das funktionieren würde, schauen Sie sich folgende Abfrage an: mysql> SELECT Code, Name FROM Oceania WHERE Name = 'Australia';
Und so könnte der Server sie ausführen – der Name der temporären Tabelle steht hier nur zur Demonstration: mysql> CREATE TEMPORARY TABLE TMP_Oceania_123 AS -> SELECT * FROM Country WHERE Continent = 'Oceania'; mysql> SELECT Code, Name FROM TMP_Oceania_123 WHERE Name = 'Australia';
Es gibt bei diesem Ansatz offensichtlich Probleme mit der Performance und der Abfrageoptimierung. Eine bessere Methode, um Sichten zu implementieren, besteht darin, eine Abfrage umzuschreiben, die sich auf die Sicht bezieht, wobei das SQL der Sicht mit dem SQL der Abfrage zusammengefasst wird. Das folgende Beispiel zeigt, wie die Abfrage aussehen könnte, nachdem MySQL sie mit der Definition der Sicht gemischt hat: mysql> SELECT Code, Name FROM Country -> WHERE Continent = 'Oceania' AND Name = 'Australia';
MySQL kann beide Methoden anwenden. Es ruft die beiden Algorithmen MERGE und TEMPTABLE auf und versucht nach Möglichkeit, den MERGE-Algorithmus anzuwenden. MySQL kann sogar geschachtelte Sichtdefinitionen zusammenfassen, wenn eine Sicht auf einer anderen Sicht aufbaut. Sie sehen die Ergebnisse der neuen Abfrage mit EXPLAIN EXTENDED, gefolgt von SHOW WARNINGS.
250 | Kapitel 5: Erweiterte MySQL-Funktionen
Wenn eine Sicht den TEMPTABLE-Algorithmus benutzt, zeigt EXPLAIN sie normalerweise als DERIVED-Tabelle. Abbildung 5-4 verdeutlicht die beiden Implementierungen. Merge-Algorithmus
Temptable-Algorithmus
Client
Benutzer stellt Abfrage an Sicht
Client
Benutzer stellt Abfrage an Sicht SQL
SQL
Server fängt Abfrage ab
Server fängt Abfrage ab
Sicht
Server fasst Sicht-SQL und Abfrage-SQL zusammen
Sicht
SQL
SQL
SQL
Server liefert Ergebnis an Client zurück
Server liefert Ergebnis an Client zurück Server schreibt Abfrage so um, dass sie sich auf die temporäre Tabelle bezieht
SQL
Server führt die Abfrage des Benutzers an der temporären Tabelle aus
Server führt SQL an zugrundeliegenden Tabellen aus
SQL
Server speichert die Ergebnisse in einer temporären Tabelle mit der gleichen Struktur wie die Sicht
Server
Server führt das Sicht-SQL an den zugrundeliegenden Tabellen aus
Data
Server
Abbildung 5-4: Zwei Implementierungen von Sichten
MySQL verwendet TEMPTABLE, wenn die Definition der Sicht GROUP BY, DISTINCT, Aggregatfunktionen, UNION, Unterabfragen oder irgendein anderes Konstrukt enthält, das keine Eins-zu-eins-Beziehung zwischen den Zeilen der zugrunde liegenden Basistabellen und den Zeilen bewahrt, die aus der Sicht zurückgeliefert werden. Diese Liste ist nicht vollständig und kann sich in Zukunft auch noch ändern. Falls Sie wissen wollen, ob eine Sicht MERGE oder TEMPTABLE verwendet, sollten Sie eine triviale SELECT-Abfrage mit EXPLAIN anhand der Sicht profilieren: mysql> EXPLAIN SELECT * FROM ; +----+-------------+ | id | select_type | +----+-------------+ | 1 | PRIMARY | | 2 | DERIVED | +----+-------------+
Das Vorhandensein des Select-Typs DERIVED zeigt an, dass die Sicht den TEMPTABLE-Algorithmus benutzt.
Sichten | 251
Update-fähige Sichten Eine update-fähige Sicht erlaubt es Ihnen, die zugrunde liegenden Basistabellen über die Sicht zu aktualisieren. Solange bestimmte Bedingungen erfüllt sind, können Sie in einer Sicht wie in einer normalen Tabelle mit UPDATE, DELETE und sogar INSERT agieren. Dies ist z.B. eine gültige Operation: mysql> UPDATE Oceania SET Population = Population * 1.1 WHERE Name = 'Australia';
Eine Sicht ist nicht update-fähig, wenn sie GROUP BY, UNION, eine Aggregatfunktion oder einige andere Ausnahmen enthält. Eine Abfrage, die Daten ändert, könnte ein Join enthalten, allerdings müssen die Spalten, die geändert werden sollen, sich alle in einer einzigen Tabelle befinden. Sichten, die den TEMPTABLE-Algorithmus nutzen, sind nicht update-fähig. Die CHECK OPTION-Klausel, die wir der Sicht im vorangegangenen Abschnitt hinzugefügt haben, stellt sicher, dass Zeilen, die durch die Sicht geändert wurden, auch nach der Änderung der WHERE-Klausel der Sicht entsprechen. Wir können also weder die ContinentSpalte ändern noch eine Zeile einfügen, die einen anderen Continent besitzt. Beides würde den Server zu einer Fehlermeldung veranlassen: mysql> UPDATE Oceania SET Continent = 'Atlantis'; ERROR 1369 (HY000): CHECK OPTION failed 'world.Oceania'
Manche Datenbankprodukte erlauben INSTEAD OF-Trigger in Sichten, so dass man exakt definieren kann, was passiert, wenn eine Anweisung versucht, die Daten einer Sicht zu modifizieren. Allerdings gehört MySQL nicht dazu. Einige der Beschränkungen von MySQL in Bezug auf update-fähige Sichten verschwinden vielleicht in Zukunft, wodurch interessante und nützliche Anwendungen ermöglicht werden. Eine Möglichkeit wäre, Merge-Tabellen über Tabellen mit unterschiedlichen Storage-Engines herzustellen. Diese Art der Nutzung von Sichten wäre sehr leistungsfähig und sinnvoll.
Auswirkungen von Sichten auf die Leistung Die meisten Leute denken beim Einsatz von Sichten nicht an die Performance, allerdings kann ihre Benutzung tatsächlich die Leistung in MySQL verbessern. Man kann mit ihnen auch andere Leistungsverbesserungen unterstützen. Wenn Sie z.B. ein Schema so umbauen, dass in einigen Stadien Sichten verwendet werden, dann ermöglichen Sie es, dass ein Teil des Codes weiterarbeitet, während Sie die Tabelle ändern, auf die er zugreift. Manche Anwendungen benutzen eine Tabelle pro Benutzer – im Allgemeinen, um eine Form von Sicherheit auf Zeilenebene zu implementieren. Eine Sicht, die ähnlich derjenigen ist, die wir zuvor gezeigt haben, könnte eine vergleichbare Sicherheit innerhalb einer einzigen Tabelle bieten, und wenn weniger Tabellen offen sind, verbessert sich auch die Leistung. Viele Open-Source-Projekte, die in riesigen Hosting-Umgebungen eingesetzt werden, sammeln mit der Zeit Millionen von Tabellen an und könnten daher von diesem Ansatz profitieren. Hier ist ein Beispiel für einen hypothetischen Blog-Hosting-Datenbankserver:
252 | Kapitel 5: Erweiterte MySQL-Funktionen
CREATE VIEW blog_posts_for_user_1234 AS SELECT * FROM blog_posts WHERE user_id = 1234 WITH CHECK OPTION;
Mithilfe von Sichten können Sie Spaltenberechtigungen implementieren, ohne dass Sie tatsächlich den Aufwand auf sich nehmen müssen, diese Berechtigungen anzulegen, der ziemlich hoch sein kann. Spaltenberechtigungen verhindern außerdem, dass Abfragen an der Tabelle im Abfrage-Cache gespeichert werden. Eine Sicht kann den Zugriff auf die gewünschten Spalten beschränken, ohne dass diese Probleme verursacht: CREATE VIEW public.employeeinfo AS SELECT firstname, lastname -- but not socialsecuritynumber FROM private.employeeinfo; GRANT SELECT ON public.* TO public_user;
Manchmal bieten auch pseudotemporäre Sichten gute Ergebnisse. Es ist eigentlich nicht möglich, eine wirklich temporäre Sicht zu erzeugen, die nur für die Dauer Ihrer aktuellen Verbindung besteht. Allerdings können Sie eine Sicht unter einem speziellen Namen, vielleicht in einer extra dafür reservierten Datenbank, erzeugen, die Sie später guten Gewissens verwerfen. Anschließend benutzen Sie die Sicht in der FROM-Klausel fast genauso wie eine Unterabfrage in der FROM-Klausel. Die beiden Ansätze sind theoretisch gleich, allerdings hat MySQL für Sichten eine andere Codebasis, so dass Sie aus der temporären Sicht wahrscheinlich eine bessere Leistung ziehen. Hier ist ein Beispiel: -- Assuming 1234 is the result of CONNECTION_ID( ) CREATE VIEW temp.cost_per_day_1234 AS SELECT DATE(ts) AS day, sum(cost) AS cost FROM logs.cost GROUP BY day; SELECT c.day, c.cost, s.sales FROM temp.cost_per_day_1234 AS c INNER JOIN sales.sales_per_day AS s USING(day); DROP VIEW temp.cost_per_day_1234;
Beachten Sie, dass wir die Verbindungs-ID als eindeutiges Suffix verwendet haben, um Namenskonflikte zu vermeiden. Dieser Ansatz erleichtert das Aufräumen, falls die Anwendung abstürzt und die temporäre Sicht nicht wegwirft. In »Fehlende temporäre Tabellen« auf Seite 429 finden Sie weitere Informationen über diese Technik. Sichten, die den TEMPTABLE-Algorithmus benutzen, funktionieren unter Umständen schlecht (allerdings immer noch besser als eine äquivalente Abfrage ohne Sicht). MySQL führt sie als rekursiven Schritt beim Optimieren der äußeren Abfrage aus, bevor die äußere Abfrage vollständig optimiert wurde. Sie haben deshalb nicht so viel von den Optimierungen, wie Sie vielleicht von anderen Datenbankprodukten gewöhnt sind. Der Abfrage, die die temporäre Tabelle aufbaut, werden keine WHERE-Bedingungen von der äußeren Abfrage übergeben, und die temporäre Tabelle enthält keine Indizes. Hier ist ein Beispiel, wieder mit der Sicht temp.cost_per_day_1234:
Sichten | 253
mysql> SELECT c.day, c.cost, s.sales -> FROM temp.cost_per_day_1234 AS c -> INNER JOIN sales.sales_per_day AS s USING(day) -> WHERE day BETWEEN '2007-01-01' AND '2007-01-31';
Was passiert wirklich in dieser Abfrage? Der Server führt die Sicht aus und setzt das Ergebnis in eine temporäre Tabelle. Anschließend führt er die sales_per_day-Tabelle in einem Join mit dieser temporären Tabelle zusammen. Die BETWEEN-Beschränkung in der WHERE-Klausel wird nicht in die Sicht »hineingedrückt«, so dass die Sicht eine Ergebnismenge für alle Daten in der Tabelle und nicht nur für den gewünschten Monat erzeugt. Die temporäre Tabelle enthält darüber hinaus keine Indizes. In diesem Beispiel ist das kein Problem: Der Server setzt die temporäre Tabelle an die erste Stelle in der Join-Reihenfolge, so dass der Join den Index in der sales_per_day-Tabelle benutzen kann. Falls Sie allerdings zwei solcher Sichten mit einem Join zusammenführen, würde der Join nicht mit irgendwelchen Indizes optimiert werden. Sie sollten immer Benchmark-Tests oder wenigstens ein ausführliches Profiling durchführen, wenn Sie versuchen, die Performance mithilfe von Sichten zu verbessern. Selbst MERGE-Sichten verursachen Mehraufwand, und es lässt sich nur schwer vorhersagen, wie eine Sicht die Leistung beeinflusst. Wenn die Leistung wichtig ist, dann raten Sie nicht, sondern messen Sie, und zwar immer. Sichten verursachen einige Probleme, die nicht MySQL-spezifisch sind. Entwickler nehmen möglicherweise an, dass sie einfach sind, obwohl sie in Wirklichkeit ziemlich kompliziert sind. Ein Entwickler, der die zugrunde liegende Komplexität nicht verstanden hat, denkt sich vielleicht nichts dabei, wenn wiederholt etwas abgefragt wird, was wie eine Tabelle aussieht, aber tatsächlich eine teure Sicht ist. Uns sind Fälle begegnet, in denen eine augenscheinlich einfache Abfrage Hunderte von Zeilen EXPLAIN-Ausgabe verursacht hat, weil es sich bei einer oder mehreren der »Tabellen«, auf die sie sich bezog, eigentlich um eine Sicht gehandelt hat, die auf viele weitere Tabellen und Sichten verwies.
Beschränkungen von Sichten MySQL bietet keine Unterstützung für die materialisierten Sichten, an die Sie möglicherweise von anderen Datenbankservern her gewöhnt sind. (Eine materialisierte Sicht speichert ihre Ergebnisse im Allgemeinen in einer unsichtbaren Tabelle hinter den Kulissen; diese Tabelle wird regelmäßig aus den Quelldaten heraus aktualisiert.) MySQL unterstützt auch keine indizierten Sichten. Sie können materialisierte und/oder indizierte Sichten jedoch mithilfe von Cache- und Summary-Tabellen simulieren; in MySQL 5.1 lassen sich diese Aufgaben mit Events steuern. Die MySQL-Implementierung der Sichten birgt einige Ärgernisse. Das größte Ärgernis besteht darin, dass MySQL das SQL Ihrer Originalsicht nicht bewahrt. Falls Sie also jemals versuchen, eine Sicht zu bearbeiten, indem Sie SHOW CREATE VIEW ausführen, und das resultierende SQL ändern, wartet eine hässliche Überraschung auf Sie: Die Abfrage wird auf das komplett kanonisierte und quotierte interne Format erweitert, ohne Formatierung, Kommentare und Einrückungen.
254 | Kapitel 5: Erweiterte MySQL-Funktionen
Falls Sie eine Sicht bearbeiten müssen und die hübsch anzusehende Abfrage verloren haben, die Sie ursprünglich benutzt hatten, um die Sicht zu erzeugen, dann suchen Sie in der letzten Zeile der .frm-Datei der Sicht. Wenn Sie die FILE-Berechtigung haben und die .frm-Datei von allen Benutzern gelesen werden kann, können Sie den Inhalt der Datei sogar mit der LOAD_FILE( )-Funktion laden. Mit ein wenig Stringmanipulation bekommen Sie – wiederum dank der Kreativität von Roland Bouman – Ihren Originalcode heil wieder zurück: mysql> SELECT -> REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( -> REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( -> SUBSTRING_INDEX(LOAD_FILE('/var/lib/mysql/world/Oceania.frm'), -> '\nsource=', -1), -> '\\_','\_'), '\\%','\%'), '\\\\','\\'), '\\Z','\Z'), '\\t','\t'), -> '\\r','\r'), '\\n','\n'), '\\b','\b'), '\\\"','\"'), '\\\'','\''), -> '\\0','\0') -> AS source; +-------------------------------------------------------------------------+ | source | +-------------------------------------------------------------------------+ | SELECT * FROM Country WHERE continent = 'Oceania' | | WITH CHECK OPTION | | | +-------------------------------------------------------------------------+
Zeichensätze und Sortierreihenfolgen Ein Zeichensatz ist eine Zuordnung von Binärkodierungen auf eine definierte Menge von Symbolen; stellen Sie es sich als Darstellung eines bestimmten Alphabets in Form von Bits vor. Die Sortierreihenfolge (auch Kollation) bezeichnet eine Reihe von Sortierregeln für einen Zeichensatz. Seit MySQL 4.1 kann jeder zeichenbasierte Wert einen Zeichensatz und eine Sortierreihenfolge besitzen.7 Die MySQL-Unterstützung für Zeichensätze und Kollationen ist erstklassig, kann aber die Komplexität erhöhen und in manchen Fällen auf Kosten der Leistung gehen. In diesem Abschnitt werden die Einstellungen und Funktionen erklärt, die Sie für die meisten Situationen benötigen. Falls Sie die esoterischeren Einzelheiten interessieren, dann lesen Sie das MySQL-Handbuch.
Wie MySQL Zeichensätze benutzt Zeichensätze können mehrere Sortierreihenfolgen aufweisen; außerdem besitzt jeder Zeichensatz eine vorgegebene Sortierreihenfolge. Sortierreihenfolgen wiederum gehören zu einem bestimmten Zeichensatz und können nicht zusammen mit einem anderen verwen-
7 Bis MySQL 4.0 gab es eine globale Einstellung für den gesamten Server, und man konnte zwischen mehreren 8-Bit-Zeichensätzen wählen.
Zeichensätze und Sortierreihenfolgen | 255
det werden. Sie benutzen einen Zeichensatz und eine Sortierreihenfolge immer zusammen, weshalb wir beide von nun an zusammenfassend als Zeichensatz bezeichnen. MySQL besitzt eine Vielzahl von Optionen, mit denen Zeichensätze kontrolliert werden. Die Optionen und die Zeichensätze werden leicht verwechselt, behalten Sie deshalb diese Unterscheidung im Hinterkopf: Nur zeichenbasierte Werte können wirklich einen Zeichensatz »haben«. Alles andere ist nur eine Einstellung, die angibt, welcher Zeichensatz für Vergleiche und andere Operationen verwendet werden soll. Ein zeichenbasierter Wert kann der Wert sein, der in einer Spalte gespeichert ist, ein Literal in einer Abfrage, das Ergebnis eines Ausdrucks, eine Benutzervariable usw. Die MySQL-Einstellungen lassen sich in zwei Klassen unterteilen: Vorgaben zum Erzeugen von Objekten und Einstellungen, die die Kommunikation zwischen Server und Client steuern.
Vorgabewerte zum Erzeugen von Objekten MySQL hat für den Server, für jede Datenbank und für jede Tabelle einen vorgegebenen Zeichensatz und eine Sortierreihenfolge. Diese formen eine Hierarchie aus Vorgabewerten, die den Zeichensatz beeinflussen, der verwendet wird, wenn Sie eine Spalte erzeugen. Das wiederum teilt dem Server mit, welcher Zeichensatz für die Werte benutzt werden soll, die Sie in der Spalte speichern. Sie können in jeder Stufe der Hierarchie explizit einen Zeichensatz angeben. Es ist aber auch möglich, dem Server die Wahl der passenden Vorgabe zu überlassen: • Wenn Sie eine Datenbank erzeugen, übernimmt er die serverweite character_set_ server-Einstellung. • Wenn Sie eine Tabelle erzeugen, übernimmt er sie aus der Datenbank. • Wenn Sie eine Spalte erzeugen, übernimmt er sie aus der Tabelle. Denken Sie daran, dass Spalten die einzige Stelle sind, in der MySQL Werte speichert, so dass die höheren Ebenen in der Hierarchie nur Vorgabewerte darstellen. Der vorgegebene Zeichensatz einer Tabelle beeinflusst die Werte nicht, die in den Tabellen gespeichert sind, sondern teilt MySQL nur mit, welchen Zeichensatz es benutzen soll, wenn Sie eine Spalte erzeugen, ohne explizit einen Zeichensatz anzugeben.
Einstellungen für die Client/Server-Kommunikation Wenn Server und Client miteinander kommunizieren, senden sie möglicherweise Daten in unterschiedlichen Zeichensätzen hin und her. Der Server übersetzt sie entsprechend: • Der Server nimmt an, dass der Client Anweisungen in dem Zeichensatz sendet, der durch character_set_client angegeben ist. • Nachdem der Server eine Anweisung vom Client empfangen hat, übersetzt er sie in den Zeichensatz, der durch character_set_connection angegeben ist. Diese Einstellung benutzt er außerdem, um festzustellen, wie Zahlen in Strings übersetzt werden sollen.
256 | Kapitel 5: Erweiterte MySQL-Funktionen
• Wenn der Server Ergebnisse oder Fehlermeldungen an den Client zurückliefert, übersetzt er sie in character_set_result. Abbildung 5-5 verdeutlicht diesen Vorgang. Server Konvertiert character_set_client in character_set_connection
Anweisung Verarbeitet Abfrage Ergebnis Client Konvertiert character_set_connection in character_set_client
Abbildung 5-5: Client- und Serverzeichensätze
Sie können diese Einstellungen bei Bedarf mit den Anweisungen SET NAMES bzw. SET CHARACTER SET ändern. Beachten Sie allerdings, dass dieser Befehl nur die Einstellungen des Servers beeinflusst. Das Clientprogramm und die Client-API müssen ebenfalls korrekt eingestellt sein, um Kommunikationsprobleme mit dem Server zu vermeiden. Nehmen Sie einmal an, dass Sie eine Clientverbindung mit Latin1 (dem vorgegebenen Zeichensatz, falls Sie ihn nicht mit mysql_options( ) geändert haben) öffnen und dann dem Server mit SET NAMES utf8 mitteilen, dass er annehmen soll, dass der Client seine Daten in UTF-8 sendet. Sie haben eine Fehlanpassung der Zeichensätze geschaffen, die Fehler und sogar Sicherheitsprobleme verursachen kann. Sie sollten den Zeichensatz des Clients einstellen und mysql_real_escape_string( ) benutzen, wenn Sie Werte schützen. In PHP ändern Sie den Zeichensatz des Clients mit mysql_set_charset( ).
Wie MySQL Werte vergleicht Wenn MySQL zwei Werte mit unterschiedlichen Zeichensätzen vergleicht, muss es sie für den Vergleich in denselben Zeichensatz konvertieren. Wenn die Zeichensätze nicht kompatibel sind, kann ein Fehler auftreten, wie etwa »ERROR 1267 (HY000): Illegal mix of collations«. In diesem Fall müssen Sie im Allgemeinen explizit die Funktion CONVERT( ) benutzen, um einen der Werte in einen Zeichensatz zu zwingen, der kompatibel zu dem anderen ist. Seit MySQL 5.0 wird diese Konvertierung oft implizit vorgenommen, so dass dieser Fehler eher aus MySQL 4.1 bekannt ist. MySQL weist den Werten außerdem eine sogenannte Anpassbarkeit (Coercibility) zu. Diese ermittelt die Priorität des Zeichensatzes eines Wertes und beeinflusst, welchen
Zeichensätze und Sortierreihenfolgen | 257
Wert MySQL implizit konvertiert. Ihnen stehen die Funktionen CHARSET( ), COLLATION( ) und COERCIBILITY( ) zur Verfügung, um Fehler im Zusammenhang mit Zeichensätzen und Sortierreihenfolgen zu beheben. Sie können Introducer und Collate-Klauseln benutzen, um den Zeichensatz und/oder die Sortierreihenfolge für literale Werte in Ihren SQL-Anweisungen anzugeben. Zum Beispiel: mysql> SELECT _utf8 'hello world' COLLATE utf8_bin; +--------------------------------------+ | _utf8 'hello world' COLLATE utf8_bin | +--------------------------------------+ | hello world | +--------------------------------------+
Besondere Verhaltensweisen Die Verhaltensweisen der MySQL-Zeichensätze halten einige Überraschungen bereit. Auf folgende Dinge sollten Sie achten: Die magische character_set_database-Einstellung Die Einstellung character_set_database entspricht standardmäßig dem vorgegebenen Wert der Datenbank. Wenn Sie Ihre Standarddatenbank ändern, ändert sich auch dieser Wert. Falls Sie die Verbindung zum Server ohne vorgegebene Datenbank vornehmen, wird standardmäßig character_set_server eingestellt. LOAD DATA INFILE LOAD DATA INFILE interpretiert eingehende Daten entsprechend der aktuellen Einstellung von character_set_database. Manche Versionen von MySQL akzeptieren eine optionale CHARACTER SET-Klausel in der Anweisung LOAD DATA INFILE, darauf sollten
Sie sich aber nicht verlassen. Wir haben festgestellt, dass die beste Methode, um zuverlässige Ergebnisse zu bekommen, darin besteht, mit USE die gewünschte Datenbank anzugeben, SET NAMES auszuführen, um einen Zeichensatz zu wählen, und dann nur die Daten zu laden. MySQL interpretiert alle geladenen Daten mit dem gleichen Zeichensatz, ungeachtet der Zeichensätze, die tatsächlich für die Zielspalten festgelegt wurden. SELECT INTO OUTFILE
MySQL schreibt alle Daten aus SELECT INTO OUTFILE, ohne sie zu konvertieren. Es gibt momentan keine Möglichkeit, einen Zeichensatz für die Daten einzustellen, ohne jede Spalte in eine CONVERT( )-Funktion einzupacken. Eingebettete Escape-Folgen MySQL interpretiert Escape-Folgen in Anweisungen entsprechend character_ set_client, selbst wenn es einen Introducer oder eine Collate-Klausel gibt. Das liegt daran, dass der Parser die Escape-Folgen in literalen Werten interpretiert. Der Parser achtet nicht auf die Sortierreihenfolge – sofern es ihn betrifft, ist ein Introducer kein Befehl, sondern nur ein Token.
258 | Kapitel 5: Erweiterte MySQL-Funktionen
Einen Zeichensatz und eine Sortierreihenfolge wählen Ab Version 4.1 unterstützt MySQL eine große Anzahl von Zeichensätzen und Sortierreihenfolgen. Dazu gehören auch Multibyte-Zeichen mit der UTF-8-Kodierung des UnicodeZeichensatzes (MySQL unterstützt eine drei Byte große Teilmenge des vollständigen UTF 8, das die meisten Zeichen aus den meisten Sprachen speichern kann). Sie können sich die unterstützten Zeichensätze mit den Befehlen SHOW CHARACTER SET und SHOW COLLATION anzeigen lassen.
Machen Sie es sich einfach Ein Mix aus Zeichensätzen in Ihrer Datenbank kann eine echte Plage sein. Inkompatible Zeichensätze sind in der Regel schrecklich verwirrend. Sie können gut funktionieren, bis bestimmte Zeichen in Ihren Daten auftauchen. Ab diesem Moment kommt es in allen möglichen Operationen zu Problemen (etwa in Joins zwischen Tabellen). Sie können diese Probleme nur lösen, indem Sie ALTER TABLE einsetzen, um die Spalten in kompatible Zeichensätze umzuwandeln oder indem Sie Werte mit Introducern und Collate-Klauseln in den SQL-Anweisungen in den gewünschten Zeichensatz bringen. Vernünftigerweise ist es am besten, wenn man auf der Serverebene und möglicherweise auf der Datenbankebene sinnvolle Standardwerte wählt. Dann kann man nämlich besondere Ausnahmen fallweise behandeln, z.B. auf Spaltenebene.
Die gebräuchlichsten Entscheidungen für Sortierreihenfolgen (Kollationen) sind, ob die Buchstaben unter Beachtung der Groß- und Kleinschreibung sortiert werden sollen oder entsprechend dem Binärwert der Kodierung. Die Namen der Kollationen enden im Allgemeinen mit _cs (case sensitive, d.h. unter Beachtung von Groß-/Kleinschreibung), _ci (case insensitive, d.h. nicht unter Beachtung von Groß-/Kleinschreibung) oder _bin, so dass Sie sie leicht auseinanderhalten können. Wenn Sie einen Zeichensatz explizit festlegen, müssen Sie nicht sowohl den Zeichensatz als auch die Sortierreihenfolge angeben. Falls Sie eine oder beide Angaben weglassen, setzt MySQL die fehlenden Werte aus der passenden Vorgabe ein. Tabelle 5-2 zeigt, wie MySQL über Zeichensatz und Sortierreihenfolge entscheidet. Tabelle 5-2: Wie MySQL Vorgaben für Zeichensätze und Sortierreihenfolgen ermittelt Sie legen fest
Resultierender Zeichensatz
Resultierende Sortierreihenfolge
Sowohl den Zeichensatz als auch die Sortierreihenfolge
Wie angegeben
Wie angegeben
Nur den Zeichensatz
Wie angegeben
Vorgegebene Sortierreihenfolge des Zeichensatzes
Nur die Sortierreihenfolge
Zeichensatz, zu der die Sortierreihenfolge gehört
Wie angegeben
Nichts
Passende Vorgabe
Passende Vorgabe
Zeichensätze und Sortierreihenfolgen | 259
Die folgenden Befehle zeigen, wie man eine Datenbank, eine Tabelle und eine Spalte mit explizit angegebenen Zeichensätzen und Sortierreihenfolgen erzeugt: CREATE DATABASE d CHARSET latin1; CREATE TABLE d.t( col1 CHAR(1), col2 CHAR(1) CHARSET utf8, col3 CHAR(1) COLLATE latin1_bin ) DEFAULT CHARSET=cp1251;
Die Spalten der resultierenden Tabelle weisen folgende Sortierreihenfolgen auf: mysql> SHOW FULL COLUMNS FROM d.t; +-------+---------+-------------------+ | Field | Type | Collation | +-------+---------+-------------------+ | col1 | char(1) | cp1251_general_ci | | col2 | char(1) | utf8_general_ci | | col3 | char(1) | latin1_bin | +-------+---------+-------------------+
Wie Zeichensätze und Sortierreihenfolgen Abfragen beeinflussen Manche Zeichensätze erfordern möglicherweise mehr CPU-Operationen, verbrauchen mehr Speicher und Festplattenplatz oder verhindern sogar eine Indizierung. Das sollten Sie bei der Wahl der Zeichensätze und Sortierreihenfolgen beachten. Das Konvertieren zwischen Zeichensätzen oder Sortierreihenfolgen kann den Overhead für bestimmte Operationen vergrößern. So besitzt z.B. die sakila.film-Tabelle einen Index in der title-Spalte, der ORDER BY-Abfragen beschleunigen kann: mysql> EXPLAIN SELECT title, release_year FROM sakila.film ORDER BY title\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film type: index possible_keys: NULL key: idx_title key_len: 767 ref: NULL rows: 953 Extra:
Allerdings kann der Server den Index nur dann für die Sortierung benutzen, wenn er mit der gleichen Sortierreihenfolge sortiert ist, wie in der Abfrage angegeben. Der Index wird mit der Sortierreihenfolge der Spalte sortiert, in diesem Fall also utf8_general_ci. Sollen die Ergebnisse nach einer anderen Sortierreihenfolge sortiert werden, muss der Server ein Filesort durchführen: mysql> EXPLAIN SELECT title, release_year -> FROM sakila.film ORDER BY title COLLATE utf8_bin\G *************************** 1. row ***************************
260 | Kapitel 5: Erweiterte MySQL-Funktionen
id: select_type: table: type: possible_keys: key: key_len: ref: rows: Extra:
1 SIMPLE film ALL NULL NULL NULL NULL 953 Using filesort
MySQL muss nicht nur die Vorgaben bezüglich des Zeichensatzes der Verbindung und anderer Angaben beachten, die Sie explizit in Ihren Abfragen getroffen haben, sondern muss auch Zeichensätze konvertieren, damit es sie vergleichen kann, falls sie nicht gleich sind. Falls Sie z.B. zwei Tabellen aus Zeichenspalten mit ungleichen Zeichensätzen mit einem Join verbinden, muss MySQL einen der beiden Zeichensätze konvertieren. Diese Umwandlung kann den Einsatz eines Index verhindern, da sie sich genau wie eine Funktion verhält, die die Spalte einschließt. Der UTF-8-Multibyte-Zeichensatz speichert die einzelnen Zeichen mit einer unterschiedlichen Anzahl von Bytes (zwischen einem und drei). MySQL verwendet intern für viele Stringoperationen Puffer fester Größe, muss also genug Platz für die maximal mögliche Länge reservieren. So erfordert z.B. ein CHAR(10), das mit UTF-8 kodiert ist, 30 Bytes für die Speicherung, selbst wenn der eigentliche String keine sogenannten Wide-CharacterZeichen enthält. Für Felder variabler Länge (VARCHAR, TEXT) macht das auf der Festplatte nichts aus, temporäre Tabellen im Speicher, die zum Verarbeiten und Sortieren von Abfragen benutzt werden, reservieren dagegen immer die maximal benötigte Länge. In Multibyte-Zeichensätzen ist ein Zeichen nicht mehr das Gleiche wie ein Byte. Infolgedessen besitzt MySQL separate LENGTH( )- und CHAR_LENGTH( )-Funktionen, die für Multibyte-Zeichen nicht die gleichen Ergebnisse zurückliefern. Wenn Sie mit MultibyteZeichensätzen arbeiten, sollten Sie darauf achten, dass Sie die Funktion CHAR_LENGTH( ) benutzen, wenn Sie Zeichen zählen wollen (z.B. wenn Sie SUBSTRING( )-Operationen durchführen). Der gleiche Hinweis gilt für Multibyte-Zeichen in Anwendungssprachen. Eine weitere mögliche Überraschung sind Indexbeschränkungen. Wenn Sie eine UTF-8Spalte indizieren, muss MySQL davon ausgehen, dass jedes Zeichen bis zu drei Bytes annehmen kann, so dass die üblichen Längenbeschränkungen plötzlich um den Faktor drei schrumpfen: mysql> CREATE TABLE big_string(str VARCHAR(500), KEY(str)) DEFAULT CHARSET=utf8; Query OK, 0 rows affected, 1 warning (0.06 sec) mysql> SHOW WARNINGS; +---------+------+---------------------------------------------------------+ | Level | Code | Message | +---------+------+---------------------------------------------------------+ | Warning | 1071 | Specified key was too long; max key length is 999 bytes | +---------+------+---------------------------------------------------------+
Zeichensätze und Sortierreihenfolgen | 261
Beachten Sie, dass MySQL den Index automatisch auf ein 333-Zeichen-Präfix gekürzt hat: mysql> SHOW CREATE TABLE big_string\G *************************** 1. row *************************** Table: big_string Create Table: CREATE TABLE `big_string` ( `str` varchar(500) default NULL, KEY `str` (`str`(333)) ) ENGINE=MyISAM DEFAULT CHARSET=utf8
Falls Sie die Warnung nicht bemerkt und die Tabellendefinition überprüft haben, ist Ihnen möglicherweise entgangen, dass der Index nur auf einem Präfix der Spalte angelegt wurde. Das hat Nebenwirkungen wie etwa das Deaktivieren abdeckender Indizes. Manche Leute empfehlen, einfach global UTF-8 zu verwenden, um »sich das Leben zu erleichtern«. Das ist jedoch eine eher schlechte Idee, wenn Sie eine gute Performance erwarten. Viele Anwendungen benötigen überhaupt kein UTF-8, und je nach Ihren Daten kann UTF-8 auf der Festplatte sehr viel Speicherplatz in Anspruch nehmen. Wenn Sie sich für einen Zeichensatz entscheiden, müssen Sie die Art der Daten in Betracht ziehen, die Sie speichern werden. Falls Sie etwa hauptsächlich englische Texte speichern, gibt es bei der Speicherung praktisch keinen Aufschlag aufgrund der Verwendung von UTF-8, da die meisten Zeichen in der englischen Sprache in UTF-8 in ein Byte passen. Andererseits macht es einen großen Unterschied, wenn Sie Sprachen speichern, die keine lateinischen Buchstaben verwenden, wie etwa Russisch oder Arabisch. Eine Anwendung, die nur Arabisch speichern muss, könnte den cp1256-Zeichensatz benutzen, der alle arabischen Zeichen in jeweils einem Byte darstellen kann. Muss die Anwendung dagegen viele verschiedene Sprachen speichern, und Sie wählen stattdessen UTF-8, benötigen die gleichen arabischen Zeichen viel mehr Platz. Falls Sie eine Spalte aus einem nationalen Zeichensatz nach UTF-8 konvertieren, kann sich der erforderliche Speicherplatz deutlich erhöhen. Bei InnoDB könnte sich die Datengröße derart erhöhen, dass die Werte nicht auf die Seite passen und externen Speicher benötigen, was wiederum zu einer Menge verschwendetem Speicher und zu Fragmentierung führt. Mehr Informationen zu diesem Thema finden Sie in »Für BLOB- und TEXT-Lasten optimieren« auf Seite 323. Manchmal müssen Sie überhaupt keinen Zeichensatz einsetzen. Zeichensätze sind für Vergleiche ohne Beachtung der Groß-/Kleinschreibung, Sortierung und Stringoperationen, die Zeichen erkennen müssen, wie etwa SUBSTRING( ), am nützlichsten. Falls der Datenbankserver die Zeichen nicht erkennen muss, speichern Sie alles einfach in BINARYSpalten, auch die UTF-8-Daten. Sie könnten in diesem Fall eine Spalte hinzufügen, die Ihnen mitteilt, welchen Zeichensatz Sie für die Kodierung der Daten benutzt haben. Dieser Ansatz wird von manchen Leuten zwar schon sehr lange verfolgt, allerdings muss man dabei auch mehr aufpassen. Er kann Fehler verursachen, die schwer zu erkennen sind, wie etwa Fehler mit SUBSTRING( ) und LENGTH( ), wenn Sie vergessen, dass ein Byte nicht unbedingt ein Zeichen ist. Wir empfehlen Ihnen, diese Technik nach Möglichkeit nicht zu verwenden. 262 | Kapitel 5: Erweiterte MySQL-Funktionen
Volltextsuche Die meisten der Abfragen, die Sie schreiben, enthalten wahrscheinlich WHERE-Klauseln, die Werte auf Gleichheit untersuchen, Zeilenbereiche herausfiltern usw. Allerdings kann es auch vorkommen, dass Sie eine Stichwortsuche durchführen müssen, die auf Relevanz und nicht auf dem Vergleich von Werten beruht. Für diesen Zweck gibt es Volltextsuchsysteme. Volltextsuchen erfordern eine besondere Abfragesyntax. Sie können mit oder ohne Indizes arbeiten – Indizes beschleunigen allerdings die Vergleiche. Indizes, die für Volltextsuchen benutzt werden, weisen eine besondere Struktur auf, um das Auffinden der Dokumente zu unterstützen, die die gewünschten Schlüsselwörter enthalten. Sie sind sich dessen vielleicht nicht bewusst, doch Sie sind bereits mit wenigstens einer Art von Volltextsuchsystem vertraut: Internet-Suchmaschinen. Diese arbeiten zwar in einem viel größeren Maßstab und haben normalerweise keine relationale Datenbank im Hintergrund, die Prinzipien sind allerdings ähnlich. In MySQL unterstützt nur die MyISAM-Storage-Engine die Volltextindizierung. Sie erlaubt es Ihnen, zeichenbasierten Inhalt zu suchen (CHAR-, VARCHAR- und TEXT-Spalten), und unterstützt sowohl natürlichsprachige als auch Boolesche Suchen. Die Implementierung der Volltextsuche unterliegt einigen Einschränkungen und Grenzen8 und ist recht kompliziert, ist aber dennoch weit verbreitet, da sie im Server enthalten ist und sich für viele Anwendungen eignet. In diesem Abschnitt schauen wir uns ganz allgemein an, wie man sie benutzt und wie man die Volltextsuche so gestaltet, dass man eine adäquate Leistung erreicht. Ein MyISAM-Volltextindex arbeitet auf einer Volltextsammlung, die aus einer oder mehreren Zeichenspalten aus einer einzigen Tabelle erzeugt wird. Im Prinzip baut MySQL den Index auf, indem es die Spalten in der Sammlung aneinanderhängt und sie dann als einen langen Textstring indiziert. Bei einem MyISAM-Volltextindex handelt es sich um eine besondere Art von B-BaumIndex mit zwei Ebenen. Die erste Ebene enthält die Schlüsselwörter. Die zweite Ebene enthält dann für jedes Schlüsselwort eine Liste mit passend zugewiesenen Dokumentzeigern, die auf Volltextsammlungen verweisen, in denen dieses Schlüsselwort vorhanden ist. Der Index enthält nicht jedes Wort aus der Sammlung. Er wird folgendermaßen gekürzt: • Eine Liste mit Stoppwörtern siebt Füllwörter aus, indem sie verhindert, dass diese indiziert werden. Die Stoppwortliste basiert standardmäßig auf gebräuchlichem englischsprachigem Einsatz, Sie können sie jedoch mithilfe der Option ft_stopword_ file durch eine Liste aus einer externen Datei ersetzen. 8 Sie stellen vielleicht fest, dass die Beschränkungen der Volltextfunktionen von MySQL den Einsatz für Ihre Anwendung unpraktisch oder unmöglich erscheinen lassen. Wir besprechen die Verwendung von Sphinx als externe Volltextsuchmaschine in Anhang C.
Volltextsuche | 263
• Der Index ignoriert Wörter, die weniger als ft_min_word_len Zeichen und mehr als ft_max_word_len Zeichen aufweisen. Volltextindizes speichern keine Informationen darüber, in welcher Spalte in der Sammlung ein Schlüsselwort auftaucht. Falls Sie also in unterschiedlichen Kombinationen aus Spalten suchen wollen, müssen Sie mehrere Indizes erzeugen. Das bedeutet außerdem, dass Sie eine MATCH AGAINST-Klausel nicht anweisen können, Wörter aus einer bestimmten Spalte als wichtiger zu betrachten als Wörter aus anderen Spalten. Wenn Sie Suchmaschinen für Websites bauen, ist das jedoch eine häufig gestellte Anforderung. So wollen Sie ja möglicherweise, dass Suchergebnisse an vorderen Stellen auftauchen, wenn die Stichwörter im Titel eines Eintrags stehen. In diesem Fall müssen Sie kompliziertere Abfragen schreiben. (Wir zeigen später ein Beispiel.)
Natürlichsprachige Volltextsuchen Eine natürlichsprachige Suchabfrage ermittelt die Relevanz jedes Dokuments für die Abfrage. Die Relevanz basiert auf der Anzahl der passenden Wörter und der Häufigkeit, mit der sie in dem Dokument auftauchen. Wörter, die in dem gesamten Index weniger gebräuchlich sind, erhöhen die Relevanz eines Treffers. Im Gegensatz dazu lohnt sich die Suche nach ausgesprochen häufig auftretenden Wörtern überhaupt nicht. Eine natürlichsprachige Volltextsuche schließt Wörter aus, die in mehr als 50 % der Zeilen in der Tabelle vorkommen, auch wenn sie nicht in der Liste der Stoppwörter stehen.9 Die Syntax einer Volltextsuche unterscheidet sich ein wenig von anderen Abfragearten. Sie weisen MySQL mit MATCH AGAINST in der WHERE-Klausel an, einen Volltextvergleich durchzuführen. Wir wollen uns ein Beispiel anschauen. In der normalen Sakila-Beispieldatenbank hat die film_text-Tabelle einen Volltextindex in den Spalten title und description: mysql> SHOW INDEX FROM sakila.film_text; +-----------+-----------------------+-------------+------------+ | Table | Key_name | Column_name | Index_type | +-----------+-----------------------+-------------+------------+ | ... | film_text | idx_title_description | title | FULLTEXT | | film_text | idx_title_description | description | FULLTEXT | +-----------+-----------------------+-------------+------------+
Hier ist ein Beispiel für eine natürlichsprachige Volltextsuchabfrage: mysql> SELECT film_id, title, RIGHT(description, 25), -> MATCH(title, description) AGAINST('factory casualties') AS relevance -> FROM sakila.film_text -> WHERE MATCH(title, description) AGAINST('factory casualties');
9 Häufig wird bei Tests der Fehler gemacht, einige Zeilen mit Beispieldaten in einen Volltextsuchindex zu setzen, nur um dann festzustellen, dass keine Abfrage einen Treffer ergibt. Das Problem besteht darin, dass jedes Wort in mehr als der Hälfte der Zeilen auftaucht.
264 | Kapitel 5: Erweiterte MySQL-Funktionen
+---------+-----------------------+---------------------------+-----------------+ | film_id | title | RIGHT(description, 25) | relevance | +---------+-----------------------+---------------------------+-----------------+ | 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory | 8.4692449569702 | | 126 | CASUALTIES ENCINO | Face a Boy in A Monastery | 5.2615661621094 | | 193 | CROSSROADS CASUALTIES | a Composer in The Outback | 5.2072987556458 | | 369 | GOODFELLAS SALUTE | d Cow in A Baloon Factory | 3.1522686481476 | | 451 | IGBY MAKER | a Dog in A Baloon Factory | 3.1522686481476 |
MySQL führte die Volltextsuche aus, indem es den Suchstring in Wörter aufteilte und diese jeweils mit den title- und description-Feldern verglich, die in der Volltextsammlung kombiniert sind, auf der der Index aufbaut. Beachten Sie, dass nur eines der Ergebnisse beide Wörter enthält und dass die drei Ergebnisse, die »casualties« enthalten (es gibt nur drei in der gesamten Tabelle), zuerst aufgeführt werden. Das liegt daran, dass der Index die Ergebnisse nach absteigender Relevanz sortiert. Im Gegensatz zu normalen Abfragen werden die Ergebnisse der Volltextsuche automatisch nach der Relevanz sortiert. MySQL kann einen Index nicht für die Sortierung benutzen, wenn Sie eine Volltextsuche durchführen. Daher sollten Sie keine ORDER BY-Klausel angeben, wenn Sie einen Filesort vermeiden wollen.
Wie Sie in unserem Beispiel sehen können, gibt die MATCH( )-Funktion die Relevanz tatsächlich als Fließkommazahl zurück. Sie können diesen Wert verwenden, um nach der Relevanz zu filtern oder um die Relevanz in einer Benutzeroberfläche darzustellen. Es ergibt sich kein zusätzlicher Overhead, wenn man die MATCH( )-Funktion zweimal angibt; MySQL erkennt, dass die Funktionen gleich sind, und führt die Operation nur einmal durch. Falls Sie jedoch die MATCH( )-Funktion in eine ORDER BY-Klausel setzen, benutzt MySQL ein Filesort, um die Ergebnisse zu sortieren. Sie müssen die Spalten in der MATCH( )-Klausel exakt so angeben, wie sie in einem Volltextindex vorgegeben sind, da MySQL ansonsten den Index nicht verwenden kann. Schließlich zeichnet der Index nicht auf, in welcher Spalte ein Schlüsselwort aufgetaucht ist. Wie bereits erwähnt wurde, bedeutet das außerdem, dass Sie eine Volltextsuche nicht einsetzen können, um anzugeben, dass ein Schlüsselwort in einer bestimmten Spalte des Index stehen soll. Dafür gibt es jedoch eine Lösung: Sie können mit mehreren Volltextindizes in unterschiedlichen Kombinationen aus Spalten eine eigene Sortierung durchführen, um das gewünschte Ranking zu berechnen. Nehmen Sie an, wir wollen, dass die Titel-Spalte wichtiger ist. Fügen wir dieser Spalte einen weiteren Index hinzu: mysql> ALTER TABLE film_text ADD FULLTEXT KEY(title) ;
Jetzt können wir zum Zwecke des Rankings den Titel doppelt so wichtig machen: mysql> SELECT film_id, RIGHT(description, 25), -> ROUND(MATCH(title, description) AGAINST('factory casualties'), 3) -> AS full_rel, -> ROUND(MATCH(title) AGAINST('factory casualties'), 3) AS title_rel
Volltextsuche | 265
-> FROM sakila.film_text -> WHERE MATCH(title, description) AGAINST('factory casualties') -> ORDER BY (2 * MATCH(title) AGAINST('factory casualties')) -> + MATCH(title, description) AGAINST('factory casualties') DESC; +---------+---------------------------+----------+-----------+ | film_id | RIGHT(description, 25) | full_rel | title_rel | +---------+-------------- ------------+----------+-----------+ | 831 | a Car in A Baloon Factory | 8.469 | 5.676 | | 126 | Face a Boy in A Monastery | 5.262 | 5.676 | | 299 | jack in The Sahara Desert | 3.056 | 6.751 | | 193 | a Composer in The Outback | 5.207 | 5.676 | | 369 | d Cow in A Baloon Factory | 3.152 | 0.000 | | 451 | a Dog in A Baloon Factory | 3.152 | 0.000 | | 595 | a Cat in A Baloon Factory | 3.152 | 0.000 | | 649 | nizer in A Baloon Factory | 3.152 | 0.000 |
Dieser Ansatz ist allerdings nicht sehr effizient, da er Filesorts verursacht.
Boolesche Volltextsuchen Bei Booleschen Suchen gibt die Abfrage selbst die relative Relevanz der einzelnen Wörter in einem Treffer an. Boolesche Suchen benutzen die Stoppwortliste, um Stoppwörter auszufiltern. Die Notwendigkeit, dass Wörter länger als ft_min_word_len Zeichen und kürzer als ft_max_word_len Zeichen sind, ist hingegen deaktiviert. Die Ergebnisse sind unsortiert. Wenn Sie eine Boolesche Suchabfrage konstruieren, können Sie Präfixe benutzen, um die relative Rangfolge der einzelnen Schlüsselwörter in dem Suchstring zu verändern. Die gebräuchlichsten Modifikatoren sehen Sie in Tabelle 5-3. Tabelle 5-3: Gebräuchliche Modifikatoren für die Boolesche Volltextsuche Beispiel
Bedeutung
dinosaurier
Zeilen, die »dinosaurier« enthalten, werden höher eingeordnet.
~dinosaurier
Zeilen, die »dinosaurier« enthalten, werden niedriger eingeordnet.
+dinosaurier
Zeilen müssen »dinosaurier« enthalten.
-dinosaurier
Zeilen dürfen nicht »dinosaurier« enthalten.
dino*
Zeilen, die Wörter enthalten, die mit »dino« beginnen, werden höher eingeordnet.
Sie können auch andere Operatoren verwenden, etwa Klammern zum Gruppieren. Auf diese Weise lassen sich komplexe Suchen konstruieren. Als Beispiel wollen wir wieder in der Tabelle sakila.film_text nach Filmen suchen, die sowohl »factory« als auch »casualties« enthalten. Wie wir zuvor gesehen haben, liefert eine natürlichsprachige Suche Ergebnisse zurück, die entweder einem oder beiden Begriffen entsprechen. Nutzen wir dagegen eine Boolesche Suche, dann können wir darauf bestehen, dass beide Begriffe auftauchen müssen:
266 | Kapitel 5: Erweiterte MySQL-Funktionen
mysql> SELECT film_id, title, RIGHT(description, 25) -> FROM sakila.film_text -> WHERE MATCH(title, description) -> AGAINST('+factory +casualties' IN BOOLEAN MODE); +---------+---------------------+---------------------------+ | film_id | title | RIGHT(description, 25) | +---------+---------------------+---------------------------+ | 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory | +---------+---------------------+---------------------------+
Sie können auch nach einer Wortgruppe suchen, indem Sie mehrere Wörter in Anführungszeichen setzen, so dass sie exakt so auftauchen müssen, wie sie in der Abfrage angegeben wurden: mysql> SELECT film_id, title, RIGHT(description, 25) -> FROM sakila.film_text -> WHERE MATCH(title, description) -> AGAINST('"spirited casualties"' IN BOOLEAN MODE); +---------+---------------------+---------------------------+ | film_id | title | RIGHT(description, 25) | +---------+---------------------+---------------------------+ | 831 | SPIRITED CASUALTIES | a Car in A Baloon Factory | +---------+---------------------+---------------------------+
Wortgruppensuchen sind oft ziemlich langsam. Der Volltextindex allein kann eine solche Abfrage nicht beantworten, weil er nicht verzeichnet, wo Wörter sich relativ zueinander in der Original-Volltextsammlung befinden. Daraus folgt, dass der Server tatsächlich in die Zeilen schauen muss, um eine Wortgruppensuche durchzuführen. Um eine solche Suche auszuführen, sucht der Server alle Dokumente, die sowohl »spirited« als auch »casualties« enthalten. Er holt dann die Zeilen, aus denen die Dokumente aufgebaut sind, und prüft, ob in der Sammlung die genaue Wortgruppe enthalten ist. Da er den Volltextindex benutzt, um die anfängliche Liste der passenden Dokumente zu suchen, könnten Sie denken, dass das sehr schnell ist – viel schneller als eine äquivalente LIKE-Operation. Um genau zu sein, ist es sehr schnell, solange die Wörter in der Wortgruppe ungewöhnlich sind und nicht sehr viele Ergebnisse aus dem Volltextindex an den Booleschen Vergleich zurückgeliefert werden. Wenn die Wörter in der Wortgruppe jedoch normal sind, kann LIKE viel schneller ablaufen, weil es die Zeilen sequenziell holt und nicht in einer quasizufälligen Indexreihenfolge und weil es nicht erst einen Volltextindex lesen muss. Eine Boolesche Volltextsuche verlangt nicht, dass ein Volltextindex in Aktion tritt. Sie benutzt einen Volltextindex, wenn einer vorhanden ist. Gibt es dagegen keinen, scannt sie einfach die gesamte Tabelle. Sie können eine Boolesche Volltextsuche sogar in Spalten aus mehreren Tabellen einsetzen, etwa in den Ergebnissen eines Joins. In all diesen Fällen ist sie allerdings langsam.
Volltextsuche | 267
Volltextänderungen seit MySQL 5.1 MySQL 5.1 brachte einige Änderungen in Bezug auf die Volltextsuche mit sich. Dazu gehören Leistungsverbesserungen und die Möglichkeit, zusätzliche Parser herzustellen und zu benutzen, die die vorhandenen Fähigkeiten verbessern. So können z.B. Plug-Ins die Funktionsweise der Indizierung verändern. Sie können Text viel flexibler als vorgegeben in Wörter aufteilen (indem Sie z.B. angeben, dass »C++« ein einziges Wort ist), eine Vorverarbeitung durchführen, unterschiedliche Inhaltstypen (wie etwa PDF) indizieren oder eigene Wortstamm-Techniken einsetzen. Die Plug-Ins können auch die Funktionsweise des Suchens beeinflussen – etwa indem Suchbegriffe auf Wortstämme zurückgeführt werden (»Stemming«). Die InnoDB-Entwickler arbeiten momentan an der Unterstützung für die Volltextindizierung, Wir wissen allerdings nicht, wann diese zur Verfügung stehen wird.
Volltextprobleme und -lösungen Die MySQL-Implementierung der Volltextsuche weist verschiedene Designbeschränkungen auf. Diese können bestimmten Zwecken zuwiderlaufen, es gibt allerdings viele Möglichkeiten, sie zu umgehen. Es gibt z.B. nur eine Form des Relevanz-Ranking bei der MySQL-Volltextindizierung: Häufigkeit. Der Index vermerkt nicht die Position des indizierten Wortes im String, so dass die Umgebung nichts zur Relevanz beiträgt. Das ist bei vielen Aufgaben zwar in Ordnung – vor allem, wenn es nur um kleine Datenmengen geht –, allerdings ist es möglicherweise nicht das, was Sie brauchen, und die MySQL-Volltextindizierung bietet Ihnen nicht die Flexibilität, einen anderen Ranking-Algorithmus zu wählen. (Sie speichert nicht einmal die Daten, die Sie für ein umgebungsbasiertes Ranking benötigen würden.) Größe ist ein weiteres Problem. Die MySQL-Volltextindizierung funktioniert prima, wenn der Index in den Speicher passt. Befindet sich der Index hingegen nicht im Speicher, kann sie sehr langsam sein, vor allem bei großen Feldern. Wenn Sie Wortgruppen suchen lassen, müssen sowohl die Daten als auch die Indizes in den Speicher passen, um eine gute Leistung zu erzielen. Verglichen mit anderen Indexarten, kann es sehr teuer sein, Zeilen in einen Volltextindex einzufügen, sie zu aktualisieren oder zu löschen: • Das Verändern eines Stücks Text mit 100 Wörtern erfordert nicht 1, sondern bis zu 100 Indexoperationen. • Die Feldlänge hat normalerweise bei anderen Indexarten keinen besonders starken Einfluss. Bei einer Volltextindizierung dagegen haben Text mit 3 Wörtern und Text mit 10.000 Wörtern Leistungsprofile, die sich um Größenordnungen unterscheiden. • Volltextsuchindizes sind außerdem viel anfälliger für Fragmentierung, so dass Sie vermutlich viel häufiger OPTIMIZE TABLE einsetzen müssen.
268 | Kapitel 5: Erweiterte MySQL-Funktionen
Volltextindizes beeinflussen auch, wie der Server Abfragen optimiert. Indexwahl, WHEREKlauseln und ORDER BY funktionieren anders, als Sie erwarten würden: • Wenn es einen Volltextindex gibt und die Abfrage eine MATCH AGAINST-Klausel enthält, die sie benutzen kann, verwendet MySQL den Volltextindex, um die Abfrage zu verarbeiten. Es vergleicht den Volltextindex nicht mit den anderen Indizes, die für die Abfrage benutzt werden könnten. Einige dieser anderen Indizes sind vielleicht für die Abfrage sogar besser, aber MySQL beachtet sie nicht. • Der Volltextsuchindex kann nur Volltextvergleiche durchführen. Alle anderen Kriterien in der Abfrage, wie etwa WHERE-Klauseln, müssen angewandt werden, nachdem MySQL die Zeile aus der Tabelle gelesen hat. Dieses Verhalten ist anders als das anderer Indexarten, mit denen man mehrere Teile einer WHERE-Klausel auf einmal prüfen kann. • Volltextindizes speichern nicht den eigentlichen Text, den sie indizieren. Das heißt, Sie können einen Volltextindex niemals als abdeckenden Index benutzen. • Volltextindizes können nicht für irgendeine Art von Sortierung benutzt werden, außer für die Sortierung nach Relevanz im natürlichsprachigen Modus. Falls Sie nach einem anderen Kriterium als der Relevanz sortieren müssen, benutzt MySQL ein Filesort. Schauen wir uns an, wie diese Einschränkungen die Abfragen beeinflussen. Nehmen Sie einmal an, Sie haben eine Million Dokumente mit einem normalen Index über den Dokumentautor und einem Volltextindex im Inhalt. Sie wollen eine Volltextsuche im Dokumenteninhalt durchführen, allerdings nur für Autor 123. Die Abfrage könnten Sie folgendermaßen schreiben: ... WHERE MATCH(content) AGAINST ('High Performance MySQL') AND author = 123;
Diese erste Abfrage ist jedoch sehr ineffizient. MySQL durchsucht zuerst die eine Million Dokumente, weil es den Volltextindex bevorzugt. Anschließend wendet es die WHEREKlausel an, um die Ergebnisse auf den angegebenen Autor einzuschränken. Allerdings ist diese Filteroperation nicht in der Lage, den Index im Autor einzusetzen. Eine Lösung besteht darin, die Autoren-IDs in den Volltextindex aufzunehmen. Sie können ein Präfix wählen, das mit hoher Wahrscheinlichkeit nicht im Text auftauchen wird, dann die ID des Autors daran anhängen und dieses »Wort« in eine filters-Spalte aufnehmen, die separat gepflegt wird (möglicherweise über einen Trigger). Anschließend erweitern Sie den Volltextindex so, dass er die filters-Spalte enthält, und schreiben die Abfrage folgendermaßen um: ... WHERE MATCH(content, filters) AGAINST ('High Performance MySQL +author_id_123' IN BOOLEAN MODE);
Das könnte effizienter sein, wenn die Autoren-ID sehr selektiv ist, da MySQL die Liste der Dokumente schnell einschränken kann, indem der Volltextindex nach »author_id_ 123« durchsucht wird. Ist die ID dagegen nicht sehr selektiv, könnte die Leistung sogar schlechter sein. Seien Sie also vorsichtig mit diesem Ansatz.
Volltextsuche | 269
Manchmal lassen sich Volltextindizes auch für Bounding-Box-Suchen einsetzen. Falls Sie z.B. die Suche auf einen Bereich aus Koordinaten (für Geo-Anwendungen) beschränken wollen, können Sie die Koordinaten in der Volltextsammlung kodieren. Nehmen Sie an, die Koordinaten für eine bestimmte Zeile sind X=123 und Y=456. Sie können die Koordinaten so schachteln, dass die wichtigsten Stellen zuerst kommen, also etwa XY142536, und sie dann in eine Spalte setzen, die in den Volltextindex aufgenommen wird. Wenn Sie nun die Suchen z.B. auf ein Rechteck beschränken wollen, das in X-Richtung zwischen 100 und 199 und in Y-Richtung zwischen 400 und 499 liegt, dann können Sie »+XY14*« zu der Suchabfrage hinzufügen. Das geht möglicherweise viel schneller als die Filterung mit einer WHERE-Klausel. Eine Technik, die manchmal gut mit Volltextindizes funktioniert, besonders bei paginierten Darstellungen, besteht darin, eine Liste mit Primärschlüsseln über eine Volltextabfrage auszuwählen und die Ergebnisse dann im Cache abzulegen. Wenn die Anwendung bereit ist, einige Ergebnisse darzustellen, kann sie eine weitere Abfrage ausführen, die die gewünschten Zeilen anhand ihrer IDs holt. Diese zweite Abfrage kann kompliziertere Kriterien oder Joins enthalten, die andere Indizes benötigen, um gut zu funktionieren. Zwar unterstützt nur MyISAM Volltextindizes, Sie müssen sich aber dennoch keine Sorgen machen, falls Sie stattdessen InnoDB oder eine andere Storage-Engine benutzen müssen. Eine gebräuchliche Methode ist, Ihre Tabellen auf einen Slave zu replizieren, dessen Tabellen die MyISAM-Storage-Engine benutzen, und dann die Volltextabfragen auf dem Slave zu erledigen. Falls Sie keine Abfragen auf einem anderen Server durchführen wollen, können Sie die Tabelle vertikal partitionieren, indem Sie sie in zwei Tabellen aufteilen, wobei Sie die Textspalten von den restlichen Daten trennen. Sie können auch einige Spalten in eine Tabelle duplizieren, die volltextindiziert ist. Diese Strategie kommt in der Tabelle sakila.film_text zum Einsatz, die mit Triggern gepflegt wird. Eine weitere Alternative besteht darin, eine externe Volltextsuchmaschine zu verwenden, wie etwa Lucene oder Sphinx. Mehr über Sphinx erfahren Sie in Anhang C. GROUP BY-Abfragen mit Volltextsuchen können tödlich für die Leistung sein, weil auch hier die Volltextabfrage typischerweise viele Treffer findet; diese verursachen zufällige Plattenzugriffe, gefolgt von einer temporären Tabelle oder einem Filesort für die Gruppierung. Da solche Abfragen oft einfach nur nach den obersten Elementen einer Gruppe suchen, besteht eine gute Optimierung darin, die Ergebnisse stichprobenartig auszugeben, anstatt zu versuchen, völlig akkurat zu sein. Holen Sie z.B. die ersten 1.000 Zeilen in eine temporäre Tabelle, und liefern Sie dann das oberste Ergebnis pro Gruppe aus dieser Tabelle zurück.
Volltexteinstellung und -optimierung Um die Leistung zu verbessern, ist die regelmäßige Pflege Ihrer Volltextindizes unerlässlich. Die Doppel-B-Baum-Struktur der Volltextindizes in Kombination mit der großen Anzahl der Schlüsselwörter in typischen Dokumenten bedeutet, dass Volltextindizes
270 | Kapitel 5: Erweiterte MySQL-Funktionen
stärker unter Fragmentierung leiden als normale Indizes. Benutzen Sie oft OPTIMIZE TABLE, um die Indizes zu defragmentieren. Falls Ihr Server ein-/ausgabegebunden ist, könnte es schneller sein, die Volltextindizes regelmäßig zu verwerfen und neu zu erzeugen. Ein Server, der Volltextsuchen gut ausführen soll, benötigt Schlüsselpuffer, die groß genug sind, um die Volltextindizes aufzunehmen, da diese viel besser funktionieren, wenn sie sich im Speicher befinden. Sie können spezielle Schlüsselpuffer einsetzen, um sicherzugehen, dass andere Indizes Ihre Volltextindizes nicht aus dem Schlüsselpuffer verdrängen. In »Der MyISAM-Schlüssel-Cache« auf Seite 296 finden Sie nähere Informationen über die MyISAM-Schlüsselpuffer. Es ist außerdem wichtig, eine gute Stoppwortliste anzugeben. Die Vorgaben funktionieren ganz gut mit englischer Prosa, eignen sich aber vermutlich nicht für andere Sprachen oder spezialisierte Texte, wie etwa technische Dokumente. Falls Sie z.B. ein Dokument über MySQL indizieren, werden Sie »mysql« in die Stoppwortliste aufnehmen, weil es zu oft vorkommt, um eine Hilfe zu sein. Sie können die Leistung meist verbessern, indem Sie kurze Wörter auslassen. Die Länge lässt sich mit dem Parameter ft_min_word_len konfigurieren. Vergrößern Sie den Standardwert, werden mehr Wörter übersprungen, wodurch der Index kleiner und schneller, aber auch weniger genau wird. Denken Sie auch daran, dass Sie für besondere Zwecke möglicherweise sehr kurze Wörter benötigen. Beispielsweise würde eine Suche nach Heimelektronik bei der Abfrage »CD Player« wahrscheinlich viele irrelevante Ergebnisse liefern, wenn kurze Wörter im Index nicht erlaubt wären. Ein Anwender, der nach »CD Player« sucht, möchte in den Ergebnissen keine MP3- und DVD-Player finden. Wenn die minimale Wortlänge aber die vorgegebenen vier Zeichen beträgt, wird eigentlich nur nach »Player« gesucht, so dass alle Arten von Abspielgeräten mit dieser Bezeichnung zurückgeliefert werden. Die Stoppwortliste und die minimale Wortlänge können die Suchgeschwindigkeit verbessern, indem einige Wörter aus dem Index herausgehalten werden. Darunter kann allerdings die Suchqualität leiden. Die richtige Wahl ist abhängig von der Anwendung. Wenn Sie eine gute Performance und hochwertige Ergebnisse benötigen, müssen Sie beide Parameter für Ihre Anwendung anpassen. Es bietet sich an, irgendeine Art von Logging einzubauen und dann gebräuchliche Suchen, ungebräuchliche Suchen, Suchen, die keine Ergebnisse liefern, und Suchen, die viele Ergebnisse liefern, zu untersuchen. Auf diese Weise gewinnen Sie Erkenntnisse über Ihre Benutzer und Ihre suchbaren Inhalte. Mit diesen Erkenntnissen lassen sich dann die Performance und die Qualität Ihrer Suchergebnisse verbessern. Falls Sie die minimale Wortlänge ändern, müssen Sie den Index mit OPTIMIZE TABLE neu aufbauen, damit die Änderung wirksam wird. Ein verwandter Parameter ist ft_max_word_len, der hauptsächlich als Sicherheitsvorkehrung dient, um die Indizierung sehr langer Schlüsselwörter zu vermeiden.
Volltextsuche | 271
Wenn Sie viele Daten in einen Server importieren und in einigen Spalten eine Volltextindizierung durchführen wollen, dann deaktivieren Sie die Volltextindizes vor dem Import mit DISABLE KEYS, und aktivieren Sie sie hinterher wieder mit ENABLE KEYS. Das geht normalerweise wegen der hohen Kosten für das Aktualisieren des Index bei jeder eingefügten Zeile viel schneller, und darüber hinaus erhalten Sie als nette Zugabe einen defragmentierten Index. Bei großen Datenmengen müssen Sie eventuell die Daten manuell auf viele Knoten aufteilen und sie parallel suchen. Das ist eine schwierige Aufgabe, für die Sie vielleicht besser eine externe Volltextsuchmaschine wie Lucene oder Sphinx einsetzen sollten. Unsere Erfahrungen haben gezeigt, dass Sie damit eine um Größenordnungen bessere Performance erreichen.
Fremdschlüsselbeschränkungen InnoDB ist momentan die Storage-Engine, die Fremdschlüssel in MySQL unterstützt, wodurch Ihre Auswahl an Storage-Engines für diesen Fall doch ziemlich eingeschränkt ist.10 Die Firma MySQL AB hat allerdings versprochen, dass der Server selbst irgendwann Fremdschlüssel unterstützen wird, die unabhängig von der Storage-Engine sind. Momentan sieht es allerdings so aus, als würde InnoDB noch für einige Zeit allein auf weiter Flur bleiben. Wir konzentrieren uns deshalb auf Fremdschlüssel in InnoDB. Fremdschlüssel sind nicht frei. Sie erfordern es typischerweise, dass der Server jedes Mal einen Lookup in einer anderen Tabelle ausführt, wenn Sie Daten ändern. Obwohl InnoDB einen Index verlangt, um diese Operation zu beschleunigen, sind die Auswirkungen dieser Überprüfungen spürbar. Sie können sogar zu einem sehr großen Index mit scheinbar keiner Selektivität führen. Nehmen Sie z.B. an, dass Sie eine status-Spalte in einer riesigen Tabelle haben und den Status auf gültige Werte beschränken wollen. Allerdings gibt es nur drei solcher Werte. Der zusätzliche Index, der verlangt wird, kann die Gesamtgröße der Tabelle deutlich erhöhen – sogar dann, wenn die Spalte selbst klein ist, und besonders dann, wenn der Primärschlüssel groß ist – und ist für alles andere als Fremdschlüsselüberprüfungen nicht zu gebrauchen. Dennoch können Fremdschlüssel die Leistung in einigen Fällen verbessern. Wenn Sie garantieren müssen, dass zwei verwandte Tabellen konsistente Daten aufweisen, kann es effizienter sein, den Server diese Überprüfung durchführen zu lassen, als dies in Ihrer Anwendung zu erledigen. Fremdschlüssel sind auch bei gestaffelten Lösch- oder UpdateVorgängen hilfreich, obwohl sie zeilenweise arbeiten und damit langsamer sind als Mehrtabellen-Lösch- oder Batch-Operationen. Fremdschlüssel können Ihre Abfrage veranlassen, in andere Tabellen »hinüberzulangen«, was bedeutet, dass Sperren gesetzt werden. Wenn Sie z.B. eine Zeile in eine Kindtabelle einfügen, veranlasst die Fremdschlüsselbeschränkung InnoDB, in der Elterntabelle nach 10 PBXT unterstützt sie auch.
272 | Kapitel 5: Erweiterte MySQL-Funktionen
einem dazugehörenden Wert zu suchen. Außerdem muss die Zeile in der Elterntabelle gesperrt werden, damit diese nicht gelöscht wird, bevor die Transaktion abgeschlossen ist. Dies kann die Ursache für unerwartete Lock-Wartezustände und sogar Deadlocks in Tabellen sein, die Sie eigentlich gar nicht direkt anfassen. Solche Probleme sind unter Umständen schwer zu finden und zu beseitigen. Manchmal können Sie anstelle von Fremdschlüsseln Trigger benutzen. Fremdschlüssel sind bei Aufgaben wie gestaffelten Updates besser als Trigger. Allerdings lässt sich ein Fremdschlüssel, der nur als Beschränkung verwendet wird, wie in unserem status-Beispiel, effizienter in einen Trigger mit einer expliziten Liste der erlaubten Werte umschreiben. (Sie können auch einfach einen ENUM-Datentyp einsetzen.) Anstatt Fremdschlüssel als Beschränkungen zu benutzen, ist es oft besser, die Werte in der Anwendung zu beschränken.
Merge-Tabellen und Partitionierung Merge-Tabellen und Partitionierung sind verwandte Konzepte, deren Unterschied verwirrend sein kann. Merge-Tabellen sind eine MySQL-Eigenschaft, die mehrere MyISAMTabellen in einer einzigen »virtuellen Tabelle« kombiniert, etwa wie eine Sicht, die ein UNION über die Tabellen ausführt. Sie erzeugen eine Merge-Tabelle mit der Merge-Storage-Engine. Eine Merge-Tabelle ist eigentlich keine echte Tabelle; stellen Sie sie sich eher wie einen Container für ähnlich definierte Tabellen vor. Im Gegensatz dazu scheinen partitionierte Tabellen normale Tabellen zu sein, die besondere Anweisungen enthalten, mit denen MySQL mitgeteilt wird, wo die Zeilen physisch gespeichert werden sollen. Das schmutzige kleine Geheimnis lautet, dass der Speichercode für partitionierte Tabellen stark dem Code für Merge-Tabellen ähnelt! Um genau zu sein, ist jede Partition auf einer niedrigen Ebene einfach nur eine eigene Tabelle mit eigenen Indizes; die partitionierte Tabelle ist ein Wrapper um eine Sammlung von HandlerObjekten herum. Eine partitionierte Tabelle sieht wie eine einzige Tabelle aus und verhält sich auch so, hinter den Kulissen handelt es sich jedoch um ein ganzes Bündel separater Tabellen. Es gibt allerdings keine Möglichkeit, wie bei Merge-Tabellen direkt auf die zugrunde liegenden Tabellen zuzugreifen. Partitionierung ist eine neue Funktion in MySQL 5.1, Merge-Tabellen dagegen gibt es schon lange. Beide haben zum Teil die gleichen Vorteile. Sie können damit: • statische und sich ändernde Daten trennen • die physische Nachbarschaft verwandter Daten nutzen, um Abfragen zu optimieren • Ihre Tabellen so gestalten, dass Abfragen auf weniger Daten zugreifen • sehr große Datenmengen leichter pflegen (Das ist ein Bereich, in dem Merge-Tabellen Vorteile gegenüber partitionierten Tabellen aufweisen.) Da die MySQL-Implementierungen von Partitionierung und Merge-Tabellen vieles gemeinsam haben, weisen sie auch teilweise die gleichen Einschränkungen auf. Beispiels-
Merge-Tabellen und Partitionierung | 273
weise gibt es praktische Grenzen hinsichtlich der Anzahl der zugrunde liegenden Tabellen oder Partitionen, die Sie in einer einzigen Merge- oder partitionierten Tabelle haben können. In den meisten Fällen wird es bei einigen Hundert Tabellen langsam ineffizient. Wir erwähnen die Beschränkungen der beiden Systeme, wenn wir sie genauer vorstellen.
Merge-Tabellen Sie können sich Merge-Tabellen als eine ältere, beschränktere Version der Partitionierung vorstellen. Sie haben aber dennoch ihren Nutzen und bieten Eigenschaften, die Partitionen nicht haben. Die Merge-Tabelle ist eigentlich ein Container, der die echten Tabellen aufnimmt. Sie geben mit einer speziellen UNION-Syntax in CREATE TABLE an, welche Tabellen aufgenommen werden sollen. Hier ist ein Beispiel, das viele Aspekte von Merge-Tabellen demonstriert: mysql> CREATE TABLE t1(a INT NOT NULL PRIMARY KEY)ENGINE=MyISAM; mysql> CREATE TABLE t2(a INT NOT NULL PRIMARY KEY)ENGINE=MyISAM; mysql> INSERT INTO t1(a) VALUES(1),(2); mysql> INSERT INTO t2(a) VALUES(1),(2); mysql> CREATE TABLE mrg(a INT NOT NULL PRIMARY KEY) -> ENGINE=MERGE UNION=(t1, t2) INSERT_METHOD=LAST; mysql> SELECT a FROM mrg; +------+ | a | +------+ | 1 | | 1 | | 2 | | 2 | +------+
Beachten Sie, dass die zugrunde liegenden Tabellen exakt die gleiche Anzahl und Art von Spalten enthalten und dass alle Indizes, die in der Merge-Tabelle existieren, auch in den zugrunde liegenden Tabellen vorhanden sind. Das ist notwendig, wenn Sie eine MergeTabelle erzeugen. Beachten Sie außerdem, dass es einen Primärschlüssel in der einzigen Spalte jeder Tabelle gibt, dass die resultierende Merge-Tabelle jedoch duplizierte Zeilen aufweist. Dies ist eine der Beschränkungen von Merge-Tabellen: Die einzelnen Tabellen innerhalb des Merge verhalten sich normal, die Merge-Tabelle erzwingt allerdings keine Beschränkungen über die ganze Gruppe von Tabellen. Die Anweisung INSERT_METHOD=LAST an die Tabelle teilt MySQL mit, dass es alle INSERTAnweisungen an die letzte Tabelle in dem Merge senden soll. Die Angabe von FIRST oder LAST ist Ihre einzige Möglichkeit, zu steuern, wo die Zeilen, die in die Merge-Tabelle eingefügt werden, platziert werden (Sie können jedoch weiterhin direkt in die zugrunde liegenden Tabellen einfügen). Partitionierte Tabellen bieten mehr Kontrolle darüber, wo die Daten gespeichert werden.
274 | Kapitel 5: Erweiterte MySQL-Funktionen
Die Ergebnisse eines INSERT sind sowohl in der Merge-Tabelle als auch in der zugrunde liegenden Tabelle sichtbar: mysql> INSERT INTO mrg(a) VALUES(3); mysql> SELECT a FROM t2; +---+ | a | +---+ | 1 | | 2 | | 3 | +---+
Merge-Tabellen haben einige weitere interessante Funktionen und Beschränkungen. Was geschieht z.B., wenn Sie eine Merge-Tabelle oder eine ihrer zugrunde liegenden Tabellen verwerfen? Wird eine Merge-Tabelle verworfen, dann bleiben ihre »Kind«-tabellen davon unberührt. Wird dagegen eine der Kindtabellen verworfen, hat das andere Auswirkungen, die vom Betriebssystem abhängen. Unter GNU/Linux bleibt z.B. der Dateideskriptor der zugrunde liegenden Tabelle offen, und die Tabelle existiert weiter, wenn auch nur über die Merge-Tabelle: mysql> DROP TABLE t1, t2; mysql> SELECT a FROM mrg; +------+ | a | +------+ | 1 | | 1 | | 2 | | 2 | | 3 | +------+
Es gibt noch eine Vielzahl weiterer Beschränkungen und besonderer Verhaltensweisen. Näheres erfahren Sie aus dem Handbuch. Wir wollen zumindest noch anmerken, dass REPLACE in einer Merge-Tabelle nicht funktioniert und dass AUTO_INCREMENT anders funktioniert, als Sie erwarten würden.
Auswirkungen von Merge-Tabellen auf die Performance Die Art und Weise, wie MySQL Merge-Tabellen implementiert, hat wichtige Folgen für die Leistung. Das macht sie für manche Anwendungsfälle besser geeignet als für andere. Hier sind einige Aspekte von Merge-Tabellen, die Sie bedenken sollten: • Eine Merge-Tabelle verlangt mehr offene Dateideskriptoren als eine Nicht-MergeTabelle, die die gleichen Daten enthält. Obwohl eine Merge-Tabelle wie eine einzige Tabelle aussieht, öffnet sie die zugrunde liegenden Tabellen separat. Daraus folgt, das ein einziger Tabellen-Cache-Eintrag viele Dateideskriptoren erzeugen kann. Das heißt: Selbst wenn Sie den Tabellen-Cache so konfiguriert haben, dass er Ihren Server vor der Ausweitung der Dateideskriptorgrenzen pro Prozess Ihres Betriebssystems schützt, können Merge-Tabellen dazu führen, dass diese Grenze überschritten wird.
Merge-Tabellen und Partitionierung | 275
• Die CREATE-Anweisung, die eine Merge-Tabelle erzeugt, überprüft nicht, ob die zugrunde liegenden Tabellen kompatibel sind. Sind die zugrunde liegenden Tabellen unterschiedlich definiert, könnte MySQL eine Merge-Tabelle erzeugen, die es später gar nicht nutzen kann. Falls Sie eine der zugrunde liegenden Tabellen ändern, nachdem Sie eine gültige Merge-Tabelle erzeugt haben, hört diese auf zu funktionieren, und Sie erhalten folgende Fehlermeldung: »ERROR 1168 (HY000): Unable to open underlying table which is differently defined or of non-MyISAM type or doesn’t exist«. • Abfragen, die auf die Merge-Tabelle zugreifen, greifen auf jede zugrunde liegende Tabelle zu. Dadurch können einzeilige Schlüsselsuchen, verglichen mit einem Lookup in einer einzigen Tabelle, sehr langsam sein. Es ist deshalb keine schlechte Idee, die Anzahl der zugrunde liegenden Tabellen in einer Merge-Tabelle zu begrenzen, vor allem, wenn sie die zweite oder eine spätere Tabelle in einem Join ist. Auf je weniger Daten Sie mit jeder Operation zugreifen, umso wichtiger werden die Kosten für den Zugriff auf die einzelnen Tabellen in Bezug auf die gesamte Operation. Folgendes sollten Sie beachten, wenn Sie planen, wie Sie Merge-Tabellen benutzen wollen: • Bereichs-Lookups sind weniger von dem Aufwand betroffen, der beim Zugriff auf alle zugrunde liegenden Tabellen auftritt, als Lookups nach einzelnen Elementen. • Tabellenscans sind in Merge-Tabellen fast genauso schnell wie in normalen Tabellen. • Lookups nach eindeutigen Schlüsseln und Primärschlüsseln stoppen, sobald sie erfolgreich sind. In diesem Fall greift der Server nacheinander auf die zugrunde liegenden Merge-Tabellen zu, bis ein Wert gefunden wird. Anschließend wird nicht auf weitere Tabellen zugegriffen. • Die zugrunde liegenden Tabellen werden in der Reihenfolge gelesen, die in der CREATE TABLE-Anweisung angegeben ist. Wenn Sie oft Daten in einer bestimmten Reihenfolge benötigen, können Sie diese Tatsache ausnutzen, um die Merge-Sortieroperation zu beschleunigen.
Stärken von Merge-Tabellen Merge-Tabellen sind besonders gut für Daten geeignet, die natürlicherweise einen aktiven und einen inaktiven Teil aufweisen. Ein klassisches Beispiel ist das Logging. An Logs wird üblicherweise nur etwas angehängt, man könnte also ein Schema wie »eine Tabelle pro Tag« wählen. Jeden Tag würde man eine neue zugrunde liegende Tabelle erzeugen und entsprechend in die Merge-Tabelle aufnehmen. Die Tabelle des vorhergehenden Tages könnte man aus der Merge-Tabelle entfernen, sie in komprimiertes MyISAM umwandeln und dann wieder einfügen.
276 | Kapitel 5: Erweiterte MySQL-Funktionen
Das ist jedoch nicht der einzige Anwendungsfall für Merge-Tabellen. Sie kommen oft in Data-Warehousing-Anwendungen zum Einsatz, da eine ihrer weiteren Stärken die Art und Weise ist, wie sie bei der Verwaltung riesiger Datenmengen helfen. Es ist praktisch unmöglich, eine einzige Tabelle mit Terabytes an Daten zu verwalten. Diese Aufgabe lässt sich viel leichter bewältigen, wenn es sich um eine Merge-Sammlung aus 50-GByteTabellen handelt. Wenn Sie extrem große Datenbanken verwalten, müssen Sie nicht nur an normale Operationen denken, sondern auch Szenarien für Abstürze und Wiederherstellung entwickeln. Es ist sinnvoll, Tabellen nach Möglichkeit klein zu halten. Eine Sammlung kleiner Tabellen lässt sich viel schneller überprüfen und reparieren als eine riesige Tabelle, vor allem, wenn die riesige Tabelle nicht in den Speicher passt. Bei mehreren Tabellen können Sie Überprüfung und Reparatur sogar parallel durchführen. Ein weiteres Anliegen beim Data-Warehousing ist die Frage, wie alte Daten beseitigt werden. Der Einsatz von DELETE zum Entfernen von Zeilen aus einer riesigen Tabelle ist im besten Fall ineffizient und im schlimmsten Fall katastrophal. Andererseits ist es sehr einfach, die Definition einer Merge-Tabelle zu ändern und DROP TABLE einzusetzen, um alte Daten loszuwerden. Sie können das leicht automatisieren. Merge-Tabellen sind nicht nur für das Logging und für riesige Datenmengen von Nutzen. Sie eignen sich auch zum bedarfsweisen Erzeugen von Tabellen. Das Erzeugen und Verwerfen von Merge-Tabellen ist recht unaufwendig, so dass Sie sie ungefähr wie Sichten mit UNION ALL benutzen können; der Overhead ist jedoch geringer, da der Server die Ergebnisse nicht in einer temporären Tabelle zwischenspeichert, bevor er sie an den Client sendet. Dadurch eignen sie sich sehr gut für die Anforderungen des Reportings und des Data-Warehousings. So können Sie z.B. einen nächtlichen Job anlegen, der die Daten von gestern mit den Daten von vor 8 Tagen, von vor 15 Tagen usw. in einem Merge zusammenfasst, um so wochenweise Berichte zu ermöglichen. Auf diese Weise können Ihre Berichtsabfragen ohne Modifikationen ausgeführt werden und automatisch auf die passenden Daten zugreifen. Sie können sogar temporäre Merge-Tabellen herstellen – was mit Sichten nicht geht. Da Merge-Tabellen die zugrunde liegenden MyISAM-Tabellen nicht verbergen, bieten sie Eigenschaften, die Partitionen nicht haben: • Eine MyISAM-Tabelle kann Mitglied in vielen Merge-Tabellen sein. • Sie können die zugrunde liegenden Tabellen zwischen Servern kopieren, indem Sie die .frm-, .MYI- und .MYD-Dateien kopieren. • Sie können zu einer Merge-Sammlung leicht weitere Tabellen hinzufügen; erzeugen Sie dazu einfach eine neue Tabelle, und ändern Sie die Merge-Definition. • Sie können temporäre Merge-Tabellen erzeugen, die nur die gewünschten Daten enthalten, etwa Daten aus einem speziellen Zeitraum, was mit Partitionen nicht möglich ist.
Merge-Tabellen und Partitionierung | 277
• Sie können eine Tabelle aus der Merge-Tabelle entfernen, falls Sie sie sichern, wiederherstellen, reparieren oder andere Operationen auf ihr ausführen wollen. Wenn Sie fertig sind, fügen Sie sie einfach wieder ein. • Mit myisampack können Sie einige oder alle zugrunde liegenden Tabellen komprimieren. Im Gegensatz dazu werden die Partitionen einer partitionierten Tabelle vom MySQL-Server verborgen und können nur über die partitionierte Tabelle erreicht werden.
Partitionierte Tabellen Die MySQL-Implementierung der Partitionierung ähnelt hinter den Kulissen stark der Implementierung der Merge-Tabellen. Sie ist allerdings stark in den Server integriert und weist einen wesentlichen Unterschied zu den Merge-Tabellen auf: Jede Datenzeile darf nur in einer der Partitionen gespeichert werden. Die Definition der Tabelle legt fest, welche Zeilen zu welchen Partitionen gehören. Diese Angabe beruht auf einer Partitionierungsfunktion, die wir später erklären. Primärschlüssel und eindeutige Schlüssel funktionieren also wie erwartet in der ganzen Tabelle, und der MySQL-Abfrageoptimierer kann Abfragen an partitionierte Tabellen viel intelligenter optimieren als an MergeTabellen. Hier sind einige wichtige Vorteile von partitionierten Tabellen: • Sie können angeben, dass bestimmte Zeilen zusammen in einer Partition gespeichert werden, wodurch sich die Datenmenge vermindert, die der Server untersuchen muss, und Abfragen sich beschleunigen. Falls Sie z.B. nach dem Datenbereich partitionieren und dann in einem Datenbereich eine Abfrage ausführen, der nur auf eine Partition zugreift, liest der Server nur diese Partition. • Partitionierte Daten lassen sich leichter pflegen als nichtpartitionierte Daten. Außerdem ist es einfacher, alte Daten loszuwerden, indem man eine ganze Partition verwirft. • Partitionierte Daten können physisch verteilt werden, wodurch der Server in die Lage versetzt wird, mehrere Festplatten effizienter einzusetzen. Die MySQL-Implementierung der Partitionierung ist noch im Fluss. Sie ist außerdem zu kompliziert, um sie hier ganz genau zu erklären. Wir wollen uns auf ihre Auswirkungen auf die Performance konzentrieren, weshalb wir Sie für die Grundlagen auf das MySQLHandbuch verweisen, das eine Menge Material zu diesem Thema bereithält. Sie sollten das ganze Kapitel über die Partitionierung lesen und sich dann noch die Abschnitte über CREATE TABLE, SHOW CREATE TABLE, ALTER TABLE, die INFORMATION_SCHEMA.PARTITIONS-Tabelle und EXPLAIN anschauen. Die Partitionierung hat dafür gesorgt, dass die Befehle CREATE TABLE und ALTER TABLE deutlich komplexer wurden. Genau wie eine Merge-Tabelle besteht eine partitionierte Tabelle eigentlich aus einer Sammlung separater Tabellen (den Partitionen) mit separaten Indizes auf der StorageEngine-Ebene. Das bedeutet, dass der Speicher und die Anforderungen an die Datei278 | Kapitel 5: Erweiterte MySQL-Funktionen
deskriptoren für eine partitionierte Tabelle mit denen einer Merge-Tabelle vergleichbar sind. Allerdings kann man auf die Partitionen nicht unabhängig von der Tabelle zugreifen, außerdem kann jede Partition nur zu einer Tabelle gehören. Wie bereits angemerkt wurde, benutzt MySQL eine Partitionierungsfunktion, um zu entscheiden, welche Zeilen in welchen Partitionen gespeichert werden. Die Funktion muss einen nichtkonstanten, deterministischen Integer-Wert zurückliefern. Es gibt mehrere Arten der Partitionierung. Die Bereichspartitionierung richtet für jede Partition einen Bereich von Werten ein und weist dann auf der Grundlage der Bereiche, in die sie fallen, Zeilen zu Partitionen zu. MySQL unterstützt auch Schlüssel-, Hash- und Listenpartitionierungsmethoden. Jeder Typ hat seine Stärken und Schwächen. Einige der Typen sind auch Beschränkungen unterworfen, speziell wenn es um den Umgang mit Primärschlüsseln geht.
Wieso die Partitionierung funktioniert Der Schlüssel zum Entwerfen partitionierter Tabellen in MySQL besteht darin, sich die Partitionierung als eine grobkörnige Art von Indizierung vorzustellen. Nehmen Sie einmal an, Sie haben eine Tabelle mit einer Milliarde Zeilen historischer, tage- und objektweise aufgenommener Verkaufsdaten, und jede Zeile ist relativ groß – sagen wir 500 Byte. Sie fügen neue Zeilen ein, aktualisieren existierende Zeilen aber nie und führen hauptsächlich Abfragen aus, die Bereiche mit Daten untersuchen. Das Hauptproblem beim Ausführen von Abfragen an dieser Tabelle besteht darin, dass sie riesig ist: Sie ist fast ein halbes Terabyte groß – ohne Indizes –, wenn Sie die Daten nicht komprimieren. Ein Ansatz zum Beschleunigen der tageweisen Abfragen könnte darin bestehen, einen Primärschlüssel in (day, itemno) hinzuzufügen und InnoDB zu benutzen. Damit werden die Daten der einzelnen Tage physisch gruppiert, so dass die Bereichsabfragen weniger Daten zu untersuchen haben. Alternativ könnten Sie MyISAM benutzen und die Zeilen in der gewünschten Reihenfolge einfügen, damit ein Indexscan nicht so viele zufällige Ein-/Ausgaben verursacht. Eine andere Möglichkeit wäre es, den Primärschlüssel wegzulassen und die Daten anhand des Tages zu partitionieren. Jede Abfrage, die auf Bereiche aus Tagen zugreift, muss ganze Partitionen scannen. Das könnte aber in einer solch riesigen Tabelle besser sein, als Index-Lookups durchzuführen. Die Partitionierung ist ein bisschen wie ein Index: Sie teilt MySQL ungefähr mit, wo eine bestimmte Zeile zu finden ist, wenn Sie den Tag kennen. Dabei belegt sie aber fast keinen Plattenplatz oder Speicher, da die Partitionierung nicht exakt auf die Zeile zeigt (wie es ein Index tut). Lassen Sie sich jedoch nicht dazu hinreißen, einen Primärschlüssel und eine Partition hinzuzufügen – damit könnten Sie die Performance sogar herabsetzen, vor allem bei Abfragen, die auf alle Partitionen zugreifen müssen. Wenn Sie über Partitionierung nachdenken, sollten Sie sorgfältig Benchmark-Tests durchführen, weil partitionierte Tabellen die Leistung nicht immer verbessern.
Merge-Tabellen und Partitionierung | 279
Beispiele für Partitionierung Wir zeigen Ihnen hier zwei kurze Beispiele, bei denen die Partitionierung hilfreich ist. Zuerst wollen wir uns anschauen, wie man eine partitionierte Tabelle gestaltet, um datumsbasierte Daten zu speichern. Nehmen Sie an, Sie haben Leistungsstatistiken für Bestellungen und Verkäufe von Produkten gesammelt. Da Sie oft Abfragen in Bereichen aus Daten durchführen, setzen Sie das Bestelldatum als Erstes in den Primärschlüssel und erzeugen dann mithilfe der InnoDB-Storage-Engine Daten-Cluster anhand des Datums. Indem Sie Bereiche aus Daten partitionieren, können Sie Cluster auf einer höheren Ebene anlegen. Dies ist die grundlegende Tabellendefinition ohne eine Angabe der Partitionierung: CREATE TABLE sales_by_day ( day DATE NOT NULL, product INT NOT NULL, sales DECIMAL(10, 2) NOT NULL, returns DECIMAL(10, 2) NOT NULL, PRIMARY KEY(day, product) ) ENGINE=InnoDB;
Genau wie die Partitionierung nach dem Tag ist die Partitionierung nach dem Jahr üblich für den Umgang mit datumsbasierten Daten. Die Funktionen YEAR( ) und TO_DAYS( ) eignen sich in diesem Fall gut als Partitionsfunktionen. Im Allgemeinen hat eine gute Funktion für die Bereichspartitionierung eine lineare Beziehung zu den Werten, nach denen partitioniert werden soll. Auf diese Funktionen trifft diese Beschreibung zu. Partitionieren wir nach dem Jahr: mysql> ALTER TABLE sales_by_day -> PARTITION BY RANGE(YEAR(day)) ( -> PARTITION p_2006 VALUES LESS THAN (2007), -> PARTITION p_2007 VALUES LESS THAN (2008), -> PARTITION p_2008 VALUES LESS THAN (2009), -> PARTITION p_catchall VALUES LESS THAN MAXVALUE );
Wenn wir nun Zeilen einfügen, werden diese in der passenden Partition gespeichert, je nach dem Wert der day-Spalte: mysql> INSERT INTO sales_by_day(day, product, sales, returns) VALUES -> ('2007-01-15', 19, 50.00, 52.00), -> ('2008-09-23', 11, 41.00, 42.00);
Wir verwenden diese Daten gleich noch in einem Beispiel. Bevor wir jedoch fortfahren, wollen wir darauf hinweisen, dass es hier eine wichtige Beschränkung gibt: Werden später noch mehr Jahre hinzugefügt, wird es notwendig, die Tabelle zu verändern, was bei einer großen Tabelle sehr teuer ist (wovon wir jetzt einmal ausgehen, da wir sonst keine Partitionen benutzen würden). Vielleicht sollten Sie einfach vorpreschen und mehr Jahre definieren, als Sie möglicherweise brauchen werden. Auch wenn Sie sie noch lange nicht benutzen, wird dadurch die Performance nicht wesentlich beeinflusst. Eine weitere gebräuchliche Anwendung für partitionierte Tabellen ist die Verteilung der Zeilen in einer großen Tabelle. Nehmen Sie z.B. an, dass Sie eine große Anzahl von
280 | Kapitel 5: Erweiterte MySQL-Funktionen
Abfragen an einer riesigen Tabelle durchführen. Falls Sie wollen, dass unterschiedliche physische Festplatten die Daten bereitstellen, während mehrere Abfragen an der Tabelle ausgeführt werden, könnten Sie veranlassen, dass MySQL die Zeilen über die Festplatten verteilt. In diesem Fall müssen Sie sich keine Gedanken darüber machen, verwandte Daten dicht beisammenzuhalten, sondern wollen die Daten lediglich gleichmäßig verteilen, ohne darüber nachdenken zu müssen. Folgendes sorgt dafür, dass MySQL die Zeilen um den Betrag des Primärschlüssels verteilt. Dies ist sehr gut geeignet, um Daten gleichförmig über die Partitionen zu verteilen: mysql> ALTER TABLE mydb.very_big_table -> PARTITION BY KEY() ( -> PARTITION p0 DATA DIRECTORY='/data/mydb/big_table_p0/', -> PARTITION p1 DATA DIRECTORY='/data/mydb/big_table_p1/');
Sie können das gleiche Ziel auch auf andere Weise mit einem RAID-Controller erreichen. Das kann manchmal besser sein: weil diese Methode in Hardware implementiert ist, sind die Einzelheiten ihrer Funktionsweise verborgen, so dass die Komplexität Ihres Schemas und Ihrer Abfragen nicht unnütz vergrößert wird. Sie bietet möglicherweise auch eine bessere, gleichartigere Performance, falls Ihr einziges Ziel darin besteht, Ihre Daten physisch zu verteilen.
Beschränkungen partitionierter Tabellen Partitionierte Tabellen sind keine »Wunderwaffe«. Hier sind einige der Beschränkungen der aktuellen Implementierung: • Momentan müssen alle Partitionen die gleiche Storage-Engine benutzen. Sie können z.B. nicht nur einige Partitionen komprimieren, wie es bei den Merge-Tabellen möglich ist, bei denen Sie auch nur einige der zugrunde liegenden Tabellen komprimieren können. • Jeder eindeutige Index in einer partitionierten Tabelle muss die Spalten enthalten, auf die sich die Partitionsfunktion bezieht. Deshalb vermeiden viele als Anleitung dienende Beispiele die Verwendung eines Primärschlüssels. Es ist zwar bei Data Warehouses üblich, dass sie Tabellen ohne Primärschlüssel oder eindeutige Indizes enthalten, bei OLTP-Systemen ist das allerdings weniger gebräuchlich. Daraus folgt, dass Sie bei der Wahl der passenden Partitionierung Ihrer Daten wahrscheinlich stärker eingeschränkt sind, als Sie zunächst vermuten würden. • Obwohl MySQL während einer Abfrage nicht unbedingt auf alle Partitionen in einer partitionierten Tabelle zugreifen muss, werden alle Partitionen gesperrt. • Es gibt einige Einschränkungen hinsichtlich der Funktionen und Ausdrücke, die Sie in einer Partitionierungsfunktion benutzen können. • Manche Storage-Engines funktionieren nicht mit Partitionierung. • Fremdschlüssel funktionieren nicht. • Sie können LOAD INDEX INTO CACHE nicht benutzen.
Merge-Tabellen und Partitionierung | 281
Es gibt natürlich noch weitere Einschränkungen (zumindest zum Zeitpunkt der Entstehung dieses Buches, als MySQL 5.1 noch nicht allgemein verfügbar war). Tatsächlich sind partitionierte Tabellen in mancher Hinsicht weniger flexibel als Merge-Tabellen. Zum Beispiel können Sie nicht einmal eben schnell zu einer partitionierten Tabelle Stück für Stück einen Index hinzufügen: Das ALTER sperrt die gesamte Tabelle und baut sie neu auf. Merge-Tabellen bieten Ihnen da mehr Möglichkeiten, wie den Index zu einer zugrundeliegenden Tabelle auf einmal hinzuzufügen. Sie können auch nicht einfach eine Partition allein sichern oder wiederherstellen, was bei den einer Merge-Tabelle zugrunde liegenden Tabellen ohne Weiteres geht. Ob eine Tabelle von der Partitionierung profitiert, hängt von vielen Faktoren ab. Sie müssen Ihre eigenen Anwendungen mit Benchmarks testen, um festzustellen, ob Partitionierung eine gute Lösung für Sie darstellt.
Abfragen an partitionierte Tabellen optimieren Die Partitionierung bringt neue Möglichkeiten mit sich, um Abfragen zu optimieren (und entsprechende Fallstricke zu umgehen). Der Optimierer kann die Partitionierungsfunktion benutzen, um Partitionen zu leeren oder sie ganz aus einer Abfrage zu entfernen. Er tut dies, indem er schließt, dass die gewünschten Zeilen nur in bestimmten Partitionen zu finden sind. Das Kürzen sorgt also dafür, dass die Abfragen auf viel weniger Daten zugreifen müssen, als es ansonsten nötig wäre (im besten Fall). Es ist sehr wichtig, den partitionierten Schlüssel in der WHERE-Klausel anzugeben, selbst wenn er ansonsten redundant ist, damit der Optimierer die unnötigen Partitionen leeren kann. Wenn Sie dies tun, muss die Abfrageausführungs-Engine, genau wie bei MergeTabellen, auf alle Partitionen in der Tabelle zugreifen. Das kann bei großen Tabellen außerordentlich langsam verlaufen. Mithilfe von EXPLAIN PARTITIONS können Sie feststellen, ob der Optimierer Partitionen leert. Kehren wir noch einmal zu unseren Beispieldaten zurück: mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2006,p_2007,p_2008 type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 3 Extra:
Wie Sie sehen können, greift die Abfrage auf alle Partitionen zu. Schauen Sie sich den Unterschied an, wenn wir zur WHERE-Klausel eine Bedingung hinzufügen:
282 | Kapitel 5: Erweiterte MySQL-Funktionen
mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE day > '2007-01-01'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2007,p_2008
Der Optimierer ist ziemlich gut im Entleeren. Er kann sogar Bereiche in Listen aus diskreten Werten umwandeln und dann die einzelnen Elemente in der Liste kürzen. Allerdings ist er nicht allwissend. So ist z.B. die folgende WHERE-Klausel theoretisch kürzbar, dennoch kann MySQL sie nicht leeren: mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day WHERE YEAR(day) = 2007\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2006,p_2007,p_2008
Momentan kann MySQL nur in Vergleichen mit den Spalten der Partitionierungsfunktion kürzen bzw. leeren. Beim Ergebnis eines Ausdrucks ist das dagegen nicht möglich, selbst wenn der Ausdruck identisch mit der Partitionierungsfunktion ist. Sie können die Abfrage jedoch in eine äquivalente Form umwandeln: mysql> EXPLAIN PARTITIONS SELECT * FROM sales_by_day -> WHERE day BETWEEN '2007-01-01' AND '2007-12-31'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales_by_day partitions: p_2007
Da die WHERE-Klausel sich nun direkt auf die Partitionierungsspalte bezieht und nicht auf einen Ausdruck, kann der Optimierer seine vorteilhaften Kürzungen vornehmen. Der Optimierer ist auch klug genug, um die Partitionen während der Abfrageverarbeitung zu leeren. Falls z.B. eine partitionierte Tabelle die zweite Tabelle in einem Join ist und die Join-Bedingung der partitionierte Schlüssel ist, sucht MySQL nur in der relevanten Partition bzw. in den relevanten Partitionen nach passenden Zeilen. Das ist ein wichtiger Unterschied zu den Merge-Tabellen, die bei diesem Szenario immer alle zugrunde liegenden Tabellen abfragen.
Verteilte (XA-) Transaktionen Während Storage-Engine-Transaktionen innerhalb der Storage-Engine ACID-Eigenschaften realisieren, ist eine verteilte (XA-) Transaktion eine Transaktion auf höherer Ebene, die einige ACID-Eigenschaften außerhalb der Storage-Engine – und sogar außerhalb der Datenbank – mit einem zweiphasigen Commit erweitern kann. MySQL bietet ab Version 5.0 teilweise Unterstützung für XA-Transaktionen.
Verteilte (XA-) Transaktionen | 283
Eine XA-Transaktion erfordert einen Transaktionskoordinator, der alle Teilnehmer auffordert, sich auf das Commit vorzubereiten (Phase eins). Wenn der Koordinator von allen Teilnehmern ein »Fertig« empfängt, weist er sie an, fortzufahren und das Commit auszuführen. Das ist Phase zwei. MySQL kann in XA-Transaktionen als Teilnehmer auftreten, nicht jedoch als Koordinator. Es gibt genau genommen zwei Arten von XA-Transaktionen in MySQL. Der MySQL-Server kann an einer extern verwalteten verteilten Transaktion teilnehmen, er benutzt XA aber auch intern, um Storage-Engines und Binär-Logging zu koordinieren.
Interne XA-Transaktionen Die Ursache dafür, dass MySQL intern Xa-Transaktionen verwendet, ist die architektonische Trennung zwischen dem Server und den Storage-Engines. Storage-Engines sind völlig unabhängig voneinander und nehmen einander nicht wahr, so dass jede Engineübergreifende Transaktion von Natur aus verteilt ist und eine dritte Partei erfordert, um sie zu koordinieren. Diese dritte Partei ist der MySQL-Server. Gäbe es keine XA-Transaktionen, müssten z.B. bei jedem Engine-übergreifenden Transaktions-Commit nacheinander alle beteiligten Storage-Engines aufgefordert werden, ihre Bestätigung abzugeben. Dies bringt die Gefahr eines Absturzes mit sich, nachdem eine Engine ihr Commit abgegeben hat, aber bevor die anderen Engines so weit waren, was den Transaktionsregeln zuwiderlaufen würde. (Erinnern Sie sich: Transaktionen sollen als Alles-oder-NichtsOperationen agieren.) Wenn Sie das Binärlog als »Storage-Engine« für Log-Ereignisse betrachten, können Sie feststellen, weshalb XA-Transaktionen auch dann notwendig sind, wenn nur eine einzige transaktionsfähige Engine beteiligt ist. Das Synchronisieren eines Storage-Engine-Commits mit dem »Commit« eines Ereignisses am Binärlog ist eine verteilte Transaktion, weil der Server – und nicht die Storage-Engine – das Binärlog bedient. Momentan schafft XA ein Performance-Dilemma. Es hat die InnoDB-Unterstützung für das Gruppen-Commit (eine Technik, die mehrere Transaktionen mit einer einzigen Ein/Ausgabe-Operation bestätigen kann) seit MySQL 5.0 aufgehoben, so dass es viel mehr fsync( )-Aufrufe verursacht, als es eigentlich sollte. Es sorgt außerdem dafür, dass jede Transaktion eine Binärlog-Synchronisation erfordert, wenn Binärlogs aktiviert sind, und verlangt zwei Log-Entleerungen pro Commit statt nur einer. Falls Sie mit anderen Worten wollen, dass das Binärlog sicher mit Ihren Transaktionen synchronisiert wird, dann erfordert jede Transaktion wenigstens drei fsync( )-Aufrufe. Die einzige Möglichkeit, dies zu vermeiden, besteht darin, das Binärlog zu deaktivieren und innodb_support_xa auf 0 zu setzen. Diese Einstellungen sind inkompatibel mit der Replikation. Replikation erfordert BinärLogging und XA-Unterstützung, und darüber hinaus müssen Sie – um so sicher wie möglich zu sein – sync_binlog auf 1 setzen, damit die Storage-Engine und das Binärlog synchronisiert werden. (Die XA-Unterstützung ist ansonsten wertlos, da das Binärlog möglicherweise nicht auf Festplatte »bestätigt« wird.) Das ist einer der Gründe, weshalb 284 | Kapitel 5: Erweiterte MySQL-Funktionen
wir Ihnen dringend empfehlen, einen RAID-Controller mit einem batteriegesicherten Schreib-Cache zu benutzen: Der Cache kann die zusätzlichen fsync( )-Aufrufe beschleunigen und die Leistung wiederherstellen. Im nächsten Kapitel geht es genauer darum, wie man Transaktions- und Binär-Logging konfiguriert.
Externe XA-Transaktionen MySQL kann an extern verteilten Transaktionen zwar teilnehmen, ist aber nicht in der Lage, sie zu verwalten. Es bietet keine Unterstützung für die vollständige XA-Spezifikation. Zum Beispiel erlaubt die XA-Spezifikation, dass Verbindungen in einer einzigen Transaktion zusammengeführt werden, was mit MySQL momentan jedoch nicht möglich ist. Externe XA-Transaktionen sind sogar noch teurer als interne, da sie eine zusätzliche Latenz aufweisen sowie eine höhere Wahrscheinlichkeit mit sich bringen, dass einer der Teilnehmer ausfällt. Die Verwendung von XA über ein WAN oder sogar über das Internet ist aufgrund der unvorhersehbaren Netzwerkleistung eine verbreitete Falle. Es ist im Allgemeinen am besten, XA-Transaktionen zu vermeiden, wenn es eine unvorhersehbare Komponente gibt, wie etwa ein langsames Netzwerk oder einen Benutzer, der lange nicht den »Sichern«-Button anklickt. Alles, was ein Commit verzögert, ist mit hohen Kosten verbunden, weil es nicht nur in einem System, sondern potenziell in vielen Systemen Verzögerungen hervorruft. Es gibt jedoch andere Methoden, um leistungsstarke verteilte Transaktionen zu schaffen. So können Sie z.B. die Daten lokal einfügen und in eine Warteschlange einreihen und sie dann atomar, also einzeln in einer viel kleineren, schnelleren Transaktion verteilen. Sie können die Daten auch mithilfe der MySQL-Replikation von einem Platz zum anderen schicken. Wir haben festgestellt, das manche Anwendungen, die verteilte Transaktionen benutzen, diese eigentlich überhaupt nicht brauchen. Nichtsdestoweniger können XA-Transaktionen eine sinnvolle Methode darstellen, um Daten zwischen Servern zu synchronisieren. Diese Methode funktioniert gut, wenn Sie aus irgendeinem Grund keine Replikation einsetzen können oder wenn Updates nicht so entscheidend für die Performance sind.
Verteilte (XA-) Transaktionen | 285
KAPITEL 6
Die Servereinstellungen optimieren
Oft hört man die Frage: »Wie sieht die optimale Konfigurationsdatei für meinen Server mit 16 GByte RAM und 100 GByte Daten aus?« Um ehrlich zu sein, eine solche Datei gibt es nicht. Server benötigen je nach Hardware, Datengröße, den Arten von Abfragen, die auf ihnen ausgeführt werden, und den Anforderungen des Systems – Antwortzeit, Transaktionsdauer, Konsistenz usw. – sehr unterschiedliche Konfigurationen. Die Standardkonfiguration ist so gestaltet, dass sie nicht zu viele Ressourcen benutzt, da MySQL sehr vielseitig sein soll und wahrscheinlich nicht das einzige System ist, das auf dem Server läuft, auf dem es installiert ist. Diese Konfiguration verwendet standardmäßig genügend Ressourcen, um MySQL zu starten und einfache Abfragen mit wenigen Daten auszuführen. Sie müssen sie sicherlich anpassen, wenn Sie mehr als nur ein paar Megabyte an Daten haben. Sie können mit einer der Beispielkonfigurationsdateien beginnen, die in der MySQL-Serverdistribution enthalten sind, und sie nach Wunsch verändern. Erwarten Sie keine großen Leistungszuwächse von jeder kleinen Konfigurationsänderung. Abhängig von Ihrer Last können Sie die Leistung normalerweise zwei- oder dreifach steigern, indem Sie die passenden Werte für eine Handvoll Konfigurationseinstellungen wählen (welche Optionen genau diesen Unterschied ausmachen, hängt von einer Vielzahl von Faktoren ab). Danach erfolgen Verbesserungen schrittweise. Sie werden eine bestimmte Abfrage bemerken, die langsam läuft und die Sie verbessern, indem Sie eine oder zwei Einstellungen verändern, aber Sie werden normalerweise Ihre Serverleistung nicht um Größenordnungen steigern können. Um einen solchen Fortschritt zu erzielen, müssen Sie im Allgemeinen Ihr Schema, die Abfragen und die Architektur der Anwendungen überprüfen. Am Anfang dieses Kapitels zeigen wir Ihnen, wie die Konfigurationsoptionen von MySQL funktionieren und wie Sie sie ändern können. Anschließend erklären wir, wie MySQL Speicher benutzt und wie Sie seine Speicherverwendung optimieren. Dann behandeln wir detailliert die Ein-/Ausgabe und die Festplattenspeicherung. Es folgt ein Abschnitt über die lastbasierte Feineinstellung, mit deren Hilfe Sie MySQL so anpassen können, dass es am besten für Ihre Last funktioniert. Schließlich folgen noch einige Hinweise zum dynamischen Tuning von Variablen für bestimmte Abfragen, die eigene Einstellungen erfordern.
286 |
Ein Hinweis zur Terminologie: Da viele MySQL-Kommandozeilenoptionen bestimmten Servervariablen entsprechen, verwenden wir manchmal die Begriffe Option und Variable synonym.
Grundlagen der Konfiguration In diesem Abschnitt erhalten Sie einen Überblick darüber, wie Sie MySQL erfolgreich konfigurieren. Zuerst erläutern wir, wie die MySQL-Konfiguration tatsächlich funktioniert, dann zeigen wir einige optimale Verfahren. MySQL ist im Allgemeinen ziemlich nachsichtig, was seine Konfiguration betrifft. Sie sparen jedoch eine Menge Arbeit und Zeit, wenn Sie diese Vorschläge befolgen. Zuerst müssen Sie wissen, woher MySQL seine Konfigurationsinformationen bezieht: von Kommandozeilenargumenten und Einstellungen in seiner Konfigurationsdatei. Auf Unix-artigen Systemen befindet sich die Konfigurationsdatei typischerweise unter /etc/my.cnf oder /etc/mysql/my.cnf. Wenn Sie die Startskripte Ihres Betriebssystems verwenden, dann bilden diese üblicherweise den einzigen Ort, an dem Sie Konfigurationseinstellungen angeben. Falls Sie MySQL – etwa im Rahmen einer Testinstallation – manuell starten, können Sie diese Werte auch auf der Kommandozeile angeben. Die meisten Variablen tragen die gleichen Namen wie ihre entsprechenden Kommandozeilenoptionen, es gibt aber auch einige Ausnahmen. Zum Beispiel setzt --memlock die locked_in_memory-Variable.
Alle Einstellungen, die Sie permanent verwenden wollen, sollten nicht auf der Kommandozeile angegeben, sondern in die globale Konfigurationsdatei gesetzt werden. Ansonsten riskieren Sie, den Server versehentlich ohne sie zu starten. Es bietet sich außerdem an, alle Konfigurationsdateien an einer einzigen Stelle aufzuheben, damit Sie sie leicht untersuchen können. Vergewissern Sie sich, wo sich die Konfigurationsdatei des Servers tatsächlich befindet! Wir haben Leute gesehen, die erfolglos versucht haben, einen Server mithilfe einer Datei zu verändern, die dieser überhaupt nicht gelesen hat, wie etwa /etc/my.cnf bei Debian GNU/Linux-Servern, die in /etc/mysql/my.cnf nach ihrer Konfiguration suchen. Manchmal befinden sich Dateien an mehreren Stellen, weil möglicherweise ein früherer Systemadministrator ebenfalls durcheinander war. Falls Sie nicht wissen, welche Dateien Ihr Server liest, dann fragen Sie ihn einfach: $ which mysqld /usr/sbin/mysqld $ /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options' Default options are read from the following files in the given order: /etc/mysql/my.cnf ~/.my.cnf /usr/etc/my.cnf
Das gilt für typische Installationen, bei denen ein einziger Server auf einem Host läuft. Sie können kompliziertere Konfigurationen schaffen, es gibt dafür allerdings keine Standard-
Grundlagen der Konfiguration | 287
methode. Die MySQL-Serverdistribution enthält ein Programm namens mysqlmanager, das mehrere Instanzen aus einer einzigen Konfiguration mit separaten Abschnitten ausführen kann. (Dies ist ein Ersatz für das ältere Skript mysqld_multi.) Viele Betriebssystemdistributionen enthalten dieses Programm jedoch nicht oder benutzen es nicht in ihren Startskripten. Viele verwenden nicht einmal das mit MySQL gelieferte Startskript. Die Konfigurationsdatei ist in Abschnitte unterteilt, die jeweils mit einer Zeile beginnen, die den Namen des Abschnitts in eckigen Klammern enthält. Ein MySQL-Programm liest im Allgemeinen den Abschnitt, der den gleichen Namen trägt wie das Programm. Viele Clientprogramme lesen außerdem den Abschnitt client, in dem Sie gebräuchliche Einstellungen ablegen können. Der Server liest normalerweise den Abschnitt mysqld. Achten Sie darauf, die Einstellungen in den richtigen Abschnitt der Datei zu legen, da sie sonst keine Wirkung haben.
Syntax, Geltungsbereich und Dynamismus Konfigurationseinstellungen werden kleingeschrieben, einzelne Wörter werden durch Unterstriche oder Bindestriche voneinander getrennt. Folgende Einstellungen sind äquivalent. Ihnen können beide Formen auf Kommandozeilen und in Konfigurationsdateien begegnen: /usr/sbin/mysqld --auto-increment-offset=5 /usr/sbin/mysqld --auto_increment_offset=5
Wir empfehlen Ihnen, sich für einen Stil zu entscheiden und diesen dann konsistent zu benutzen. Das erleichtert es Ihnen, in Ihren Dateien nach Einstellungen zu suchen. Konfigurationseinstellungen können mehrere Geltungsbereiche aufweisen. Manche Einstellungen gelten serverweit (globaler Geltungsbereich), andere sind bei jeder Verbindung anders (Geltungsbereich ist die Sitzung), wieder andere sind objektbezogen. Viele für eine Sitzung geltende Variablen haben globale Gegenstücke, die Sie sich als Standardwerte vorstellen können. Wenn Sie die Sitzungsvariable ändern, dann beeinflusst das nur die Verbindung, in der Sie sie geändert haben. Die Änderungen gehen verloren, wenn die Verbindung geschlossen wird. Hier sind einige Beispiele für die vielen Verhaltensweisen, die Sie beachten sollten: • Die Variable query_cache_size gilt global. • Die Variable sort_buffer_size besitzt einen globalen Standardwert, Sie können sie aber auch pro Sitzung einstellen. • Die Variable join_buffer_size hat einen globalen Standardwert und kann pro Sitzung eingestellt werden, allerdings kann eine einzelne Abfrage, die mehrere Tabellen in einem Join zusammenführt, einen Join-Puffer pro Join reservieren, so dass es mehrere Join-Puffer pro Abfrage geben kann. Sie können Variablen nicht nur in der Konfigurationsdatei einstellen, sondern viele von ihnen (aber nicht alle) ändern, während der Server läuft. MySQL bezeichnet diese als
288 | Kapitel 6: Die Servereinstellungen optimieren
dynamische Konfigurationsvariablen. Die folgenden Anweisungen zeigen unterschiedliche Methoden, um die Sitzungs- und die globalen Werte von sort_buffer_size dynamisch zu ändern: SET sort_buffer_size SET GLOBAL sort_buffer_size SET @@sort_buffer_size SET @@session.sort_buffer_size SET @@global.sort_buffer_size
= = := := :=
<Wert>; <Wert>; <Wert>; <Wert>; <Wert>;
Wenn Sie Variablen dynamisch einstellen, dann sollten Sie sich bewusst sein, dass diese Einstellungen verloren gehen, wenn MySQL beendet wird. Falls Sie die Einstellungen behalten wollen, müssen Sie auch Ihre Konfigurationsdatei anpassen. Wenn Sie den globalen Wert einer Variablen einstellen, während der Server läuft, werden die Werte für die aktuelle Sitzung und alle weiteren bereits existierenden Sitzungen nicht beeinflusst. Das liegt daran, dass die Sitzungswerte aus dem globalen Wert initialisiert werden, wenn die Verbindungen aufgebaut werden. Sie sollten nach jeder Änderung die Ausgabe von SHOW GLOBAL VARIABLES untersuchen, um sicherzustellen, dass sie die gewünschte Wirkung hatte. Variablen verwenden unterschiedliche Einheiten. Sie müssen für jede Variable die richtige Einheit kennen. So gibt z.B. die Variable table_cache die Anzahl der Tabellen an, die im Cache abgelegt werden können, und nicht die Größe des Tabellen-Cache in Byte. Die Variable key_buffer_size wird in Byte angegeben, während andere Variablen in Anzahl der Seiten oder in Prozent festgelegt werden. Viele Variablen können mit einem Suffix angegeben werden, wie z.B. 1M für ein Megabyte. Das funktioniert allerdings nur in der Konfigurationsdatei oder als Kommandozeilenargument. Wenn Sie den SQL-Befehl SET benutzen, müssen Sie den tatsächlichen Wert 1048576 oder einen Ausdruck wie 1024 * 1024 benutzen. Ausdrücke können Sie wiederum nicht in Konfigurationsdateien verwenden. Es gibt außerdem einen besonderen Wert, den Sie Variablen mit dem SET-Befehl zuweisen können: das Schlüsselwort DEFAULT. Wenn Sie diesen Wert einer sitzungsweit geltenden Variablen zuweisen, dann wird diese Variable auf den entsprechenden globalen Wert gesetzt; weisen Sie ihn einer global geltenden Variablen zu, wird die Variable auf den bei der Kompilierung festgelegten Standardwert gesetzt (nicht auf den Wert, der in der Konfigurationsdatei angegeben wurde). Das bietet sich an, wenn Sie sitzungsweit geltende Variablen wieder auf die Werte zurücksetzen wollen, die sie beim Öffnen der Verbindung hatten. Wir raten Ihnen, dies nicht für globale Variablen einzusetzen, da Sie damit wahrscheinlich nicht den gewünschten Effekt erzielen – d.h., dass die Werte nicht auf den Wert beim Serverstart zurückgesetzt werden.
Nebenwirkungen beim Einstellen von Variablen Das dynamische Einstellen von Variablen kann unerwartete Nebenwirkungen haben, etwa das Entfernen von schmutzigen Blöcken aus den Puffern. Seien Sie vorsichtig mit
Grundlagen der Konfiguration | 289
Einstellungen, die Sie online ändern, da diese dem Server unter Umständen eine Menge Arbeit machen. Manchmal lässt der Name einer Variablen Rückschlüsse auf ihr Verhalten zu. So macht z.B. max_heap_table_size genau das, was der Name besagt: Sie gibt die maximale Größe an, auf die implizite, im Speicher befindliche temporäre Tabellen anwachsen dürfen. Allerdings sind die Namenskonventionen nicht völlig konsistent, so dass Ihnen ein Blick auf den Namen nicht immer unbedingt verrät, was die Variable tut. Wir wollen uns nun einige wichtige Variablen anschauen und feststellen, welche Wirkungen es hat, wenn man sie dynamisch ändert: key_buffer_size
Durch das Einstellen dieser Variablen wird der gewünschte Platz für den Schlüsselpuffer (oder Schlüssel-Cache) gleich auf einmal reserviert. Allerdings stellt das Betriebssystem den Speicher erst dann zur Verfügung, wenn er tatsächlich benötigt wird. Zum Beispiel bedeutet das Einstellen der Schlüsselpuffergröße auf ein Gigabyte nicht, dass der Server auf der Stelle veranlasst wird, tatsächlich ein Gigabyte an Speicher dafür zu übergeben. (Im nächsten Kapitel erfahren Sie, wie Sie die Speicherbenutzung des Servers überwachen können.) MySQL erlaubt es Ihnen, mehrere Schlüssel-Caches zu erzeugen, wie wir später in diesem Kapitel erläutern. Wenn Sie diese Variable für einen nicht standardgemäßen Schlüssel-Cache auf 0 setzen, verschiebt MySQL alle Indizes aus dem angegebenen Cache in den Standard-Cache und löscht den angegebenen Cache, wenn er nicht mehr benutzt wird. Setzen Sie diese Variable für einen nicht existierenden Cache, dann wird dieser erzeugt. Wenn Sie diese Variable für einen existierenden Cache auf einen Wert ungleich null setzen, dann wird der Speicher des angegebenen Cache geleert. Technisch gesehen, handelt es sich hier um eine Online-Operation, allerdings werden alle Operationen blockiert, die versuchen, auf den Cache zuzugreifen, während er geleert wird. table_cache_size
Das Einstellen dieser Variablen hat keine unmittelbare Wirkung – diese wird verzögert, bis ein Thread das nächste Mal eine Tabelle öffnet. In diesem Fall überprüft MySQL den Wert der Variablen. Ist der Wert größer als die Anzahl der Tabellen im Cache, kann der Thread die neu geöffnete Tabelle in den Cache einfügen. Ist der Wert kleiner als die Anzahl der Tabellen im Cache, löscht MySQL unbenutzte Tabellen aus dem Cache. thread_cache_size
Das Setzen dieser Variablen hat keine unmittelbare Wirkung – diese wird verzögert, bis das nächste Mal eine Verbindung geschlossen wird. Zu diesem Zeitpunkt überprüft MySQL, ob im Cache Platz ist, um den Thread zu speichern. Ist dies der Fall, speichert es den Thread für eine künftige Wiederverwendung durch eine andere Verbindung im Cache. Ist es nicht der Fall, wird der Thread nicht gespeichert, sondern beendet. Hier wird die Anzahl der Threads im Cache und daher die Menge an
290 | Kapitel 6: Die Servereinstellungen optimieren
Speicher, die der Thread-Cache verwendet, nicht sofort verkleinert; sie verkleinern sich nur, wenn eine neue Verbindung einen Thread aus dem Cache entfernt, um ihn zu benutzen. (MySQL fügt nur dann Threads in den Cache ein, wenn Verbindungen geschlossen werden, und entfernt sie nur dann aus dem Cache, wenn neue Verbindungen erzeugt werden.) query_cache_size
MySQL reserviert und initialisiert die angegebene Speichermenge für den AbfrageCache auf einmal beim Serverstart. Wenn Sie diese Variable aktualisieren (sogar, wenn Sie sie auf ihren aktuellen Wert setzen), löscht MySQL sofort alle im Cache gespeicherten Abfragen, ändert die Größe des Cache auf den angegebenen Wert und reinitialisiert den Speicher des Cache. read_buffer_size
MySQL reserviert erst dann Speicher für diesen Puffer, wenn eine Abfrage ihn tatsächlich braucht, allerdings wird in diesem Fall sofort der komplette hier angegebene Speicher belegt. read_rnd_buffer_size
MySQL reserviert erst dann Speicher für diesen Puffer, wenn eine Abfrage ihn tatsächlich braucht; in diesem Fall wird nur so viel Speicher reserviert, wie nötig ist. (Der Name max_read_rnd_buffer_size würde diese Variable genauer beschreiben.) sort_buffer_size
MySQL reserviert erst dann Speicher für diesen Puffer, wenn eine Abfrage eine Sortierung durchführen muss. In diesem Fall belegt MySQL allerdings sofort den kompletten Speicher, ob nun die volle Größe benötigt wird oder nicht. Wir erläutern später noch genauer, was diese Variablen tun. Hier wollten wir Ihnen einfach nur zeigen, welches Verhalten Sie zu erwarten haben, wenn Sie diese wichtigen Variablen ändern.
Der Einstieg Seien Sie vorsichtig, wenn Sie Variablen setzen. Mehr ist nicht immer besser, und wenn Sie die Variablen zu hoch einstellen, können Sie sich leicht Probleme einhandeln: Ihnen geht der Speicher aus, Ihr Server muss deshalb Daten auf die Platte auslagern, oder sein Adressraum wird knapp. Wir empfehlen Ihnen, eine Benchmark-Suite zu entwickeln, bevor Sie mit der Feineinstellung Ihres Servers beginnen. (Benchmarking erklären wir in Kapitel 2.) Um die Konfiguration Ihres Servers zu optimieren, brauchen Sie eine Benchmark-Suite, die Ihre Gesamtlast darstellt und Grenzfälle einschließt, wie etwa sehr große und komplexe Abfragen. Wenn Sie eine bestimmte Problemstelle identifiziert haben – wie etwa eine Abfrage, die nur langsam läuft – können Sie auch versuchen, für diesen Fall zu optimieren. Allerdings laufen Sie dann Gefahr, unbemerkt andere Abfragen negativ zu beeinflussen.
Grundlagen der Konfiguration | 291
Sie sollten immer ein Überwachungssystem laufen lassen, um zu messen, ob eine Änderung die Gesamtleistung Ihres Servers im tatsächlichen Betrieb verbessert oder beeinträchtigt. Benchmarks reichen nicht, weil sie nicht umfassend genug sind. Wenn Sie die Gesamtleistung Ihres Servers nicht messen, beeinträchtigen Sie möglicherweise unabsichtlich die Leistung. Uns sind viele Fälle bekannt, in denen jemand die Konfiguration eines Servers geändert hat und der Meinung war, dass er damit die Leistung steigern könnte, während die Leistung des Servers in Wirklichkeit aufgrund einer anderen Last zu einem anderen Zeitpunkt des Tages oder eines anderen Wochentages insgesamt sank. Wir befassen uns in Kapitel 14 mit einigen Überwachungssystemen. Am besten ist es, wenn Sie nur eine oder zwei Variablen ein wenig ändern und nach jeder Änderung die Benchmark-Tests durchführen. Manchmal werden die Ergebnisse Sie überraschen: Sie erhöhen eine Variable ein wenig und stellen eine Verbesserung fest, Sie erhöhen sie weiter, und plötzlich bemerken Sie einen starken Einbruch der Leistung. Wenn die Performance nach einer Änderung leidet, haben Sie möglicherweise eine Ressource zu stark in Anspruch genommen, haben z.B. zu viel Speicher für einen Puffer belegt, der häufig reserviert und wieder freigegeben wird. Vielleicht ist es auch zu einer Fehlanpassung zwischen MySQL und Ihrem Betriebssystem oder der Hardware gekommen. Wir haben z.B. festgestellt, dass der optimale sort_buffer_size-Wert davon beeinflusst werden könnte, wie der CPU-Cache arbeitet, und dass der read_buffer_size-Wert daran angepasst werden muss, wie das Read-Ahead des Servers und das allgemeine Ein-/Ausgabe-Subsystem konfiguriert sind. Größer ist nicht immer besser. Manche Variablen sind auch von anderen abhängig, was Sie erst durch Erfahrung und mit zunehmendem Wissen über die Architektur des Systems lernen. So hängt z.B. der beste innodb_log_ file_size-Wert von innodb_buffer_pool_size ab. Wenn Sie sich Notizen machen, etwa mithilfe von Kommentaren in der Konfigurationsdatei, ersparen Sie sich (und Ihren Nachfolgern) mit Sicherheit eine Menge Arbeit. Noch besser ist es, wenn Sie die Konfigurationsdatei einer Versionskontrolle unterstellen. Das ist sowieso eine gute Vorgehensweise, da Sie auf diese Weise Änderungen widerrufen können. Um die Komplexität beim Verwalten vieler Konfigurationsdateien zu verringern, erzeugen Sie einfach einen symbolischen Link von der Konfigurationsdatei zum zentralen Versionskontroll-Repository. Mehr zu diesem Thema erfahren Sie in einem guten Buch über die Systemadministration. Bevor Sie damit beginnen, Ihre Konfiguration zu verfeinern, sollten Sie an Ihren Abfragen und Ihrem Schema arbeiten, um wenigstens die offensichtlichsten Optimierungen durchzuführen, wie etwa das Anlegen von Indizes. Wenn Sie stark an der Konfiguration herumbasteln und dann Ihre Abfragen oder Ihr Schema ändern, müssen Sie möglicherweise noch einmal an die Konfiguration Hand anlegen. Denken Sie daran, dass das Feineinstellen ein laufender, iterativer Prozess ist. Wenn Ihre Hardware, die Last und die Daten nicht völlig statisch sind, müssen Sie immer wieder an Ihrer Konfiguration arbeiten. Das bedeutet, dass Sie nicht das letzte Quäntchen Leistung aus Ihrem Server herausquetschen müssen. Vermutlich wäre der Nutzen, den Sie aus dem dafür erforderlichen, immensen Zeitaufwand ziehen würden, sowieso relativ gering. Wir empfehlen Ihnen, Ihre Konfigu-
292 | Kapitel 6: Die Servereinstellungen optimieren
ration zu verfeinern, bis sie »gut genug« ist, und sie dann so zu lassen, bis Sie irgendwann Grund zu der Annahme haben, dass doch noch (oder wieder?) eine deutliche Leistungssteigerung möglich ist. Auch wenn Sie Ihr Schema oder Ihre Abfragen ändern, sollten Sie sich die Konfiguration noch einmal vornehmen. Wir entwickeln im Allgemeinen Beispielkonfigurationsdateien für verschiedene Zwecke und benutzen sie als unsere eigenen Vorgaben, vor allem, wenn wir viele ähnliche Server in einer Installation verwalten. Doch wie wir bereits am Anfang dieses Kapitels warnend angemerkt haben, besitzen wir keine allumfassende »beste Konfigurationsdatei« für z.B. einen Vier-CPU-Server mit 16 GByte Speicher und 12 Festplatten. Sie müssen wirklich Ihre eigenen Konfigurationen entwickeln, weil sogar eine gute Ausgangsposition je nach Einsatz Ihres Servers stark variiert.
Allgemeines Tuning Sie können die Konfiguration als einen zweiteiligen Vorgang betrachten: Benutzen Sie einige der grundlegenden Fakten über Ihre Installation, um einen sinnvollen Ausgangspunkt zu schaffen, und modifizieren Sie diesen dann anhand der Details über Ihre Arbeitslast. Wahrscheinlich sollten Sie eines der Beispiele als Ausgangspunkt verwenden, die MySQL mitbringt. Beachten Sie bei der Wahl des passenden Musters Ihre Hardware. Wie viele Festplatten und CPUs haben Sie und wie viel Speicher? Die Beispiele tragen sinnvolle Namen wie my-huge.cnf, my-large.cnf und my-small.cnf, es sollte also klar sein, mit welchem Sie beginnen. Allerdings gelten die Beispieldateien nur für MyISAM-Tabellen. Wenn Sie eine andere Storage-Engine benutzen, müssen Sie eine eigene Konfiguration herstellen.
Die Speicherbenutzung anpassen Für eine gute Leistung ist es entscheidend, dass MySQL den Speicher richtig benutzt. Sie müssen die Speicherbenutzung von MySQL mit hoher Wahrscheinlichkeit an Ihre Anforderungen anpassen. Im Prinzip lässt sich die Speicherbenutzung von MySQL in zwei Kategorien einteilen: Speicher, den Sie kontrollieren können, und Speicher, bei dem Sie das nicht können. Sie können nicht kontrollieren, wie viel Speicher MySQL lediglich zum Ausführen des Servers, zum Parsen der Abfragen und zum Verwalten seiner internen Angelegenheiten verwendet, Sie haben aber eine Menge Kontrolle darüber, wie viel Speicher es für besondere Aufgaben einsetzt. Es ist nicht schwer, den Speicher, den Sie kontrollieren können, gut einzusetzen, verlangt aber, dass Sie wissen, was Sie konfigurieren. Sie können die Speicheranpassung schrittweise vornehmen: 1. Ermitteln Sie die absolute obere Grenze dessen, was MySQL an Speicher möglicher-
weise nutzen kann. 2. Stellen Sie fest, wie viel Speicher MySQL für seine verbindungsbezogenen Bedürf-
nisse benutzt, wie etwa Sortierpuffer und temporäre Tabellen. Allgemeines Tuning | 293
3. Stellen Sie fest, wie viel Speicher das Betriebssystem benötigt, um gut zu laufen.
Rechnen Sie Speicher für andere Programme hinzu, die auf der gleichen Maschine laufen, wie etwa regelmäßig ausgeführte Jobs. 4. Vorausgesetzt, das ist jetzt noch sinnvoll, verwenden Sie den Rest des Speichers für
die MySQL-Caches, wie etwa den InnoDB-Pufferpool. Wir werden diese Schritte in den folgenden Abschnitten durchgehen. Anschließend werfen wir einen ausführlicheren Blick auf die Anforderungen der verschiedenen MySQLCaches.
Wie viel Speicher kann MySQL benutzen? Es gibt auf jedem System eine strikte obere Grenze für den Speicher, der MySQL höchstens zur Verfügung gestellt werden kann. Der Ansatzpunkt ist die Menge des physisch vorhandenen Speichers. Wenn Ihr Server ihn nicht hat, kann MySQL ihn nicht benutzen. Sie müssen außerdem an die Grenzen des Betriebssystems oder der Architektur denken, wie z.B. an die Beschränkungen, die 32-Bit-Betriebssysteme in Bezug auf den Speicher auferlegen, den ein bestimmter Prozess ansprechen kann. Da MySQL in einem einzelnen Prozess mit mehreren Threads läuft, kann die Speichermenge, die es insgesamt benutzen kann, durch solche Beschränkungen stark begrenzt sein – z.B. begrenzen 32-Bit-LinuxKernel die Menge an Speicher, die ein Prozess adressieren kann, auf einen Wert, der typischerweise zwischen 2,5 und 2,7 GByte liegt. Es ist sehr gefährlich, wenn einem der Adressraum ausgeht, und kann zum Absturz von MySQL führen. Es gibt viele weitere betriebssystemspezifische Parameter und Eigenarten, die man in Betracht ziehen muss. Dazu gehören nicht nur die prozessweisen Beschränkungen, sondern auch Stack-Größen und andere Einstellungen. Die glibc-Bibliotheken des Systems können auch Beschränkungen pro einzelner Allozierung verhängen. So könnte es sein, dass Sie nicht in der Lage sind, innodb_buffer_pool größer als 2 GByte einzustellen, wenn das alles ist, was Ihre glibc-Bibliotheken in einer einzigen Allozierung unterstützen. Selbst auf 64-Bit-Servern gelten Beschränkungen. So sind z.B. viele der Puffer, die wir hier besprechen, wie etwa der Schlüsselpuffer, auf einem 64-Bit-Server auf 4 GByte beschränkt. Einige dieser Einschränkungen wurden in MySQL 5.1 aufgehoben, und künftig gibt es wahrscheinlich noch weitere Änderungen, weil man bei MySQL AB aktiv daran arbeitet, dass MySQL leistungsfähigere Hardware besser ausnutzen kann. Im MySQL-Handbuch sind die Maximalwerte der einzelnen Variablen dokumentiert.
Speicheranforderungen pro Verbindung MySQL braucht wenigstens ein bisschen Speicher, um eine Verbindung (Thread) offen zu halten. Auch zum Ausführen einer Abfrage benötigt es eine gewisse Grundmenge an Speicher. Sie müssen genügend Speicher für MySQL vorsehen, um Abfragen während Spitzenzeiten auszuführen, da die Abfragen ansonsten aufgrund des Speichermangels entweder schlecht laufen oder fehlschlagen.
294 | Kapitel 6: Die Servereinstellungen optimieren
Es ist sinnvoll zu wissen, wie viel Speicher MySQL in Spitzenzeiten benötigt, allerdings kann es vorkommen, dass einige Nutzungsmuster unerwarteterweise viel Speicher in Anspruch nehmen, was sich schwer vorhersagen lässt. Vorbereitete Anweisungen sind ein Beispiel dafür, weil Sie viele von ihnen auf einmal offen haben können. Ein weiteres Beispiel ist der InnoDB-Tabellen-Cache (mehr darüber später). Sie müssen nicht gleich ein Worst-Case-Szenario annehmen, wenn Sie versuchen, den Speicherverbrauch in Spitzenzeiten vorherzusagen. Falls Sie z.B. MySQL so konfigurieren, dass maximal 100 Verbindungen erlaubt sind, könnte es theoretisch möglich sein, gleichzeitig große Abfragen auf allen 100 Verbindungen auszuführen. In der Realität dagegen wird dieser Fall wohl nicht eintreten. Falls Sie z.B. myisam_sort_buffer_size auf 256M setzen, liegt der Verbrauch im schlimmsten Fall bei wenigstens 25 GByte, allerdings wird dieser Fall mit hoher Wahrscheinlichkeit nicht eintreten. Anstatt Worst-Case-Szenarien zu berechnen, besteht ein besserer Ansatz darin, Ihren Server unter einer echten Arbeitslast zu beobachten und zu ermitteln, wie viel Speicher er benutzt. Das können Sie feststellen, wenn Sie die Größe des virtuellen Speichers des Prozesses beobachten. Bei vielen Unix-artigen Systemen finden Sie diesen Wert in der VIRTSpalte in top oder unter VSZ in ps. Im nächsten Kapitel erfahren Sie mehr darüber, wie Sie die Speicherbenutzung überwachen.
Speicher für das Betriebssystem reservieren Genau wie für Abfragen müssen Sie auch für das Betriebssystem genügend Speicher reservieren, damit es seine Arbeit verrichten kann. Sie erkennen, dass das Betriebssystem genug Speicher hat, wenn es nicht aktiv virtuellen Speicher auf die Festplatte auslagert (Swapping). (In »Swapping« auf Seite 364 finden Sie mehr zu diesem Thema.) Sie sollten nicht mehr als ein oder zwei Gigabyte für das Betriebssystem reservieren müssen, auch nicht auf Maschinen mit viel Speicher. Fügen Sie eine Sicherheitsreserve hinzu, die Sie noch ein wenig erhöhen sollten, wenn Sie regelmäßig speicherintensive Jobs auf der Maschine ausführen (z.B. Backups). Fügen Sie keinen Speicher für die Caches des Betriebssystems hinzu, da diese sehr groß werden können. Im Allgemeinen verwendet das Betriebssystem übrig gebliebenen Speicher für diese Caches. Wir betrachten diese getrennt von den betriebssystemeigenen Anforderungen in den folgenden Abschnitten.
Speicher für Caches belegen Wenn der Server speziell für MySQL vorgesehen ist, steht jeder Speicher, den Sie nicht für das Betriebssystem oder für die Abfrageverarbeitung reservieren, für die Caches zur Verfügung. MySQL braucht mehr Speicher für Caches als für alles andere. Es benutzt Caches, um Festplattenzugriffe zu vermeiden, die um Größenordnungen langsamer sind als Zugriffe auf die Daten im Speicher. Das Betriebssystem kann im Namen von MySQL (besonders für MyISAM) Daten im Cache ablegen. Allerdings braucht MySQL für sich selbst schon eine Menge Speicher.
Allgemeines Tuning | 295
Im Folgenden sehen Sie die wichtigsten Caches, die Sie für die meisten Installationen berücksichtigen müssen: • die Betriebssystem-Caches für MyISAM-Daten • MyISAM-Schlüssel-Caches • den InnoDB-Pufferpool • den Abfrage-Cache Es gibt weitere Caches, die aber im Allgemeinen nicht so viel Speicher benutzen. Wir haben den Abfrage-Cache im vorangegangenen Kapitel ausführlich besprochen, so dass wir uns in den folgenden Abschnitten auf die Caches konzentrieren wollen, die MyISAM und InnoDB brauchen, um gut zu funktionieren. Es ist viel einfacher, einen Server zu verfeinern, wenn Sie nur eine Storage-Engine benutzen. Falls Sie nur mit MyISAM-Tabellen arbeiten, können Sie InnoDB komplett deaktivieren. Benutzen Sie dagegen nur InnoDB, müssen Sie nur minimale Ressourcen für MyISAM vorsehen (MySQL verwendet intern MyISAM-Tabellen für einige Operationen). Benutzen Sie hingegen eine Mischung aus Storage-Engines, dann ist es nicht leicht, das richtige Gleichgewicht zwischen ihnen zu finden. Der beste Ansatz, den wir gefunden haben, besteht darin, zuerst gezielte Vermutungen anzustellen und dann einen Benchmark-Test durchzuführen.
Der MyISAM-Schlüssel-Cache Die MyISAM-Schlüssel-Caches werden auch als Schlüssel-Puffer bezeichnet. Standardmäßig gibt es einen, Sie können aber auch mehrere davon anlegen. Im Gegensatz zu InnoDB und einigen anderen Storage-Engines legt MyISAM selbst nur Indizes im Cache ab, keine Daten (dies überlässt es dem Betriebssystem). Wenn Sie hauptsächlich MyISAM benutzen, sollten Sie für die Schlüssel-Caches nicht viel Speicher vorsehen. Viele der Ratschläge in diesem Abschnitt gehen davon aus, dass Sie nur MyISAM-Tabellen verwenden. Falls Sie einen Mix aus MyISAM und einer anderen Engine, wie etwa InnoDB, einsetzen, müssen Sie die Anforderungen beider Storage-Engines berücksichtigen.
Die wichtigste Option ist key_buffer_size. Sie sollten versuchen, deren Wert auf 25 % bis 50 % der Speichermenge einzustellen, die Sie für Caches reserviert haben. Der Rest steht für die Betriebssystem-Caches zur Verfügung, die das Betriebssystem normalerweise mit Daten aus den .MYD-Dateien von MyISAM füllt. MySQL 5.0 besitzt, ungeachtet der verwendeten Architektur, eine strenge obere Grenze von 4 GByte für diese Variable. MySQL 5.1 erlaubt größere Größen. Schauen Sie in die aktuelle Dokumentation für Ihre Version des Servers. Standardmäßig legt MyISAM alle Indizes im vorgegebenen Schlüsselpuffer ab, allerdings können Sie mehrere benannte Schlüsselpuffer erzeugen. So haben Sie die Möglichkeit,
296 | Kapitel 6: Die Servereinstellungen optimieren
mehr als 4 GByte Indizes auf einmal im Speicher zu behalten. Um Schlüsselpuffer mit den Namen key_buffer_1 und key_buffer_2 zu erzeugen, die jeweils 1 GByte groß sind, schreiben Sie Folgendes in die Konfigurationsdatei: key_buffer_1.key_buffer_size = 1G key_buffer_2.key_buffer_size = 1G
Jetzt gibt es drei Schlüsselpuffer: die beiden explizit mit diesen Zeilen erzeugten und den Standardpuffer. Mit dem Befehl CACHE INDEX ordnen Sie Tabellen zu Caches zu. Mit der folgenden SQL-Anweisung weisen Sie MySQL an, key_buffer_1 für die Indizes aus den Tabellen t1 und t2 zu benutzen: mysql> CACHE INDEX t1, t2 IN key_buffer_1;
Wenn MySQL nun Blöcke aus den Indizes in diesen Tabellen liest, speichert es die Blöcke in dem angegebenen Puffer. Sie können die Indizes der Tabellen sogar vorab in den Cache laden, und zwar mit dem Befehl LOAD INDEX: mysql> LOAD INDEX INTO CACHE t1, t2;
Sie können dieses SQL in eine Datei setzen, die ausgeführt wird, wenn MySQL startet. Der Dateiname muss in der Option init_file angegeben werden. Die Datei kann mehrere SQL-Befehle enthalten, die jeweils auf einer eigenen Zeile stehen (Kommentare sind nicht erlaubt). Alle Indizes, die Sie nicht explizit zu einem Schlüsselpuffer zuordnen, werden dem Standardpuffer zugewiesen, wenn MySQL das erste Mal auf die .MYI-Datei zugreifen muss. Sie können die Leistung und die Benutzung der Schlüsselpuffer mit Informationen aus SHOW STATUS und SHOW VARIABLES überwachen. Mit den folgenden Gleichungen berechnen
Sie die Trefferrate und den Prozentsatz des benutzten Puffers: Cache-Trefferrate 100 - ( (Key_reads * 100) / Key_read_requests )
Verwendeter Puffer in Prozent 100 - ( (Key_blocks_unused * key_cache_block_size) * 100 / key_buffer_size ) In Kapitel 14 untersuchen wir einige Werkzeuge, wie etwa innotop zur bequemeren Performance-Überwachung.
Es ist auch ganz gut, wenn man die Cache-Trefferrate kennt, allerdings kann diese Zahl auch irreführend sein. So sieht z.B. der Unterschied zwischen 99 % und 99,9 % klein aus, repräsentiert jedoch eine zehnfache Zunahme. Die Cache-Trefferrate ist auch anwendungsabhängig: Manche Anwendungen würden gut bei 95 % funktionieren, während andere ein-/ausgabegebunden bei 99,9 % liegen. Bei entsprechend bemessenen Caches könnten Sie sogar eine Trefferrate von 99,99 % erzielen. Die Anzahl der Cache-Misses, also der Cache-Fehler, pro Sekunde ist im Allgemeinen eher empirisch von Nutzen. Nehmen Sie einmal an, Sie haben eine Festplatte, die 100
Allgemeines Tuning | 297
wahlfreie Lesezugriffe pro Sekunde schafft. Fünf Misses pro Sekunde sorgen noch nicht dafür, dass Ihre Last ein-/ausgabegebunden ist, 80 Misses pro Sekunde verursachen dagegen wahrscheinlich Probleme. Mit der folgenden Gleichung können Sie diesen Wert berechnen: Key_reads / Uptime
Berechnen Sie die Anzahl der Cache-Fehler ansteigend über Intervalle von 10 bis 100 Sekunden, um eine Vorstellung von der aktuellen Leistung zu erhalten. Der folgende Befehl zeigt die zunehmenden Werte alle 10 Sekunden: $ mysqladmin extended-status -r -i 10 | grep Key_reads
Wenn Sie entscheiden, wie viel Speicher Sie für die Schlüssel-Caches reservieren, hilft es vermutlich zu wissen, wie viel Platz Ihre MyISAM-Indizes tatsächlich auf der Festplatte belegen. Sie müssen die Schlüssel-Puffer nicht größer machen als die Daten, die sie im Cache ablegen. Auf einem Unix-artigen System ermitteln Sie die Größe der Dateien, die die Indizes speichern, mit einem solchen Befehl: $ du -sch `find /path/to/mysql/data/directory/ -name "*.MYI"`
Denken Sie daran, dass MyISAM den Betriebssystem-Cache für die Datendateien benutzt, die oft größer sind als die Indizes. Es ist daher oft sinnvoll, für den Betriebssystem-Cache mehr Speicher bereitzustellen als für die Schlüssel-Caches. Und selbst wenn Sie keine MyISAM-Tabellen haben, müssen Sie daran denken, dass Sie noch key_buffer_ size auf eine kleine Menge Speicher setzen müssen, wie etwa 32M. Der MySQL-Server nutzt MyISAM-Tabellen manchmal für interne Zwecke, wie z.B. temporäre Tabellen für GROUP BY-Abfragen.
Die Größe des MyISAM-Schlüsselblocks Die Schlüsselblockgröße ist aufgrund der Art, wie sie MyISAM, den BetriebssystemCache und das Dateisystem zur Interaktion veranlasst, wichtig (besonders für schreibintensive Aufgaben). Wenn die Schlüsselblockgröße zu klein ausfällt, werden Sie möglicherweise Read-around Writes bemerken. Dabei handelt es sich um Schreibvorgänge, die das Betriebssystem nicht ausführen kann, ohne zuerst einige Daten von der Festplatte zu lesen. Und so kommt es zu einem Read-around Write (es wird angenommen, dass die Seitengröße des Betriebssystems 4 KByte beträgt, was typisch für die x86-Architektur ist, und dass ein Schlüsselblock 1 KByte groß ist): 1. MyISAM fordert einen 1 KByte großen Schlüsselblock von der Festplatte an. 2. Das Betriebssystem liest 4 KByte Daten von der Festplatte und speichert sie im
Cache, dann übergibt es die gewünschten 1 KByte Daten an MyISAM. 3. Das Betriebssystem verwirft die im Cache gespeicherten Daten zugunsten irgend-
welcher anderen Daten. 4. MyISAM modifiziert den 1 KByte großen Schlüsselblock und bittet das Betriebssys-
tem, ihn wieder zurück auf die Festplatte zu schreiben.
298 | Kapitel 6: Die Servereinstellungen optimieren
5. Das Betriebssystem liest die gleichen 4 KByte Daten von der Festplatte in den
Betriebssystem-Cache ein, modifiziert das 1 KByte, das MyISAM geändert hat, und schreibt die gesamten 4 KByte zurück auf die Festplatte. Das Read-around Write trat in Schritt 5 auf, als MyISAM das Betriebssystem gebeten hat, nur einen Teil einer 4-KByte-Seite zu schreiben. Hätte die Blockgröße von MyISAM derjenigen des Betriebssystems entsprochen, dann hätte das Lesen von der Festplatte in Schritt 5 vermieden werden können.1 Leider gab es bis MySQL 5.0 keine Möglichkeit, die Schlüsselblockgröße zu konfigurieren. Seit MySQL 5.1 dagegen können Sie Read-around Writes vermeiden, indem Sie die Schlüsselblockgröße von MyISAM genauso groß machen wie die des Betriebssystems. Die Variable myisam_block_size steuert die Schlüsselblockgröße. Sie können die Größe der einzelnen Schlüssel mit der Option KEY_BLOCK_SIZE in den Anweisungen CREATE TABLE oder CREATE INDEX angeben. Da aber alle Schlüssel in derselben Datei gespeichert werden, müssen tatsächlich alle von ihnen Blöcke haben, die so groß sind wie die des Betriebssystems oder größer, um Probleme mit der Anordnung zu vermeiden, die immer noch Readaround Writes verursachen könnten. (Falls z.B. ein Schlüssel 1-KByte-Blöcke hat und ein anderer 4-KByte-Blöcke, dann passen die 4-KByte-Blockgrenzen möglicherweise nicht auf die Seitengrenzen des Betriebssystems.)
Der InnoDB-Pufferpool Wenn Sie hauptsächlich InnoDB-Tabellen benutzen, benötigt der InnoDB-Pufferpool wahrscheinlich mehr Speicher als alles andere. Im Gegensatz zum MyISAM-SchlüsselCache speichert der InnoDB-Pufferpool nicht nur Indizes: Er nimmt auch Zeilendaten, den adaptiven Hash-Index (siehe »Hash-Indizes« auf Seite 108), den Eingabepuffer, Sperren und andere interne Strukturen auf. InnoDB nutzt den Pufferpool auch, um Schreibvorgänge verzögern zu lassen, so dass es viele Schreibvorgänge verbinden und sequenziell ausführen kann. Kurz gesagt, InnoDB verlässt sich sehr stark auf den Pufferpool, und Sie sollten ihm auf jeden Fall genügend Speicher reservieren. Das MySQL-Handbuch empfiehlt, bei einem dedizierten Server bis zu 80 % des physischen Speichers der Maschine für den Pufferpool vorzusehen; in Wirklichkeit können Sie sogar noch mehr dafür verwenden, wenn die Maschine viel Speicher besitzt. Wie bei den MyISAM-Schlüsselpuffern können Sie Variablen der SHOW-Befehle oder Werkzeuge wie innotop einsetzen, um die Speichernutzung und die Leistung Ihres InnoDB-Pufferpools zu überwachen. Es gibt kein Äquivalent zu LOAD INDEX INTO CACHE für InnoDB-Tabellen. Falls Sie jedoch versuchen, einen Server aufzuwärmen, und bringen ihn dazu, mit einer großen Last umzugehen, können Sie Abfragen aufrufen, die vollständige Tabellenscans oder vollständige Indexscans durchführen. 1 Theoretisch verhält es sich so: Falls Sie sicherstellen könnten, dass die ursprünglichen 4 KByte Daten immer noch im Cache des Betriebssystems wären, wäre der Lesevorgang nicht erforderlich. Allerdings haben Sie keine Kontrolle darüber, welche Blöcke das Betriebssystem letztendlich im Cache behält. Mit dem fincore-Programm, das unter http://net.doit.wisc.edu/~plonka/fincore/ zur Verfügung steht, können Sie feststellen, welche Blöcke sich im Cache befinden.
Allgemeines Tuning | 299
In den meisten Fällen sollten Sie den InnoDB-Pufferpool so groß machen, wie der verfügbare Speicher es zulässt. Unter seltenen Umständen können große Pufferpools (sagen wir, mit 50 GByte) lange Verzögerungen hervorrufen. So könnte ein großer Pufferpool im Verlauf von Checkpoints oder einfügenden Puffer-Merge-Operationen sehr langsam werden und als Folge des Lockings kann die Nebenläufigkeit ausfallen. Wenn Ihnen solche Probleme begegnen, müssen Sie die Größe des Pufferpools verringern. Sie können die Variable innodb_max_dirty_pages_pct so ändern, dass sie InnoDB anweist, mehr oder weniger schmutzige (modifizierte) Seiten im Pufferpool zu halten. Falls Sie viele schmutzige Seiten zulassen, braucht InnoDB unter Umständen lange zum Herunterfahren, da es beim Beenden die schmutzigen Seiten in die Datendateien schreibt. Sie können es zwingen, schnell herunterzufahren, allerdings muss es dann beim Neustart viele Recovery-Aktionen durchführen, so dass Sie den Beenden-und-Neustart-Zyklus eigentlich nicht beschleunigen können. Wenn Sie vorher wissen, wann Sie herunterfahren müssen, können Sie die Variable auf einen niedrigeren Wert setzen, warten, bis der Reinigungs-Thread den Pufferpool gesäubert hat, und dann InnoDB herunterfahren, sobald die Anzahl der schmutzigen Seiten gering genug ist. Sie überwachen die Anzahl der schmutzigen Seiten, indem Sie sich die Statusvariable Innodb_buffer_pool_pages_ dirty anschauen oder innotop einsetzen, um SHOW INNODB STATUS zu kontrollieren. Das Verkleinern des Wertes der Variablen innodb_max_dirty_pages_pct bietet eigentlich keine Garantie dafür, dass InnoDB weniger schmutzige Seiten in den Pufferpool aufnimmt. Stattdessen steuert es den Schwellenwert, ab dem InnoDB aufhört, »faul« zu sein. Das Standardverhalten von InnoDB sieht vor, dass schmutzige Seiten mit einem Hintergrund-Thread entfernt, Schreibvorgänge zusammengefasst und aus Effizienzgründen sequenziell ausgeführt werden. Dieses Verhalten wird als »faul« bezeichnet, weil es InnoDB erlaubt, das Entfernen von schmutzigen Seiten aus dem Pufferpool so lange zu verzögern, bis es den Platz für andere Daten braucht. Wenn der Prozentsatz an schmutzigen Seiten den Schwellenwert erreicht, entfernt InnoDB die Seiten so schnell, wie es kann, um den Zähler für schmutzige Seiten niedriger zu halten. Der Vorgabewert der Variablen beträgt 90, InnoDB ist also beim Entleeren standardmäßig faul, bis der Pufferpool zu 90 % mit schmutzigen Seiten gefüllt ist. Sie können den Schwellenwert an Ihre Last anpassen, falls Sie die Schreiboperationen ein wenig ausweiten wollen. Verringern Sie den Wert etwa auf 50, wird InnoDB im Allgemeinen veranlasst, mehr Schreiboperationen auszuführen, weil es die Seiten früher entfernt und daher nicht in der Lage ist, die Schreiboperationen ebenfalls stapelweise zu verarbeiten. Weist Ihre Last jedoch viele Schreibspitzen auf, kann ein niedrigerer Wert InnoDB helfen, die Spitzen besser zu kompensieren: Es hat mehr »überschüssigen« Speicher, um schmutzige Seiten aufzunehmen, so dass es nicht warten muss, bis andere schmutzige Seiten auf die Festplatte verschoben werden.
300 | Kapitel 6: Die Servereinstellungen optimieren
Der Thread-Cache Der Thread-Cache nimmt Threads auf, die momentan nicht mit einer Verbindung verknüpft sind, aber für neue Verbindungen bereit sind. Wenn im Cache ein Thread vorliegt und eine neue Verbindung gestartet wird, entfernt MySQL den Thread aus dem Cache und übergibt ihn an die neue Verbindung. Wird die Verbindung geschlossen, setzt MySQL den Thread wieder zurück in den Cache, falls dort Platz ist. Ist kein Platz vorhanden, zerstört MySQL den Thread. Solange MySQL einen freien Thread im Cache hat, kann es sehr schnell auf Verbindungsanforderungen antworten, weil es nicht extra einen neuen Thread erzeugen muss. Die Variable thread_cache_size gibt die Anzahl der Threads an, die MySQL im Cache aufbewahren kann. Wahrscheinlich werden Sie diesen Wert nicht verändern wollen, es sei denn, Ihr Server empfängt viele Verbindungsanforderungen. Um zu überprüfen, ob der Thread-Cache groß genug ist, beobachten Sie die Statusvariable Threads_created. Im Allgemeinen versuchen wir, den Thread-Cache so groß zu halten, dass weniger als 10 neue Threads pro Sekunde erzeugt werden. Oft ist es aber recht einfach, diese Zahl auf unter 1 pro Sekunde zu drücken. Ein guter Ansatz besteht darin, die Variable Threads_connected zu beobachten und zu versuchen, thread_cache_size so groß zu machen, dass der typischen Fluktuation in der Arbeitslast begegnet werden kann. Falls sich z.B. Threads_connected normalerweise zwischen 100 und 200 bewegt, können Sie die Cache-Größe auf 100 setzt. Bleibt die Variable zwischen 500 und 700, sollte ein Thread-Cache von 200 groß genug sein. Stellen Sie es sich so vor: Bei 700 Verbindungen gibt es wahrscheinlich keine Threads im Cache, bei 500 Verbindungen gibt es 200 Threads im Cache, die bereit sind, falls die Last wieder auf 700 steigt. Für die meisten Einsatzzwecke ist es wahrscheinlich nicht notwendig, den Thread-Cache sehr groß zu machen. Allerdings sparen Sie kaum Speicher, wenn Sie ihn klein halten: Das bringt also keine Vorteile. Jeder Thread, der sich im Thread-Cache befindet oder schläft, belegt typischerweise etwa 256 KByte an Speicher. Das ist sehr wenig im Vergleich zu der Menge an Speicher, die ein Thread benutzen kann, wenn eine Verbindung aktiv eine Abfrage verarbeitet. Im Allgemeinen sollten Sie den Thread-Cache groß genug halten, um zu verhindern, dass Threads_created allzu oft erhöht wird. Ist diese Zahl jedoch sehr groß (z.B. viele Tausend Threads), werden Sie ihn sicher verkleinern, da manche Betriebssysteme mit sehr großen Thread-Zahlen nicht gut zurechtkommen, selbst wenn die meisten Threads schlafen.
Der Tabellen-Cache Vom Konzept her ist der Tabellen-Cache vergleichbar dem Thread-Cache, allerdings speichert er Objekte, die Tabellen repräsentieren. Jedes Objekt in dem Cache enthält die geparste .frm-Datei der dazugehörigen Tabelle sowie weitere Daten. Was sich genau in dem Objekt befindet, hängt von der Storage-Engine der Tabelle ab. So enthält es z.B. für
Allgemeines Tuning | 301
MyISAM die Tabellendaten und/oder die Indexdateideskriptoren. Für Merge-Tabellen kann es viele Dateideskriptoren enthalten, da Merge-Tabellen viele zugrunde liegende Tabellen besitzen können. Der Tabellen-Cache kann Ihnen helfen, Ressourcen wiederzuverwenden. Wenn z.B. eine Abfrage Zugriff auf eine MyISAM-Tabelle anfordert, könnte MySQL ihr einen Dateideskriptor aus einem im Cache befindlichen Objekt übergeben, anstatt die Datei zu öffnen. Der Tabellen-Cache kann auch dazu beitragen, dass einige der Ein-/Ausgaben vermieden werden, die erforderlich sind, um eine MyISAM-Tabelle in den Index-Headern als »in Benutzung« zu kennzeichnen.2 Das Design des Tabellen-Cache ist ein wenig MyISAM-zentriert – es handelt sich um einen der Bereiche, in denen (aus historischen Gründen) die Trennung zwischen dem Server und den Storage-Engines nicht vollkommen sauber ist. Der Tabellen-Cache ist für InnoDB nicht ganz so wichtig, da InnoDB nicht so häufig darauf zurückgreift (so besitzt es zum Aufnehmen von Dateideskriptoren eine eigene Version eines Tabellen-Cache). Allerdings profitiert selbst InnoDB von der Speicherung der geparsten .frm-Dateien im Cache. In MySQL 5.1 besteht der Tabellen-Cache aus zwei Teilen: einem Cache mit offenen Tabellen und einen Tabellendefinitions-Cache (die über die Variablen table_open_cache und table_definition_cache konfiguriert werden). Die Tabellendefinitionen (die geparsten .frm-Dateien) sind also von den anderen Ressourcen, wie etwa den Dateideskriptoren, getrennt. Geöffnete Tabellen gelten weiterhin pro Thread, pro benutzter Tabelle. Tabellendefinitionen dagegen sind global und können effizient auch von anderen Verbindungen genutzt werden. Im Allgemeinen können Sie table_definition_cache so hoch einstellen, dass alle Tabellendefinitionen Platz im Cache finden. Wenn es nicht gerade um Zehntausende von Tabellen geht, ist das wahrscheinlich am einfachsten. Wenn die Statusvariable Opened_tables groß ist oder ansteigt, dann ist der TabellenCache nicht groß genug, und Sie sollten die Systemvariable table_cache erhöhen (bzw. in MySQL 5.1 table_open_cache). Der einzige tatsächliche Nachteil, der sich ergibt, wenn der Tabellen-Cache sehr groß ist, sind möglicherweise längere Shutdown-Zeiten, wenn der Server viele MyISAM-Tabellen hat, da die Schlüsselblöcke geleert und die Tabellen als nicht länger geöffnet gekennzeichnet werden müssen. Aus dem gleichen Grund kann auch FLUSH TABLES WITH READ LOCK sehr lange brauchen, bis es fertig ist.
2 Das Konzept einer geöffneten Tabelle kann ein wenig verwirrend sein. MySQL sieht eine Tabelle als sehr oft geöffnet an, wenn unterschiedliche Abfragen gleichzeitig darauf zugreifen, oder sogar, wenn eine einzelne Abfrage sich mehr als einmal auf die gleiche Tabelle bezieht, wie in einer Unterabfrage oder in einem SelbstJoin. Die MyISAM-Indexdateien enthalten einen Zähler, den MyISAM erhöht, wenn die Tabelle geöffnet wurde, und den es verkleinert, wenn sie geschlossen wird. Dadurch kann MyISAM feststellen, wann die Tabelle nicht sauber geschlossen wurde: Wenn es eine Tabelle das erste Mal öffnet und der Zähler nicht auf null steht, wurde die Tabelle nicht sauber geschlossen.
302 | Kapitel 6: Die Servereinstellungen optimieren
Falls Sie Fehler empfangen, die besagen, dass MySQL keine weiteren Dateien öffnen kann (das Dienstprogramm perror verrät Ihnen, was die Fehlernummer bedeutet), müssen Sie vermutlich auch die Anzahl der Dateien vergrößern, die MySQL offen halten darf. Sie erledigen das mit der Servervariablen open_files_limit in Ihrer my.cnf-Datei. Die Thread- und Tabellen-Caches brauchen eigentlich nicht viel Speicher, bringen aber Vorteile mit sich, weil sie Ressourcen aufnehmen. Im Vergleich zu anderen Dingen, die MySQL tun könnte, ist das Erzeugen eines neuen Threads bzw. das Öffnen einer neuen Datei nicht sehr aufwendig, bei einer stark nebenläufigen Last kann sich dieser Aufwand aber schnell vergrößern. Das Speichern von Threads und Tabellen in einem Cache kann daher die Effizienz erhöhen.
Das InnoDB-Data-Dictionary InnoDB besitzt einen eigenen Tabellen-Cache, der wahlweise als TabellendefinitionsCache oder als Data-Dictionary bezeichnet wird und den Sie nicht konfigurieren können. Wenn InnoDB eine Tabelle öffnet, fügt es das dazugehörende Objekt in das Data-Dictionary ein. Jede Tabelle kann bis zu 4 KByte oder mehr an Speicher belegen (obwohl in MySQL 5.1 viel weniger Speicher erforderlich ist). Tabellen werden beim Schließen nicht aus dem Data-Dictionary entfernt. Das größte Problem für die Performance ist – abgesehen von den Speicheranforderungen – das Öffnen der Tabellen und das Berechnen von Statistiken für die Tabellen, das aufwendig ist, weil es viele Ein-/Ausgaben erfordert. Im Gegensatz zu MyISAM speichert InnoDB die Statistiken nicht dauerhaft in den Tabellen, sondern berechnet sie jedes Mal neu, wenn es startet. Diese Operation wird in aktuellen MySQL-Versionen durch einen globalen Mutex serialisiert, kann also nicht parallel ausgeführt werden. Wenn Sie viele Tabellen haben, braucht Ihr Server manchmal Stunden zum Starten und vollständigen Aufwärmen. In dieser Zeit macht er kaum etwas anderes, als auf eine Ein-/Ausgabe-Operation nach der anderen zu warten. Wir erwähnen das hier, damit Sie darüber Bescheid wissen, auch wenn Sie es nicht ändern können. Das stellt normalerweise nur dann ein Problem dar, wenn Sie viele (Tausende oder Zehntausende) große Tabellen haben, die dafür sorgen, dass der Prozess ein-/ausgabegebunden ist. Wenn Sie die InnoDB-Option innodb_file_per_table verwenden (diese wird später in »Den Tablespace konfigurieren« auf Seite 315 beschrieben), gibt es noch eine weitere Grenze bezüglich der Zahl der .ibd-Dateien, die InnoDB geöffnet haben kann. Dafür ist die InnoDB-Storage-Engine zuständig, nicht der MySQL-Server, und die Zahl wird von innodb_open_files kontrolliert. InnoDB öffnet Dateien anders als MyISAM: Während MyISAM den Tabellen-Cache für die Dateideskriptoren der offenen Dateien einsetzt, gibt es in InnoDB keine direkte Beziehung zwischen offenen Tabellen und offenen Dateien. InnoDB benutzt einen einzigen, globalen Dateideskriptor für jede .ibd-Datei. Wenn Sie es sich leisten können, dann ist es am besten, innodb_open_files so groß zu machen, dass der Server alle .ibd-Dateien gleichzeitig offenhalten kann.
Allgemeines Tuning | 303
Das Ein-/Ausgabeverhalten von MySQL anpassen Einige Konfigurationsoptionen haben Einfluss darauf, wie MySQL Daten auf der Festplatte synchronisiert und Wiederherstellungsoperationen (Recovery) durchführt. Sie können die Performance drastisch beeinflussen, weil sie teure Ein-/Ausgabe-Operationen beinhalten. Sie repräsentieren außerdem einen Kompromiss zwischen Leistung und Datensicherheit. Im Allgemeinen ist es teuer, sicherzustellen, dass Ihre Daten sofort und konsistent auf die Festplatte geschrieben werden. Wenn Sie gewillt sind, das Risiko einzugehen, dass ein Schreibvorgang auf die Festplatte es nicht bis zur dauerhaften Speicherung schafft, können Sie die Nebenläufigkeit erhöhen und/oder Ein-/Ausgabe-Wartezyklen verringern. Allerdings müssen Sie selbst entscheiden, welches Risiko Sie eingehen können.
MyISAM-Ein-/Ausgabe verbessern Wir wollen zunächst darüber nachdenken, wie MyISAM die Ein-/Ausgaben für seine Indizes durchführt. Normalerweise reicht MyISAM Indexänderungen nach jedem Schreiben auf die Festplatte durch. Falls Sie jedoch an einer Tabelle viele Änderungen vornehmen, könnte es schneller gehen, wenn Sie diese Schreiboperationen in Form einer Stapelverarbeitung zusammenfassen. Eine Möglichkeit dazu besteht mit LOCK TABLES, das Schreiboperationen aufschiebt, bis Sie die Sperren auf den Tabellen aufheben. Diese Technik eignet sich gut, um die Leistung zu verbessern, da sie es Ihnen erlaubt, exakt zu steuern, welche Schreiboperationen verschoben und welche auf die Festplatte gebracht werden. Sie können genau die Anweisungen angeben, für welche die Schreiboperationen ausgesetzt werden sollen. Es ist auch möglich, mit der Variablen delay_key_write Indexschreibvorgänge aufzuschieben. In diesem Fall werden modifizierte Schlüsselpufferblöcke erst dann geschrieben, wenn die Tabelle geschlossen wird.3 Hier sind die möglichen Einstellungen: OFF
MyISAM schiebt modifizierte Blöcke aus dem Schlüsselpuffer (Schlüssel-Cache) nach jedem Schreiben auf die Festplatte, es sei denn, die Tabelle ist mit LOCK TABLES gesperrt. ON
Es wird verzögertes Schreiben der Schlüssel aktiviert, allerdings nur für Tabellen, die mit der Option DELAY_KEY_WRITE erzeugt wurden. ALL
Alle MyISAM-Tabellen nutzen verzögertes Schreiben der Schlüssel.
3 Die Tabelle kann aus mehreren Gründen geschlossen werden. Beispielsweise könnte der Server die Tabelle schließen, weil es nicht genug Platz im Tabellen-Cache gibt, oder jemand könnte FLUSH TABLES ausführen.
304 | Kapitel 6: Die Servereinstellungen optimieren
Das Verzögern des Schlüsselschreibens kann in manchen Fällen sinnvoll sein, bringt jedoch normalerweise keinen großen Leistungsgewinn. Am nützlichsten ist es bei kleineren Datengrößen, wenn die Lesetrefferrate des Schlüssel-Cache gut, die Schreibtrefferrate hingegen schlecht ist. Es hat auch einige Nachteile: • Wenn der Server abstürzt und die Blöcke noch nicht auf die Festplatte übertragen wurden, wird der Index beschädigt. • Wenn viele Schreiboperationen verzögert werden, dauert es länger, bis MySQL eine Tabelle schließen kann, weil es warten muss, bis die Puffer auf die Festplatte geleert werden. In MySQL 5.0 kann das zu lang anhaltendem Sperren des Tabellen-Cache führen. • FLUSH TABLES kann aus den gerade genannten Gründen sehr lange dauern. Das wiederum kann die Zeit vergrößern, die es dauert, FLUSH TABLES WITH READ LOCK für einen LVM-Schnappschuss oder eine andere Backup-Operation durchzuführen. • Nicht geleerte schmutzige Blöcke im Schlüsselpuffer lassen möglicherweise keinen Platz im Puffer für neue Blöcke, die von der Festplatte gelesen werden sollen. Daher werden vielleicht Abfragen angehalten, während sie darauf warten, dass MyISAM etwas Platz im Schlüsselpuffer schafft. Zusätzlich zu MyISAMs Index-Ein-/Ausgabe können Sie konfigurieren, wie MyISAM versucht, sich nach einer Beschädigung wieder zu erholen. Die Option myisam_recover steuert, wie MyISAM nach Fehlern sucht und diese repariert. Sie müssen diese Option in der Konfigurationsdatei oder auf der Kommandozeile setzen. Sie können den Wert der Option mit dieser SQL-Anweisung anschauen, allerdings nicht ändern (das ist kein Tippfehler – die Systemvariable hat einen anderen Namen als die entsprechende Kommandozeilenoption): mysql> SHOW VARIABLES LIKE 'myisam_recover_options';
Indem Sie diese Option aktivieren, weisen Sie MySQL an, die MyISAM-Tabellen auf Beschädigungen zu überprüfen, wenn es sie öffnet, und zu reparieren bzw. zu regenerieren, falls Probleme bemerkt werden. Sie können die folgenden Werte einstellen: DEFAULT (oder keine Einstellung)
MySQL versucht, jede Tabelle zu reparieren, die als beschädigt gekennzeichnet oder nicht als sauber geschlossen markiert ist. Die Standardeinstellung führt neben der Reparatur keine weiteren Aktionen aus. Im Gegensatz zur Funktionsweise der meisten Variablen ist dieser DEFAULT-Wert nicht die Anweisung, die Variable auf ihren bei der Kompilierung festgelegten Wert zurückzusetzen. Stattdessen bedeutet sie tatsächlich »keine Einstellung«. BACKUP
Veranlasst MySQL, ein Backup der Datendatei in eine .BAK-Datei zu schreiben, die Sie anschließend untersuchen können. FORCE
Sorgt dafür, dass die Reparatur fortgesetzt wird, selbst wenn mehr als eine Zeile aus der .MYD-Datei verloren geht.
Das Ein-/Ausgabeverhalten von MySQL anpassen | 305
QUICK
Überspringt die Regeneration, es sei denn, es gibt Löschblöcke. Dabei handelt es sich um Blöcke mit gelöschten Zeilen, die immer noch Platz einnehmen und für künftige INSERT-Anweisungen wiederverwendet werden können. Das kann nützlich sein, weil die MyISAM-Reparatur bei großen Tabellen eventuell sehr lange dauert. Sie können mehrere Einstellungen verwenden, die dann jeweils durch Kommas getrennt werden. Beispielsweise erzwingt BACKUP,FORCE die Regeneration und erzeugt ein Backup. Wir empfehlen Ihnen, diese Option zu aktivieren, vor allem, wenn Sie nur einige kleine MyISAM-Tabellen haben. Es ist gefährlich, einen Server mit beschädigten MyISAMTabellen zu betreiben, da dies manchmal zu weiteren Datenschäden und sogar zu Serverabstürzen führt. Bei großen Tabellen jedoch kann eine automatische Reparatur unpraktisch sein: Sie veranlasst den Server, alle MyISAM-Tabellen zu überprüfen und zu reparieren, wenn sie geöffnet werden; ein sehr ineffizientes Vorgehen. Während dieser Zeit neigt MySQL dazu, Verbindungen am Verrichten irgendwelcher Arbeit zu hindern. Wenn Sie sehr viele MyISAM-Tabellen haben, ist es wahrscheinlich besser, ein weniger aufdringliches Vorgehen zu wählen, das CHECK TABLES und REPAIR TABLES nach dem Start ausführt. Dennoch ist es sehr wichtig, die Tabellen zu überprüfen und zu reparieren. Die Aktivierung des Memory-mapped-Zugriffs auf die Datendateien ist eine weitere sinnvolle Option zum Verfeinern von MyISAM. Memory-Mapping erlaubt es MyISAM, auf die .MYD-Dateien direkt über den Seiten-Cache des Betriebssystems zuzugreifen, wodurch teure Systemaufrufe vermieden werden. Seit MySQL 5.1 können Sie das Memory-Mapping mit der Option myisam_use_mmap einschalten. Ältere Versionen von MySQL benutzen Memory-Mapping nur für komprimierte MyISAM-Tabellen.
InnoDB-Ein-/Ausgabe-Tuning InnoDB ist komplexer als MyISAM. Daraus folgt, dass Sie nicht nur steuern können, wie es die Daten regeneriert, sondern auch, wie es sie öffnet und auf die Platte schreibt, was die Regeneration und die gesamte Performance stark beeinflusst. Der Regenerationsvorgang bei InnoDB erfolgt automatisch und wird immer dann ausgeführt, wenn InnoDB startet. Allerdings können Sie beeinflussen, welche Aktionen es durchführt. Mehr dazu finden Sie in Kapitel 11. Auch wenn wir die Regeneration beiseite lassen und davon ausgehen, dass niemals etwas abstürzt oder schiefgeht, muss immer noch viel für InnoDB konfiguriert werden. Es besitzt eine komplexe Kette aus Puffern und Dateien, die die Leistung verbessern und ACID-Eigenschaften garantieren sollen. Jedes Teil der Kette kann konfiguriert werden. Abbildung 6-1 zeigt diese Dateien und Puffer. Zu den wichtigsten Dingen, die für die normale Benutzung geändert werden müssen, gehören die Größe der InnoDB-Log-Datei, sowie die Art und Weise, wie InnoDB seinen Log-Puffer leert und wie InnoDB Ein-/Ausgaben durchführt.
306 | Kapitel 6: Die Servereinstellungen optimieren
Pufferpool
Log-Puffer Transaktions-Log-Schreibvorgänge
InnoDB I/O threads SchreibThread
LeseThread
LogThread Betriebssystem-Cache Dateisystem
Tablespace DoublewritePuffer
Datendatei
Datendatei
Kreisförmige sequenzielle Schreibvorgänge
Daten, Indizes, Widerrufen-Logs usw.
Datendatei
Transaktions-Log
LogDatei
LogDatei
LogDatei
Abbildung 6-1: Die Puffer und Dateien von InnoDB
Das InnoDB-Transaktions-Log InnoDB verwendet sein Log, um die Kosten für das Bestätigen von Transaktionen zu verringern. Anstatt nach jedem Bestätigen einer Transaktion den Pufferpool auf die Festplatte zu schreiben, verzeichnet es seine Transaktionen im Log. Die Änderungen, die Transaktionen an den Daten und Indizes vornehmen, werden oft zufälligen Stellen im Tablespace zugeordnet, so dass das Übertragen dieser Änderungen auf die Festplatte zufällige Ein-/Ausgaben erfordern würde. Sie können sich merken, dass zufällige Ein-/ Ausgaben viel teurer sind als sequenzielle Ein-/Ausgaben, weil es mehr Zeit kostet, die richtige Stelle auf der Festplatte zu suchen und zu warten, bis der gewünschte Teil der Festplatte unter dem Lese-/Schreibkopf angekommen ist. InnoDB benutzt sein Log, um diese zufälligen Ein-/Ausgaben in sequenzielle Ein-/Ausgaben umzuwandeln. Sobald das Log sicher auf der Festplatte angekommen ist, sind die Transaktionen permanent, auch wenn die Änderungen noch nicht in die Datendateien geschrieben wurden. Geschieht etwas Schlimmes (wie etwa ein Stromausfall), kann InnoDB das Log erneut abspielen und die bestätigten Transaktionen wiederherstellen. Natürlich muss InnoDB die Änderungen schließlich in die Datendateien schreiben, weil das Log eine feste Größe besitzt. Das Schreiben erfolgt »im Kreis herum«: Wenn es das Ende des Logs erreicht, springt es wieder an den Anfang. Es kann einen Log-Eintrag nicht überschreiben, wenn die Änderungen, die dieser enthält, noch nicht in die Datendateien übertragen wurden, weil dadurch der einzige permanente Datensatz der bestätigten Transaktion gelöscht werden würde. Das Ein-/Ausgabeverhalten von MySQL anpassen | 307
InnoDB benutzt einen Hintergrund-Thread, um die Änderungen intelligent in die Datendateien zu übertragen. Dieser Thread kann Schreibvorgänge zusammenfassen und das Schreiben der Daten aus Gründen der Effizienz sequenziell organisieren. Im Prinzip wandelt das Transaktions-Log die zufälligen Ein-/Ausgabe-Zugriffe auf die Datendatei in vorwiegend sequenzielle Ein-/Ausgabe-Zugriffe auf die Log- und die Datendatei um. Da das Verschieben der Daten im Hintergrund erfolgt, werden Abfragen schneller abgeschlossen und Lastspitzen im Ein-/Ausgabe-System abgefedert. Die Gesamtgröße der Log-Datei wird von innodb_log_file_size und innodb_log_files_ in_group kontrolliert und ist für die Schreib-Performance sehr wichtig. Die Gesamtgröße ist die Summe der Größen der einzelnen Dateien. Standardmäßig gibt es zwei 5-MByteDateien mit insgesamt 10 MByte. Das reicht für eine hochleistungsfähige Arbeitslast nicht aus. Die obere Grenze für die Gesamtgröße des Logs beträgt 4 GByte, und typische Größen für außerordentlich schreibintensive Lasten liegen nur bei Hunderten von Megabyte (vielleicht insgesamt 256 MByte). In den folgenden Abschnitten wird erläutert, wie Sie eine gute Größe für Ihre Last ermitteln. InnoDB benutzt mehrere Dateien für ein einziges umlaufendes Log. Normalerweise müssen Sie die vorgegebene Anzahl der Logs nicht ändern, sondern nur die Größe der einzelnen Log-Dateien. Dazu fahren Sie MySQL sauber herunter, verschieben die alten Logs an eine andere Stelle, konfigurieren MySQL neu und starten es wieder. Achten Sie darauf, dass MySQL sauber und geordnet heruntergefahren wird, oder die Log-Dateien enthalten Einträge, die auf die Datendateien angewandt werden müssen! Beobachten Sie das MySQL-Fehler-Log, wenn Sie den Server neu starten. Nach dem erfolgreichen Neustart können Sie die alten Log-Dateien löschen. Größe der Log-Datei und Log-Puffer: Um die ideale Größe für Ihre Log-Dateien zu ermitteln, müssen Sie den Aufwand für Änderungen der Routinedaten gegen die Regenerationszeit im Absturzfall abwägen. Falls das Log zu klein ist, muss InnoDB mehr Prüfpunkte setzen, wodurch mehr in das Log geschrieben wird. In Extremfällen werden Schreibabfragen blockiert und müssen darauf warten, dass Änderungen in die Datendateien übertragen werden, damit wieder Platz geschaffen wird, um in das Log zu schreiben. Falls andererseits das Log zu groß ist, hat InnoDB möglicherweise bei der Regeneration zu viel zu tun. Das kann sich negativ auf die Regenerationszeit auswirken. Auch die Datengröße und die Zugriffsmuster beeinflussen die Regenerationszeit. Nehmen Sie an, Sie haben ein Terabyte an Daten und einen 16-GByte-Pufferpool und Ihre Log-Gesamtgröße beträgt 128 MByte. Wenn Sie viele schmutzige Seiten im Pufferpool haben (d.h. Seiten, deren Änderungen noch nicht in die Datendateien übertragen wurden), die gleichmäßig über Ihr Terabyte an Daten verteilt sind, könnte die Reparatur nach einem Absturz sehr lange dauern. InnoDB muss das Log durchsuchen, die Datendateien untersuchen und bei Bedarf Änderungen auf die Datendateien anwenden. Das erfordert viele Lese- und Schreibvorgänge! Falls andererseits die Änderungen lokalisiert wurde – falls z.B. nur einige Gigabyte an Daten regelmäßig aktualisiert werden –, könnte die Regeneration schnell vonstatten gehen, auch wenn die Daten- und Log-Dateien groß
308 | Kapitel 6: Die Servereinstellungen optimieren
sind. Die Regenerationszeit hängt darüber hinaus von der Größe einer typischen Änderung ab, die in Beziehung zu Ihrer durchschnittlichen Zeilenlänge steht. Bei kurzen Zeilen passen mehr Modifikationen in das Log, so dass InnoDB bei einer Regeneration mehr Modifikationen erneut abspielen müsste. Wenn InnoDB Daten ändert, schreibt es einen Beleg für diese Änderung in seinen LogPuffer, den es im Speicher vorhält. InnoDB überträgt den Puffer in die Log-Dateien auf der Festplatte, wenn der Puffer voll ist, wenn eine Transaktion bestätigt wird oder einmal pro Sekunde – je nachdem, welcher Fall zuerst eintritt. Durch das Erhöhen der Puffergröße, die standardmäßig 1 MByte beträgt, kann man bei großen Transaktionen die Ein-/ Ausgaben verringern. Die Variable, die die Puffergröße steuert, heißt innodb_log_buffer_ size. Es sollte nicht nötig sein, den Puffer sehr groß zu machen. Der empfohlene Bereich beträgt 1 bis 8 MByte. Das sollte mehr als ausreichen, wenn Sie nicht gerade viele riesige BLOB-Datensätze schreiben. Die Log-Einträge sind im Vergleich zu den normalen InnoDBDaten sehr kompakt. Außerdem sind sie nicht seitenbasiert, so dass sie keinen Platz dadurch verschwenden, dass sie ganze Seiten auf einmal speichern. InnoDB macht LogEinträge darüber hinaus so kurz wie möglich. Sie werden manchmal sogar als Funktionsnummer und Parameter einer C-Funktion gespeichert! Sie können das InnoDB-Log und die Ein-/Ausgabe-Leistung des Log-Puffers überwachen, indem Sie den LOG-Abschnitt der SHOW INNODB STATUS-Ausgabe untersuchen. Die Statusvariable Innodb_os_log_written verrät Ihnen, wie viele Daten InnoDB in die LogDateien schreibt. Eine gute Faustregel besagt, dass Sie sie in Intervallen von 10 bis 100 Sekunden beobachten und den Spitzenwert notieren sollten. Damit können Sie dann beurteilen, ob Ihr Log-Puffer die richtige Größe besitzt. Falls Sie z.B. sehen, dass pro Sekunde eine Spitze von 100 KByte in das Log geschrieben wird, dann ist ein Log-Puffer von 1 MByte Größe wahrscheinlich reichlich bemessen. Mit diesem Maß können Sie außerdem eine gute Größe für Ihre Log-Dateien wählen. Wenn die Spitze bei 100 KByte pro Sekunde liegt, dann reicht eine 256-MByte-Log-Datei aus, um wenigstens 2.560 Sekunden mit Log-Einträgen zu speichern, was wahrscheinlich ausreichend ist. In »SHOW INNODB STATUS« auf Seite 616 erfahren Sie mehr über die Überwachung und Interpretation des Log- und Pufferstatus. Wie InnoDB den Log-Puffer leert: Wenn InnoDB den Log-Puffer an die Log-Dateien auf der Festplatte überträgt, sperrt es den Puffer mit einem Mutex, leert ihn bis zu dem gewünschten Punkt und verschiebt dann alle verbliebenen Einträge im Puffer nach vorn. Wenn der Mutex freigegeben wird, kann es passieren, dass mehr als eine Transaktion bereit ist, um ihre Log-Einträge zu übertragen. InnoDB besitzt eine Gruppen-CommitFunktion, die alle auf einmal in einer einzigen Ein-/Ausgabe-Operation in dem Log bestätigen kann, allerdings funktioniert diese in MySQL 5.0 nicht, wenn das Binärlog aktiviert ist.
Das Ein-/Ausgabeverhalten von MySQL anpassen | 309
Der Log-Puffer muss in den dauerhaften Speicher übertragen werden, um sicherzustellen, dass bestätigte Transaktionen vollkommen dauerhaft sind. Wenn es Ihnen mehr um die Leistung als um die Beständigkeit geht, können Sie innodb_flush_log_at_trx_commit ändern, um zu kontrollieren, wohin und wie oft der Log-Puffer übertragen wird. Folgende Einstellungen sind möglich: 0 1
2
Schreibt den Log-Puffer in die Log-Datei und überträgt die Log-Datei einmal pro Sekunde, tut aber bei einem Transaktions-Commit nichts. Schreibt den Log-Puffer in die Log-Datei und überträgt ihn immer dann in den dauerhaften Speicher, wenn eine Transaktion bestätigt wird. Dies ist die Standardeinstellung (und auch die sicherste Einstellung); sie garantiert, dass Sie keine bestätigten Transaktionen verlieren, es sei denn, die Festplatte oder das Betriebssystem täuschen das Übertragen nur vor. Schreibt den Log-Puffer bei jedem Commit in die Log-Datei, überträgt ihn aber nicht. InnoDB setzt einmal pro Sekunde ein Übertragen an. Der wichtigste Unterschied zur Einstellung 0 (der 2 zur bevorzugten Einstellung macht) besteht darin, dass 2 keine Transaktionen verliert, wenn der MySQL-Prozess abstürzt. Stürzt allerdings der gesamte Server ab oder bleibt er aufgrund eines Stromausfalls stehen, können Sie dennoch Transaktionen verlieren.
Es ist wichtig, den Unterschied zwischen dem Schreiben des Log-Puffers in die Log-Datei und dem Übertragen oder Entleeren des Logs in den dauerhaften Speicher zu verstehen. Bei den meisten Betriebssystemen werden beim Schreiben des Puffers in das Log einfach die Daten aus dem Speicherpuffer von InnoDB in den Cache des Betriebssystems verschoben, der sich ebenfalls im Speicher befindet. Die Daten werden nicht in einen dauerhaften Speicher geschrieben. Daher führen die Einstellungen 0 und 2 normalerweise zu höchstens einer Sekunde an verlorenen Daten, wenn es einen Absturz oder einen Stromausfall gibt, da die Daten möglicherweise nur im Cache des Betriebssystems existieren. Wir sagen »normalerweise«, da InnoDB auf jeden Fall versucht, die Log-Datei ungefähr einmal pro Sekunde auf die Festplatte zu übertragen. Allerdings können manchmal auch mehr als eine Sekunde an Transaktionen verloren gehen, wenn z.B. das Übertragen blockiert wird. Im Gegensatz dazu bedeutet das Übertragen des Logs in den dauerhaften Speicher, dass InnoDB das Betriebssystem auffordert, die Daten tatsächlich aus dem Cache zu holen und sicherzustellen, dass sie auf die Festplatte geschrieben werden. Dies erfolgt mithilfe eines blockierenden Ein-/Ausgabe-Aufrufs, der erst dann abgeschlossen wird, wenn die Daten vollständig geschrieben wurden. Da das Schreiben der Daten auf eine Festplatte so langsam geht, kann sich die Anzahl der Transaktionen drastisch verringern, die InnoDB pro Sekunde bestätigen kann, wenn innodb_flush_log_at_trx_commit auf 1 gesetzt ist. Heutige superschnelle Laufwerke4 können ganz einfach aufgrund der Beschränkungen der Drehgeschwindigkeit und der Suchzeit nur einige Hundert echte Festplattentransaktionen pro Sekunde durchführen. 4 Wir reden über klassische Festplatten mit sich drehenden Scheiben, nicht über Solid State Drives, die völlig andere Leistungsmerkmale aufweisen.
310 | Kapitel 6: Die Servereinstellungen optimieren
Manchmal täuscht der Festplatten-Controller oder das Betriebssystem das Übertragen nur vor, indem er die Daten in einen anderen Cache legt, wie etwa den festplatten-eigenen Cache. Das ist zwar schneller, aber auch sehr gefährlich, da die Daten verloren gehen könnten, wenn das Laufwerk stromlos wird. Das ist sogar noch schlimmer, als innodb_flush_log_at_trx_commit auf einen anderen Wert als 1 zu setzen, weil es nicht nur zu verloren gegangenen Transaktionen, sondern auch zu Datenbeschädigungen führen kann. Wenn Sie innodb_flush_log_at_trx_commit auf einen anderen Wert als 1 setzen, gehen unter Umständen Transaktionen verloren. Möglicherweise jedoch finden Sie die anderen Einstellungen ganz nützlich, wenn Beständigkeit Ihnen nicht ganz so wichtig ist. Vielleicht wollen Sie einige der anderen Eigenarten von InnoDB nutzen, wie etwa ClusterIndizes, Resistenz gegen Datenbeschädigung und Sperren auf Zeilenebene. Das ist nicht ungewöhnlich, wenn man InnoDB einzig dazu benutzt, um MyISAM aus Leistungsgründen zu ersetzen. Für die beste Konfiguration für hochleistungsfähige, transaktionsgebundene Anforderungen lässt man innodb_flush_log_at_trx_commit auf 1 und platziert die Log-Dateien auf ein RAID-Laufwerk mit einem batteriegesicherten Schreib-Cache. Das ist sowohl sicher als auch sehr schnell. In »RAID-Leistungsoptimierung« auf Seite 345 erfahren Sie mehr über RAID.
Wie InnoDB Log- und Datendateien öffnet und leert Mit der Option innodb_flush_method konfigurieren Sie, wie InnoDB tatsächlich mit dem Dateisystem zusammenarbeitet. Anders als der Name nahelegt, hat sie nicht nur Einfluss darauf, wie InnoDB Daten schreibt, sondern auch darauf, wie es sie liest. Die Windowsund Nicht-Windows-Werte für diese Option schließen einander aus: Sie können async_unbuffered, unbuffered und normal nur unter Windows benutzen; andere Werte sind unter Windows nicht möglich. Der Standardwert ist unbuffered unter Windows und fdatasync bei allen anderen Systemen. (Wenn SHOW GLOBAL VARIABLES die Variable mit einem leeren Wert anzeigt, dann bedeutet dies, dass sie auf ihren Vorgabewert gesetzt ist.) Indem Sie ändern, wie InnoDB Ein-/Ausgabe-Operationen durchführt, ändern Sie auch die Performance. Führen Sie auf jeden Fall einen Benchmark-Test durch!
Hier sind die möglichen Werte: fdatasync
Der Standardwert auf Nicht-Windows-Systemen: InnoDB benutzt fsync( ), um sowohl Daten- als auch Log-Dateien auf die Platte zu übertragen und zu leeren. Im Allgemeinen verwendet InnoDB fsync( ) anstelle von fdatasync( ), obwohl dieser Wert das Gegenteil zu bedeuten scheint. fdatasync( ) ist wie fsync( ), allerdings
Das Ein-/Ausgabeverhalten von MySQL anpassen | 311
überträgt es nur die Daten der Datei, nicht ihre Metadaten (den Zeitpunkt der letzten Änderung usw.). Daher kann fsync( ) mehr Ein-/Ausgaben verursachen. Indes sind die InnoDB-Entwickler sehr konservativ und haben festgestellt, dass fdatasync( ) in manchen Fällen Schäden verursacht hat. InnoDB stellt fest, welche Methoden sicher eingesetzt werden können; manche Optionen werden beim Kompilieren festgelegt, andere werden erst zur Laufzeit erkannt. Es benutzt die schnellste sichere Methode, die möglich ist. Der Nachteil bei der Benutzung von fsync( ) besteht darin, dass das Betriebssystem wenigstens einige der Daten in seinem eigenen Cache puffert. Theoretisch ist dies eine überflüssige Doppelpufferung, weil InnoDB seine eigenen Puffer intelligenter verwaltet als das Betriebssystem. Die endgültige Wirkung ist jedoch stark systemund dateisystemabhängig. Die Doppelpufferung ist nicht so schlimm, wenn sie es dem Dateisystem erlaubt, das Ein-/Ausgabe-Scheduling und -Batching geschickter zu erledigen. Manche Dateisysteme und Betriebssysteme können Schreiboperationen sammeln und zusammen ausführen, für eine höhere Effizienz umsortieren oder parallel auf mehrere Geräte schreiben. Sie könnten außerdem Read-Ahead-Optimierungen durchführen, wie etwa die Festplatte anweisen, den nächsten sequenziellen Block schon einmal vorab zu lesen, wenn mehrere in Folge angefordert wurden. Manchmal sind diese Optimierungen hilfreich, manchmal aber nicht. Sie können in der Manpage Ihres Systems für fsync(2) nachlesen, was genau Ihre Version von fsync( ) tut. innodb_file_per_table sorgt dafür, dass jede Datei separat mit fsync( ) synchronisiert
wird, was bedeutet, dass Schreiboperationen in mehrere Tabellen nicht zu einer einzigen Ein-/Ausgabe-Operation kombiniert werden können. Daher muss InnoDB möglicherweise insgesamt eine höhere Anzahl an fsync( )-Operationen durchführen. O_DIRECT
InnoDB benutzt das Flag O_DIRECT bzw. (je nach System) directio( ) mit den Datendateien. Diese Option hat keinen Einfluss auf die Log-Dateien und steht nicht unbedingt auf allen Unix-artigen Betriebssystemen zur Verfügung. Doch zumindest GNU/Linux, FreeBSD und Solaris (ab neueren 5.0-Versionen) unterstützen sie. Im Gegensatz zum Flag O_DSYNC sind sowohl Lese- als auch Schreiboperationen betroffen. Diese Einstellung benutzt ebenfalls fsync( ), um die Dateien auf die Festplatte zu übertragen, weist aber das Betriebssystem an, die Daten nicht im Cache zu speichern und kein Read-Ahead zu benutzen. Damit wird der Cache des Betriebssystems vollständig deaktiviert, und alle Lese- und Schreiboperationen richten sich direkt an das Speichergerät, wodurch eine Doppelpufferung vermieden wird. Bei den meisten Systemen wird dies durch einen Aufruf an fcntl( ) implementiert, das O_DIRECT-Flag auf dem Dateideskriptor zu setzen. Entsprechende Details für Ihr System finden Sie in der fcntl(2)-Manpage. Auf Solaris benutzt diese Option directio( ).
312 | Kapitel 6: Die Servereinstellungen optimieren
Falls Ihre RAID-Karte ein Read-Ahead durchführt, wird es durch diese Einstellung nicht deaktiviert. Sie deaktiviert nur die Read-Ahead-Fähigkeiten des Betriebssystems und/oder des Dateisystems. Im Allgemeinen werden Sie den Schreib-Cache Ihrer RAID-Karte nicht deaktivieren, wenn Sie O_DIRECT verwenden, weil dieser üblicherweise das Einzige ist, was für eine gute Performance sorgt. O_DIRECT zu benutzen, wenn es keinen Puffer zwischen InnoDB und dem eigentlichen Speichergerät gibt (also wenn Sie keinen SchreibCache in Ihrer RAID-Karte haben), kann die Leistung stark beeinträchtigen. Diese Einstellung sorgt unter Umständen dafür, dass sich die Aufwärmzeit des Servers stark verlängert, vor allem, wenn der Betriebssystem-Cache sehr groß ist. Sie kann auch einen kleinen Pufferpool (z.B. einen Pufferpool in Standardgröße) viel langsamer machen, als eine gepufferte Ein-/Ausgabe es tun würde. Das liegt daran, dass das Betriebssystem nicht »aushilft«, indem es mehr Daten in seinen eigenen Cache übernimmt. Wenn die gewünschten Daten sich nicht im Pufferpool befinden, muss InnoDB sie direkt von der Festplatte lesen. Diese Einstellung verursacht keine zusätzlichen Nachteile wegen der Benutzung von innodb_file_per_table. O_DSYNC
Diese Option setzt das O_SYNC-Flag für die Log-Dateien auf den open( )-Aufruf. Sie sorgt dafür, dass alle Schreiboperationen synchron verlaufen. Mit anderen Worten: Schreiboperationen kehren erst zurück, wenn die Daten auf die Festplatte geschrieben wurden. Diese Option hat keinen Einfluss auf die Datendateien. Der Unterschied zwischen dem O_SYNC-Flag und dem O_DIRECT-Flag besteht darin, dass O_SYNC die Cache-Speicherung auf der Betriebssystemebene nicht deaktiviert. Daher vermeidet es die Doppelpufferung nicht und lässt Schreiboperationen nicht direkt auf die Festplatte gehen. Mit O_SYNC werden bei Schreiboperationen die Daten im Cache geändert, diese werden dann an die Festplatte geschickt. Synchrones Schreiben mit O_SYNC mag zwar sehr nach dem klingen, was fsync( ) tut, allerdings können die beiden sowohl auf Betriebssystem- als auch auf Hardwareebene sehr unterschiedlich implementiert werden. Wenn das O_SYNC-Flag verwendet wird, könnte das Betriebssystem ein »Bitte synchrone Ein-/Ausgabe benutzen«-Flag an die Hardwareebene durchreichen, wodurch dem Gerät mitgeteilt wird, dass keine Caches benutzt werden sollen. Andererseits weist fsync( ) das Betriebssystem an, modifizierte Puffer an das Gerät zu übertragen, gefolgt von einer Anweisung für das Gerät, gegebenenfalls seine eigenen Caches zu leeren, so dass es sicher ist, dass die Daten auf dem physischen Medium aufgezeichnet wurden. Ein weiterer Unterschied besteht darin, dass mit O_SYNC jede write( )- oder pwrite( )-Operation Daten auf die Festplatte synchronisiert, bevor sie beendet wird, wodurch der aufrufende Prozess blockiert wird. Im Gegensatz dazu erlaubt das Schreiben ohne O_SYNC-Flag und ein anschließendes Aufrufen von fsync( ), Schreiboperationen im Cache zu sammeln (was alle Schreiboperationen beschleunigt) und alle auf einmal zu übertragen.
Das Ein-/Ausgabeverhalten von MySQL anpassen | 313
Auch diese Option setzt, anders als ihr Name suggeriert, das O_SYNC-Flag, nicht das O_DSYNC-Flag, weil die InnoDB-Entwickler Bugs bei O_DSYNC gefunden haben. O_SYNC und O_DSYNC sind vergleichbar mit fysnc( ) und fdatasync( ): O_SYNC synchronisiert sowohl Daten als auch Metadaten, während O_DSYNC nur Daten synchronisiert. async_unbuffered
Dies ist der vorgegebene Wert unter Windows. Diese Option veranlasst InnoDB, für die meisten Schreiboperationen ungepufferte Ein-/Ausgaben zu benutzen; die Ausnahme sieht so aus, dass es gepufferte Ein-/Ausgaben für die Log-Dateien benutzt, wenn innodb_flush_log_at_trx_commit auf 2 gesetzt ist. Diese Einstellung sorgt dafür, dass InnoDB die betriebssystemeigene asynchrone (überschneidende) Ein-/Ausgabe sowohl zum Lesen als auch zum Schreiben unter Windows 2000, XP und neueren Windows-Versionen benutzt. Bei älteren Windows-Versionen verwendet InnoDB seine eigene asynchrone Ein-/Ausgabe, die mit Threads implementiert ist. unbuffered
Nur für Windows. Diese Option ist vergleichbar mit async_unbuffered, benutzt aber keine native asynchrone Ein-/Ausgabe. normal
Nur für Windows. Diese Option veranlasst InnoDB, nicht die native asynchrone Ein-/Ausgabe oder die ungepufferte Ein-/Ausgabe zu benutzen. nosync und littlesync
Nur für Entwicklungszwecke. Diese Optionen sind nicht dokumentiert und für den Produktionseinsatz unsicher; sie sollten nicht benutzt werden. Wenn Ihr RAID-Controller einen batteriegesicherten Schreib-Cache besitzt, empfehlen wir den Einsatz von O_DIRECT. Falls nicht, ist wahrscheinlich – je nach Ihrer Anwendung – entweder die Vorgabe oder O_DIRECT die beste Wahl. Sie können die Anzahl der Ein-/Ausgabe-Threads unter Windows konfigurieren, nicht jedoch auf einer anderen Plattform. Wenn Sie innodb_file_io_threads auf einen Wert setzen, der höher als 4 ist, erzeugt InnoDB mehr Lese- und Schreib-Threads für die Datenein- und -ausgabe. Es gibt dann nur einen Eingabepuffer-Thread und einen Log-Thread, so dass z.B. der Wert 8 bedeutet, dass es einen Eingabepuffer-Thread, einen Log-Thread, drei Lese-Threads und drei Schreib-Threads gibt.
Der InnoDB-Tablespace InnoDB bewahrt seine Daten in einem Tablespace auf, bei dem es sich prinzipiell um ein virtuelles Dateisystem handelt, das eine oder mehrere Dateien auf der Festplatte umfasst. InnoDB verwendet den Tablespace für viele Zwecke, nicht nur zum Speichern von Tabellen und Indizes. Es legt auch sein »Undo«-Log (alte Zeilenversionen), den Eingabepuffer, den Doublewrite-Puffer (dieser wird später noch beschrieben) und weitere interne Strukturen im Tablespace ab.
314 | Kapitel 6: Die Servereinstellungen optimieren
Den Tablespace konfigurieren: Sie geben die Tablespace-Dateien mit der Konfigurationsoption innodb_data_file_path an. Die Dateien sind alle in dem Verzeichnis enthalten, das mit innodb_data_home_dir festgelegt wird. Hier ist ein Beispiel: innodb_data_home_dir = /var/lib/mysql/ innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G
Dadurch wird ein 3 GByte großer Tablespace in drei Dateien erzeugt. Manchmal fragen sich Leute, ob man mehrere Dateien verwenden kann, um die Last auf die Laufwerke zu verteilen, also etwa so: innodb_data_file_path = /disk1/ibdata1:1G;/disk2/ibdata2:1G;...
Damit werden zwar tatsächlich Dateien in unterschiedliche Verzeichnisse gesetzt, die in diesem Beispiel unterschiedliche Laufwerke repräsentieren, allerdings verkettet InnoDB die Dateien Ende-an-Ende. Das heißt, auf diese Weise gewinnen Sie normalerweise nicht viel. InnoDB füllt die erste Datei, dann die zweite, wenn die erste voll ist, usw. Die Last wird auf diese Weise nicht so verteilt, wie Sie es für eine hohe Performance bräuchten. Ein RAID-Controller bietet eine klügere Methode, um die Last zu verteilen. Damit der Tablespace wachsen kann, wenn ihm der Platz ausgeht, können Sie veranlassen, dass die letzte Datei automatisch erweitert wird: ...ibdata3:1G:autoextend
Standardmäßig wird eine einzelne, 10 MByte große, sich automatisch erweiternde Datei angelegt. Es ist im Fall von sich automatisch erweiternden Dateien keine schlechte Idee, die Größe des Tablespace nach oben zu begrenzen, um zu verhindern, dass er zu groß wird. Wenn er nämlich einmal gewachsen ist, schrumpft er nicht wieder. Im folgenden Beispiel wird die automatisch sich erweiternde Datei auf 2 GByte beschränkt: ...ibdata3:1G:autoextend:max:2G
Das Verwalten eines einzigen Tablespace kann eine Mühsal sein, vor allem, wenn er sich automatisch erweitert und Sie den Platz wieder zurückfordern wollen (aus diesem Grund empfehlen wir, die autoextend-Funktion zu deaktivieren). Die einzige Möglichkeit, den Platz wieder zurückzuerhalten, besteht darin, Ihre Daten zu sichern, MySQL zu beenden, alle Dateien zu löschen, die Konfiguration zu ändern, neu zu starten, InnoDB neue leere Dateien erzeugen zu lassen und Ihre Daten wiederherzustellen. InnoDB ist absolut erbarmungslos, was seinen Tablespace betrifft – Sie können nicht einfach Dateien löschen oder ihre Größen ändern. Es weigert sich einfach zu starten, wenn Sie seinen Tablespace beschädigen. Genauso streng ist es mit seinen Log-Dateien. Falls Sie daran gewöhnt sind, gelegentlich Dateien mit MyISAM herumzuschieben, dann denken Sie daran! Die Option innodb_file_per_table erlaubt es Ihnen seit MySQL 4.1, InnoDB so zu konfigurieren, dass es eine Datei pro Tabelle benutzt. Es speichert die Daten im Datenbankverzeichnis als Tabellenname.ibd-Dateien. Dies macht es leichter, Platz zurückzufordern, wenn Sie eine Tabelle verwerfen, und kann sich beim Verteilen von Tabellen über mehrere Festplatten als sinnvoll erweisen. Allerdings wird insgesamt mehr Platz verschwendet, wenn man Daten in mehreren Dateien ablegt, weil man sich anstelle der
Das Ein-/Ausgabeverhalten von MySQL anpassen | 315
internen Fragmentierung in dem einzelnen InnoDB-Tablespace nun Platzverschwendung in den .ibd-Dateien einhandelt. Das ist vor allem für sehr kleine Tabellen problematisch, da die Seitengröße von InnoDB 16 KByte beträgt. Selbst wenn Ihre Tabelle nur 1 KByte an Daten enthält, erfordert sie auf der Festplatte wenigstens 16 KByte. Selbst wenn Sie die Option innodb_file_per_table aktivieren, benötigen Sie den HauptTablespace für die Undo-Logs und andere Systemdaten. (Er ist kleiner, wenn Sie nicht alle Daten darin speichern. Dennoch ist es eine gute Idee, das automatische Erweitern zu deaktivieren, da Sie die Datei nicht schrumpfen lassen können, ohne alle Daten neu zu laden.) Sie werden auch nicht dazu in der Lage sein, Tabellen zu verschieben, zu sichern oder wiederherzustellen, indem Sie einfach die Dateien kopieren. Es ist zwar möglich, erfordert aber einige zusätzliche Schritte; zwischen Servern lassen sich Tabellen überhaupt nicht kopieren. Mehr dazu erfahren Sie in »Rohe Dateien erneuern« auf Seite 547. Manche Leute benutzen innodb_file_per_table einfach nur deshalb gern, weil es ihnen zusätzliche Handhabbarkeit und Übersichtlichkeit bietet. Man kann z.B. viel schneller die Größe einer Tabelle ermitteln, wenn man eine einzige Datei untersucht, als wenn man SHOW TABLE STATUS bemüht, das den Pufferpool scannen muss, um festzustellen, wie viele Seiten für eine Tabelle reserviert sind. Wir sollten außerdem noch anmerken, dass Sie Ihre InnoDB-Dateien eigentlich nicht in einem traditionellen Dateisystem speichern müssen. Genau wie viele traditionelle Datenbankserver bietet auch InnoDB die Möglichkeit, das rohe Gerät – d.h. eine unformatierte Partition – für seinen Speicher zu benutzen. Heutige Dateisysteme sind allerdings dazu in der Lage, mit ausreichend großen Dateien umzugehen, so dass Sie diese Option nicht benötigen sollten. Die Verwendung der rohen Geräte könnte die Leistung um einige Prozentpunkte verbessern, allerdings glauben wir nicht, dass dieser kleine Zuwachs die Nachteile ausgleicht, die es mit sich bringt, wenn Sie nicht die Möglichkeit besitzen, die Daten als Dateien zu manipulieren. Wenn Sie Ihre Daten auf einer rohen Partition speichern, können Sie mv, cp oder andere Werkzeuge nicht einsetzen. Wir sind auch der Meinung, dass Schnappschussfähigkeiten, die etwa vom Logical Volume Manager (LVM) unter GNU/Linux angeboten werden, ein großer Segen sind. Sie können ein rohes Gerät auf ein logisches Volume setzen. Dann verfehlt es allerdings seinen Zweck – es ist eigentlich nicht mehr roh. Die winzigen Leistungsvorteile, die Ihnen der Einsatz roher Geräte bringt, sind den zusätzlichen Ärger nicht wert. Alte Zeilenversionen und der Tablespace: Der Tablespace von InnoDB kann in einer Umgebung mit vielen Schreibvorgängen stark anwachsen. Wenn Transaktionen lange offen bleiben (selbst wenn sie keine Arbeit verrichten) und die vorgegebene Transaktionsisolationsebene REPEATABLE READ verwenden, kann InnoDB alte Zeilenversionen nicht entfernen, weil die nicht bestätigten Transaktionen immer noch in der Lage sein müssen, sie zu sehen. InnoDB speichert die alten Versionen im Tablespace, so dass dieser weiter wächst, wenn weitere Daten aktualisiert werden. Manchmal besteht das Problem nicht in nicht bestätigten Transaktionen, sondern einfach in der Arbeitslast: Der Aufräumprozess ist
316 | Kapitel 6: Die Servereinstellungen optimieren
nur ein einziger Thread und kann möglicherweise nicht mit der Anzahl der alten Zeilenversionen Schritt halten, die entfernt werden müssen. In jedem Fall kann Ihnen die Ausgabe von SHOW INNODB STATUS helfen, das Problem zu erkennen. Schauen Sie sich die erste und die zweite Zeile des TRANSACTIONS-Abschnitts an, die die aktuelle Transaktionsnummer und die Stelle zeigen, bis zu der das Aufräumen abgeschlossen wurde. Ist die Differenz sehr groß, haben Sie wahrscheinlich viele nicht aufgeräumte Transaktionen. Hier ist ein Beispiel: -----------TRANSACTIONS -----------Trx id counter 0 80157601 Purge done for trx's n:o <0 80154573 undo n:o <0 0
Der Transaktionsidentifikator ist eine 64-Bit-Zahl, die aus zwei 32-Bit-Zahlen gebildet wird, so dass Sie ein wenig rechnen müssen, um die Differenz festzustellen. In diesem Fall ist es einfach, weil die hohen Bits null sind: Es gibt 80157601 – 80154573 = 3028 potenziell nicht aufgeräumte Transaktionen (innotop kann diese Berechnungen für Sie durchführen). Wir sagten »potenziell«, weil eine große Differenz nicht unbedingt bedeutet, dass es viele nicht aufgeräumte Zeilen gibt. Nur Transaktionen, die Daten ändern, erzeugen alte Zeilenversionen, und es kann natürlich viele Transaktionen geben, die überhaupt keine Daten geändert haben. (Im Gegenzug könnte eine einzige Transaktion viele Zeilen geändert haben.) Wenn Sie viele nicht aufgeräumte Transaktionen haben und Ihr Tablespace deswegen anwächst, können Sie MySQL zwingen, sich so weit zu verlangsamen, dass der AufräumThread von InnoDB aufholt. Das klingt zwar vielleicht nicht attraktiv, es gibt aber keine Alternative. Ansonsten nämlich schreibt InnoDB weiterhin Daten und füllt Ihre Festplatte, bis auf dieser kein Platz mehr ist oder bis der Tablespace die definierten Grenzen erreicht. Um die Schreibvorgänge zu drosseln, setzen Sie die Variable innodb_max_purge_lag auf einen anderen Wert als 0. Dieser Wert gibt die maximale Anzahl an Transaktionen an, die darauf warten können, aufgeräumt zu werden, bevor InnoDB damit beginnt, weitere Abfragen zu verzögern, welche Daten aktualisieren. Sie müssen Ihre Last kennen, um einen guten Wert festzulegen. Falls z.B. Ihre durchschnittliche Transaktion 1 KByte an Zeilen beeinflusst und Sie 100 MByte an nicht aufgeräumten Zeilen tolerieren können, könnten Sie den Wert auf 100000 setzen. Denken Sie immer daran, dass nicht aufgeräumte Zeilenversionen alle Abfragen betreffen, weil sie im Prinzip Ihre Tabellen und Indizes vergrößern. Wenn der Aufräum-Thread einfach nicht Schritt halten kann, sinkt die Leistung möglicherweise ganz beträchtlich. Das Setzen der Variablen innodb_max_purge_lag verringert die Leistung zwar ebenfalls, aber das ist die weniger schlimme Variante.
Das Ein-/Ausgabeverhalten von MySQL anpassen | 317
Der Doublewrite-Puffer InnoDB benutzt einen Doublewrite-Puffer, um Datenbeschädigung im Fall von teilweisen Seitenschreibvorgängen zu vermeiden. Eine Seite wird nur teilweise geschrieben, wenn ein Schreibvorgang auf der Festplatte nicht vollständig abgeschlossen wird und nur ein Teil einer 16 KByte großen Seite auf die Festplatte geschrieben wird. Es gibt hierfür eine Reihe von Gründen (Abstürze, Bugs usw.). Der Doublewrite-Puffer schützt in diesem Fall vor einer Beschädigung der Daten. Beim Doublewrite-Puffer handelt es sich um einen besonders reservierten Bereich des Tablespace, groß genug, um 100 Seiten in einem zusammenhängenden Block aufzunehmen. Im Prinzip ist er eine Sicherungskopie der kürzlich geschriebenen Seiten. Wenn InnoDB Seiten aus dem Pufferpool auf die Festplatte überträgt, schreibt (und entleert) es sie zuerst in den Doublewrite-Puffer und dann in den Hauptdatenbereich, in den sie tatsächlich gehören. Dies stellt sicher, dass jeder Seitenschreibvorgang atomar und dauerhaft ist. Bedeutet es nicht, dass jede Seite zweimal geschrieben wurde? Ja, das tut es, aber da InnoDB mehrere Seiten sequenziell in den Doublewrite-Puffer schreibt und erst dann fsync( ) aufruft, um sie mit der Festplatte zu synchronisieren, sind die Auswirkungen auf die Leistung relativ gering – im Allgemeinen nur einige Prozentpunkte. Was viel wichtiger ist: Diese Strategie erhöht die Effizienz der Log-Dateien. Da der Doublewrite-Puffer InnoDB eine sehr starke Garantie gibt, dass die Datenseiten nicht beschädigt sind, müssen die InnoDB-Log-Einträge nicht die vollständigen Seiten enthalten, sondern sozusagen nur die binären Differenzbeträge der Seiten. Wenn es im Doublewrite-Puffer selbst zu einem teilweisen Schreibvorgang kommt, befindet sich die Originalseite immer noch an ihrer eigentlichen Stelle auf der Festplatte. Nach der Regeneration benutzt InnoDB die Originalseite anstelle der beschädigten Kopie im Doublewrite-Puffer. Falls jedoch der Doublewrite-Puffer erfolgreich ist und das Schreiben an den wirklichen Ablageort der Seite fehlschlägt, benutzt InnoDB während der Wiederherstellung die Kopie aus dem Doublewrite-Puffer. InnoDB weiß, wenn eine Seite beschädigt ist, da jede Seite eine Prüfsumme am Ende besitzt; die Prüfsumme ist das Letzte, was geschrieben wird. Wenn also der Inhalt der Seite nicht der Prüfsumme entspricht, dann ist die Seite beschädigt. Bei der Wiederherstellung liest InnoDB daher einfach jede Seite im Doublewrite-Puffer und verifiziert die Prüfsummen. Ist die Prüfsumme einer Seite inkorrekt, liest es die Seite von ihrem ursprünglichen Ablageort. In manchen Fällen ist der Doublewrite-Puffer allerdings wirklich nicht erforderlich – z.B. werden Sie ihn auf Slaves sicher deaktivieren. Manche Dateisysteme (wie etwa ZFS) erledigen diese Dinge auch einfach selbst, es wäre also redundant, wenn InnoDB sie übernehmen würde. Sie können den Doublewrite-Puffer deaktivieren, indem Sie innodb_doublewrite auf 0 setzen.
318 | Kapitel 6: Die Servereinstellungen optimieren
Weitere Optionen zum Ein-/Ausgabe-Tuning Die Option sync_binlog steuert, wie MySQL das Binärlog auf die Festplatte entleert. Ihr Vorgabewert ist 0, was bedeutet, dass MySQL kein Entleeren vornimmt und es am Betriebssystem ist zu entscheiden, wann es seinen Cache in einen dauerhaften Speicher überträgt. Wenn der Wert größer als 0 ist, gibt sie an, wie viele Binärlog-Schreiboperationen zwischen den einzelnen Übertragungen auf die Festplatte geschehen. (Jede Schreiboperation ist eine einzelne Anweisung, falls autocommit gesetzt ist, und ansonsten eine Transaktion.) Diese Option wird nur selten auf etwas anderes als 0 oder 1 gesetzt. Falls Sie sync_binlog nicht auf 1 setzen, wird Ihr Binärlog mit hoher Wahrscheinlichkeit bei einem Absturz nicht mehr synchron mit Ihren transaktionsgebundenen Daten sein. Dadurch kann eine Replikation sehr leicht schiefgehen, und eine exakte Wiederherstellung wird unmöglich. Allerdings kostet die Sicherheit, die man erhält, wenn man diese Option auf 1 setzt, einen hohen Preis. Das Synchronisieren des Binärlogs und des Transaktions-Logs verlangt von MySQL, zwei Dateien an zwei getrennte Stellen zu übertragen. Das könnte eine Festplattensuche erfordern, was relativ langsam ist. Falls Sie Binär-Logging sowie InnoDB in MySQL 5.0 oder einer höheren Version benutzen und vor allem, wenn Sie von einer früheren Version umsteigen, sollten Sie sehr vorsichtig wegen der neuen XA-Transaktionsunterstützung sein. Sie ist dazu gedacht, Transaktions-Commits zwischen Storage-Engines und dem Binärlog zu synchronisieren, deaktiviert aber auch das Gruppen-Commit von InnoDB. Das kann die Leistung stark beeinträchtigen, da viel mehr fsync( )-Aufrufe beim Bestätigen von Transaktionen benötigt werden. Sie können das Problem mildern, indem Sie das Binärlog und die XA-Unterstützung von InnoDB mit innodb_support_xa=0 deaktivieren. Wenn Sie einen batteriegesicherten RAID-Cache haben, sind die einzelnen fsync( )-Aufrufe schnell, so dass das kein Problem darstellen dürfte.
Genau wie bei der InnoDB-Log-Datei erfahren Sie auch hier einen enormen Leistungszuwachs, wenn Sie das Binärlog auf ein RAID-Volume mit einem batteriegesicherten Schreib-Cache setzen. Ein Hinweis zu Binärlogs, der nicht leistungsbezogen ist: Falls Sie die Option expire_logs_days benutzen wollen, um alte Binärlogs automatisch zu entfernen, dann löschen Sie sie nicht mit rm. Der Server wird verwirrt und weigert sich in der Folge, sie automatisch zu entfernen, und PURGE MASTER LOGS stellt die Arbeit ein. Falls Sie sich in dieser Situation wiederfinden, dann synchronisieren Sie manuell die Datei hostnamebin.index mit der Liste der Dateien, die noch auf der Festplatte existieren. Wir behandeln RAID in Kapitel 7 ausführlicher, wollen hier aber noch einmal anmerken, dass hochwertige RAID-Controller mit batteriegesicherten Schreib-Caches, die auf eine Write-Back-Regelung eingestellt sind, Tausende von Schreibvorgängen pro Sekunde verarbeiten können und dennoch eine dauerhafte Speicherung gewährleisten. Die Daten
Das Ein-/Ausgabeverhalten von MySQL anpassen | 319
werden in einen schnellen Cache mit einer Batterie geschrieben, überleben also auch dann, wenn beim System der Strom ausfällt. Kommt der Strom zurück, schreibt der RAID-Controller die Daten aus dem Cache auf die Festplatte zurück, bevor er die Festplatte zur Benutzung zur Verfügung stellt. Ein guter RAID-Controller mit einem ausreichend großen, batteriegesicherten Schreib-Cache kann daher die Leistung drastisch verbessern und ist eine sehr gute Investition.
Die MySQL-Nebenläufigkeit anpassen Wenn Sie MySQL mit einer stark nebenläufigen Last betreiben, können Engstellen auftreten, die Ihnen anderenfalls nicht begegnen würden. Die folgenden Abschnitte erläutern, wie Sie diese Probleme erkennen, wenn sie auftreten, und wie Sie für MyISAM und InnoDB die bestmögliche Leistung unter diesen Lasten herausholen.
MyISAM bei Nebenläufigkeit anpassen Das gleichzeitige Lesen und Schreiben muss sorgfältig kontrolliert werden, damit die Leser keine inkonsistenten Ergebnisse sehen. MyISAM erlaubt unter bestimmten Bedingungen paralleles Einfügen und Lesen und ermöglicht es Ihnen, einige Operationen »einzutakten«, um so wenige Blockaden wie möglich zu erreichen. Bevor wir uns die Nebenläufigkeitseinstellungen von MyISAM anschauen, müssen Sie verstehen, wie MyISAM Zeilen löscht und einfügt. Bei Löschoperationen wird nicht die gesamte Tabelle neu angeordnet, sondern es werden lediglich Zeilen als gelöscht markiert, so dass »Löcher« in der Tabelle bleiben. MyISAM versucht, die Löcher nach Möglichkeit zu füllen und die Stellen für eingefügte Zeilen wiederzuverwenden. Wenn es keine Löcher gibt, hängt es neue Zeilen an das Ende der Tabelle an. Obwohl MyISAM Sperren auf Tabellenebene besitzt, kann es neue Zeilen parallel zum Lesen anhängen. Dazu stoppt es die Leseoperationen bei der letzten Zeile, die existierte, als sie begannen. Dadurch werden inkonsistente Leseoperationen vermieden. Es ist jedoch viel schwieriger, konsistente Leseoperationen zu gewährleisten, wenn sich etwas in der Mitte der Tabelle ändert. MVCC stellt die beliebteste Methode dar, um dieses Problem zu lösen: Es erlaubt es Lesern, alte Versionen der Daten zu lesen, während Schreiboperationen neue Versionen erzeugen. MyISAM unterstützt MVCC nicht, es unterstützt also auch kein nebenläufiges Einfügen, es sei denn, am Ende der Tabelle. Sie können das Verhalten von MyISAM bei nebenläufigen Einfügeoperationen mit der Variablen concurrent_insert konfigurieren, die folgende Werte annehmen kann: 0 1
MyISAM erlaubt keine nebenläufigen Einfügeoperationen; bei jedem Einfügen wird die Tabelle exklusiv gesperrt. Dies ist der Vorgabewert. MyISAM erlaubt nebenläufiges Einfügen, solange es keine Löcher in der Tabelle gibt.
320 | Kapitel 6: Die Servereinstellungen optimieren
2
Diesen Wert gibt es seit MySQL 5.0. Er erzwingt, dass nebenläufige Einfügungen an das Ende der Tabelle angehängt werden, selbst wenn es Löcher gibt. Wenn keine Threads aus der Tabelle lesen, setzt MySQL die neuen Zeilen in die Löcher. Die Tabelle kann mit dieser Einstellung stärker fragmentiert werden als normal, so dass Sie – je nach aktueller Last – Ihre Tabellen möglicherweise häufiger optimieren müssen.
Sie können MySQL auch so konfigurieren, dass es einige Operationen auf einen späteren Zeitpunkt verschiebt, wenn sie zugunsten einer größeren Effizienz kombiniert werden können. Zum Beispiel können Sie das Schreiben in den Index mit der Variablen delay_key_write verzögern, die wir bereits weiter vorn in diesem Kapitel erwähnt haben. Man muss hier wieder einmal abwägen, ob man den Index sofort wegschreiben (sicher, aber aufwendig) oder warten und hoffen will, dass vor dem Schreiben nicht der Strom ausfällt (schneller, aber mit der Gefahr massiver Schäden behaftet, die im Falle eines Absturzes eintreten, weil die Indexdatei ziemlich alt ist). Sie können auch INSERT-, REPLACE-, DELETE- und UPDATE-Abfragen mit der Option low_priority_updates eine niedrigere Priorität geben als SELECT-Abfragen. Das ist äquivalent zum globalen Anwenden des Modifikators LOW_PRIORITY auf UPDATE-Abfragen. Mehr dazu finden Sie in »Hinweise für den Abfrageoptimierer« auf Seite 210. Und schließlich, obwohl häufiger über die Skalierbarkeitsprobleme von InnoDB gesprochen wird, hatte auch MyISAM sehr lange Probleme mit Mutexes. Bis MySQL 4.0 schützt ein globaler Mutex alle Ein-/Ausgaben in den Schlüsselpuffer, was Skalierbarkeitsprobleme bei mehreren CPUs und mehreren Festplatten verursachte. Der Schlüsselpuffercode von MySQL 4.1 wurde verbessert und besitzt dieses Problem nicht mehr, enthält aber immer noch einen Mutex für jeden Schlüsselpuffer. Das ist problematisch, wenn ein Thread Schlüsselblöcke aus dem Schlüsselpuffer in seinen lokalen Speicher kopiert, anstatt sie von der Festplatte zu lesen. Der Flaschenhals auf der Festplatte ist weg, es gibt aber immer noch einen Engpass, wenn auf Daten im Schlüsselpuffer zugegriffen werden soll. Manchmal können Sie dieses Problem mit mehreren Schlüsselpuffern umgehen, allerdings ist dieser Ansatz nicht immer erfolgreich. Beispielsweise gibt es keine Möglichkeit, das Problem zu lösen, wenn nur ein einziger Index beteiligt ist. Daher können nebenläufige SELECT-Abfragen auf Mehr-CPU-Maschinen deutlich schlechter ablaufen als auf EinzelCPU-Maschinen, selbst wenn es die einzigen Abfragen sind, die ausgeführt werden.
InnoDB bei Nebenläufigkeit anpassen Das Design von InnoDB unterstützt eine hohe Nebenläufigkeit, allerdings ist es darin nicht perfekt. Die InnoDB-Architektur kann ihre Herkunft von Systemen mit beschränktem Speicher, nur einer CPU und einer einzigen Festplatte nicht verleugnen. Einige Aspekte der Performance von InnoDB fallen unter stark nebenläufigen Gegebenheiten stark ab. Als einzige Gegenmaßnahme hilft es, die Nebenläufigkeit zu beschränken. Sie können feststellen, ob InnoDB Nebenläufigkeitsprobleme hat, indem Sie den Abschnitt SEMAPHORES der SHOW INNODB STATUS-Ausgabe untersuchen. Weitere Informationen finden Sie in »SEMAPHORES« auf Seite 617.
Die MySQL-Nebenläufigkeit anpassen | 321
InnoDB besitzt einen eigenen »Thread Scheduler«, der steuert, wie Threads seinen Kernel betreten, um auf Daten zuzugreifen, und was sie tun können, wenn sie erst einmal im Kernel sind. Bei der einfachsten Methode, um die Nebenläufigkeit zu beschränken, kommt die Variable innodb_thread_concurrency zum Einsatz, mit der festgelegt werden kann, wie viele Threads sich auf einmal im Kernel befinden können. Ein Wert von 0 bedeutet, dass es keine Beschränkung für die Anzahl der Threads gibt. Falls Sie bei InnoDB Probleme mit der Nebenläufigkeit haben, dann ist diese Variable am wichtigsten. Es ist unmöglich, für bestimmte Architekturen und Lasten einen guten Wert anzugeben. Theoretisch hilft diese Formel: Nebenläufigkeit = Anzahl der CPUs * Anzahl der Festplatten * 2
In der Praxis kann es besser sein, einen viel kleineren Wert zu verwenden. Sie müssen probieren und Benchmark-Tests durchführen, um den besten Wert für Ihr System zu ermitteln. Falls sich bereits mehr als die erlaubte Anzahl an Threads im Kernel befindet, kann kein weiterer Thread in den Kernel eintreten. InnoDB setzt einen zweiphasigen Vorgang ein, um die Threads so effizient wie möglich in den Kernel eintreten zu lassen. Die Zweiphasenregelung reduziert den Aufwand für Kontextwechsel, der vom Betriebssystem-Scheduler verursacht wird. Der Thread schläft erst für innodb_thread_sleep_delay Mikrosekunden und versucht es dann noch einmal. Wenn er immer noch nicht eintreten kann, kommt er in eine Warteschlange mit wartenden Threads und gibt wieder an das Betriebssystem zurück. Die vorgegebene Schlafzeit beträgt in der ersten Phase 10.000 Mikrosekunden. In Umgebungen mit hoher Nebenläufigkeit kann es ganz hilfreich sein, diesen Wert zu ändern, wenn die CPU zu wenig ausgelastet ist, weil viele Threads sich im Zustand »schlafend vor dem Eintritt in die Warteschlange« befinden. Der Vorgabewert kann auch viel zu groß sein, wenn Sie viele kleine Abfragen haben, weil er die Abfragelatenz um weitere 10 Millisekunden erhöht. Sobald sich der Thread im Kernel befindet, besitzt er eine bestimmte Anzahl von »Tickets«, die es ihm erlauben, »kostenlos«, also ohne weitere Nebenläufigkeitsprüfungen, wieder in den Kernel zurückzukommen. Dieser Wert schränkt ein, wie viel Arbeit der Thread verrichten kann, bevor er wieder zurück in die Schlange zu den anderen wartenden Threads gehen muss. Die Option innodb_concurrency_tickets kontrolliert die Anzahl der Tickets. Sie muss selten geändert werden, es sei denn, Sie haben viele außerordentlich lange laufende Abfragen. Tickets werden pro Abfrage gewährt, nicht pro Transaktion. Sobald eine Abfrage abgeschlossen ist, werden ihre unbenutzten Tickets verworfen. Neben den Engpässen im Pufferpool und in anderen Strukturen gibt es einen weiteren Flaschenhals im Commit-Stadium, der wegen der Übertragungsoperationen stark ein/ausgabegebunden ist. Die Variable innodb_commit_concurrency bestimmt, wie viele Threads gleichzeitig ein Commit abgeben können. Das Konfigurieren dieser Option kann helfen, wenn viele Threads scheitern, obwohl innodb_thread_concurrency auf einen niedrigen Wert gesetzt wurde. 322 | Kapitel 6: Die Servereinstellungen optimieren
Das InnoDB-Team arbeitet daran, diese Probleme zu lösen. Es gab in MySQL 5.0.30 und 5.0.32 auch schon größere Verbesserungen.
Lastbasierte Anpassungen Das ultimative Ziel des Server-Tunings besteht darin, Ihren Server an Ihre spezielle Last anzupassen. Dies erfordert eine genaue Kenntnis der Anzahl, Art und Häufigkeit aller Arten von Serveraktivitäten – nicht nur der Abfragen, sondern auch anderer Aktivitäten, wie etwa der Verbindungsaufnahmen zum Server und der Übertragungen von Tabellen. Sie müssen auch wissen, wie Sie den Status und die Aktivität von MySQL sowie des Betriebssystems überwachen und interpretieren. In den Kapiteln 7 und 14 erfahren Sie mehr zu diesen Themen. Als Erstes müssen Sie sich mit Ihrem Server vertraut machen, falls Sie das noch nicht getan haben. Stellen Sie fest, welche Arten von Abfragen auf ihm laufen. Überwachen Sie ihn mit innotop oder anderen Werkzeugen. Es hilft nicht nur zu wissen, was der Server insgesamt tut, sondern auch, womit die einzelnen MySQL-Abfragen ihre Zeit verbringen. Sie können dieses Wissen z.B. erwerben, indem Sie die Ausgabe von SHOW PROCESSLIST anhand der Command-Spalte mit einem Skript sammeln (in innotop ist diese Fähigkeit bereits enthalten) oder indem Sie sie einfach so untersuchen. Suchen Sie nach Threads, die viel Zeit in einem bestimmten Zustand verbringen. Schauen Sie sich die Prozessliste einmal zu einem Zeitpunkt an, zu dem Ihr Server mit voller Leistung läuft. Das ist nämlich die beste Methode, um herauszufinden, welche Arten von Abfragen am meisten leiden. Gibt es z.B. viele Abfragen, die Ergebnisse in temporäre Tabellen kopieren oder Ergebnisse sortieren? In diesem Fall wissen Sie, dass Sie sich die Konfigurationseinstellungen für temporäre Tabellen und Sortierpuffer anschauen müssen. (Wahrscheinlich müssen Sie die Abfragen selbst optimieren.) Wir empfehlen normalerweise, die Patches zu benutzen, die wir für die MySQL-Logs entwickelt haben und die Ihnen ausführliche Informationen darüber liefern, was die einzelnen Abfragen tun, und die es Ihnen ermöglichen, Ihre Last umfassender zu analysieren. Diese Patches sind in den aktuellen offiziellen MySQL-Serverdistributionen enthalten, befinden sich also möglicherweise schon auf Ihrem Server. Näheres erfahren Sie unter »Feinere Kontrolle über das Logging« auf Seite 70.
Für BLOB- und TEXT-Lasten optimieren BLOB- und TEXT-Spalten stellen für MySQL eine besondere Art von Last dar. (Wir bezeichnen der Einfachheit halber alle BLOB- und TEXT-Typen als BLOB, da sie zur gleichen Klasse von Datentypen gehören.) Es gibt verschiedene Beschränkungen für BLOB-Werte, die dafür sorgen, dass der Server sie anders behandelt als andere Typen. Einer der wichtigsten Gesichtspunkte ist der, dass der Server keine im Speicher befindlichen temporären Tabellen für BLOB-Werte benutzen kann. Falls daher eine Abfrage, die BLOB-Werte einbezieht, eine temporäre Tabelle verlangt, gelangt diese sofort auf die Festplatte – egal, wie
Lastbasierte Anpassungen | 323
klein sie ist. Das ist sehr ineffizient, vor allem bei ansonsten kleinen und schnellen Abfragen. Die temporäre Tabelle wäre sonst das teuerste an der Abfrage. Es gibt zwei Möglichkeiten, um diesen Nachteil zu lindern: Sie wandeln die Werte mit der Funktion SUBSTRING( ) in VARCHAR um (mehr dazu in »Stringtypen« auf Seite 90), oder Sie machen die temporären Tabellen schneller. Am besten beschleunigen Sie die temporären Tabellen, indem Sie sie in ein speicherbasiertes Dateisystem setzen (wie etwa tmpfs unter GNU/Linux). Damit verringert sich der Overhead teilweise, obwohl das Ganze dann immer noch langsamer ist als die Benutzung von Tabellen im Speicher. Die Verwendung eines speicherbasierten Dateisystems ist hilfreich, weil das Betriebssystem dann versucht, das Schreiben der Daten auf die Festplatte zu vermeiden.5 Normale Dateisysteme werden ebenfalls im Cache abgelegt, allerdings könnte es passieren, dass das Betriebssystem normale Dateisystemdaten alle paar Sekunden auf die Festplatte überträgt. Ein tmpfs-Dateisystem dagegen wird niemals geleert. Das tmpfs-Dateisystem ist auch für einen niedrigen Aufwand und für Einfachheit gedacht. So muss das Dateisystem z.B. keine Vorkehrungen für eine Wiederherstellung treffen und ist dadurch schneller. Die Servereinstellung tmpdir steuert, wo die temporären Tabellen abgelegt werden. Überwachen Sie, wie voll das Dateisystem wird, um sicherzustellen, dass Sie immer genügend Platz für temporäre Tabellen haben. Falls nötig, können Sie sogar mehrere Stellen für temporäre Tabellen angeben, die MySQL in Round-Robin-Manier benutzt. Falls Ihre BLOB-Spalten sehr groß sind und Sie InnoDB benutzen, wollen Sie möglicherweise die Puffergröße des InnoDB-Log erhöhen. Wir haben weiter vorn in diesem Kapitel schon etwas dazu angemerkt. Für Spalten mit langen Variablen (z.B. BLOB, TEXT und lange Zeichenspalten) speichert InnoDB ein 768 Byte langes Präfix auf der Seite zusammen mit dem Rest der Zeile.6 Ist der Wert der Spalte länger als diese Präfixlänge, dann könnte InnoDB externen Speicherplatz außerhalb der Zeile belegen, um den Rest dieses Wertes zu speichern. Es reserviert diesen Platz, wie alle anderen InnoDB-Seiten, in kompletten 16-KByte-Seiten, und jede Spalte bekommt eine eigene Seite. (Spalten teilen sich den externen Speicherplatz nicht.) InnoDB reserviert externen Speicherplatz für eine Spalte seitenweise, bis 32 Seiten belegt sind, dann reserviert es 64 Seiten auf einmal. Beachten Sie, dass wir gesagt haben, InnoDB könnte externen Speicher reservieren. Wenn die Gesamtlänge der Zeile einschließlich des vollständigen Wertes der langen Spalte kürzer ist als die maximale Zeilenlänge für InnoDB (etwas weniger als 8 KByte), reserviert InnoDB keinen externen Speicher, und zwar auch dann nicht, wenn der Wert der langen Spalte die Präfixlänge überschreitet.
5 Daten können dennoch auf die Festplatte gelangen, wenn das Betriebssystem sie auslagert. 6 Das ist lang genug, um einen 255-Zeichen-Index in einer Spalte zu erzeugen, selbst wenn es sich um utf8 handelt, das bis zu 3 Byte pro Zeichen verlangen könnte.
324 | Kapitel 6: Die Servereinstellungen optimieren
Wenn InnoDB schließlich eine lange Spalte aktualisiert, die in einen externen Speicher gelegt wurde, tut es dies nicht an dieser Stelle, sondern schreibt stattdessen den neuen Wert an eine neue Stelle in dem externen Speicher und löscht den alten Wert. All dies zieht folgende Konsequenzen nach sich: • Lange Spalten können in InnoDB eine Menge Platz verschwenden. Falls Sie z.B. einen Spaltenwert speichern, der ein Byte zu lang ist, um in eine Zeile zu passen, wird eine ganze Seite zum Speichern des überstehenden Bytes benutzt, wodurch ein Großteil der Seite verschwendet wird. Haben Sie einen Wert, der etwas mehr als 32 Seiten lang ist, werden möglicherweise tatsächlich 96 Seiten auf der Festplatte verwendet. • Die externe Speicherung deaktiviert den adaptiven Hash-Index, der die vollständige Länge der Spalten vergleichen muss, um sicherzugehen, dass er die richtigen Daten gefunden hat. (Der Hash hilft InnoDB dabei, »Schätzungen« sehr schnell zu finden, muss dabei aber überprüfen, ob seine »Schätzung« richtig ist.) Da der adaptive Hash-Index komplett im Speicher vorliegt und direkt »auf« häufig zugegriffenen Seiten im Pufferpool aufbaut, funktioniert er mit einem externen Speicher nicht. • Lange Werte können Abfragen mit einer WHERE-Klausel durchführen, die keinen Index benutzt. MySQL liest alle Spalten, bevor es die WHERE-Klausel anwendet, so dass es InnoDB auffordern könnte, eine Menge des externen Speichers zu lesen, dann die WHERE-Klausel zu überprüfen und alle Daten wegzuwerfen, die es gelesen hat. Es ist niemals gut, Spalten auszuwählen, die man nicht benötigt, aber dies ist ein besonderer Fall, in dem es noch wichtiger ist, es zu vermeiden. Falls Sie feststellen, dass Ihre Abfragen unter dieser Einschränkung leiden, können Sie versuchen, abdeckende Indizes einzusetzen. Mehr dazu erfahren Sie in »Abdeckende Indizes« auf Seite 128. • Wenn Sie viele lange Spalten in einer einzigen Tabelle haben, wäre es besser, die Daten, die sie speichern, in einer einzigen Spalte zu kombinieren, vielleicht als XML-Dokument. Das erlaubt es allen Werten, den externen Speicher gemeinsam zu benutzen, anstatt ihre eigenen Seiten zu verwenden. • Manchmal erzielen Sie deutliche Vorteile in Bezug auf Platz und Leistung, wenn Sie lange Spalten in einem BLOB speichern und mit COMPRESS( ) komprimieren oder wenn Sie sie in der Anwendung komprimieren, bevor Sie sie an MySQL senden.
Für Filesorts optimieren MySQL enthält zwei Variablen, mit deren Hilfe Sie kontrollieren können, wie es Filesorts ausführt. Denken Sie zurück an »Sortieroptimierungen« auf Seite 190: MySQL besitzt zwei Filesort-Algorithmen. Es verwendet einen zweistufigen Algorithmus, wenn die Gesamtgröße aller Spalten, die für die Abfrage benötigt werden, plus die ORDER BY-Spalten größer ist als max_length_for_sort_data Byte. Es setzt diesen Algorithmus außerdem ein, wenn
Lastbasierte Anpassungen | 325
irgendeine der benötigten Spalten – auch wenn sie nicht für ORDER BY gebraucht werden – eine BLOB- oder TEXT-Spalte ist. (Sie können SUBSTRING( ) benutzen, um solche Spalten in Typen umzuwandeln, die mit dem einstufigen Algorithmus zurechtkommen.) Sie beeinflussen MySQLs Wahl des Algorithmus, indem Sie den Wert der Variablen max_length_for_sort_data ändern. Da der einstufige Algorithmus für jede Zeile, die er sortiert, einen Puffer fester Größe erzeugt, ist die maximale Länge von VARCHAR-Spalten das, was für max_length_for_sort_data zählt, nicht die tatsächliche Größe der gespeicher-
ten Daten. Dies ist einer der Gründe, weshalb wir empfehlen, dass Sie diese Spalten nur so groß wie nötig machen. Wenn MySQL in BLOB- oder TEXT-Spalten sortieren muss, verwendet es nur ein Präfix und ignoriert die restlichen Werte. Dies tut es, weil es eine Struktur fester Größe reservieren muss, um die Werte aufzunehmen, und das Präfix aus dem externen Speicher in diese Struktur kopieren muss. Sie können mit der Variablen max_sort_length angeben, wie lang dieses Präfix sein soll. Leider bietet Ihnen MySQL keinen wirklichen Einblick in die Art des Sortieralgorithmus, den es benutzt. Wenn Sie die Variable max_length_for_sort_data erhöhen und die Festplattennutzung steigt, sinkt die CPU-Benutzung, und die Statusvariable Sort_merge_ passes beginnt, schneller zu wachsen als vor der Änderung. Wahrscheinlich haben Sie in diesem Fall mehr Sortierungen dazu gezwungen, den einstufigen Algorithmus zu benutzen. Mehr über die BLOB- und TEXT-Typen erfahren Sie in »Stringtypen« auf Seite 90.
Die MySQL-Serverstatusvariablen untersuchen Eine der produktivsten Methoden, um MySQL an Ihre Last anzupassen, besteht darin, die Ausgabe von SHOW GLOBAL STATUS zu untersuchen, um festzustellen, welche Einstellungen einer Änderung bedürfen. Wenn Sie gerade erst damit beginnen, einen Server anzupassen, und mit mysqlreport vertraut sind, dann führen Sie dies aus, und untersuchen Sie den relativ leicht verständlichen Bericht, den es generiert. Sie sparen damit eine Menge Zeit. Dieser Bericht hilft Ihnen, potenzielle Problempunkte zu lokalisieren, so dass Sie die relevanten Variablen genauer mit SHOW GLOBAL STATUS untersuchen können. Wenn Sie etwas bemerken, das aussieht, als könnte es verbessert werden, dann ändern Sie es. Schauen Sie sich anschließend die inkrementelle Ausgabe von mysqladmin extended -r -i60 an, um die Wirkungen Ihrer Änderungen zu sehen. Am besten ist es, wenn Sie sich sowohl die absoluten Werte als auch die Änderungen über die Zeit anschauen. In Kapitel 13 gibt es eine ausführlichere Liste der Variablen, die Sie mit SHOW GLOBAL STATUS untersuchen können. Die folgende Liste zeigt nur die Variablen, deren Untersuchung am produktivsten ist: Aborted_clients
Wenn der Wert dieser Variablen mit der Zeit zunimmt, sollten Sie sich fragen, ob Sie Ihre Verbindungen ordnungsgemäß schließen. Falls nicht, prüfen Sie die Netzwerkleistung, und untersuchen Sie die Konfigurationsvariable max_allowed_packet. Abfragen, die max_allowed_packet überschreiten, werden unsanft abgebrochen. 326 | Kapitel 6: Die Servereinstellungen optimieren
Aborted_connects
Dieser Wert sollte nahe null liegen. Falls er das nicht tut, haben Sie möglicherweise Netzwerkprobleme. Ein paar abgebrochene Verbindungen sind normal. Sie können z.B. auftreten, wenn jemand versucht, vom falschen Host eine Verbindung herzustellen, den falschen Benutzernamen oder das falsche Passwort verwendet oder eine ungültige Datenbank angibt. Binlog_cache_disk_use und Binlog_cache_use Wenn das Verhältnis von Binlog_cache_disk_use zu Binlog_cache_use groß ist, erhöhen Sie binlog_cache_size. Die meisten Transaktionen sollen in den Binärlog-Cache
passen, es ist aber in Ordnung, wenn gelegentlich eine auf die Festplatte hinüberschwappt. Das Reduzieren der Binärlog-Cache-Misses ist keine exakte Wissenschaft. Am besten ist es, die Einstellung binlog_cache_size zu erhöhen und abzuwarten, ob sich die Anzahl der Cache-Misses verringert. Sobald Sie sie auf einen bestimmten Punkt gedrückt haben, bringt es nichts mehr, den Cache weiter zu vergrößern. Nehmen Sie an, es tritt ein Miss pro Sekunde auf. Sie vergrößern den Cache und kommen dann auf einen Miss pro Minute. Das ist gut genug – viel niedriger werden Sie die Miss-Rate nicht bekommen. Falls doch, nützt es Ihnen nicht besonders viel, so dass Sie den Speicher lieber für etwas anderes aufsparen sollten. Bytes_received und Bytes_sent
Diese Werte helfen Ihnen dabei, festzustellen, ob es wegen eines zu starken Verkehrs vom oder zum Server ein Problem mit dem Server gibt.7 Sie könnten auch auf ein Problem irgendwo in Ihrem Code hinweisen, etwa auf eine Abfrage, die mehr Daten holt, als sie benötigt. (Mehr zu diesem Thema finden Sie in »Das MySQLClient/Server-Protokoll« auf Seite 173.) Com_*
Sie müssen überprüfen, dass Sie keine höheren Werte als erwartet für ungewöhnliche Variablen wie Com_rollback erhalten. Eine schnelle Methode, um auf vernünftige Werte zu testen, bietet der Command-Summary-Modus von innotop. (In Kapitel 14 erfahren Sie mehr über innotop.) Connections
Diese Variable repräsentiert die Anzahl der Verbindungsversuche (nicht die Anzahl der aktuellen Verbindungen, die Threads_connected beträgt). Wenn dieser Wert rasant zunimmt – d.h., auf Hunderte Verbindungen pro Sekunde steigt – müssen Sie in das Verbindungs-Pooling schauen oder den Netzwerk-Stack des Betriebssystems anpassen. (Im nächsten Kapitel erfahren Sie mehr über die Netzwerkkonfiguration.)
7 Selbst wenn die Kapazität Ihres Netzwerks ausreicht, sollten Sie das nicht als Leistungsengpass ausschließen. Die Latenz des Netzwerks kann zu einer schlechten Performance beitragen.
Lastbasierte Anpassungen | 327
Created_tmp_disk_tables
Wenn dieser Wert hoch ist, kann eines von zwei Dingen schiefgegangen sein: Ihre Abfragen könnten temporäre Tabellen erzeugen, während BLOB- oder TEXT-Spalten ausgewählt werden, oder Ihre tmp_table_size- und/oder max_heap_table_size-Werte sind nicht groß genug. Created_tmp_tables
Um etwas an einem hohen Wert für diese Variable zu ändern, können Sie nur Ihre Abfragen optimieren. In den Kapiteln 3 und 4 gibt es Tipps für die Optimierung. Handler_read_rnd_next Handler_read_rnd_next / Handler_read_rnd liefert Ihnen die ungefähre Durchschnitts-
größe für einen vollständigen Tabellenscan. Ist der Wert groß, müssen Sie vermutlich Ihr Schema, die Indizierung oder die Abfragen optimieren. Key_blocks_used Wenn Key_blocks_used * key_cache_block_size auf einem aufgewärmten Server viel kleiner ist als key_buffer_size, dann ist der Wert für key_buffer_size größer als
nötig, und Sie verschwenden Speicher. Key_reads
Beobachten Sie, wie viele Leseoperationen pro Sekunde Sie sehen, und vergleichen Sie diesen Wert mit Ihrem Ein-/Ausgabe-System, um festzustellen, wie stark Sie dessen Grenzen ausreizen. Mehr dazu finden Sie in Kapitel 7. Max_used_connections
Wenn dieser Wert genauso groß ist wie max_connections, ist entweder max_ connections zu niedrig eingestellt oder Sie hatten eine Spitze, die die konfigurierten Grenzen Ihres Servers erreicht hat. Gehen Sie jedoch nicht automatisch davon aus, dass Sie max_connections erhöhen sollten! Es handelt sich dabei eher um eine Notgrenze, die verhindern soll, dass Ihr Server unter zu viel Last zusammenbricht. Wenn Sie eine Spitze in der Nachfrage sehen, dann sollten Sie überprüfen, ob Ihre Anwendung sich anständig verhält, ob Ihr Server richtig eingestellt ist und ob Ihr Schema gut gestaltet ist. Es ist besser, etwas an der Anwendung zu ändern, als einfach die max_connections-Grenze des Servers zu erhöhen. Open_files
Achten Sie darauf, dass dieser Wert nicht den Wert von open_files_limit erreicht. In diesem Fall sollten Sie wahrscheinlich die Grenze erhöhen. Open_tables und Opened_tables
Vergleichen Sie diesen Wert mit dem table_cache-Wert. Wenn Sie viele Opened_tables (geöffnete Tabellen) pro Sekunde sehen, ist Ihr table_cache-Wert möglicherweise nicht groß genug. Explizite temporäre Tabellen können ebenfalls für eine wachsende Anzahl offener Tabellen sorgen, selbst wenn der Tabellen-Cache nicht vollständig benutzt wird, so dass es keinen Grund geben muss, sich Sorgen zu machen.
328 | Kapitel 6: Die Servereinstellungen optimieren
Qcache_*
Mehr Informationen über den Abfrage-Cache finden Sie in »Der MySQL-AbfrageCache« auf Seite 220. Select_full_join
Vollständige Joins sind Joins ohne Indizes, die sich verheerend auf die Performance auswirken können. Es ist am besten, sie zu eliminieren; selbst einer pro Minute kann zu viel sein. Sie sollten Ihre Abfragen und Indizes optimieren, falls Sie Joins ohne Indizes haben. Select_full_range_join
Wenn diese Zahl hoch ist, dann führen Sie viele Abfragen aus, die eine BereichsLookup-Strategie fahren, um Tabellen zusammenzuführen. Diese können langsam sein und bilden einen guten Ansatzpunkt für eine Optimierung. Select_range_check
Diese Variable verfolgt Abfragepläne, die Schlüsselselektionen für jede Zeile in einem Join erneut überprüfen, was sehr aufwendig ist. Wenn dieser Wert sehr hoch ist oder anwächst, dann haben Sie Abfragen, die keine guten Indizes finden, die sie benutzen könnten. Slow_launch_threads
Ein großer Wert für diese Statusvariable bedeutet, dass irgendetwas neue Threads beim Verbindungsaufbau verzögert. Dies ist ein Zeichen dafür, dass mit Ihrem Server etwas nicht stimmt, zeigt aber nicht an, was das ist. Normalerweise deutet es auf eine Systemüberlastung hin, die verhindert, dass das Betriebssystem neu erzeugten Threads CPU-Zeit zuteilt. Sort_merge_passes
Ein hoher Wert für diese Variable bedeutet, dass Sie den Wert für sort_buffer_size erhöhen müssen – vielleicht nur für einige Abfragen. Überprüfen Sie Ihre Abfragen, und stellen Sie fest, welche Filesorts verursachen. Sie sollten in der Lage sein, diese zu optimieren. Table_locks_waited
Diese Variable teilt Ihnen mit, wie viele Tabellen gesperrt wurden und auf der Serverebene Wartezeiten für Sperren verursacht haben. (Das Warten auf Storage-EngineSperren, wie etwa die Row-Level-Locks bei InnoDB, erhöht diese Variable nicht.) Wenn dieser Wert hoch ist und weiter ansteigt, haben Sie möglicherweise einen ernsthaften Engpass bei der Nebenläufigkeit. Sie sollten darüber nachdenken, InnoDB oder eine andere Storage-Engine einzusetzen, die Row-Level-Locks benutzt, oder darüber, große Tabellen manuell oder mit der MySQL-eigenen (ab MySQL 5.1) Partitionierung zu partitionieren, Ihre Abfragen zu optimieren, nebenläufige Einfügeoperationen zu aktivieren oder die Einstellungen für die Sperren anzupassen. MySQL verrät Ihnen nicht, wie lange gewartet wurde. Momentan finden Sie das wahrscheinlich am besten mit dem mikrosekundengenauen Slow-Query-Log heraus. Mehr dazu erfahren Sie in »MySQL-Profiling« auf Seite 68.
Lastbasierte Anpassungen | 329
Threads_created
Wenn dieser Wert groß ist oder zunimmt, müssen Sie wahrscheinlich die Variable thread_cache_size erhöhen. Prüfen Sie Threads_cached, um festzustellen, wie viele Threads sich bereits im Cache befinden.
Verbindungsbezogene Werte anpassen Sie sollten einen Wert, der für eine Verbindung gilt, nicht global anheben, wenn Sie sich nicht völlig sicher sind, dass dies das Richtige ist. Manchmal werden alle Puffer auf einmal reserviert, selbst wenn nicht alle benötigt werden, so dass eine große globale Einstellung eine große Platzverschwendung sein kann. Stattdessen können Sie den Wert anheben, wenn eine Abfrage ihn benötigt. Das gebräuchlichste Beispiel für eine Variable, die Sie klein halten und nur für bestimmte Abfragen erhöhen sollten, ist sort_buffer_size. Mit dieser Variable kontrollieren Sie, wie groß der Sortierpuffer für Filesorts sein sollte. Sie wird selbst für sehr kleine Sortierungen auf ihre volle Größe gesetzt. Das bedeutet, dass Sie Speicher verschwenden und die Kosten für die Reservierung erhöhen, wenn Sie den Wert größer machen, als die durchschnittliche Sortierung verlangt. Wenn Sie eine Abfrage finden, die einen größeren Sortierpuffer benötigt, um gut zu funktionieren, können Sie den sort_buffer_size-Wert direkt vor der Abfrage anheben und ihn hinterher wieder auf DEFAULT zurücksetzen. Hier ein Beispiel: SET @@session.sort_buffer_size := <Wert>; -- Ausführen der Abfrage... SET @@session.sort_buffer_size := DEFAULT;
Für diese Art von Code sind Wrapper-Funktionen ganz praktisch. Andere Variablen, die Sie auf Verbindungsbasis setzen könnten, sind read_buffer_size, read_rnd_buffer_size, tmp_table_size und myisam_sort_buffer_size (falls Sie Tabellen reparieren). Falls Sie einen möglicherweise selbst eingestellten Wert sichern und wiederherstellen müssen, könnten Sie eine solche Aktion durchführen: SET @saved_<eindeutiger_Variablenname> := @@session.sort_buffer_size; SET @@session.sort_buffer_size := <Wert>; -- Ausführen der Abfrage... SET @@session.sort_buffer_size := @saved_<eindeutiger_Variablenname>;
330 | Kapitel 6: Die Servereinstellungen optimieren
KAPITEL 7
Betriebssystem- und Hardwareoptimierung
Ihr MySQL-Server kann nur so gut arbeiten wie sein schwächster Bestandteil, und oft sind das Betriebssystem und die Hardware, auf der dieses läuft, begrenzende Faktoren. Die Festplattengröße, der verfügbare Speicher und die CPU-Ressourcen, das Netzwerk und die Komponenten, die das alles verbinden, beschränken letztendlich die Kapazität des Systems. In den früheren Kapiteln haben wir uns darauf konzentriert, den MySQL-Server und Ihre Anwendung zu optimieren. Diese Art der Einstellung ist ausgesprochen wichtig, Sie müssen jedoch auch Ihre Hardware berücksichtigen und das Betriebssystem entsprechend konfigurieren. Falls z.B. Ihre Last ein-/ausgabegebunden ist, besteht ein Ansatz darin, Ihre Anwendung so zu gestalten, dass die Ein-/Ausgabe-Last von MySQL minimiert wird. Oft ist es allerdings geschickter, das Ein-/Ausgabe-Subsystem aufzurüsten, mehr Speicher zu installieren oder vorhandene Festplatten neu zu konfigurieren. Hardware ändert sich sehr schnell, so dass wir hier nicht unterschiedliche Produkte miteinander vergleichen oder bestimmte Komponenten nennen wollen. Unser Ziel besteht stattdessen darin, Ihnen einige Richtlinien und Ansätze zum Lösen von Hardware- und Betriebssystem-Engstellen zu liefern. Wir schauen uns zuerst an, was die Performance von MySQL beschränkt. Am häufigsten gibt es Probleme mit der CPU, dem Speicher und der Ein-/Ausgabe, allerdings können diese Probleme und Engpässe sich anders darstellen, als Sie vielleicht auf den ersten Blick vermuten würden. Wir untersuchen, wie Sie CPUs für MySQL-Server wählen sollten, und anschließend versuchen wir, Speicher- und Festplattenressourcen miteinander abzuwägen. Wir vergleichen verschiedene Arten der Ein-/Ausgabe (zufällige und sequenzielle, Lesen und Schreiben) und erläutern, wie Sie Ihren Arbeitssatz zu verstehen haben. Dieses Wissen hilft Ihnen dabei, ein effektives Verhältnis von Speicher zu Festplatte zu wählen. Danach präsentieren wir Ihnen Tipps für die Auswahl von Festplatten für MySQL-Server und kommen zum wichtigen Thema der RAID-Optimierung. Wir beenden unsere Diskussion der Speicherung mit einem Blick auf die Möglichkeiten der externen Speicherung (wie etwa SANs) und einigen Ratschlägen zur Benutzung von Volumes mit mehreren Festplatten für die MySQL-Daten und Logs.
| 331
Vom Speicher kommen wir zur Netzwerkleistung und zur Auswahl eines Betriebs- und Dateisystems. Anschließend untersuchen wir die Thread-Unterstützung, die MySQL benötigt, um gut zu funktionieren, und betrachten, wie Sie das Auslagern von Daten (Swapping) vermeiden können. Wir schließen das Kapitel mit Beispielen für die Statusausgaben des Betriebssystems.
Was beschränkt die Leistung von MySQL? Es gibt viele unterschiedliche Hardwarekomponenten, die die Leistung von MySQL beeinträchtigen können. Die unserer Meinung nach am häufigsten auftretenden Engpässe sind CPU-Sättigung und Ein-/Ausgabe-Sättigung. CPU-Sättigung tritt auf, wenn MySQL mit Daten arbeitet, die entweder in den Speicher passen oder so schnell wie nötig von der Festplatte gelesen werden können. Beispiele hierfür sind intensive kryptografische Operationen und Joins ohne Indizes, aus denen schließlich Kreuzprodukte werden. Ein-/Ausgabe-Sättigung dagegen tritt im Allgemeinen ein, wenn Sie mit viel mehr Daten arbeiten müssen, als in den Speicher passen. Ist Ihre Anwendung über ein Netzwerk verteilt oder haben Sie eine riesige Anzahl von Abfragen und/oder niedrige Latenzanforderungen, dann kann sich dieser Engpass auf das Netzwerk verlagern. Schauen Sie über das Offensichtliche hinaus, wenn Sie glauben, einen Engpass gefunden zu haben. Eine Schwäche in einem Bereich führt oft dazu, dass ein anderes Subsystem belastet wird, das dann das Problem zu sein scheint. Falls Sie z.B. nicht genug Speicher haben, muss MySQL möglicherweise Caches leeren, um Platz für die benötigten Daten zu schaffen – und dann muss es einen Augenblick später die Daten wieder zurücklesen, die es gerade auf die Festplatte übertragen hat (das kommt sowohl bei Lese- als auch bei Schreiboperationen vor). Der Speichermangel kann daher als fehlende Ein-/AusgabeKapazität erscheinen. In gleicher Weise kann ein gesättigter Speicherbus wie ein CPUProblem aussehen. Wenn wir daher sagen, dass eine Anwendung einen »CPU-Engpass« aufweist oder »CPU-gebunden« ist, meinen wir in Wirklichkeit, dass es ein Engpass beim Rechner ist. Damit befassen wir uns gleich.
Wie Sie CPUs für MySQL auswählen Sie müssen überlegen, ob Ihre Last CPU-gebunden ist, wenn Sie vorhandene Hardware aufrüsten oder neue Hardware kaufen. Eine CPU-gebundene Last können Sie erkennen, indem Sie die CPU-Auslastung überprüfen. Schauen Sie aber nicht einfach nur danach, wie stark Ihre CPUs insgesamt belastet sind, sondern versuchen Sie, das Gleichgewicht zwischen CPU-Benutzung und Ein-/Ausgabe für Ihre wichtigsten Abfragen zu betrachten, und stellen Sie fest, ob die CPUs gleichmäßig belastet werden. Werkzeuge wie mpstat, iostat und vmstat (Beispiele finden Sie am Ende dieses Kapitels) helfen Ihnen dabei, festzustellen, was die Performance Ihres Servers begrenzt.
332 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Was ist besser: Schnelle CPUs oder viele CPUs? Wenn Sie eine CPU-gebundene Last haben, profitiert MySQL im Allgemeinen am meisten von schnelleren CPUs (und nicht von vielen CPUs). Das gilt allerdings nicht immer, weil es von der Last und der Anzahl der CPUs abhängt. Die aktuelle MySQL-Architektur hat jedoch Skalierungsprobleme bei mehreren CPUs, und MySQL ist nicht in der Lage, eine einzige Abfrage parallel auf vielen CPUs auszuführen. Daher begrenzt die CPU-Geschwindigkeit die Antwortzeit jeder einzelnen CPUgebundenen Abfrage. Allgemein gesagt gibt es zwei Arten von wünschenswerter Leistung: Niedrige Latenz (schnelle Antwortzeit) Um dies zu erreichen, brauchen Sie schnelle CPUs, da jede Abfrage nur eine einzige CPU benutzt. Hoher Durchsatz Wenn Sie viele Abfragen gleichzeitig ausführen können, profitieren Sie vermutlich davon, wenn viele CPUs die Abfragen bedienen. Ob das allerdings in der Praxis funktioniert, hängt von vielen Faktoren ab. Da MySQL schlecht auf mehreren CPUs skaliert, ist es oft besser, stattdessen weniger, aber schnellere CPUs zu verwenden. Falls Sie mehrere CPUs haben und die Abfragen nicht nebenläufig ausführen, kann MySQL die zusätzlichen CPUs immer noch für Hintergrundaufgaben wie das Bereinigen der InnoDB-Puffer, Netzwerkoperationen usw. einsetzen. Diese Jobs sind jedoch meist klein im Vergleich zum Ausführen der Abfragen. Wenn Sie ein System mit zwei CPUs veranlassen, konstant eine einzige CPU-gebundene Abfrage auszuführen, ist die zweite CPU wahrscheinlich für 90 % der Zeit untätig. Die MySQL-Replikation (auf die wir im nächsten Kapitel kommen) funktioniert ebenso am besten mit schnellen und nicht mit vielen CPUs. Wenn Ihre Last CPU-gebunden ist, kann eine parallele Last auf dem Master leicht zu einer Last auf dem Slave serialisiert werden, die der Slave nicht bewältigen kann, sogar dann, wenn der Slave leistungsfähiger ist als der Master. Nichtsdestoweniger ist normalerweise das Ein-/Ausgabe-Subsystem und nicht die CPU der Engpass auf einem Slave. Wenn Sie eine CPU-gebundene Last haben, dann besteht ein anderes Herangehen an die Frage, ob Sie eine schnelle oder doch besser viele CPUs brauchen, in der Überlegung, was Ihre Abfragen wirklich tun. Auf der Hardwareebene kann eine Abfrage entweder ausgeführt werden oder warten. Die am häufigsten auftretenden Gründe für das Warten sind Aufenthalte in der Ausführungswarteschlange (wenn der Prozess lauffähig ist, aber alle CPUs ausgelastet sind), das Warten auf Register oder Sperren und das Warten auf die Festplatte oder das Netzwerk. Was glauben Sie, worauf Ihre Abfragen warten? Wenn sie sich in der Ausführungswarteschlange befinden oder auf Register oder Sperren warten, brauchen Sie im Allgemeinen schnellere CPUs. (Es kann Ausnahmen geben, wie etwa eine Abfrage, die auf den InnoDB-Log-Puffer-Mutex wartet, der erst dann frei wird, wenn die Ein-/Ausgabe abgeschlossen ist – das könnte darauf hindeuten, dass Sie eigentlich eine höhere Ein-/Ausgabe-Kapazität brauchen.)
Wie Sie CPUs für MySQL auswählen | 333
Dennoch kann MySQL bei manchen Lasten viele CPUs effektiv einsetzen. Nehmen Sie z.B. an, dass Sie viele Verbindungen haben, die getrennte Tabellen abfragen (und daher nicht um Tabellensperren konkurrieren, was bei MyISAM- und Memory-Tabellen zu einem Problem werden kann), und dass der Gesamtdurchsatz des Servers wichtiger ist als die Antwortzeit einer einzelnen Abfrage. Der Durchsatz kann in diesem Szenario sehr hoch sein, weil die Threads nebenläufig ausgeführt werden können, ohne miteinander zu konkurrieren. Aber auch das funktioniert in der Theorie vermutlich besser als in der Praxis: InnoDB hat Skalierungsprobleme, ob nun die Abfragen aus getrennten Tabellen lesen oder nicht, und MyISAM besitzt globale Sperren auf jedem Schlüsselpuffer. Vollständige Tabellenscans in MyISAM-Tabellen sind ein Beispiel für Abfragen, die nebenläufig ausgeführt werden können, ohne einander zu stören. MySQL preist die Falcon-Storage-Engine als diejenige an, die in der Lage ist, Server mit wenigstens acht CPUs auszunutzen. Künftig kann MySQL also vielleicht viele CPUs viel effektiver benutzen als zurzeit. Allerdings werden erst die Zukunft und die Erfahrung zeigen, ob Falcon tatsächlich so gut skalierbar sein wird.
Die CPU-Architektur 64-Bit-Architekturen sind heutzutage viel verbreiteter als noch vor einigen Jahren. MySQL funktioniert gut auf 64-Bit-Architekturen, obwohl einige seiner Interna noch nicht 64-Bit-fähig sind. Beispielsweise ist in MySQL 5.0 jeder MyISAM-Schlüsselpuffer auf 4 GByte beschränkt, also die Größe, die mit einem 32-Bit-Integer adressierbar ist. (Um das zu umgehen, können Sie allerdings mehrere Schlüsselpuffer anlegen.) Günstigerweise wählen Sie für sämtliche Hardware, die Sie neu kaufen, eine 64-BitArchitektur. Falls Sie auf 64-Bit-CPU und 64-Bit-Betriebssystem verzichten, können Sie einen großen Speicher nicht effizient benutzen: Obwohl einige 32-Bit-Systeme mit großen Mengen an Speicher zurechtkommen, sind sie doch nicht so effizient wie ein 64-BitSystem, und auch MySQL kann den Speicher dann nicht so gut benutzen.
Für viele CPUs und Kerne skalieren Mehrere CPUs können bei einem Online-Transaktionsverarbeitungssystem hilfreich sein. Diese Systeme führen im Allgemeinen nur kleine Operationen aus, die auf mehreren CPUs laufen können, weil sie von mehreren Verbindungen stammen. In dieser Umgebung kann die Nebenläufigkeit zu einem Flaschenhals werden. Die meisten Webanwendungen fallen in diese Kategorie. Server für die Online-Transaktionsverarbeitung (OLTP) setzen meist InnoDB ein, das bei vielen CPUs einige ungelöste Probleme mit der Nebenläufigkeit zeigt. Allerdings kann nicht nur InnoDB zu einem Flaschenhals werden. Jede freigegebene und gemeinsam genutzte Ressource ist ein potenzieller Konkurrenzpunkt. InnoDB bekommt eine Menge Aufmerksamkeit, weil es die gebräuchlichste Storage-Engine für hoch nebenläufige Umgebungen ist, allerdings ist MyISAM nicht besser, wenn Sie es wirklich ausreizen,
334 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
selbst wenn Sie überhaupt keine Daten ändern. Viele der Nebenläufigkeitsengstellen, wie etwa Sperren auf Zeilenebene bei InnoDB und Tabellen-Locks bei MyISAM, können intern nicht wegoptimiert werden – es gibt keine Lösung, als die Arbeit so schnell wie möglich zu verrichten, damit die Sperren den Objekten, die darauf warten, gewährt werden können. Es spielt keine Rolle, wie viele CPUs Sie haben, wenn eine einzige Sperre dafür sorgt, dass sie alle warten müssen. Daher profitieren sogar einige stark nebenläufige Lasten von schnelleren CPUs. Es gibt zwei Arten von Nebenläufigkeitsproblemen in Datenbanken, und Sie brauchen zwei unterschiedliche Ansätze, um sie zu lösen: Logische Nebenläufigkeitsprobleme Konkurrenz um Ressourcen, die für die Anwendung sichtbar sind, wie etwa Tabellen- oder Zeilensperren. Diese Probleme erfordern normalerweise Taktiken wie das Ändern Ihrer Anwendung, die Benutzung einer anderen Storage-Engine, das Ändern der Serverkonfiguration oder den Einsatz anderer Locking-Hinweise oder Transaktionsisolationsebenen. Interne Nebenläufigkeitsprobleme Konkurrenz um Ressourcen wie Semaphore, Zugriff auf Seiten im InnoDB-Pufferpool usw. Sie können versuchen, diese Probleme zu umgehen, indem Sie die Servereinstellungen ändern, das Betriebssystem ändern oder andere Hardware benutzen, müssen aber vielleicht dennoch mit ihnen leben. Manchmal kann eine andere Storage-Engine oder ein Patch helfen, diese Probleme zu lindern. Die Anzahl der CPUs, die MySQL tatsächlich benutzen kann, und wie es unter zunehmender Last skaliert – sein »Skalierungsmuster« – hängen sowohl von der Last als auch von der Systemarchitektur ab. Mit »Systemarchitektur« meinen wir das Betriebssystem und die Hardware, nicht die Anwendung, die MySQL benutzt. Das Skalierungsmuster von MySQL wird von der CPU-Architektur (RISC, CISC, Tiefe der Pipeline usw.), dem CPU-Modell und dem Betriebssystem beeinflusst. Deshalb sind Benchmark-Tests so wichtig: Manche Systeme funktionieren auch bei zunehmender Nebenläufigkeit gut, andere dagegen fallen stark ab. Manche Systeme liefern mit zunehmender Prozessorzahl sogar eine niedrigere Gesamtleistung ab. Das kommt ziemlich oft vor; wir wissen von vielen Leuten, die versucht haben, ihr Vier-Kern-System auf acht Prozessorkerne zu erweitern, nur um dann aufgrund der geringeren Leistung wieder zu vier Kernen zurückzukehren (oder den MySQLProzess an nur vier der acht Kerne zu binden). Sie müssen Benchmark-Tests durchführen und feststellen, wie Ihr System mit der Last zurechtkommt. Einige Engpässe in der Skalierbarkeit von MySQL liegen beim Server, während andere auf der Storage-Engine-Ebene zu finden sind. Es ist entscheidend, wie die Storage-Engine aufgebaut ist. Manchmal ist es möglich, zu einer anderen Storage-Engine zu wechseln und eine bessere Leistung aus mehreren CPUs herauszuholen.
Wie Sie CPUs für MySQL auswählen | 335
Die »Kriege« um die Prozessorgeschwindigkeit, die um die Jahrhundertwende herum tobten, haben bis zu einem gewissen Grad nachgelassen, die CPU-Hersteller konzentrieren sich nun stärker auf Multicore-CPUs und Variationen wie Multithreading. Die Zukunft des CPU-Designs sind möglicherweise ja wirklich Hunderte von Prozessorkernen; QuadCore-CPUs sind heutzutage schon üblich. Die internen Architekturen unterscheiden sich zwischen den einzelnen Herstellern so stark, dass es unmöglich ist, allgemeine Aussagen über die Interaktion zwischen Threads, CPUs und Kernen zu treffen. Auch die Gestaltung von Speicher und Bus ist wichtig. Es hängt also letztendlich immer von der Architektur ab, ob es besser ist, mehrere Kerne oder mehrere physische CPUs zu haben.
Speicher- und Festplattenressourcen abwägen Sie sollten nicht deshalb viel Speicher haben, um viele Daten im Speicher ablegen zu können, sondern um Festplatten-Ein-/Ausgaben zu vermeiden, die um Größenordnungen langsamer sind als Datenzugriffe im Speicher. Der Trick besteht darin, Speicher- und Festplattengröße, Geschwindigkeit, Kosten und andere Werte so gegeneinander abzuwägen, dass für Ihre Last eine gute Leistung herauskommt. Bevor wir uns das anschauen, wollen wir uns für einen Moment noch einmal den Grundlagen zuwenden. Computer enthalten eine Pyramide aus jeweils kleineren, schnelleren, teureren CacheSpeichern, wie in Abbildung 7-1 dargestellt wird.
Schneller Kleiner Teurer
CPURegister CPU-Cache(s) Hauptspeicher Festplatte
Abbildung 7-1: Die Cache-Hierarchie
Jede Ebene in dieser Cache-Hierarchie wird am besten dazu verwendet, »heiße« Daten zu speichern, damit schneller auf sie zugegriffen werden kann, wobei Heuristiken zum Einsatz kommen wie »kürzlich benutzte Daten werden wahrscheinlich bald wieder gesucht« und »Daten, die sich in der Nähe von kürzlich benutzten Daten befinden, werden wahrscheinlich bald wieder verwendet«. Diese Heuristiken funktionieren aufgrund der räumlichen und zeitlichen Lokalitätseigenschaft. Vom Standpunkt des Programmierers sind CPU-Register und Caches transparent und architekturspezifisch. Es ist Aufgabe von Compiler und CPU, sie zu verwalten. Allerdings
336 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
sind sich Programmierer des Unterschieds zwischen dem Hauptspeicher und der Festplatte durchaus bewusst, und die Programme behandeln sie normalerweise ganz verschieden.1 Das gilt vor allen Dingen für Datenbankserver, deren Verhalten oft den Vorhersagen widerspricht, die von den gerade erwähnten Heuristiken getroffen werden. Ein gut gestalteter Datenbank-Cache (wie etwa der InnoDB-Pufferpool) ist normalerweise effizienter als der Cache eines Betriebssystems, der sich eher für allgemeine Aufgaben eignet. Der Datenbank-Cache weiß viel besser über seine Datenanforderungen Bescheid und enthält seine spezielle Logik, die ihm hilft, diese Anforderungen zu erfüllen. Außerdem ist kein Systemaufruf erforderlich, um auf die Daten im Datenbank-Cache zuzugreifen. Diese speziellen Cache-Anforderungen sind der Grund dafür, dass Sie Ihre Cache-Hierarchie an die besonderen Zugriffsmuster eines Datenbankservers anpassen müssen. Da die Register und auf dem Chip befindlichen Caches nicht vom Benutzer konfiguriert werden können, sind der Speicher und die Festplatte die einzigen Dinge, die eine Änderung erlauben.
Zufällige oder sequenzielle Ein-/Ausgabe? Datenbankserver benutzen sowohl sequenzielle als auch zufällige Ein-/Ausgaben, wobei zufällige Ein-/Ausgaben am meisten von der Cache-Speicherung profitieren. Sie können sich davon überzeugen, indem Sie sich eine typische gemischte Last vorstellen, die sich zwischen Einzeilen-Lookups und Mehrzeilen-Scans bewegt. Das typische Muster sieht so aus, dass die »heißen« Daten zufällig verteilt sind; werden diese Daten im Cache vorgehalten, dann vermeidet man teure Suchvorgänge auf der Festplatte. Im Gegensatz dazu werden beim sequenziellen Lesen die Daten nur einmal durchlaufen, so dass es sinnlos ist, sie im Cache zu speichern, es sei denn, sie passen komplett in den Speicher. Ein weiterer Grund, weshalb sequenzielle Lesevorgänge nicht von einer Cache-Speicherung profitieren, besteht darin, dass sie schneller sind als zufällige Lesevorgänge. Das hat zwei Ursachen: Sequenzielle Ein-/Ausgaben sind schneller als zufällige Ein-/Ausgaben. Sequenzielle Operationen werden schneller ausgeführt als zufällige Operationen, und zwar sowohl im Speicher als auch auf der Festplatte. Nehmen Sie an, Ihre Festplatten können 100 zufällige Ein-/Ausgabe-Operationen pro Sekunde durchführen und 50 MByte pro Sekunde sequenziell lesen. (Das ist ungefähr das, was eine handelsübliche Festplatte heutzutage erreicht.) Bei 100-Byte-Zeilen ergeben sich 100 Zeilen pro Sekunde zufällig gegenüber 500.000 Zeilen pro Sekunde sequenziell – ein Unterschied von 5.000-mal oder mehreren Größenordnungen. Die zufällige Ein-/Ausgabe profitiert in diesem Szenario also stärker von der Cache-Speicherung.
1 Jedoch könnten sich Programme auf das Betriebssystem verlassen, um viele Daten im Speicher abzulegen, die sich konzeptuell »auf der Festplatte« befinden. MyISAM macht das beispielsweise .
Speicher- und Festplattenressourcen abwägen | 337
Auch der sequenzielle Zugriff auf im Speicher befindliche Zeilen ist schneller als der zufällige Zugriff auf solche Zeilen. Heutige Speicherchips können typischerweise auf etwa 250.000 100-Byte-Zeilen pro Sekunde auf zufällige Weise zugreifen, sequenziell erreichen sie 5 Millionen pro Sekunde. Beachten Sie, dass zufällige Zugriffe im Speicher fast 2.500-mal schneller sind als auf der Festplatte, während sequenzielle Zugriffe im Speicher nur zehnmal schneller sind als auf der Festplatte. Storage-Engines können sequenzielle Lesevorgänge schneller ausführen als zufällige Lesevorgänge. Ein zufälliger Lesevorgang bedeutet im Allgemeinen, dass die Storage-Engine Indexoperationen durchführen muss. (Es gibt Ausnahmen von dieser Regel, sie gilt aber für InnoDB und MyISAM.) Das erfordert normalerweise das Navigieren durch eine B-Baum-Datenstruktur und das Vergleichen von Werten mit anderen Werten. Im Gegensatz dazu erfordern sequenzielle Lesevorgänge meist das Durchlaufen einer einfacheren Datenstruktur, wie etwa einer verknüpften Liste. Das bedeutet viel weniger Arbeit, und deshalb sind sequenzielle Lesevorgänge schneller. Sie können sich Arbeit ersparen, indem Sie sequenzielle Lesevorgänge im Cache speichern. Noch mehr Arbeit ersparen Sie sich jedoch, indem Sie stattdessen zufällige Lesevorgänge im Cache ablegen. Mit anderen Worten: Die beste Lösung für Ein-/AusgabeProbleme beim zufälligen Lesen besteht darin, den Speicher zu erweitern, wenn Sie es sich leisten können.
Caching, Lese- und Schreibvorgänge Wenn Sie ausreichend Speicher haben, können Sie die Festplatte vollkommen von Leseanforderungen abschirmen. Falls alle Daten in den Speicher passen, ist jeder Lesevorgang ein Cache-Treffer, sobald die Caches des Servers aufgewärmt sind. Es gibt weiterhin logische Lesevorgänge, aber keine physischen Leseoperationen mehr. Beim Schreiben ist es allerdings etwas anders. Ein Schreibvorgang kann genau wie ein Lesevorgang im Speicher erledigt werden, aber früher oder später muss auf die Festplatte geschrieben werden, damit das Geschriebene Bestand hat. Mit anderen Worten: Ein Cache kann Schreiboperationen zwar verzögern, aber nicht ganz eliminieren. Neben der Verzögerung des Schreibens kann das Speichern im Cache es auch erlauben, dass Schreiboperationen auf zwei wichtige Arten zusammengefasst werden: Viele Schreiboperationen, eine Übertragung Ein einziges Stück Daten kann im Speicher oft geändert werden, ohne dass alle neuen Daten auf die Festplatte geschrieben werden. Wenn die Daten dann schließlich auf die Festplatte übertragen werden, sind alle Modifikationen, die seit dem letzten physischen Schreiben vorgenommen wurden, permanent. Beispielsweise könnten viele Anweisungen einen im Speicher befindlichen Zähler aktualisieren. Wenn der Zähler 100-mal inkrementiert und dann auf die Festplatte geschrieben wird, dann wurden 100 Modifikationen zu einem Schreiben zusammengefasst.
338 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Ein-/Ausgabe-Merging Viele unterschiedliche Daten können im Speicher geändert werden, und diese Modifikationen können dann gesammelt werden, damit das physische Schreiben als eine einzige Festplattenoperation durchgeführt wird. Aus diesem Grund benutzen viele transaktionsfähige Systeme eine Write-Ahead-LoggingStrategie. Write-Ahead-Logging erlaubt es ihnen, Änderungen an den Seiten im Speicher vorzunehmen, ohne dass die Änderungen auf die Festplatte übertragen werden, was normalerweise zufällige Ein-/Ausgaben umfassen würde und damit langsam wäre. Stattdessen wird ein Eintrag über die Änderungen in eine sequenzielle Log-Datei geschrieben. Das geht viel schneller. Ein Hintergrund-Thread kann die modifizierten Seiten später auf die Festplatte übertragen, dabei kann er die Schreiboperationen gleich optimieren. Schreiboperationen profitieren ausgesprochen gut von der Pufferung, weil diese zufällige Ein-/Ausgaben in sequenzielle Ein-/Ausgaben konvertiert. Asynchrone (gepufferte) Schreiboperationen werden typischerweise vom Betriebssystem erledigt und als Stapel verarbeitet, damit sie auf optimale Weise auf die Festplatte gelangen können. Synchrone (nichtgepufferte) Schreiboperationen müssen auf die Festplatte geschrieben werden, bevor sie fertig sind. Deswegen profitieren sie von einer Pufferung in einem batteriegesicherten Write-Back-Cache eines RAID-Controllers (wir kommen später auf RAID zurück).
Wie sieht Ihr Arbeitssatz aus? Jede Anwendung besitzt einen »Arbeitssatz« an Daten – das sind die Daten, die sie tatsächlich benötigt, um ihre Arbeit zu verrichten. Viele Datenbanken verfügen außerdem über eine Menge Daten, die nicht im Arbeitssatz liegen. Sie können sich die Datenbank als einen Schreibtisch mit vielen Schubladen vorstellen. Der Arbeitssatz besteht aus den Papieren, die auf dem Schreibtisch liegen müssen, damit Sie arbeiten können. In dieser Analogie ist der Schreibtisch der Hauptspeicher, während die Schubladen die Festplatten darstellen. Genau wie Sie nicht jedes Stück Papier auf dem Schreibtisch haben müssen, um arbeiten zu können, muss für eine optimale Performance auch nicht die ganze Datenbank in den Speicher passen – der Arbeitssatz reicht. Die Größe des Arbeitssatzes hängt stark von der Anwendung ab. Bei manchen Anwendungen kann die Größe des Arbeitssatzes 1 % der gesamten Datengröße betragen, während es bei anderen Anwendungen fast 100 % sein müssen. Wenn der Arbeitssatz nicht in den Speicher passt, muss der Datenbankserver Daten zwischen der Festplatte und dem Speicher hin- und herschieben, um seine Aufgaben zu verrichten. Aus diesem Grund könnte es so aussehen, als gäbe es ein Problem mit der Ein-/Ausgabe, obwohl eigentlich der Speicher zu klein ist. Manchmal gibt es keine Möglichkeit, den gesamten Arbeitssatz in den Speicher zu bekommen, und manchmal ist das auch gar nicht gewünscht (etwa wenn Ihre Anwendung eine Menge sequenzielle Ein-/Ausgaben braucht). Die Architektur Ihrer Anwendung kann sich stark ändern, je nachdem, ob Sie den Arbeitssatz im Speicher unterbringen können oder nicht. Speicher- und Festplattenressourcen abwägen | 339
Letztendlich ist »Arbeitssatz« ein mehrdeutiger Begriff. So müssen Sie z.B. möglicherweise pro Stunde nur auf 1 % Ihrer Daten zugreifen, im Laufe eines Zeitraums von 24 Stunden würde das allerdings 20 % Ihrer Daten bedeuten. Wie sieht in dieser Lage der Arbeitssatz aus? Vielleicht hilft es, wenn Sie beim Arbeitssatz eher daran denken, wie viele Daten im Cache vorliegen müssen, damit Ihre Last hauptsächlich CPU-gebunden ist. Wenn Sie nicht genügend Daten im Cache speichern können, passt Ihr Arbeitssatz nicht in den Speicher.
Der Arbeitssatz und die Cache-Einheit Der Arbeitssatz enthält sowohl Daten als auch Indizes, und Sie sollten ihn in Cache-Einheiten zählen. Eine Cache-Einheit ist die kleinste Dateneinheit, mit der Ihre StorageEngine arbeitet. Die Größe einer Cache-Einheit und damit auch die Größe des Arbeitssatzes hängt von der Storage-Engine ab. Zum Beispiel arbeitet InnoDB immer in Seiten von 16 KByte Größe. Falls Sie einen Einzeilen-Lookup vornehmen und InnoDB dafür auf die Festplatte zugreifen muss, liest es die gesamte Seite, die diese Zeile enthält, in den Pufferpool und legt sie dort im Cache ab. Das ist eine ziemliche Verschwendung. Nehmen Sie an, Sie haben 100-Byte-Zeilen, auf die Sie zufällig zugreifen. InnoDB benutzt im Pufferpool eine Menge zusätzlichen Speicher für diese Zeilen, weil es für jede Zeile eine vollständige 16 KByte-Seite lesen und im Cache speichern muss. Da der Arbeitssatz auch die Indizes enthält, liest und speichert InnoDB auch die Teile des Indexbaums, die es benötigt hat, um die Zeilen zu finden. Die Indexseiten von InnoDB sind ebenfalls 16 KByte groß, so dass es insgesamt 32 KByte (oder mehr – je nach Tiefe des Indexbaums) speichern muss, um auf eine einzige 100-Byte-Zeile zuzugreifen. Die Cache-Einheit ist deshalb ein weiterer Grund dafür, weshalb gut gewählte Cluster-Indizes in InnoDB so wichtig sind. ClusterIndizes ermöglichen es Ihnen nicht nur, die Festplattenzugriffe zu optimieren, sondern helfen Ihnen auch dabei, verwandte Daten auf den gleichen Seiten zu halten, so dass Sie einen größeren Teil Ihres Arbeitssatzes im Cache unterbringen können. Im Gegensatz dazu ist die Cache-Einheit der Falcon-Storage-Engine eine Zeile und nicht eine Seite. Falcon ist daher effizienter bei der Cache-Speicherung von kleinen, weit verteilten Zeilen, auf die in zufälliger Weise zugegriffen wurde. Um die Schreibtischanalogie noch einmal zu bemühen: InnoDB verlangt von Ihnen, einen ganzen Aktenordner (Datenbankseite) aus der Schublade zu ziehen, wenn Sie eigentlich nur eines der Blätter aus diesem Ordner benötigen. Ohne Cluster-Indizes (oder mit schlecht gewählten Cluster-Indizes) wäre das wirklich sehr ineffizient. Falcon erlaubt Ihnen andererseits, ein beliebiges Blatt Papier aus einem Aktenordner zu ziehen, ohne dass Sie den gesamten Ordner bewegen müssen. Beide Ansätze haben ihre Vor- und Nachteile. Zum Beispiel bewahrt InnoDB die gesamte 16-KByte-Seite im Speicher auf. Falls Sie also in Zukunft auf eine andere Zeile derselben Seite zugreifen müssen, ist sie bereits da. Falcon besitzt sowohl einen Zeilen-Cache als auch einen Seiten-Cache, wodurch sich eine Kombination aus Vorteilen ergibt: Der Sei-
340 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
ten-Cache reduziert die Festplattenzugriffe, während der Zeilen-Cache den Speicher effizient ausnutzt. Dennoch stellen doppelte Caches eine Verschwendung dar, weil sie dafür sorgen, dass manche Daten doppelt im Speicher abgelegt werden. Man bezeichnet das als Doppelpufferung. Theoretisch können beide Strategien für bestimmte Lasten effizienter sein, für andere dagegen weniger effizient. Wie gehabt, hängt Ihre Wahl davon ab, was Ihre StorageEngine tatsächlich für Sie leisten soll.
Ein effektives Speicher-zu-Festplatte-Verhältnis finden Ein gutes Verhältnis von Speicher zu Festplatte ermittelt man am besten durch Versuche und/oder Benchmark-Tests. Wenn Sie alles in den Speicher bekommen, sind Sie fertig – Sie müssen nicht länger darüber nachdenken. Meist ist das aber nicht möglich, so dass Sie mit einer Teilmenge Ihrer Daten einen Benchmark durchführen und herausfinden müssen, was passiert. Im Prinzip streben Sie nach einer akzeptablen Cache-Miss-Rate. Ein Cache-Miss tritt auf, wenn Ihre Abfragen Daten anfordern, die nicht im Hauptspeicher vorliegen und daher durch den Server von der Festplatte geholt werden müssen. Die Cache-Miss-Rate bestimmt in der Tat, wie stark Ihre CPU belastet wird. Um also die Cache-Miss-Rate abzuschätzen, schauen Sie sich am besten die CPU-Auslastung an. Falls z.B. Ihre CPU 90 % der Zeit benutzt wird und 10 % der Zeit auf Ein-/Ausgaben wartet, dann ist Ihre Cache-Miss-Rate gut. Denken wir einmal darüber nach, wie Ihr Arbeitssatz Ihre Cache-Miss-Rate beeinflusst. Sie müssen erkennen, dass Ihr Arbeitssatz nicht einfach nur eine Zahl ist: Tatsächlich handelt es sich um eine statistische Verteilung, die hinsichtlich der Verteilung nichtlinear ist. Falls Sie z.B. 10 GByte Speicher haben und eine Cache-Miss-Rate von 10 % erreichen, könnten Sie der Meinung sein, dass Sie einfach nur 11 % mehr Speicher2 hinzufügen müssten, um die Cache-Miss-Rate auf null zu drücken. Tatsächlich führen Ineffizienzen wie die Größe der Cache-Einheit dazu, dass Sie theoretisch 50 GByte Speicher benötigen, um eine Miss-Rate von 1 % zu erreichen. Und selbst bei einer perfekt passenden CacheEinheit kann die theoretische Vorhersage falsch sein: Faktoren wie Datenzugriffsmuster können die Dinge noch komplizierter machen. Eine Cache-Miss-Rate von 1 % kann unter Umständen sogar 500 GByte Speicher erfordern! Man ist leicht versucht, etwas zu optimieren, was einem vielleicht gar keinen so großen Vorteil liefert. Eine Miss-Rate von 10 % könnte z.B. bereits eine CPU-Auslastung von 80 % zur Folge haben, was ziemlich gut ist. Nehmen Sie an, Sie fügen Speicher hinzu und können die Cache-Miss-Rate auf 5 % drücken. Grob vereinfacht würden Sie ungefähr 6 % weitere Daten an die CPUs liefern. Noch einmal grob vereinfacht könnten wir sagen, dass Sie Ihre CPU-Auslastung auf 84,8 % gesteigert haben. Das ist jedoch kein allzu großer Gewinn, wenn Sie bedenken, wie viel Speicher Sie kaufen mussten, um dieses Ergeb2 Die richtige Zahl lautet 11 %, nicht 10 %. Eine Miss-Rate von 10 % entspricht einer Trefferrate von 90 %, Sie müssen also 10 GByte durch 90 % teilen, was 11,111 GByte ergibt.
Speicher- und Festplattenressourcen abwägen | 341
nis zu erzielen. Und aufgrund der Unterschiede zwischen der Geschwindigkeit des Speichers und der Festplattenzugriffe, der ungeklärten Frage, was die CPU wirklich mit den Daten anstellt, sowie vieler anderer Faktoren führt die Verringerung der Cache-MissRate um 5 % in Wirklichkeit vermutlich nicht einmal zu einer besonders großen Steigerung der CPU-Auslastung. Aus diesem Grund haben wir gesagt, dass Sie nach einer akzeptablen Cache-Miss-Rate streben sollten, nicht nach einer Cache-Miss-Rate von null. Sie sollten sich keine konkrete Zahl zum Ziel setzen, da »akzeptabel« immer von Ihrer Anwendung und Ihrer Last abhängt. Manche Anwendungen laufen mit einer Cache-Miss-Rate von 1 % ganz gut, während andere 0,01 % brauchen, um gut zu funktionieren. (Eine »gute Cache-MissRate« ist wie ein »Arbeitssatz« ein recht unscharfes Konzept, das noch dadurch verkompliziert wird, dass es viele Methoden gibt, um die Miss-Rate zu ermitteln.) Das beste Speicher-zu-Festplatte-Verhältnis hängt auch von anderen Komponenten in Ihrem System ab. Nehmen Sie an, Sie haben ein System mit 16 GByte Speicher, 20 GByte Daten und Unmengen an unbenutztem Festplattenplatz. Das System arbeitet schön bei 80 % CPU-Auslastung. Um doppelt so viele Daten in das System zu bringen und dabei das gleiche Leistungsniveau zu halten, könnten Sie versuchen, die Anzahl der CPUs und die Menge des Speichers einfach zu verdoppeln. Doch sogar wenn jede Komponente in dem System sich perfekt an die erhöhte Last anpassen würde (eine völlig unrealistische Annahme), würde das vermutlich nicht funktionieren. Das System mit 20 GByte Daten benutzt wahrscheinlich bereits mehr als 50 % Kapazität einiger Komponenten – so könnte es z.B. bereits 80 % der maximal möglichen Anzahl an Ein-/Ausgabe-Operationen pro Sekunde ausführen. Es wäre nicht in der Lage, eine doppelt so hohe Last zu bewältigen. Das beste Speicher-zu-Festplatte-Verhältnis hängt daher von der schwächsten Komponente des Systems ab.
Die Wahl der Festplatten Falls Sie nicht genügend Daten in den Speicher bekommen – falls Sie z.B. schätzen, dass Sie 500 GByte Speicher brauchen würden, um Ihre CPUs mit dem aktuellen Ein-/Ausgabe-System vollständig zu laden –, sollten Sie ein leistungsstärkeres Ein-/Ausgabe-Subsystem in Betracht ziehen, und das vielleicht sogar auf Kosten des Speichers. Und gestalten Sie Ihre Anwendung so, dass sie mit Ein-/Ausgabe-Wartezuständen zurechtkommt. Das scheint jetzt nicht sehr logisch zu sein. Schließlich haben wir doch gerade gesagt, dass mehr Speicher Ihr Ein-/Ausgabe-Subsystem entlasten und Ein-/Ausgabe-Warteperioden verringern kann. Weshalb sollten Sie jetzt das Ein-/Ausgabe-Subsystem aufmotzen, wenn die Zugabe von Speicher das Problem lösen könnte? Die Antwort liegt im Gleichgewicht der beteiligten Faktoren, etwa dem Verhältnis von Lese- zu Schreibvorgängen, der Größe der einzelnen Ein-/Ausgabe-Operationen und der Anzahl dieser Operationen pro Sekunde. Falls Sie z.B. sehr schnell in das Log schreiben müssen, können Sie die Festplatte vor solchen Schreiboperationen nicht abschirmen, indem Sie die Menge des ver-
342 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
fügbaren Speichers erhöhen. In diesem Fall wäre es möglicherweise besser, in ein leistungsstarkes Ein-/Ausgabe-System mit einem batteriegesicherten Schreib-Cache zu investieren. Sie erinnern sich sicher, dass das Lesen der Daten von einer herkömmlichen Festplatte ein dreiteiliger Vorgang ist: 1. Der Lesekopf wird an die richtige Stelle auf der Oberfläche der Festplatte bewegt. 2. Es wird gewartet, dass die Festplatte sich dreht, damit die gewünschten Daten unter
den Lesekopf gelangen. 3. Es wird gewartet, dass die Festplatte sich dreht, damit die gewünschten Daten am
Lesekopf vorbeiziehen. Wie schnell die Festplatte diese Operationen ausführen kann, lässt sich anhand zweier Werte bestimmen: Zugriffszeit (die Schritte 1 und 2 kombiniert) und Übertragungsgeschwindigkeit. Diese beiden Werte legen auch die Latenz und den Durchsatz fest. Ob Sie schnelle Zugriffszeiten oder schnelle Übertragungsgeschwindigkeiten – oder eine Mischung aus beiden – brauchen, hängt von der Art der Abfragen ab, die Sie ausführen. Was die Gesamtzeit betrifft, die erforderlich ist, um einen Lesevorgang abzuschließen, so werden kleine zufällige Lookups von den Schritten 1 und 2 dominiert, während große sequenzielle Leseoperationen von Schritt 3 bestimmt werden. Die Wahl der Festplatten kann von verschiedenen weiteren Faktoren beeinflusst werden, die wiederum von Ihrer Anwendung abhängen. Stellen wir uns vor, Sie suchen Festplatten für eine Online-Anwendung wie eine beliebte News-Site, bei der viele kleine, zufällige Leseoperationen stattfinden. Sie sollten die folgenden Faktoren in Betracht ziehen: Speicherkapazität Diese stellt bei Online-Anwendungen kaum noch ein Problem dar, da die heutigen Festplatten normalerweise groß genug sind. Falls sie es nicht sind, werden üblicherweise kleinere Festplatten mit RAID kombiniert.3 Übertragungsgeschwindigkeit Wie wir bereits gesehen haben, können moderne Festplatten Daten normalerweise sehr schnell übertragen. Wie schnell genau, hängt hauptsächlich von der Drehgeschwindigkeit ab sowie davon, wie dicht die Daten auf der Oberfläche der Festplatte gepackt sind. Dazu kommen dann noch die Beschränkungen der Schnittstelle mit dem Hostsystem (viele moderne Festplatten können Daten schneller lesen, als die Schnittstelle sie übertragen kann). Ungeachtet dessen bildet die Übertragungsgeschwindigkeit für Online-Anwendungen meist keinen begrenzenden Faktor, da diese im Allgemeinen viele kleine, zufällige Zugriffe ausführen.
3 Interessanterweise kaufen manche Leute ganz bewusst größere Festplatten und nutzen dann nur 20–30 % ihrer Kapazität. Dadurch erhöht sich die Datenlokalität und verringert sich die Suchzeit, was manchmal den höheren Preis rechtfertigen kann.
Speicher- und Festplattenressourcen abwägen | 343
Zugriffszeit Dies ist normalerweise der dominierende Faktor dafür, wie schnell Ihre zufälligen Lookups ausgeführt werden. Sie sollten also versuchen, eine schnelle Zugriffszeit zu erreichen. Drehgeschwindigkeit Gebräuchliche Drehgeschwindigkeiten sind heutzutage 7.200 Umdrehungen pro Minute (Rounds per Minute; RPM), 10.000 RPM und 15.000 RPM. Die Drehgeschwindigkeit trägt ziemlich zur Geschwindigkeit von zufälligen Lookups und von sequenziellen Scans bei. Physische Größe Wenn alle anderen Dinge gleich sind, ist die Größe der Festplatte entscheidend: Je kleiner die Festplatte ist, umso weniger Zeit ist erforderlich, um den Lesekopf zu bewegen. 2,5-Zoll-Serverfestplatten sind oft schneller als ihre größeren Vettern. Sie brauchen außerdem weniger Strom, und es passen meist mehr von ihnen in ein Gehäuse. Die Festplattentechnik ändert sich häufig, so dass diese Tipps vielleicht bald nicht mehr gelten. So sind z.B. momentan Solid-State-Drives ein hochaktuelles Thema. Sie arbeiten ganz anders als drehbar gelagerte Festplatten. Allerdings sind sie noch recht teuer und noch nicht weit verbreitet. Wir kennen Projekte, die sie erfolgreich einsetzen, haben aber noch nicht genügend Erfahrungen gesammelt, um spezielle Ratschläge zu diesen Festplatten zu geben. Wie MySQL sich an mehrere Festplatten anpasst, hängt wie bei CPUs von der StorageEngine und der Last ab. InnoDB skaliert typischerweise ganz gut an 10 bis 20 Festplatten. Die Tabellen-Locks von MyISAM beschränken allerdings seine Skalierbarkeit beim Schreiben, so dass eine stark schreibbetonte Last unter MyISAM wahrscheinlich kaum von einem Vorhandensein vieler Festplatten profitiert. Betriebssystempuffer und parallele Schreiboperationen im Hintergrund helfen ein wenig, aber dennoch ist die Schreibskalierbarkeit von MyISAM von Natur aus stärker eingeschränkt als die von InnoDB. Auch bei Festplatten gilt wie bei den CPUs, dass mehr nicht unbedingt besser ist. Manche Anwendungen, die eine niedrige Latenz verlangen, benötigen schnellere Laufwerke und nicht mehr davon. So funktioniert z.B. die Replikation normalerweise besser mit schnelleren Laufwerken, da Updates auf einem Slave in einem einzigen Thread ausgeführt werden. Um festzustellen, ob Ihre Last davon profitiert, dass Sie mehr Laufwerke einsetzen, schauen Sie sich die Last auf den Laufwerken mit iostat an. Eine große Anzahl von ausstehenden Anforderungen bedeutet, dass Ihre Last effizient auf mehr Laufwerke verteilt werden könnte. Am Ende dieses Kapitels finden Sie einige iostatBeispiele.
344 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Hardware für einen Slave wählen Die Wahl der Hardware für einen Replikations-Slave erfolgt im Allgemeinen ähnlich wie die Wahl der Hardware für einen Master, obwohl es auch einige Unterschiede gibt. Falls Sie vorhaben, zur Ausfallsicherung einen Replikations-Slave einzusetzen, muss dieser normalerweise so leistungsstark sein wie der Master. Und ungeachtet der Frage, ob der Slave als Reserve bereitsteht, um den Master zu ersetzen, muss er genügend Leistung besitzen, um alle Schreiboperationen auszuführen, die auf dem Master auftreten, wobei das zusätzliche Handicap auftritt, dass er sie seriell ausführen muss. (Mehr dazu erfahren Sie im nächsten Kapitel.) Die wichtigste Überlegung für die Hardware eines Slaves sind die Kosten: Müssen Sie für die Hardware des Slaves genauso viel ausgeben wie für den Master? Können Sie den Slave anders konfigurieren, so dass Sie mehr Leistung aus ihm herausholen? Es kommt drauf an. Wenn der Slave als Reserve dient, sollen Master und Slave wahrscheinlich die gleiche Hardware und Konfiguration haben. Falls Sie allerdings die Replikation lediglich als billige Methode nutzen, um die Gesamtlesekapazität des Systems zu steigern, können Sie auf dem Slave verschiedene Kürzungen vornehmen. Sie könnten auf dem Slave z.B. eine andere Storage-Engine einsetzen. Manche Leute nehmen auch billigere Hardware oder benutzen RAID (Redundant Arrays of Inexpensive Disks) 0 anstelle von RAID 5 oder RAID 10. Sie können auch einige der Konsistenz- und Lebensdauergarantien deaktivieren, damit der Slave weniger zu tun hat. Mehr dazu finden Sie in »Das Ein-/Ausgabeverhalten von MySQL anpassen« auf Seite 304. Diese Werte können in einem großen Maßstab kosteneffizient sein, machen allerdings die Dinge in einem kleinen Maßstab möglicherweise komplexer.
RAID-Leistungsoptimierung Storage-Engines bewahren ihre Daten und/oder Indizes oft in einzelnen großen Dateien auf, was bedeutet, dass normalerweise RAID die plausibelste Lösung für die Speicherung großer Datenmengen darstellt.4 RAID kann bei Redundanz, Speichergröße, Caching und Geschwindigkeit nützen. Aber genau wie bei den anderen Optimierungen, die wir uns angeschaut haben, gibt es auch bei den RAID-Konfigurationen viele Variationen, so dass Sie darauf achten müssen, dass Sie tatsächlich eine wählen, die Ihren Anforderungen genügt.
4 Partitionierung (siehe Kapitel 5) ist eine weitere gute Praxis, da dabei die Datei normalerweise in viele Dateien aufgeteilt wird, die Sie auf unterschiedliche Geräte legen können. Verglichen jedoch selbst mit der Partitionierung bildet RAID eine einfache Lösung für sehr große Datenmengen. Es verlangt von Ihnen nicht, die Last manuell auszugleichen oder einzugreifen, wenn sich die Lastverteilung ändert, und bietet Ihnen Redundanz, die Sie nicht bekommen, wenn Sie Partitionen unterschiedlichen Festplatten zuweisen.
RAID-Leistungsoptimierung | 345
Wir werden hier nicht jedes RAID-Level behandeln oder uns detailliert damit befassen, wie die unterschiedlichen RAID-Level Daten speichern. Gutes Material zu diesem Thema finden Sie in Büchern sowie online.5 Stattdessen konzentrieren wir uns darauf, wie die RAID-Konfigurationen die Anforderungen eines Datenbankservers erfüllen. Die wichtigsten RAID-Level sind: RAID 0 RAID 0 ist die billigste und leistungsstärkste RAID-Konfiguration, zumindest, wenn Sie ganz simpel Kosten und Leistung messen. (Falls Sie z.B. die Datenwiederherstellung einbeziehen, sieht es schon nicht mehr ganz so billig aus.) Da es keine Redundanz bietet, empfehlen wir RAID 0 nur für Server, um die Sie sich keine Sorgen machen, wie etwa Slaves oder Server, die aus irgendwelchen Gründen »entbehrlich« sind. Das typische Szenario ist ein Slave-Server, der leicht aus einem anderen Slave geklont werden kann. Beachten Sie noch einmal, dass RAID 0 keine Redundanz bietet, auch wenn der erste Buchstabe in dem Akronym RAID »redundant« bedeutet. Um genau zu sein, ist die Wahrscheinlichkeit, dass ein RAID 0-Array ausfällt, sogar noch höher als die, dass eine einzelne Festplatte ausfällt, und nicht etwa niedriger! RAID 1 RAID 1 bietet für viele Szenarien eine gute Leseleistung und dupliziert Ihre Daten auf die verschiedenen Festplatten. Eine gute Redundanz ist also gegeben. RAID 1 ist beim Lesen ein wenig schneller als RAID 0. Es eignet sich gut für Server, die Logging und ähnliche Aufgaben verrichten, da sequenzielle Schreiboperationen es selten erfordern, dass viele zugrunde liegenden Festplatten zusammenarbeiten (im Gegensatz zu zufälligen Schreiboperationen, die von einer Parallelisierung profitieren können). Es handelt sich um eine typische Wahl für einfache Server, die Redundanz brauchen, aber nur über zwei Festplatten verfügen. RAID 0 und RAID 1 sind sehr einfach und können oft ganz gut in Software implementiert werden. Die meisten Betriebssysteme erlauben es Ihnen, ganz einfach Software-RAID 0- und -RAID 1-Volumes anzulegen. RAID 5 RAID 5 ist ein bisschen furchteinflößend, stellt aber aufgrund von Preisbeschränkungen und/oder der Beschränkungen hinsichtlich der Anzahl der Festplatten, die physisch in den Server passen, die zwingende Wahl für manche Anwendungen dar. Es verteilt die Daten über viele Festplatten zusammen mit sogenannten Paritätsblöcken. Falls eine Festplatte ausfällt, können die Daten aus den Paritätsblöcken neu erstellt werden. In Bezug auf die Kosten pro Speichereinheit ist dies die ökonomischste redundante Konfiguration, weil Sie nur den Wert des Speicherplatzes einer Festplatte über das gesamte Array verlieren.
5 Zwei gute Ressourcen sind der Wikipedia-Artikel zu RAID (http://de.wikipedia.org/wiki/RAID) sowie das AC&NC-Tutorial unter http://www.acnc.com/04_00.html.
346 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Zufällige Schreibvorgänge sind teuer in RAID 5, weil sie zwei Schreiboperationen und zwei RAID-Operationen für die Paritätsblöcke erfordern. Schreiboperationen können ein wenig besser ausgeführt werden, wenn sie sequenziell sind oder wenn es viele physische Festplatten gibt. Andererseits werden sowohl zufällige als auch sequenzielle Leseoperationen anständig ausgeführt. RAID 5 bildet eine akzeptable Wahl für Datenvolumes oder für Daten und Logs bei vielen Lasten. Am teuersten wird es bei RAID 5, wenn eine Festplatte ausfällt, weil zur Rekonstruktion der Daten alle anderen Festplatten gelesen werden müssen. Das beeinflusst die Performance recht stark. Falls Sie versuchen, den Server während der Neuerstellung in Betrieb zu halten, dann erwarten Sie nicht, dass die Rekonstruktion oder das Array eine besonders gute Leistung abliefern. Darüber hinaus ist die Skalierbarkeit aufgrund der Paritätsblöcke nicht unbedingt gut – jenseits von 10 Festplatten skaliert RAID 5 nicht sehr gut. Außerdem kann es Probleme mit der Cache-Speicherung geben. Eine gute RAID 5-Leistung hängt stark vom Cache des RAID-Controllers ab, der mit den Anforderungen des Datenbankservers in Konflikt geraten kann. Wir besprechen die Cache-Speicherung später. Einer der Faktoren, die für RAID 5 sprechen, ist die Tatsache, dass es so beliebt ist. Daraus folgt, dass RAID-Controller oft besonders für RAID 5 optimiert sind. Trotz der theoretisch vorhandenen Beschränkungen können schlaue Controller, die ihre Caches gut einsetzen, bei bestimmten Lasten manchmal fast so gut funktionieren wie RAID 10-Controller. Das kann zum Teil daran liegen, dass RAID 10-Controller weniger gut optimiert sind. Ungeachtet der Gründe ist es zumindest das, was wir gesehen haben. RAID 10 Wenn Sie es sich leisten können, ist RAID 10 eine sehr gute Wahl für die Datenspeicherung. Es besteht aus gespiegelten Paaren mit »Striping«, so dass sowohl Lese- als auch Schreiboperationen gut ausgeführt werden. Es ist schnell und im Vergleich zu RAID 5 leicht wieder zu rekonstruieren. Außerdem kann es ganz gut in Software implementiert werden. Der Leistungsverlust beim Ausfall einer Festplatte kann dennoch beträchtlich sein, weil dieser Streifen (Stripe) dann möglicherweise einen Flaschenhals bildet. Die Performance kann je nach Last um bis zu 50 % abfallen. Achten Sie auf RAID-Controller, die eine »verkettete Spiegel«-Implementierung für RAID 10 benutzen. Diese ist wegen des fehlenden Stripings suboptimal: Diejenigen Ihrer Daten, auf die am häufigsten zugegriffen wird, könnten nur auf ein Spindelpaar platziert werden, anstatt über viele verteilt zu werden, so dass Sie eine schwache Leistung erzielen. RAID 50 RAID 50 besteht aus RAID 5-Arrays mit Striping und kann einen guten Kompromiss aus der Wirtschaftlichkeit von RAID 5 und der Performance von RAID 10 darstellen, falls Sie viele Festplatten haben. Es eignet sich hauptsächlich für sehr große Datenmengen, wie etwa Data-Warehouses oder außerordentlich große OLTP-Systeme.
RAID-Leistungsoptimierung | 347
Tabelle 7-1 fasst die verschiedenen RAID-Konfigurationen zusammen. Tabelle 7-1: Vergleich der RAID-Level Level
Zusammenfassung
Redundanz
Festplatten erforderlich
Schnelleres Lesen
Schnelleres Schreiben
RAID 0
Billig, schnell, gefährlich
Nein
N
Ja
Ja
RAID 1
Schnelles Lesen, einfach, sicher
Ja
2 (normalerweise)
Ja
Nein
RAID 5
Ein Kompromiss aus Sicherheit, Geschwindigkeit und Kosten
Ja
N+1
Ja
Kommt drauf an
RAID 10
Teuer, schnell, sicher
Ja
2N
Ja
Ja
RAID 50
Für sehr große Datenspeicher
Ja
2(N + 1)
Ja
Ja
RAID-Ausfall, -Wiederherstellung und -Überwachung RAID-Konfigurationen bieten (mit Ausnahme von RAID 0) Redundanz. Das ist wichtig, allerdings ist es einfach, die Wahrscheinlichkeit gleichzeitiger Festplattenausfälle zu unterschätzen. Halten Sie RAID nicht für eine starke Garantie für Datensicherheit. RAID lässt den Bedarf an Backups nicht verschwinden – ja, es verringert ihn nicht einmal. Wenn ein Problem auftritt, hängt die Wiederherstellungszeit von Ihrem Controller, dem RAID-Level, der Größe des Arrays und der Festplattengeschwindigkeit ab sowie von der Frage, ob der Server während des Neuaufbaus des Arrays online bleiben muss. Es besteht die Möglichkeit, dass mehrere Festplatten gleichzeitig ausfallen. So könnte z.B. eine Spannungsspitze oder eine Überhitzung zwei oder mehr Festplatten zerstören. Häufiger kommt es jedoch vor, dass zwei Festplatten kurz nacheinander ausfallen. Oft bleiben solche Probleme unbemerkt. Ein gebräuchlicher Fall ist eine Beschädigung des physischen Mediums, das Daten enthält, auf die selten zugegriffen wird. Dies könnte monatelang unentdeckt bleiben, bis Sie entweder versuchen, die Daten zu lesen, oder eine andere Festplatte ausfällt und der RAID-Controller versucht, die beschädigten Daten zu benutzen, um das Array wiederherzustellen. Je größer die Festplatte ist, umso wahrscheinlicher ist das. Aus diesem Grund ist es so wichtig, Ihre RAID-Arrays zu überwachen. Die meisten Controller bieten Software, die Berichte über den Zustand des Arrays liefert. Sie müssen diese Berichte verfolgen, da Sie ansonsten den Ausfall einer Festplatte überhaupt nicht bemerken. Sie würden die Möglichkeit verpassen, die Daten wiederherzustellen, und entdecken das Problem erst dann, wenn eine zweite Festplatte ausfällt und es zu spät ist. Sie können dieses Risiko verkleinern, indem Sie Ihre Arrays regelmäßig auf Konsistenz überprüfen. Background Patrol Read, eine Funktion mancher Controller, die auf beschädigte Medien prüft und diese repariert, während die Laufwerke in Betrieb sind, kann ebenfalls dazu beitragen, solche Probleme abzuwenden. Sehr große Arrays lassen sich meist nur langsam überprüfen; planen Sie also entsprechend, wenn Sie solche großen Arrays anlegen.
348 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Sie können auch ein Hot-Spare-Laufwerk hinzufügen, das unbenutzt ist und als Reserve für den Controller dient, der dieses Laufwerk automatisch für die Wiederherstellung einsetzt. Das bietet sich an, wenn Sie von jedem Server abhängen. Für Server, die nur wenige Festplatten besitzen, stellt dies eine teure Lösung dar, da die Kosten für eine untätige Festplatte proportional gesehen höher sind; falls Sie jedoch viele Festplatten haben, ist es fast idiotisch, kein Hot-Spare-Laufwerk zu haben. Denken Sie daran, dass die Wahrscheinlichkeit für einen Festplattenausfall mit zunehmender Plattenanzahl rapide ansteigt.
Abwägen zwischen Hardware-RAID und Software-RAID Die Interaktion zwischen dem Betriebssystem, dem Dateisystem und der Anzahl der Festplatten, die das Betriebssystem sieht, kann kompliziert sein. Bugs oder Beschränkungen – oder einfach nur Fehlkonfigurationen – können die Leistung deutlich unter das drücken, was theoretisch möglich ist. Wenn Sie 10 Festplatten haben, sollten diese idealerweise in der Lage sein, 10 Anforderungen parallel abzuarbeiten. Manchmal jedoch serialisiert das Dateisystem, das Betriebssystem oder der RAID-Controller die Anforderungen. Eine mögliche Lösung für dieses Problem besteht darin, unterschiedliche RAID-Konfigurationen auszuprobieren. Falls Sie z.B. 10 Festplatten haben und aus Gründen der Redundanz und der Leistung eine Spiegelung vornehmen wollen, gibt es mehrere Möglichkeiten, die Festplatten zu konfigurieren: • Konfigurieren Sie ein einziges RAID 10-Volume, das aus fünf gespiegelten Paaren besteht. Das Betriebssystem sieht ein einziges großes Festplattenvolume, und der RAID-Controller verbirgt die 10 zugrunde liegenden Festplatten. • Konfigurieren Sie fünf gespiegelte RAID 1-Paare im RAID-Controller, und erlauben Sie es dem Betriebssystem, fünf Volumes zu adressieren und nicht nur eines. • Konfigurieren Sie fünf gespiegelte RAID 1-Paare im RAID-Controller, und setzen Sie dann ein Software-RAID 0 ein, um die fünf Volumes als ein logisches Volume erscheinen zu lassen. Im Prinzip implementieren Sie damit RAID 10 teilweise in Hardware und teilweise in Software. Welche Version ist am besten? Das hängt davon ab, wie die Komponenten in Ihrem System zusammenspielen. Die Konfigurationen könnten identisch arbeiten, sie können es aber auch unterlassen. Wir haben in verschiedenen Konfigurationen eine Serialisierung bemerkt. Ein Beispiel, das wir gesehen haben (auf einer veralteten GNU/Linux-Distribution), bestand aus einer Kombination aus dem ext3-Dateisystem und InnoDB mit innodb_flush_method=O_DIRECT. Diese schien Sperren auf der Inode-Ebene im Dateisystem zu verursachen, so dass zu einem Zeitpunkt nur eine Ein-/Ausgabe-Anforderung an eine Datei gesandt werden konnte. In diesem Fall trat die Serialisierung dateiweise auf; der Fehler wurde in einer späteren Softwareversion behoben.
RAID-Leistungsoptimierung | 349
In einem anderen Fall mit einem 10-Festplatten-RAID 10-Volume, dem ReiserFS-Dateisystem und InnoDB mit aktiviertem innodb_file_per_table wurden Anforderungen an jedes Gerät serialisiert. Der Wechsel zu Software-RAID 0 auf dem Hardware-RAID 1 erbrachte einen fünfmal höheren Durchsatz, weil das Speichersystem begann, sich nicht mehr wie eine physische Festplatte, sondern wie fünf physische Festplatten zu verhalten. Diese Situation wurde ebenfalls durch einen Bug hervorgerufen, der inzwischen entfernt wurde. Sie sehen daran jedoch, was passieren kann. Serialisierung kann auf jeder Ebene im Software- oder Hardware-Stack auftreten. Falls Sie dieses Problem bemerken, dann versuchen Sie, das Dateisystem zu ändern, den Kernel zu aktualisieren, dem Betriebssystem mehr Geräte zu übergeben oder eine andere Mischung aus Software- oder Hardware-RAID zu benutzen. Überprüfen Sie außerdem die Nebenläufigkeit Ihres Geräts mit iostat, und stellen Sie sicher, dass es tatsächlich parallele Ein-/Ausgaben ausführt (mehr dazu in »Wie man die iostat-Ausgabe liest« auf Seite 368). Vergessen Sie schließlich nicht, Benchmark-Tests durchzuführen! Diese helfen Ihnen festzustellen, ob Sie wirklich die erwartete Leistung erzielen. Falls z.B. eine Festplatte pro Sekunde 200 zufällige Lesevorgänge durchführen kann, dann sollte ein RAID 10-Volume mit 8 Festplatten annähernd 1.600 zufällige Leseoperationen pro Sekunde schaffen. Beobachten Sie jedoch einen viel niedrigeren Wert, z.B. 500 zufällige Lesevorgänge pro Sekunde, dann sollten Sie diesem Problem auf den Grund gehen. Sorgen Sie dafür, dass Ihre Benchmarks das Ein-/Ausgabe-Subsystem auf die gleiche Weise gebrauchen wie MySQL – benutzen Sie z.B. das O_DIRECT-Flag, und testen Sie die Ein-/Ausgabe-Leistung an einer einzelnen Datei, wenn Sie InnoDB ohne innodb_file_per_table verwenden. SysBench eignet sich hervorragend dafür. (Mehr über Benchmarks erfahren Sie in Kapitel 2.)
RAID-Konfiguration und Caching Normalerweise können Sie den RAID-Controller selbst konfigurieren, indem Sie während des Bootens der Maschine sein Setup-Dienstprogramm benutzen. Die meisten Controller bieten sehr viele Optionen. Wir konzentrieren uns hier auf zwei: die Chunk-Size (Chunk-Größe; damit wird der kleinste Datenblock pro Schreibzugriff bezeichnet, der auf eine einzelne Festplatte geschrieben werden kann) für Striped-Arrays und den OnController-Cache (auch als RAID-Cache bezeichnet; wir benutzen die Begriffe synonym).
Die RAID-Stripe-Chunk-Größe Die optimale Größe eines Stripe-Chunks ist last- und hardwarespezifisch. Theoretisch ist es gut, für zufällige Ein-/Ausgaben eine große Chunk-Größe zu haben, weil das bedeutet, dass von einer einzelnen Festplatte mehr Leseoperationen bedient werden können. Um herauszufinden, weshalb das so ist, nehmen Sie einmal die Größe einer typischen zufälligen Ein-/Ausgabe-Operation für Ihre Last. Wenn die Chunk-Größe wenigstens
350 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
diesen Wert erreicht und die Daten nicht über die Grenzen zwischen den Chunks hinausreichen, dann muss nur ein einziges Laufwerk an dem Lesevorgang teilnehmen. Ist die Chunk-Größe dagegen kleiner als die Datenmenge, die gelesen werden soll, dann gibt es keine Möglichkeit, die Beteiligung von mehr als einem Laufwerk an dem Lesevorgang zu verhindern. So viel zur Theorie. In der Praxis kommen viele RAID-Controller mit großen Chunks nicht gut zurecht. Zum Beispiel könnte der Controller die Chunk-Größe als Cache-Einheit in seinem Cache benutzen, was möglicherweise eine Verschwendung ist. Der Controller könnte auch die Chunk-Größe, die Cache-Größe und die Leseeinheit-Größe (das ist die Datenmenge, die er in einer einzigen Operation liest) abstimmen. Wenn die Leseeinheit zu groß ist, dann ist sein Cache weniger effektiv, und es werden möglicherweise mehr Daten gelesen, als wirklich benötigt werden, selbst für winzige Anforderungen. In der Praxis ist es außerdem schwer festzustellen, ob bestimmte Daten über mehrere Laufwerke hinweg reichen. Selbst wenn die Chunk-Größe 16 KByte beträgt, was der Seitengröße bei InnoDB entspricht, können Sie nicht sicher davon ausgehen, dass alle Lesevorgänge an den 16-KByte-Grenzen ausgerichtet werden. Das Dateisystem könnte die Datei fragmentieren und richtet die Fragmente typischerweise an der Blockgröße des Dateisystems aus, die oft 4 KByte beträgt. Manche Dateisysteme sind schlauer, aber darauf dürfen Sie sich nicht verlassen.
Der RAID-Cache Beim RAID-Cache handelt es sich um einen (relativ) kleinen Speicher, der physisch auf dem RAID-Controller installiert ist. Er kann zum Puffern von Daten benutzt werden, die sich zwischen den Festplatten und dem Hostsystem hin- und herbewegen. Hier sind einige der Gründe, weshalb eine RAID-Karte den Cache benutzen könnte: Caching von Leseoperationen Nachdem der Controller Daten von den Festplatten gelesen und an das Hostsystem geschickt hat, kann er die Daten speichern; dies erlaubt es ihm, künftige Abfragen nach denselben Daten zu beantworten, ohne noch einmal auf die Festplatte gehen zu müssen. Normalerweise ist das eine sehr armselige Variante, den RAID-Cache zu benutzen. Weshalb? Weil das Betriebssystem und der Datenbankserver ihre eigenen, viel größeren Caches besitzen. Wenn es in einem dieser Caches einen Cache-Treffer gibt, werden die Daten im RAID-Cache nicht benutzt. Kommt es entsprechend zu einem Miss in einem der höheren Caches, dann ist die Wahrscheinlichkeit für einen Treffer im RAID-Cache verschwindend gering. Da der RAID-Cache sehr viel kleiner ist, wurde er mit hoher Sicherheit bereits geleert und mit anderen Daten gefüllt. Wie auch immer Sie es betrachten, es ist eine Verschwendung von Speicher, die Leseoperationen im RAID-Cache zu speichern.
RAID-Leistungsoptimierung | 351
Caching von Read-Ahead-Daten Wenn der RAID-Controller sequenzielle Anforderungen nach Daten bemerkt, könnte er beschließen, eine Read-Ahead-Leseoperation durchzuführen – d.h., auf Vorrat Daten zu holen, von denen er annimmt, dass diese als Nächstes benötigt werden. Er braucht allerdings einen Ort, an dem er die Daten ablegen kann, bis sie tatsächlich angefordert werden. Dafür kann er den RAID-Cache einsetzen. Die Auswirkungen auf die Performance können ganz verschieden sein, und Sie sollten überprüfen, ob dieses Vorgehen wirklich hilft. Read-Ahead-Operationen sind nicht unbedingt sinnvoll, wenn der Datenbankserver sein eigenes intelligentes ReadAhead durchführt (wie etwa InnoDB), und können sich störend auf die ausgesprochen wichtige Pufferung synchroner Schreiboperationen auswirken. Caching von Schreiboperationen Der RAID-Controller kann Schreiboperationen in seinem Cache puffern und ihre Ausführung für einen späteren Zeitpunkt eintakten. Dieses Vorgehen bringt zwei Vorteile mit sich: Erstens kann er viel schneller einen »Erfolg« an das Hostsystem zurückmelden, als beim tatsächlichen Schreiben auf den physischen Festplatten möglich wäre, und zweitens kann er die Schreiboperationen sammeln und anschließend effizienter ausführen. Interne Operationen Manche RAID-Operationen sind sehr komplex – vor allem RAID 5-Schreibvorgänge, bei denen Paritätsbits berechnet werden müssen, um die Daten im Falle eines Ausfalls rekonstruieren zu können. Der Controller benötigt für diese Art von interner Operation Speicher. Das ist einer der Gründe, weshalb RAID 5 auf manchen Controllern nicht so gut funktioniert: Es muss viele Daten in den Cache lesen, um eine gute Performance zu erreichen. Manche Controller können das Caching für die Schreibvorgänge nicht mit dem Caching für die RAID 5-Paritätsoperationen abstimmen. Im Allgemeinen ist der Speicher des RAID-Controllers eine knappe Ressource, die Sie weise einsetzen sollten. Sie nur für das Lesen zu benutzen, ist eine Verschwendung, bei Schreiboperationen bildet er jedoch eine wichtige Möglichkeit, um die Ein-/Ausgabe-Leistung zu verbessern. Viele Controller erlauben es Ihnen zu wählen, wie Sie den Speicher belegen wollen. Sie können z.B. festlegen, welcher Anteil zum Speichern von Schreiboperationen und welcher für Leseoperationen verwendet werden soll. Bei RAID 0, RAID 1 und RAID 10 sollten Sie wahrscheinlich 100 % des Controller-Speichers zum Ablegen von Schreiboperationen reservieren. Bei RAID 5 sollten Sie einen Teil des Controller-Speichers für seine internen Operationen vorsehen. Im Allgemeinen ist das ein guter Rat, der allerdings nicht immer gilt – unterschiedliche RAID-Karten verlangen unterschiedliche Konfigurationen. Wenn Sie den RAID-Cache für das Schreib-Caching benutzen, können Sie bei vielen Controllern konfigurieren, wie lange Schreiboperationen verzögert werden dürfen (1 Sekunde, 5 Sekunden usw.). Eine längere Verzögerung bedeutet, dass mehr Schreibvorgänge gruppiert und optimal auf die Festplatten übertragen werden können. Nacht352 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
eilig ist, dass Ihre Schreiboperationen dann stärker »im Bündel« auftreten. Das muss nicht unbedingt schlecht sein, es sei denn, Ihre Anwendung schickt in dem Moment einen Haufen Schreibanforderungen, in dem sich der Cache des Controllers gefüllt hat und auf die Platte geleert werden muss. Wenn nicht genug Platz für die Schreibanforderungen Ihrer Anwendung ist, muss diese warten. Halten Sie die Verzögerung kürzer, bedeutet es, dass Sie mehr Schreiboperationen haben und diese weniger effizient sind. Allerdings werden Spitzen eher ausgeglichen, und ein größerer Teil des Caches bleibt leer, um geballten Schreibanforderungen von der Anwendung zu begegnen. (Wir vereinfachen hier – Controller besitzen oft komplexe, herstellerspezifische Balancing-Algorithmen, weshalb wir hier nur versuchen, die Grundprinzipien zu behandeln.) Der Schreib-Cache ist für synchrone Schreiboperationen sehr hilfreich, wie etwa für das Ausführen von fsync( )-Aufrufen in den Transaktions-Logs und das Erzeugen von Binärlogs mit aktiviertem sync_binlog, Sie sollten ihn aber nur einschalten, wenn Ihr Controller über eine sogenannte Battery Backup Unit (BBU) verfügt, da Sie ansonsten bei einem Stromausfall wahrscheinlich Ihre Datenbank und sogar das transaktionsfähige Dateisystem beschädigen. Haben Sie jedoch eine BBU, kann die Aktivierung des Schreib-Cache die Leistung für Lasten, die viele Log-Entleerungen durchführen (etwa beim Entleeren des Transaktions-Logs beim Bestätigen einer Transaktion), um den Faktor 20 oder mehr erhöhen. Noch eine letzte Überlegung: Viele Festplatten besitzen eigene Schreib-Caches, die fsync( )-Operationen »fälschen« können, indem Sie dem Controller vorgaukeln, dass die
Daten auf das physische Medium geschrieben wurden. Festplatten, die direkt angeschlossen sind (und nicht an einen RAID-Controller), können manchmal ihre Caches vom Betriebssystem verwalten lassen, aber auch das funktioniert nicht immer. Diese Caches werden typischerweise für ein fsync( ) geleert und für synchrone Ein-/Ausgaben übergangen, aber auch hier kann die Festplatte lügen. Sie sollten entweder dafür sorgen, dass diese Caches bei fsync( ) geleert werden, oder sie deaktivieren, da sie nicht batteriegesichert sind. Festplatten, die vom Betriebssystem oder von der RAID-Firmware nicht ordentlich verwaltet wurden, haben schon oft Datenverluste verursacht. Aus diesem und anderen Gründen ist es keine schlechte Idee, den echten Katastrophenfall zu proben (indem Sie tatsächlich den Stecker aus der Steckdose ziehen), wenn Sie neue Hardware installieren. Das ist oft die beste Möglichkeit, um leichte Fehlkonfigurationen oder eigenartige Verhaltensweisen von Festplatten zu finden. Ein praktisches Skript, das Ihnen dabei hilft, finden Sie unter http://brad.livejournal.com/2116715.html. Falls Sie sich tatsächlich auf die BBU Ihres RAID-Controllers verlassen müssen, dann denken Sie daran, beim Test der BBU den Netzstecker für eine realistische Zeitdauer herausgezogen zu lassen. Manche Einheiten halten ohne Stromzufuhr nicht so lange durch, wie sie eigentlich sollten. Auch hier gilt, dass eine schwache Stelle Ihre ganze Kette aus Speicherkomponenten hinfällig werden lassen kann.
RAID-Leistungsoptimierung | 353
Storage Area Networks und Network-Attached Storage Storage Area Networks (SANs; Speichernetzwerke) und Network-Attached Storage (NAS) stellen zwei verwandte, aber sehr unterschiedliche Möglichkeiten dar, um externe Dateispeichergeräte an einen Server anzuschließen. Ein SAN präsentiert eine blockbasierte Schnittstelle, die der Server als direkt angeschlossen betrachtet, während ein NASGerät ein dateibasiertes Protokoll wie NFS oder SMB bietet. Ein SAN wird an den Server normalerweise über das Fibre Channel Protocol (FCP) oder über iSCSI angeschlossen, während ein NAS-Gerät über eine normale Netzwerkverbindung angebunden wird.
Storage Area Networks Zu den Vorteilen eines SAN gehören eine flexiblere Speicherverwaltung und die Fähigkeit, den Speicher zu skalieren. Viele SAN-Lösungen bieten außerdem besondere Eigenschaften wie eine Schnappschussmöglichkeit sowie Unterstützung für integrierte fortlaufende Backups. Sie erlauben es einem Server, auf eine sehr große Anzahl von Festplatten – oft 50 oder mehr – zuzugreifen, und besitzen typischerweise sehr große, intelligente Caches, um Schreiboperationen zu puffern. Die blockbasierte Schnittstelle, die sie exportieren, erscheint dem Server als Logical Unit Number (LUN) oder virtuelles Volume. Viele SANs erlauben es außerdem, mehrere Knoten zu »Clustern« zusammenzufassen, um eine bessere Performance zu erzielen. Obwohl SANs ganz gut funktionieren, wenn Sie viele nebenläufige Anforderungen haben und einen hohen Durchsatz benötigen, dürfen Sie keine Wunder erwarten. Auch ein SAN ist schließlich nur eine Sammlung aus Festplatten, die nur eine begrenzte Anzahl von Ein-/Ausgabe-Operationen pro Sekunde ausführen können, und da ein SAN sich außerhalb des Servers befindet und seine eigene Verarbeitung durchführt, kommt zu jeder Ein-/Ausgabe-Anforderung noch eine gewisse Latenz hinzu. Die zusätzliche Latenz macht SANs weniger effizient, wenn Sie eine sehr hohe Performance für synchrone Ein-/ Ausgaben benötigen, so dass es normalerweise besser ist, die Transaktions-Logs auf einem direkt angeschlossenen RAID-Controller als auf einem SAN zu halten. Im Allgemeinen ist ein direkt angeschlossener Speicher schneller als die LUNs in einem SAN mit der gleichen Anzahl ähnlicher Festplatten. Das gemeinsame Benutzen der Festplatten zwischen den LUNs erschwert im Übrigen die Leistungsanalyse, weil die LUNs einander auf Arten beeinflussen, die schwer zu messen sind. Wenn Sie die Festplatten auf getrennte LUNs setzen, ist der Effekt nicht so sehr spürbar, aber manchmal fällt er doch auf – wenn Sie z.B. iSCSI benutzen, werden Sie möglicherweise eine Konkurrenzsituation auf dem Netzwerksegment bemerken. Auch die Software in dem SAN unterliegt Beschränkungen, wodurch sich die tatsächliche Performance von der theoretischen oder erwarteten Performance unterscheidet. SANs haben einen großen Nachteil: Ihre Kosten liegen typischerweise viel höher als die Kosten eines vergleichbaren direkt angeschlossenen Speichers (besonders eines internen Speichers).
354 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Die meisten Webanwendungen benutzen keine SANs. Sie sind jedoch für sogenannte Unternehmensanwendungen sehr beliebt. Dafür gibt es verschiedene Gründe: • Unternehmensanwendungen sind normalerweise hinsichtlich des Budgets weniger beschränkt, während viele Webanwendungen sich solche »luxuriösen Elemente« wie SANs nicht leisten können. • Oft betreiben Unternehmen viele Anwendungen oder viele Instanzen einer einzelnen Anwendung und haben unvorhersehbare Anforderungen, was das Wachstum betrifft. Ein SAN bietet Ihnen die Möglichkeit, viel Speicher zu kaufen, diesen freizugeben und bei Bedarf zu vergrößern. • Die großen Puffer eines SAN können Schreibspitzen aufnehmen und schnellen Zugriff auf »heiße« Daten bieten. Typischerweise eignen sich SANs dafür, die Last zwischen sehr vielen Festplatten auszugleichen. Dies alles ist normalerweise für Anwendungs-Cluster erforderlich, die vertikal skaliert sind. Webanwendungen nützt dies jedoch nicht sehr viel. Bei Webanwendungen gibt es meist keine Perioden niedriger Aktivität, gefolgt von riesigen Schreibspitzen; die meisten von ihnen schreiben andauernd Daten, so dass das Puffern der Schreiboperationen kaum hilfreich ist. Auch Lesepufferung wird nicht benötigt, da Datenbanken oft ihre eigenen (großen, intelligenten) Caches besitzen. Und die gebräuchlichste und erfolgreichste Strategie zum Aufbau einer sehr großen Webanwendung besteht darin, Anwendungen zu partitionieren, damit die Webanwendungen die Last ebenfalls auf eine große Anzahl von Festplatten verteilen.
Network-Attached Storage Ein NAS-Gerät ist im Prinzip ein geschrumpfter Dateiserver, typischerweise mit einer Webschnittstelle anstelle einer physischen Maus, eines Monitors und einer Tastatur. Es handelt sich um eine ökonomische und relativ problemlose Methode, viel Speicherplatz zur Verfügung zu stellen. Aus Gründen der Redundanz setzt ein NAS im Allgemeinen auf einem RAID-Array auf. Allerdings sind NAS-Geräte nicht sehr schnell, weil sie über das Netzwerk angebunden sind. Sie haben traditionell auch Probleme mit synchronen Ein-/Ausgaben und Sperren, weshalb wir sie nicht als allgemeine Datenbankspeicher empfehlen. Sie können sie in Sonderfällen einsetzen, bei denen ihre Schwächen nicht so sehr ins Gewicht fallen, wie etwa für gemeinsam genutzte, schreibgeschützte MyISAM-Tabellen.
Mehrere Festplatten-Volumes benutzen Früher oder später wird die Frage aufkommen, wo die Dateien abgelegt werden sollen. MySQL erzeugt eine Vielzahl von Dateien: • Daten- und Indexdateien • Transaktions-Log-Dateien
Mehrere Festplatten-Volumes benutzen | 355
• Binärlog-Dateien • Allgemeine Log-Dateien (z.B. für das Fehler-Log, das Abfrage-Log und das SlowQuery-Log) • temporäre Dateien und Tabellen MySQL besitzt nicht viele Funktionen für eine komplexe Tablespace-Verwaltung. Standardmäßig legt es einfach alle Dateien für eine Datenbank (das Schema) in ein einziges Verzeichnis. Es gibt ein paar Optionen, um zu kontrollieren, wohin die Daten gelangen. So können Sie z.B. eine Stelle angeben, wo die Indizes für MyISAM-Tabellen abgelegt werden sollen, und Sie können die partitionierten Tabellen von MySQL 5.1 benutzen. Wenn Sie die vorgegebene InnoDB-Konfiguration verwenden, kommen alle Daten und Indizes in eine einzige Gruppe von Dateien, und nur die Tabellendefinitionsdateien werden in das Datenbankverzeichnis gelegt. Aus diesem Grund platzieren die meisten Leute alle Daten und Indizes auf ein einziges Volume. Manchmal jedoch kann der Einsatz mehrerer Volumes Ihnen helfen, eine starke Ein-/ Ausgabe-Last zu verwalten. Zum Beispiel kann ein Batch-Job, der Daten in eine gewaltige Tabelle schreibt, davon profitieren, wenn dies auf einem eigenen Volume geschieht, so dass er anderen Abfragen nicht die Ein-/Ausgaben streitig macht. Idealerweise analysieren Sie den Ein-/Ausgabe-Zugriff auf die unterschiedlichen Teile Ihrer Daten, damit Sie die Daten entsprechend platzieren können. Allerdings ist das schwierig, wenn die Daten noch nicht auf verschiedenen Volumes vorliegen. Sie kennen wahrscheinlich schon den Standardratschlag, die Transaktions-Logs und die Datendateien auf unterschiedlichen Volumes abzulegen, damit die sequenziellen Ein-/ Ausgaben der Logs nicht den zufälligen Ein-/Ausgaben der Daten in die Quere kommen. Doch wenn Sie nicht gerade sehr viele Festplatten haben (20 oder so), sollten Sie sehr genau nachdenken, bevor Sie das tun. Der tatsächliche Vorteil der Trennung von Log- und Datendateien liegt in der geringeren Wahrscheinlichkeit, im Falle eines Absturzes sowohl die Daten- als auch die Log-Dateien zu verlieren. Es hat sich bewährt, sie zu trennen, wenn Sie keinen batteriegesicherten Schreib-Cache auf Ihrem RAID-Controller haben. Mit einer Battery Backup Unit (BBU) ist ein separates Volume gar nicht so oft erforderlich, wie Sie vielleicht glauben. Leistung ist selten ein bestimmender Faktor, denn obwohl es viele Schreibvorgänge in die Transaktions-Logs gibt, sind die meisten von ihnen doch klein. Der RAID-Cache fasst seine Anforderungen darum meist zusammen, und Sie erhalten typischerweise einfach eine Reihe von sequenziellen physischen Schreibanforderungen pro Sekunde. Das stellt für die zufälligen Ein-/Ausgaben in Ihre Datendateien kein echtes Problem dar. Auch die allgemeinen Logs mit ihren sequenziellen asynchronen Schreiboperationen und ihrer niedrigen Last können sich bequem ein Volume mit den Daten teilen. Es gibt jedoch noch eine andere Sicht auf diese Dinge, die viele Leute nicht in Erwägung ziehen. Erhöht sich die Performance, wenn man die Logs auf getrennte Volumes legt? Üblicherweise ja – aber lohnt es sich? Die Antwort lautet häufig Nein.
356 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Und warum? Es ist ganz einfach teuer, Festplatten für Transaktions-Logs aufzuwenden. Nehmen Sie an, Sie haben sechs Festplatten. Die offensichtliche Wahl sieht so aus, dass alle sechs in ein RAID-Volume gepackt werden oder dass vier von ihnen für die Daten und zwei für die Transaktions-Logs vorgesehen werden. In diesem Fall haben Sie allerdings die Anzahl der Festplatten, die für die Datendateien zur Verfügung stehen, um ein Drittel reduziert, was eine deutliche Abnahme darstellt; darüber hinaus sehen Sie zwei Laufwerke für eine möglicherweise triviale Aufgabe vor (vorausgesetzt, Ihr RAID-Controller besitzt einen batteriegesicherten Schreib-Cache). Falls Sie andererseits viele Festplatten haben, ist es proportional gesehen weniger teuer, einige von ihnen für die Transaktions-Logs vorzusehen, und kann sich als vorteilhaft erweisen. Bei z.B. insgesamt 30 Festplatten können Sie sicherstellen, dass das Schreiben in das Log so schnell wie möglich vonstatten geht, wenn Sie zwei Festplatten (konfiguriert als RAID 1-Volume) für die Logs vorsehen. Für eine zusätzliche Leistung könnten Sie für dieses RAID-Volume im RAID-Controller noch Platz im Schreib-Cache reservieren. Kosteneffektivität ist allerdings nicht die einzige Überlegung. Sie sollten möglicherweise auch deshalb InnoDB-Daten und Transaktions-Logs auf dem gleichen Volume vorhalten, weil diese Strategie es Ihnen erlaubt, LVM-Schnappschüsse für Lock-freie Backups einzusetzen. Manche Dateisysteme erlauben konsistente Mehr-Volume-Schnappschüsse. Für diese Dateisysteme mag das keine große Sache sein, doch für ext3 sollten Sie das im Hinterkopf behalten. Falls Sie sync_binlog eingeschaltet haben, sind Binärlogs in Bezug auf die Performance vergleichbar mit Transaktions-Logs. Tatsächlich ist es jedoch eine gute Idee, Binärlogs auf einem anderen Volume zu speichern als die Daten – das ist sicherer, damit sie im Falle eines Datenverlusts überleben können. Auf diese Weise können Sie sie für eine punktgenaue Wiederherstellung benutzen. Diese Überlegung gilt nicht für die InnoDBTransaktions-Logs, weil diese ohne die Datendateien sinnlos sind; Sie können Transaktions-Logs nicht auf die Backups der letzten Nacht anwenden. (Diese Unterscheidung zwischen Transaktions-Logs und Binärlogs mag Datenbankadministratoren, die an andere Datenbanken gewöhnt sind, künstlich erscheinen, wo diese beiden gleich sind.) Das andere gebräuchliche Szenario zum Trennen von Dateien ist das temporäre Verzeichnis, das MySQL für Filesorts und auf der Festplatte befindliche temporäre Tabellen benutzt. Falls diese nicht zu groß sind, ist es am besten, sie in ein temporäres, nur im Speicher befindliches Dateisystem wie tmpfs zu legen. Das geht am schnellsten. Ist das auf Ihrem System nicht machbar, dann legen Sie sie auf das gleiche Gerät wie Ihr Betriebssystem. Bei einem typischen Festplattenlayout hat man das Betriebssystem, die Swap-Partition und die Binärlogs auf einem RAID 1-Volume und alles andere auf einem getrennten RAID 5- oder RAID 10-Volume.
Mehrere Festplatten-Volumes benutzen | 357
Die Netzwerkkonfiguration So wie für eine Festplatte Latenz und Durchsatz beschränkende Faktoren darstellen, beschränken Latenz und Bandbreite (die tatsächlich das Gleiche bedeutet wie Durchsatz) eine Netzwerkverbindung. Das größte Problem für die meisten Anwendungen bildet die Latenz; eine typische Anwendung führt viele kleine Netzwerkübertragungen durch, so dass sich selbst leichte Verzögerungen bei den einzelnen Übertragungen schnell summieren. Ein Netzwerk, das nicht korrekt funktioniert, stellt außerdem einen großen Leistungsengpass dar. Ein verbreitetes Problem sind Paketverluste. Selbst ein Verlust von 1 % reicht, um einen deutlichen Leistungsabfall zu verursachen, da die verschiedenen Ebenen im Protokollstack versuchen, die Probleme mit bestimmten Strategien zu beheben. So wird z.B. eine Weile gewartet und das Paket dann erneut gesendet, wodurch sich die Übertragungszeit weiter erhöht. Ein anderes Problem stellt eine defekte oder langsame Namensauflösung durch das Domain Name System (DNS) dar. Die Probleme mit dem DNS können so schlimm sein, dass es sich anbietet, für Produktionsserver skip_name_resolve zu aktivieren. Eine defekte oder langsame DNSAuflösung ist für viele Anwendungen problematisch, besonders schwerwiegend ist es aber für MySQL. Wenn MySQL eine Verbindungsanforderung empfängt, führt es sowohl einen Forward- als auch einen Reverse-DNS-Lookup durch. Das kann aus vielerlei Gründen schiefgehen. Wenn das passiert, werden Verbindungen abgewiesen, der Verbindungsaufbau zum Server verlangsamt und ganz allgemein viele Schäden angerichtet, bis hin zu Denial-of-Service-Attacken. Aktivieren Sie die Option skip_name_resolve, führt MySQL überhaupt keine DNS-Anfragen durch. Das bedeutet aber auch, dass Ihre Benutzer-Accounts nur IP-Adressen, »localhost« oder IP-Adress-Wildcards in der host-Spalte haben dürfen. Jeder Benutzer-Account, bei dem ein Hostname in der host-Spalte steht, kann sich nicht anmelden. Sie müssen Ihr Netzwerk so gestalten, dass es eine gute Performance erzielt, anstatt einfach das zu akzeptieren, was Ihnen vorgegeben wird. Untersuchen Sie als Erstes, wie viele Hops sich zwischen den Knoten befinden, und bilden Sie den physischen Aufbau des Netzwerkes ab. Nehmen Sie z.B. an, dass 10 Webserver über Gigabit-Ethernet (1 GigE) an einen »Web«-Switch angeschlossen sind und dass auch dieser Switch über 1 GigE mit dem »Datenbank«-Switch verbunden ist. Wenn Sie sich nicht die Zeit nehmen, die Verbindungen zu verfolgen, werden Sie niemals merken, dass Ihre gesamte Bandbreite von allen Datenbankservern zu allen Webservern auf ein Gigabit beschränkt ist! Jeder Hop vergrößert natürlich auch die Latenz. Überwachen Sie die Netzwerkleistung und die Fehler auf allen Netzwerkports. Beobachten Sie alle Ports auf den Servern, den Routern und den Switches. Der Multi Router Traffic Grapher oder MRTG (http://oss.oetiker.ch/mrtg/) ist eine bewährte Lösung für die Geräteüberwachung. Andere verbreitete Werkzeuge zum Überwachen der Netzwerk-Performance (im Gegensatz zu den Geräten) sind Smokeping (http://oss.oetiker.ch/smokeping/) und Cacti (http://www.cacti.net). 358 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Die physische Trennung spielt eine große Rolle beim Betrieb von Netzwerken. Netzwerke zwischen Städten haben eine viel schlechtere Latenz als das LAN Ihres Rechenzentrums, obwohl die Bandweite technisch gesehen gleich ist. Wenn die Knoten wirklich weit verteilt sind, kommt es tatsächlich auf die Lichtgeschwindigkeit an. Zum Beispiel sind zwei Rechenzentren, von denen eines an der Westküste der USA und eines an der Ostküste steht, etwa 4.500 km voneinander entfernt. Die Lichtgeschwindigkeit beträgt 300.000 km pro Sekunde, so dass eine Übertragung in eine Richtung nicht weniger als 16 ms betragen kann, eine Hin- und Rückübertragung dauert wenigstens 32 ms. Der physische Abstand ist allerdings nicht der einzige Einfluss auf die Leistung: Es sind auch die Geräte, die dazwischen liegen. Auch Repeater, Router und Switches verringern die Leistung in einem gewissen Maße. Je weiter die Netzwerkknoten verteilt sind, umso weniger vorhersehbar und umso unzuverlässiger sind die Verbindungsstücke. Vermeiden Sie nach Möglichkeit rechenzentrumsübergreifende Operationen in Echtzeit.6 Wenn es sich nicht verhindern lässt, dann sorgen Sie zumindest dafür, dass Ihre Anwendung mit Netzwerkausfällen bzw. -fehlern zurechtkommt. Sie wollen z.B. nicht, dass Ihre Webserver zu viele Apache-Prozesse aufteilen, weil sie alle blockiert werden, wenn sie versuchen, über einen Link, der mit starken Paketverlusten zu kämpfen hat, eine Verbindung zu einem entfernten Rechenzentrum aufzubauen. Nutzen Sie auf lokaler Ebene wenigstens 1 GigE, falls Sie es nicht schon längst tun. Für das Backbone zwischen den Switches müssen Sie vielleicht sogar eine 10-GigE-Verbindung einsetzen. Falls Sie mehr Bandbreite benötigen, können Sie eine Netzwerkbündelung einrichten: Sie verbinden mehrere Netzwerkkarten (Network Interface Cards; NICs) für mehr Bandbreite. Im Prinzip parallelisieren Sie das Netzwerk, was Ihnen als Teil einer Hochverfügbarkeitsstrategie zugute kommt. Wenn Sie einen sehr hohen Durchsatz brauchen, können Sie die Leistung möglicherweise verbessern, indem Sie die Netzwerkkonfiguration Ihres Betriebssystems ändern. Falls Sie nicht viele Verbindungen haben, aber große Abfragen oder Ergebnismengen, dann erhöhen Sie die TCP-Puffergröße. Wie Sie das tun, hängt vom jeweiligen System ab; bei den meisten GNU/Linux-Systemen können Sie die Werte in /etc/sysctl.conf ändern und sysctl -p ausführen oder das /proc-Dateisystem benutzen, indem Sie neue Werte in die Dateien geben, die unter /proc/sys/net/ zu finden sind. Sie finden online gute Anleitungen zu diesem Thema, wenn Sie nach »TCP tuning guide« suchen. Normalerweise ist es jedoch wichtiger, Ihre Einstellungen so anzupassen, dass sie effizient mit vielen Verbindungen und kleinen Abfragen zurechtkommen. Einer der gebräuchlichsten Tricks besteht darin, den lokalen Portbereich zu ändern. Hier ist ein System, das mit den Vorgabewerten konfiguriert wurde: [root@caw2 ~]# cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000 6 Replikation zählt nicht zu den in Echtzeit ausgeführten, rechenzentrumsübergreifenden Operationen. Sie läuft eigentlich nicht in Echtzeit ab, und außerdem ist es meist keine schlechte Idee, seine Daten aus Sicherheitsgründen an eine entfernte Stelle zu replizieren. Mehr dazu erfahren Sie im nächsten Kapitel.
Die Netzwerkkonfiguration | 359
Manchmal wollen Sie diese Werte auf einen größeren Bereich ändern. Zum Beispiel: [root@caw2 ~]# echo 1024 65535 > /proc/sys/net/ipv4/ip_local_port_range
Sie können auch mehr Verbindungen zulassen: [root@caw2 ~]# echo 4096 > /proc/sys/net/ipv4/tcp_max_syn_backlog
Für Datenbankserver, die nur lokal eingesetzt werden, können Sie den Timeout kürzen, der nach dem Schließen eines Sockets auftritt, wenn ein Peer ausgefallen ist und seine Seite der Verbindung nicht geschlossen hat. Auf den meisten Systemen liegt der Vorgabewert bei einer Minute, was ziemlich lang ist: [root@caw2 ~]# echo <Wert> > /proc/sys/net/ipv4/tcp_fin_timeout
Meist können diese Einstellungen ihre Vorgabewerte behalten. Sie müssen sie üblicherweise nur dann ändern, wenn etwas Ungewöhnliches geschieht, wie etwa eine ausgesprochen schlechte Netzwerk-Performance oder eine sehr große Anzahl von Verbindungen. Eine Internet-Suche nach »TCP Variables« liefert gute Ressourcen über diese und viele weitere Variablen.
Ein Betriebssystem wählen GNU/Linux ist heutzutage das am weitesten verbreitete Betriebssystem für High-Performance-MySQL-Installationen. MySQL läuft aber auch unter vielen anderen Betriebssystemen. Solaris ist führend auf SPARC-Hardware, die oft in Anwendungen eingesetzt wird, die eine hohe Zuverlässigkeit verlangen. Solaris genießt den Ruf, auf gewisse Weise schwieriger zu benutzen zu sein als GNU/Linux, dabei ist es jedoch ein solides, leistungsstarkes Betriebssystem mit vielen erweiterten Funktionen. Speziell Solaris 10 wird immer beliebter. Es bringt sein eigenes Dateisystem mit (ZFS), verfügt über viele gute Werkzeuge zur Fehlersuche und -behebung (wie etwa DTrace), eine gute Thread-Leistung und bietet eine Virtualisierungstechnik namens Solaris Zones, die bei der Ressourcenverwaltung hilft. Sun bietet außerdem eine gute MySQL-Unterstützung. FreeBSD ist eine weitere Möglichkeit. Früher hatte es eine Reihe von Problemen mit MySQL, die meist mit der Thread-Unterstützung zusammenhingen. Neuere Versionen sind jedoch viel besser. Heutzutage ist es nicht ungewöhnlich, MySQL in großem Maßstab auf FreeBSD zu sehen. Windows wird typischerweise für die Entwicklung eingesetzt sowie dann, wenn MySQL mit Desktop-Anwendungen verwendet wird. Es gibt MySQL im Unternehmenseinsatz unter Windows, meist werden aber Unix-artige Betriebssysteme für diese Zwecke eingesetzt. Wir wollen hier keine Diskussionen über Betriebssysteme beginnen, sondern nur darauf hinweisen, dass es keine Probleme gibt, wenn man MySQL in einer heterogenen Umgebung benutzt. Es ist absolut vernünftig, den MySQL-Server auf einem Unix-artigen Betriebssystem einzusetzen und Windows auf den Webservern auszuführen, wobei die beiden über das hochwertige ADO.NET verbunden sind (das es von MySQL frei verfüg-
360 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
bar gibt). Es ist genauso einfach, eine Verbindung von Unix zu einem MySQL-Server unter Windows herzustellen wie zu einem anderen Unix-Server. Wenn Sie ein Betriebssystem wählen, dann installieren Sie die 64-Bit-Version, falls Sie eine 64-Bit-Architektur benutzen. Es klingt seltsam, aber wir finden oft 32-Bit-Betriebssysteme vor, die versehentlich auf 64-Bit-Prozessoren installiert wurden. Die Prozessoren führen diese Systeme aus, ohne sich zu beschweren. Dabei verhindern die ganzen gewöhnlichen 32-Bit-Beschränkungen (wie etwa Einschränkungen hinsichtlich der adressierbaren Speichergröße), dass die 64-Bit-Chips ihre Vorteile ausspielen. Bei GNU/Linux-Distributionen entscheiden oft die persönlichen Vorlieben. Wir glauben, dass es am besten ist, wenn man eine Distribution benutzt, die ausdrücklich für Serveranwendungen vorgesehen ist, und keine Desktop-Distribution. Informieren Sie sich über den Lebenszyklus der Distribution, die Versions- und Update-Regeln, und überprüfen Sie, ob der Hersteller irgendwelche Unterstützung liefert. Red Hat Enterprise Linux ist eine hochwertige, stabile Distribution, CentOS ist eine beliebte (und kostenlose) Binarykompatible Alternative, und auch Ubuntu gewinnt an Popularität.
Ein Dateisystem wählen Welches Dateisystem Sie einsetzen, hängt stark von der Wahl Ihres Betriebssystems ab. Bei vielen Systemen, wie etwa Windows, haben Sie eigentlich nur eine oder zwei Möglichkeiten. GNU/Linux wiederum unterstützt viele Dateisysteme. Viele Leute wollen wissen, welche Dateisysteme ihnen die beste Performance für MySQL unter GNU/Linux bieten oder, noch spezieller, welche Wahl am besten für InnoDB und welche am besten für MyISAM ist. Die Benchmarks zeigen, dass die meisten von ihnen sehr ähnliche Leistungen abliefern. Allerdings ist es nicht unbedingt sinnvoll, Dateisysteme anhand der Performance zu vergleichen. Die Leistung eines Dateisystems hängt sehr stark von der Last ab, und kein Dateisystem kann Wunder wirken. Meist arbeitet ein bestimmtes Dateisystem nicht merklich besser oder schlechter als ein anderes Dateisystem. Ausgenommen sind natürlich Situationen, in denen Sie an die Grenzen des Dateisystems stoßen, etwa wenn es um den Umgang mit der Nebenläufigkeit, die Verarbeitung vieler Dateien, die Fragmentierung usw. geht. Wichtiger ist es, die Wiederherstellungszeit nach einem Absturz in Betracht zu ziehen und ob es bestimmte Grenzen gibt, wie etwa eine niedrigere Performance bei Verzeichnissen mit vielen Dateien. (Dies ist ein berühmt-berüchtigtes Problem mit ext2 und ext3, obwohl ext3 heutzutage besser wird.) Das Dateisystem, das Sie wählen, muss die Sicherheit Ihrer Daten gewährleisten. Aus diesem Grund empfehlen wir Ihnen, nicht auf Produktionssystemen herumzuexperimentieren. Entscheiden Sie sich nach Möglichkeit für ein Journaling-Dateisystem, wie etwa ext3, ReiserFS, XFS, ZFS oder JFS. Falls Sie das nicht tun, kann die Überprüfung des Dateisystems nach einem Absturz sehr lange dauern. Wenn das System nicht so wichtig ist, dann können Nicht-Journaling-Dateisysteme besser funktionieren als transaktionsfähige
Ein Dateisystem wählen | 361
Dateisysteme. So könnte z.B. ext2 besser arbeiten als ext3, Sie können aber auch die Journaling-Eigenschaft auf ext3 mit tunefs deaktivieren. Auch die für das Mounten benötigte Zeit ist für manche Dateisysteme ein Faktor. ReiserFS braucht z.B. lange zum Mounten und zum Wiederherstellen des Journals auf Partitionen, die mehrere Terabyte groß sind. Falls Sie ext3 benutzen, haben Sie drei Möglichkeiten für das Journaling der Daten, die Sie in den Mount-Optionen in /etc/fstab einstellen: data=writeback
Diese Option bedeutet, dass nur Metadaten in das Journal geschrieben werden. Das Schreiben der Metadaten wird nicht mit dem Schreiben der Daten synchronisiert. Dies ist die schnellste Konfiguration, und sie kann normalerweise sicher mit InnoDB benutzt werden, weil es sein eigenes Transaktions-Log mitbringt. Allerdings kann ein Absturz zu genau dem richtigen Zeitpunkt eine Beschädigung in einer .frm-Datei verursachen. Hier ist ein Beispiel dafür, wie diese Konfiguration Probleme verursachen könnte. Nehmen Sie an, ein Programm beschließt, eine Datei zu erweitern, um sie größer zu machen. Die Metadaten (die Größe der Datei) werden protokolliert und geschrieben, bevor die Daten tatsächlich in die (jetzt größere) Datei geschrieben werden. Daraus resultiert, dass der Schwanz der Datei – der neu erweiterte Bereich – Müll enthält. data=ordered
Diese Option schreibt auch nur die Metadaten in das Journal, bietet aber eine gewisse Konsistenz, indem die Daten vor den Metadaten geschrieben werden. Sie ist nur ein wenig langsamer als die Option writeback und bei einem Absturz viel sicherer. Wir nehmen in dieser Konfiguration wieder an, dass ein Programm eine Datei erweitern möchte. Jetzt spiegeln die Metadaten die neue Größe der Datei erst dann wider, wenn die Daten, die sich in dem neu angelegten Bereich befinden, auch tatsächlich geschrieben wurden. data=journal
Diese Option bietet ein atomares Jounaling-Verhalten, bei dem die Daten in das Journal geschrieben werden, bevor dieses an seinen endgültigen Standort geschrieben wird. Sie ist normalerweise unnötig und bringt einen viel höheren Aufwand mit sich als die anderen beiden Optionen. In manchen Fällen kann sie jedoch die Performance verbessern, weil das Journaling es dem Dateisystem erlaubt, das Schreiben an den endgültigen Standort der Daten zu verzögern. Unabhängig vom Dateisystem gibt es bestimmte Optionen, die man am besten deaktiviert, weil sie keine Vorteile mit sich bringen und die Kosten erhöhen. Die bekannteste dieser Optionen ist das Aufzeichnen der Zugriffszeit, die selbst dann eine Schreiboperation erfordert, wenn Sie eine Datei lesen. Um diese Option zu deaktivieren, fügen Sie die Mount-Option noatime in Ihre /etc/fstab ein. Das kann die Leistung um bis zu 5–10 % steigern – je nach Last und Dateisystem (obwohl es in anderen Fällen kaum einen Unterschied macht). Hier ist eine Beispiel-/etc/fstab-Zeile für die erwähnten ext3-Optionen: /dev/sda2 /usr/lib/mysql ext3 noatime,data=writeback 0 1
362 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Sie können auch das Read-Ahead-Verhalten des Dateisystems einstellen, weil dieses möglicherweise redundant ist. So führt z.B. InnoDB seine eigene Read-Ahead-Vorhersage durch. Das Deaktivieren oder Begrenzen des Read-Aheads lohnt sich besonders beim UFS von Solaris. Wenn man O_DIRECT verwendet, wird das Read-Ahead automatisch deaktiviert. Manche Dateisysteme bieten keine Unterstützung für Eigenschaften, die Sie vielleicht brauchen. Die Unterstützung für direkte Ein-/Ausgaben kann z.B. wichtig sein, wenn Sie die Übertragungsmethode O_DIRECT für InnoDB benutzen (siehe »Wie InnoDB Log- und Datendateien öffnet und leert« auf Seite 311). Außerdem kommen einige Dateisysteme besser mit einer großen Anzahl zugrunde liegender Laufwerke zurecht als andere; so ist etwa XFS dabei oft viel besser als ext3. Falls Sie schließlich vorhaben, LVM-Schnappschüsse zum Initialisieren von Slaves oder zum Erstellen von Backups zu benutzen, sollten Sie sicherstellen, dass das gewählte Dateisystem und die LVM-Version gut zusammenarbeiten. Tabelle 7-2 fasst die Eigenschaften einiger gebräuchlicher Dateisysteme zusammen. Tabelle 7-2: Eigenschaften gebräuchlicher Dateisysteme Dateisystem
Betriebssystem
Journaling
Große Verzeichnisse
ext2
GNU/Linux
Nein
Nein
ext3
GNU/Linux
Optional
Optional/teilweise
HFS Plus
Mac OS
Optional
Ja
JFS
GNU/Linux
Ja
Nein
NTFS
Windows
Ja
Ja
ReiserFS
GNU/Linux
Ja
Ja
UFS (Solaris)
Solaris
Ja
Einstellbar
UFS (FreeBSD)
FreeBSD
Nein
Optional/teilweise
UFS2
FreeBSD
Nein
Optional/teilweise
XFS
GNU/Linux
Ja
Ja
ZFS
Solaris, FreeBSD
Ja
Ja
Threading Seit Version 5.0 benutzt MySQL einen Thread pro Verbindung. Dazu kommen Threads zur Verwaltung, Threads für besondere Aufgaben und alle Threads, die die StorageEngine erzeugt. MySQL verlangt daher eine effiziente Unterstützung für eine große Anzahl von Threads. Es braucht Unterstützung für Threads auf Kernel-Ebene im Gegensatz zu Userland-Threads, damit es mehrere CPUs effizient einsetzen kann. Darüber hinaus benötigt es effiziente Synchronisierungsprimitive, wie etwa Mutexes. Die Threading-Bibliotheken des Betriebssystems müssen all das bereitstellen.
Threading | 363
GNU/Linux bietet zwei Thread-Bibliotheken: LinuxThreads und die neuere Native POSIX Threads Library (NPTL). LinuxThreads kommt in manchen Fällen noch zum Einsatz, die meisten modernen Distributionen sind allerdings auf NPTL umgestiegen, und in vielen ist LinuxThreads überhaupt nicht mehr enthalten. NPTL ist normalerweise leichter und effizienter und leidet nicht unter vielen der Probleme, die LinuxThreads besaß. Es hatte einige Performance-Bugs, aber die meisten der Ungereimtheiten wurden inzwischen beseitigt. FreeBSD bringt ebenfalls eine Reihe von Threading-Bibliotheken mit. Historisch gesehen, bot es eine eher schwache Unterstützung für das Threading, es ist aber inzwischen viel besser geworden und liefert in manchen Tests sogar bessere Leistungen als GNU/Linux auf SMP-Systemen. Seit FreeBSD 6 ist libthr die empfohlene Threading-Bibliothek. Frühere Versionen sollten linuxthreads benutzen, bei der es sich um eine FreeBSD-Portierung der LinuxThreads von GNU/Linux handelt. Solaris bietet eine sehr gute Unterstützung für Threads.
Swapping Swapping tritt auf, wenn das Betriebssystem einen Teil des virtuellen Speichers auf die Festplatte schreibt, weil es nicht mehr genug physischen Speicher besitzt, um ihn aufzunehmen.7 Swapping ist für Prozesse, die auf dem Betriebssystem laufen, transparent. Nur das Betriebssystem weiß, ob eine bestimmte Adresse im virtuellen Speicher sich im physischen Speicher oder auf der Festplatte befindet. Swapping ist für die Leistung von MySQL sehr schlecht. Es läuft dem Zweck des Caching im Speicher zuwider und resultiert in einer niedrigeren Effizienz, als wenn man zu wenig Speicher für die Caches benutzt. MySQL und seine Storage-Engines besitzen viele Algorithmen, die Daten im Speicher anders behandeln als Daten auf der Festplatte, da sie annehmen, dass die im Speicher befindlichen Daten billiger im Zugriff sind. Da das Swapping für die Benutzerprozesse unsichtbar abläuft, merkt es MySQL (oder die Storage-Engine) nicht, wenn Daten, von denen es glaubt, dass sie sich im Speicher befinden, tatsächlich auf die Festplatte verschoben werden. Das Ergebnis kann eine sehr schwache Performance sein. Falls z.B. die Storage-Engine glaubt, dass die Daten noch im Speicher liegen, könnte sie beschließen, dass es in Ordnung ist, einen globalen Mutex (wie etwa den InnoDB-Pufferpool-Mutex) für eine »kurze« Speicheroperation zu sperren. Verursacht diese Operation allerdings eine Festplatten-Ein-/Ausgabe, blockiert sie möglicherweise alles, bis die Ein-/Ausgabe abgeschlossen ist. Das bedeutet, dass Swapping viel schlimmer ist, als einfach bei Bedarf eine Ein-/Ausgabe auszuführen.
7 Swapping wird manchmal auch als Paging bezeichnet. Technisch gesehen, sind das verschiedene Dinge, die von manchen Leuten allerdings synonym benutzt werden.
364 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Unter GNU/Linux können Sie das Swapping mit vmstat beobachten (wir zeigen im nächsten Abschnitt einige Beispiele). Sie müssen sich die Swap-Ein-/Ausgabe-Aktivität anschauen, die in den Spalten si und so gezeigt wird, und nicht die Swap-Benutzung, die in der Spalte swpd zu sehen ist. Die Spalte swpd kann Prozesse zeigen, die geladen wurden, aber nicht benutzt werden, was eigentlich kein Problem darstellt. Wir wollen, dass die Werte in den Spalten si und so 0 sind; auf jeden Fall sollten sie unter 10 Blöcken pro Sekunde liegen. In Extremfällen kann ein zu starkes Swapping dafür sorgen, dass dem Betriebssystem der Swap-Space ausgeht. Falls das geschieht, bringt der resultierende Mangel an virtuellem Speicher MySQL normalerweise zum Absturz. Aber selbst wenn der Swap-Space nicht weniger wird, kann ein sehr aktives Swapping die Ursache dafür sein, dass das Betriebssystem nicht mehr reagiert, was möglicherweise so schlimm wird, dass Sie sich nicht einmal mehr anmelden und den MySQL-Prozess beenden können. Die meisten Swapping-Probleme können Sie lösen, indem Sie Ihre MySQL-Puffer richtig konfigurieren. Manchmal allerdings beschließt das virtuelle Speichersystem des Betriebssystems dennoch, MySQL per Swapping auszulagern. Normalerweise passiert das, wenn das Betriebssystem viele Ein-/Ausgaben von MySQL bemerkt, so dass es versucht, den Datei-Cache zu vergrößern, um mehr Daten aufzunehmen. Wenn es nicht genügend Speicher gibt, muss etwas ausgelagert werden, und das könnte MySQL selbst sein. Einige ältere Linux-Kernel-Versionen haben auch kontraproduktive Prioritäten, und lagern Dinge aus, wenn sie dies eigentlich nicht sollten. Das wurde aber in neueren Kernel-Versionen behoben. Manche Leute plädieren dafür, die Swap-Datei ganz zu deaktivieren. Das funktioniert zwar in extremen Fällen, wenn der Kernel sich einfach weigert, sich zu benehmen. Es kann aber auch die Leistung des Betriebssystems herabsetzen. (Theoretisch sollte es das nicht, aber praktisch kann es das.) Das ist außerdem gefährlich, weil dem virtuellen Speicher durch das Deaktivieren des Swappings eine unflexible Beschränkung auferlegt wird. Wenn MySQL bei seinen Speicheranforderungen eine temporäre Spitze zeigt oder mehrere speicherhungrige Prozesse auf derselben Maschine laufen (z.B. nächtliche BatchJobs), kann MySQL der Speicher ausgehen, es kann abstürzen oder vom Betriebssystem unsanft beendet werden. Betriebssysteme erlauben normalerweise eine gewisse Kontrolle über den virtuellen Speicher und die Ein-/Ausgaben. Wir erwähnen hier nur einige der Methoden, um sie unter GNU/Linux zu kontrollieren. Die einfachste Methode besteht darin, den Wert von /proc/sys/vm/swappiness auf einen niedrigen Wert, also etwa 0 oder 1, zu ändern. Dies weist den Kernel an, erst dann auszulagern, wenn der Bedarf an virtuellem Speicher außerordentlich ist. So prüft und ändert man z.B. den aktuellen Wert: $ cat /proc/sys/vm/swappiness 60 $ echo 0 > /proc/sys/vm/swappiness
Swapping | 365
Eine andere Möglichkeit ist, die Art und Weise zu ändern, wie die Storage-Engines Daten lesen und schreiben. Zum Beispiel lindert die Verwendung von innodb_flush_ method=O_DIRECT den Druck auf die Ein-/Ausgaben. Direkte Ein-/Ausgaben werden nicht im Cache gespeichert, so dass das Betriebssystem keinen Grund sieht, die Größe des Datei-Cache zu erhöhen. Dieser Parameter funktioniert nur für InnoDB, obwohl auch Falcon Unterstützung für direkte Ein-/Ausgaben bietet. Sie können auch große Seiten benutzen, die nicht swap-fähig sind. Das funktioniert für MyISAM und InnoDB. Eine weitere Möglichkeit besteht darin, die MySQL-Konfigurationsoption memlock zu benutzen, die MySQL im Speicher sperrt. Dadurch wird zwar das Swapping vermieden, es ist aber auch gefährlich: Wenn nicht genügend sperrfähiger Speicher übrig bleibt, kann MySQL abstürzen, während es versucht, mehr Speicher zu reservieren. Probleme können auch auftreten, wenn zu viel Speicher gesperrt ist und nicht genügend Speicher für das Betriebssystem bleibt. Viele der Tricks gelten für eine bestimmte Kernel-Version. Passen Sie also auf, vor allem, wenn Sie die Version wechseln. Bei manchen Lasten ist es schwierig, das Betriebssystem dazu zu bringen, sich vernünftig zu verhalten, so dass Ihre einzige Zuflucht unter Umständen darin liegt, die Puffergrößen auf suboptimale Werte zu verringern.
Der Betriebssystemstatus Ihr Betriebssystem bietet wahrscheinlich Werkzeuge, mit deren Hilfe Sie herausfinden können, was das Betriebssystem und die Hardware machen. Wir zeigen Ihnen Beispiele für den Einsatz zweier verbreiteter Werkzeuge, iostat und vmstat.8 Falls Sie diese auf Ihrem System nicht finden, gibt es dort mit hoher Wahrscheinlichkeit ähnliche Programme. Wir wollen deshalb nicht erreichen, dass Sie ein Experte in der Benutzung von iostat oder vmstat werden, sondern Ihnen einfach zeigen, wonach Sie suchen müssen, wenn Sie versuchen, Probleme mit solchen Werkzeugen zu diagnostizieren. Neben diesen Werkzeugen bietet Ihr Betriebssystem sicher noch andere an, wie etwa mpstat oder sar. Falls Sie sich für andere Teile Ihres Systems interessieren, wie etwa das Netzwerk, werden Sie stattdessen sicher Werkzeuge wie ifconfig (das Ihnen unter anderem zeigt, wie viele Netzwerkfehler aufgetreten sind) oder netstat einsetzen. Standardmäßig erzeugen vmstat und iostat einfach nur einen Bericht, der die Durchschnittswerte verschiedener Zähler seit dem Start des Servers zeigt, was nicht besonders sinnvoll ist. Sie können jedoch beiden Werkzeugen ein Intervallargument übergeben. Dadurch werden inkrementelle Berichte generiert, die zeigen, was der Server jetzt gerade 8 Wir haben hier vmstat und iostat gezeigt, weil sie weit verbreitet sind und zumindest vmstat normalerweise standardmäßig auf vielen Unix-artigen Betriebssystemen installiert ist. Allerdings haben beide Werkzeuge bestimmte Beschränkungen, etwa indem sie Maßeinheiten verwechseln, in Intervallen messen, die nicht mit den Zeitpunkten korrespondieren, zu denen das Betriebssystem die Statistiken aktualisiert, oder indem sie unfähig sind, alle Metriken auf einmal zu sehen. Falls diese Werkzeuge Ihren Anforderungen nicht genügen, dann sollten Sie sich einmal dstat (http://dag.wieers.com/home-made/dstat/) oder collectl (http://collectl.sourceforge.net/) anschauen.
366 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
macht, was für das Einstellen viel relevanter ist. (Die erste Zeile zeigt die Statistiken seit dem Start des Systems; diese Zeile können Sie getrost ignorieren.)
Wie man die vmstat-Ausgabe liest Schauen wir uns zuerst ein Beispiel für vmstat an. Mit dem folgenden Befehl veranlassen Sie es, alle fünf Sekunden einen neuen Report auszugeben: $ vmstat 5 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 2632 25728 23176 740244 0 0 527 521 11 3 10 1 86 3 0 0 2632 27808 23180 738248 0 0 2 430 222 66 2 0 97 0
Sie können vmstat mit Strg-C stoppen. Die Ausgabe, die Sie sehen, hängt von Ihrem Betriebssystem ab, ziehen Sie also gegebenenfalls die Manpage zu Rate. Wie bereits angemerkt wurde, zeigt die erste Zeile die Durchschnittswerte seit dem Booten des Servers, obwohl wir um eine inkrementelle Ausgabe gebeten haben. Die zweite Zeile zeigt, was gerade jetzt geschieht, und nachfolgende Zeilen zeigen, was in FünfSekunden-Intervallen passiert. Die Spalten werden nach den Headern gruppiert: procs
Die Spalte r zeigt, wie viele Prozesse auf CPU-Zeit warten. In der Spalte b steht, wie viele in unterbrechungsfreiem Schlaf liegen, was im Allgemeinen bedeutet, dass sie auf Ein-/Ausgaben warten (Festplatte, Netzwerk, Benutzereingabe usw.). memory
Die Spalte swpd zeigt, wie viele Blöcke auf die Festplatte ausgelagert werden (paged). Die verbleibenden drei Spalten zeigen, wie viele Blöcke frei (unbenutzt) sind, wie viele für Puffer benutzt werden und wie viele für den Cache des Betriebssystems zum Einsatz kommen. swap
Diese Spalten zeigen die Swap-Aktivität: Wie viele Blöcke pro Sekunde holt das Betriebssystem (von der Festplatte), und wie viele Blöcke lagert es aus (auf die Festplatte)? Es ist viel wichtiger, diese Spalten zu überwachen als die swpd-Spalte. Am liebsten ist es uns, wenn si und so die meiste Zeit 0 sind. Auf jeden Fall sollten sie nicht mehr als 10 Blöcke pro Sekunde zeigen. Spitzen sind auch schlecht. io
Diese Spalten zeigen, wie viele Blöcke pro Sekunde von Blockgeräten eingelesen (bi) und auf Blockgeräte herausgeschrieben (bo) werden. Normalerweise spiegelt das Festplatten-Ein-/Ausgaben wider. system
Diese Spalten zeigen die Anzahl der Interrupts pro Sekunde (in) und die Anzahl der Kontextwechsel pro Sekunde (cs).
Der Betriebssystemstatus | 367
cpu
Diese Spalten zeigen, welcher Prozentsatz der gesamten CPU-Zeit mit der Ausführung von Benutzercode, System-(Kernel-)Code, untätig (idle) und auf Ein-/Ausgaben wartend verbracht wird. Eine mögliche fünfte Spalte (st) zeigt die Prozent, die von einer virtuellen Maschine »gestohlen« werden, falls Sie mit Virtualisierung arbeiten. Dies bezieht sich auf die Zeit, während der etwas auf der virtuellen Maschine lauffähig war, wo aber der Hypervisor entschieden hat, etwas anderes stattdessen auszuführen. Wenn die virtuelle Maschine nichts ausführen möchte und der Hypervisor etwas anderes ausführt, zählt das nicht als gestohlene Zeit. Die vmstat-Ausgabe ist systemabhängig. Lesen Sie deshalb die vmstat(8)-Manpage Ihres Systems, falls Ihre Ausgabe anders aussieht als das hier gezeigte Beispiel. Ein wichtiger Hinweis: Die Speicher-, Swap- und Ein-/Ausgabe-Statistiken werden in Blöcken angegeben, nicht in Byte. In Linux sind Blöcke normalerweise 1.024 Byte groß.
Wie man die iostat-Ausgabe liest Kommen wir nun zu iostat.9 Standardmäßig zeigt es teilweise die gleichen Informationen über die CPU-Benutzung wie vmstat. Wir interessieren uns jedoch meist nur für die Ein/Ausgabe-Statistiken, so dass wir uns mit dem folgenden Befehl nur die erweiterten Gerätestatistiken anzeigen lassen: $ iostat -dx 5 Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda 1.6 2.8 2.5 1.8 138.8 36.9 40.7 0.1 23.2 6.0 2.6
Wie bei vmstat zeigt der erste Bericht Durchschnittswerte seit dem Booten des Servers (wir lassen sie im Allgemeinen weg, um Platz zu sparen), und in den nachfolgenden Berichten stehen inkrementelle Durchschnittswerte. Es gibt eine Zeile pro Gerät. Mit verschiedenen Optionen kann man Spalten ein- oder ausblenden. Wir zeigen hier folgende Spalten an: rrqm/s und wrqm/s
Die Anzahl der zusammengefassten Lese- und Schreibanforderungen pro Sekunde. »Zusammengefasst« bedeutet, dass das Betriebssystem logische Anforderungen genommen und sie zu einer einzigen Anforderung an das eigentliche Gerät gruppiert hat. r/s und w/s Die Anzahl der Lese- und Schreibanforderungen, die pro Sekunde an das Gerät geschickt werden. rsec/s und wsec/s Die Anzahl der Sektoren, die pro Sekunde gelesen und geschrieben werden. Manche Systeme geben auch rkB/s und wkB/s aus: die Anzahl der Kilobyte, die pro Sekunde gelesen und geschrieben wurden. Wir lassen das um der Kürze willen weg. 9 Die iostat-Beispiele, die wir in diesem Buch gezeigt haben, wurden für den Druck ein wenig neu formatiert. Wir haben die Anzahl der Dezimalstellen in den Werten verringert, um Zeilenumbrüche zu vermeiden.
368 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
avgrq-sz
Die Größe der Anforderung in Sektoren. avgqu-sz
Die Anzahl der Anforderungen, die in der Warteschlange des Gerätes warten. await
Die Anzahl an Millisekunden, die erforderlich sind, um auf Anforderungen zu warten, einschließlich der Warteschlangenzeit und der Bedienzeit. Leider trennt iostat die Bedienzeitstatistiken für Lese- und Schreibanforderungen nicht, die so unterschiedlich sind, dass sie eigentlich nicht zu einem gemeinsamen Durchschnittswert verarbeitet werden dürften. Sie können aber wahrscheinlich zu den Leseanforderungen einfach noch hohe Ein-/Ausgabe-Wartezeiten hinzurechnen, da Schreibanforderungen oft gepuffert werden können, während das Lesen normalerweise direkt von den Festplatten bedient werden muss. svctm
Die Anzahl der Millisekunden, die mit dem Bedienen von Anforderungen verbracht werden, einschließlich der Warteschlangenzeit und der Zeit, die das Gerät dann tatsächlich braucht, um die Anforderung zu erfüllen. %util
Der Prozentsatz an CPU-Zeit, während der Anforderungen ausgelöst werden. Damit wird die tatsächliche Gerätebenutzung gezeigt: Wenn der Wert 100 % erreicht, ist das Gerät gesättigt. Sie können mit der Ausgabe versuchen, Fakten über das Ein-/Ausgabe-Subsystem einer Maschine abzuleiten. Ein wichtiges Maß ist die Anzahl der parallel bedienten Anforderungen. Da die Lese- und Schreibvorgänge pro Sekunde angegeben werden und die Einheit der Bedienzeit Tausendstel einer Sekunde ist, heben sich die Dimensionen in der folgenden Formel auf und zeigen die Anzahl der parallelen Anforderungen, die das Gerät bedient:10 Nebenläufigkeit = (r/s + w/s) * (svctm/1000)
Hier ist ein Ausschnitt aus einer iostat-Ausgabe: Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda 105 311 298 820 3236 9052 10 127 113 9 96
Wenn man die Zahlen in die gezeigte Formel einsetzt, ergibt sich eine Nebenläufigkeit von etwa 9,6.11 Das bedeutet, dass das Gerät im Durchschnitt 9,6 Anforderungen auf einmal während des gemessenen Intervalls bedient hat. Der Ausschnitt stammt von einem RAID 10-Volume mit 10 Festplatten, das Betriebssystem parallelisiert die Anforderungen
10 Eine andere Möglichkeit, die Nebenläufigkeit zu berechnen, ist mit der durchschnittlichen Warteschlangengröße, der Bedienzeit und der durchschnittlichen Wartezeit: (avuqu_sz * svctm) / await. 11 Wenn Sie nachrechnen, erhalten Sie etwa 10, weil wir die iostat-Ausgabe zur besseren Formatierung gerundet haben. Glauben Sie uns, es ist wirklich 9,6.
Der Betriebssystemstatus | 369
also ganz gut auf diesem Gerät. Im Gegensatz dazu ist hier ein Gerät, das stattdessen Anforderungen zu serialisieren scheint: Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sdc 81 0 280 0 3164 0 11 2 7 3 99
Die Nebenläufigkeitsformel zeigt, dass dieses Gerät wirklich nur eine Anforderung pro Sekunde verarbeitet. Beide Geräte sind fast vollständig ausgelastet, liefern aber unterschiedliche Leistungen ab. Falls Ihr Gerät fast die ganze Zeit beschäftigt ist, dann überprüfen Sie die Nebenläufigkeit, und stellen Sie fest, ob diese annähernd der Anzahl der physischen Festplatten in dem Gerät entspricht. Eine niedrigere Zahl kann auf Probleme hindeuten.
Eine CPU-gebundene Maschine Die vmstat-Ausgabe für einen CPU-gebundenen Server zeigt normalerweise einen hohen Wert in der us-Spalte, die die Zeit angibt, die mit dem Ausführen von Benutzercode (Nicht-Kernel-Code) verbracht wird. Meist gibt es außerdem mehrere Prozesse, die auf CPU-Zeit warten (angegeben in der Spalte r). Hier ist ein Ausschnitt: $ vmstat 5 procs -----------memory------------swap-- -----io---- --system-- ----cpu---r b swpd free buff cache si so bi bo in cs us sy id wa 10 2 740880 19256 46068 13719952 0 0 2788 11047 1423 14508 89 4 4 3 11 0 740880 19692 46144 13702944 0 0 2907 14073 1504 23045 90 5 2 3 7 1 740880 20460 46264 13683852 0 0 3554 15567 1513 24182 88 5 3 3 10 2 740880 22292 46324 13670396 0 0 2640 16351 1520 17436 88 4 4 3
Beachten Sie, dass viele Kontextwechsel auftreten (siehe die Spalte cs). Ein Kontextwechsel findet statt, wenn das Betriebssystem einen Prozess in der Ausführung stoppt und ihn durch einen anderen ersetzt. Wenn wir uns die iostat-Ausgabe für die gleiche Maschine anschauen (auch hier wurde die erste Zeile weggelassen, die Durchschnittswerte seit dem Booten zeigt), können Sie sehen, dass die Festplattenausnutzung unter 50 % liegt: $ iostat -dx 5 Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda 0 3859 54 458 2063 34546 71 3 6 1 47 dm-0 0 0 54 4316 2063 34532 8 18 4 0 47 Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda 0 2898 52 363 1767 26090 67 3 7 1 45 dm-0 0 0 52 3261 1767 26090 8 15 5 0 45
Diese Maschine ist nicht ein-/ausgabegebunden, führt aber dennoch viele Ein-/Ausgaben durch, was für einen Datenbankserver nicht ungewöhnlich ist. Andererseits verbraucht ein typischer Webserver viele CPU-Ressourcen, aber nur wenige Ein-/Ausgaben, so dass die Ausgabe für einen Webserver meist anders aussieht als dieses Beispiel.
370 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
Eine ein-/ausgabegebundene Maschine Bei einer ein-/ausgabegebundenen Last verbringen die CPUs viel Zeit damit, zu warten, dass Ein-/Ausgabe-Anforderungen abgeschlossen werden. Das bedeutet, dass vmstat viele Prozesse in unterbrechungsfreiem Schlaf zeigt (die Spalte b) und einen hohen Wert in der Spalte wa angibt. Hier ist ein Beispiel: $ vmstat 5 procs -----------memory------------swap-r b swpd free buff cache si so 5 7 740632 22684 43212 13466436 0 0 5 7 740632 22748 43396 13465436 0 0 1 8 740632 22380 43416 13464192 0 0 5 6 740632 22116 43512 13463484 0 0
-----io---bi bo 6738 17222 6150 17025 4582 21820 5955 21158
--system-in cs 1738 16648 1731 16713 1693 15211 1732 16187
----cpu---us sy id wa 19 3 15 63 18 4 21 58 16 4 24 56 17 4 23 56
Die iostat-Ausgabe dieser Maschine zeigt, dass die Festplatten vollständig gesättigt sind: $ iostat -dx 5 Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda 0 5396 202 626 7319 48187 66 12 14 1 101 dm-0 0 0 202 6016 7319 48130 8 57 9 0 101 Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util sda 0 5810 184 665 6441 51825 68 11 13 1 102 dm-0 0 0 183 6477 6441 51817 8 54 7 0 102
Der %util-Wert kann aufgrund von Rundungsfehlern größer als 100 % sein. Was bedeutet es, wenn eine Maschine ein-/ausgabegebunden ist? Wenn es genügend Pufferkapazität gibt, um Schreibanforderungen zu bedienen, dann bedeutet es im Allgemeinen – aber nicht immer –, dass die Festplatten nicht mit den Leseanforderungen schritthalten können, selbst wenn die Maschine viele Schreiboperationen ausführt. Das scheint jetzt nicht sehr intuitiv zu klingen, aber denken Sie an das Wesen der Lese- und Schreiboperationen: • Schreibanforderungen können entweder gepuffert oder synchron sein. Sie können auf jeder der Ebenen, die wir irgendwo in diesem Buch besprochen haben, gepuffert werden: im Betriebssystem, dem RAID-Controller usw. • Leseanforderungen sind von Natur aus synchron. Es ist möglich, dass ein Programm vorhersieht, dass es einige Daten braucht, und deshalb eine asynchrone Prefetch-(Read-Ahead-)Anforderung auslöst. Meist jedoch bemerken Programme, dass sie Daten brauchen, bevor sie mit der Arbeit fortfahren können. Daher muss die Anforderung synchron sein: Das Programm muss blockieren, bis die Anforderung abgeschlossen ist. Stellen Sie es sich so vor: Sie können eine Schreibanforderung auslösen, die irgendwo in einen Puffer gepackt und später abgeschlossen wird. Von diesen Schreibanforderungen können Sie sogar viele pro Sekunde auslösen. Wenn der Puffer ordentlich funktioniert und genug Platz besitzt, können die einzelnen Anforderungen sehr schnell abgeschlossen werden, und die eigentlichen Schreiboperationen können zu einem Stapel zusammengefasst und aus Effizienzgründen umsortiert werden.
Der Betriebssystemstatus | 371
Für Leseanforderungen gilt das jedoch nicht. Egal, wie wenige oder wie kleine Anforderungen es sind, es ist nicht möglich, dass die Festplatte mit »Hier sind Deine Daten, ich lese dann später einmal« antwortet. Deshalb sind Leseanforderungen normalerweise für das Warten auf Ein-/Ausgaben verantwortlich.
Eine auslagernde Maschine Eine Maschine, die Daten auslagert (Swapping), kann einen hohen Wert in der swpdSpalte zeigen (oder auch nicht). Allerdings finden Sie hohe Werte in den Spalten si und so, was Sie eigentlich nicht wollen. So sieht die vmstat-Ausgabe bei einer Maschine aus, die viel auslagert: $ vmstat 5 procs -----------memory------------- ---swap---- -----io---- --system-r b swpd free buff cache si so bi bo in cs 0 10 3794292 24436 27076 14412764 19853 9781 57874 9833 4084 8339 4 11 3797936 21268 27068 14519324 15913 30870 40513 30924 3600 7191 0 37 3847364 20764 27112 14547112 171 38815 22358 39146 2417 4640
----cpu---us sy id wa 6 14 58 22 6 11 36 47 6 8 9 77
Eine untätige Maschine Der Vollständigkeit halber sehen Sie hier die vmstat-Ausgabe auf einer untätigen (idle) Maschine. Beachten Sie, dass es keine lauffähigen oder blockierten Prozesse gibt. Die Spalte idle zeigt, dass die CPUs zu 100 % untätig sind. Dieser Ausschnitt stammt von einer Maschine, auf der Red Hat Enterprise Linux 5 läuft, und zeigt die st-Spalte, d.h. Zeit, die von einer virtuellen Maschine »gestohlen« wurde: $ vmstat 5 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 108 492556 6768 360092 0 0 345 209 2 65 2 0 97 1 0 0 0 108 492556 6772 360088 0 0 0 14 357 19 0 0 100 0 0 0 0 108 492556 6776 360084 0 0 0 6 355 16 0 0 100 0 0
372 | Kapitel 7: Betriebssystem- und Hardwareoptimierung
KAPITEL 8
Replikation
Die in MySQL integrierte Fähigkeit zur Replikation ist die Grundlage für den Aufbau großer, leistungsstarker Anwendungen auf MySQL-Basis. Die Replikation erlaubt es Ihnen, einen oder mehrere Server als Slaves oder Repliken eines anderen Servers zu konfigurieren. Das ist nicht nur für High-Performance-Anwendungen sinnvoll, sondern auch für viele andere Aufgaben, wie etwa das gemeinsame Benutzen von Daten mit einem externen Büro, das Vorhalten eines »Hot Spare« oder das Betreiben eines Servers mit einer Kopie der Daten zu Test- oder Schulungszwecken. Wir untersuchen in diesem Kapitel alle Aspekte der Replikation. Wir beginnen mit einem Überblick über die Funktionsweise, schauen uns dann die grundlegenden Servereinstellungen, die Gestaltung aufwendigerer Replikationskonfigurationen sowie die Verwaltung und Optimierung der replizierten Server an. Obwohl wir uns in diesem Buch im Allgemeinen vor allem auf die Performance konzentrieren, kümmern wir uns in Bezug auf die Replikation auch um die Korrektheit und die Zuverlässigkeit. Außerdem schauen wir uns einige künftige Änderungen und Verbesserungen bei der MySQL-Replikation an, wie etwa interessante Patches, die von Google geschaffen wurden.
Replikation im Überblick Das Grundproblem, das mithilfe der Replikation gelöst wird, ist Folgendes: Wie hält man die Daten eines Servers synchron zu den Daten eines anderen? Viele Slaves können sich mit einem einzigen Master verbinden, und ein Slave kann wiederum als Master auftreten. Sie können Master und Slaves in vielen unterschiedlichen Topologien anordnen. Sie können den gesamten Server oder nur bestimmte Datenbanken replizieren oder sogar wählen, welche Tabellen Sie replizieren wollen. MySQL unterstützt zwei Arten der Replikation: anweisungsbasierte Replikation und zeilenbasierte Replikation. Die anweisungsbasierte (oder »logische«) Replikation gibt es seit MySQL 3.23. Sie wird von den meisten Leuten heutzutage in der Praxis eingesetzt. Zeilenbasierte Replikation ist dagegen neu in MySQL 5.1. Bei beiden Arten werden Änderungen
| 373
im Binärlog des Masters1 aufgezeichnet, und das Log wird dann auf dem Slave wieder abgespielt. Beide sind asynchron – d.h., es gibt keine Garantie, dass die Kopie der Daten auf dem Slave in jedem Moment auf dem neuesten Stand ist.2 Es gibt keine Gewähr, wie groß die Latenz auf dem Slave sein könnte. Große Abfragen könnten dafür sorgen, dass der Slave Sekunden, Minuten oder sogar Stunden hinter den Master zurückfällt. Die Replikation ist in MySQL meist abwärtskompatibel. Das heißt, dass ein neuerer Server normalerweise ohne Probleme der Slave eines älteren Servers sein kann. Ältere Versionen des Servers dagegen sind oft nicht in der Lage, als Slaves der neueren Versionen zu dienen: Sie verstehen möglicherweise neue Funktionen oder die SQL-Syntax nicht, die neuere Server benutzen, außerdem können sich die bei der Replikation verwendeten Dateiformate unterscheiden. So können Sie z.B. nicht von einem MySQL-5.0-Master zu einem MySQL-4.0-Slave replizieren. Testen Sie auf jeden Fall Ihre Replikationseinstellung, bevor Sie von einer Hauptversion auf eine andere wechseln, also etwa von 4.1 auf 5.0 oder von 5.0 auf 5.1. Die Replikation erhöht im Allgemeinen den Aufwand auf dem Master nicht sehr. Auf dem Master muss das binäre Logging aktiviert sein, das recht aufwendig sein kann, allerdings brauchen Sie das sowieso für ordentliche Backups. Abgesehen vom Binär-Logging bringt beim normalen Betrieb auch jeder angeschlossene Slave eine gewisse zusätzliche Last auf dem Master mit sich (hauptsächlich Netzwerk-Ein-/Ausgaben). Replikation eignet sich relativ gut zum Skalieren von Leseoperationen, die Sie an einen Slave umleiten können, ist allerdings nur dann eine gute Methode zum Skalieren von Schreibvorgängen, wenn Sie sie richtig gestaltet haben. Beim Anschließen vieler Slaves an einen Master werden die Schreiboperationen lediglich viele Male, nämlich einmal auf jedem Slave, ausgeführt. Das gesamte System ist auf die Anzahl der Schreiboperationen beschränkt, die der schwächste Teil ausführen kann. Auch wenn man mehr als nur ein paar Slaves hat, ist eine Replikation eine Verschwendung, weil dabei im Prinzip eine Menge Daten unnötigerweise dupliziert werden. So besitzt z.B. ein einziger Master mit 10 Slaves 11 Kopien derselben Daten und dupliziert den größten Teil dieser Daten in 11 unterschiedliche Caches. Das ist analog zu einem 11fachen RAID 1 auf der Serverebene. Es ist keine ökonomische Anwendung der Hardware, kommt aber überraschend oft vor. Wir werden in diesem Kapitel Methoden vorstellen, um dieses Problem zu umgehen.
1 Wenn das Binärlog neu für Sie ist, finden Sie in Kapitel 6, in diesem Kapitel und in Kapitel 11 weitere Informationen. 2 Mehr dazu finden Sie in »Synchrone MySQL-Replikation« auf Seite 492.
374 | Kapitel 8: Replikation
Probleme, die durch die Replikation gelöst werden Hier sind einige der gebräuchlichsten Anwendungsfälle für die Replikation: Datenverteilung Die Replikation von MySQL ist normalerweise nicht sehr bandbreitenintensiv,3 und Sie können sie nach Belieben stoppen und starten. Sie eignet sich daher, um eine Kopie Ihrer Daten an einem räumlich entfernten Ort anzulegen, wie etwa in einem anderen Rechenzentrum. Der entfernte Slave kann sogar über eine Verbindung agieren, die nur sporadisch existiert (ob absichtlich oder nicht). Falls Ihre Slaves allerdings nur eine geringe Verzögerung bei der Replikation haben sollen, brauchen Sie eine stabile Verbindung mit geringer Latenz. Lastausgleich Die MySQL-Replikation kann Ihnen dabei helfen, Leseabfragen über mehrere Server zu verteilen, was auch gut für leseintensive Anwendungen funktioniert. Mit einigen einfachen Codeänderungen erreichen Sie sogar einen einfachen Lastausgleich. In kleinem Maßstab benutzen Sie vereinfachte Ansätze, wie fest kodierte Hostnamen oder Lastverteilung per DNS (bei dem ein Hostname auf mehrere IP-Adressen zeigt). Sie können auch raffiniertere Ansätze verfolgen. Normale Lösungen zum Lastausgleich, wie etwa Produkte zum Lastausgleich in Netzwerken, können die Last zwischen MySQL-Servern verteilen. Auch das LVS-Projekt (Linux Virtual Server) funktioniert ganz gut. Wir befassen uns in Kapitel 9 mit Lastausgleich. Backups Replikation bietet eine wertvolle Technik zum Unterstützen von Backups. Allerdings stellt ein Slave weder ein Backup noch einen Ersatz für Backups dar. Hochverfügbarkeit und Failover Mithilfe der Replikation können Sie vermeiden, dass MySQL der einzige Schwachpunkt in Ihrer Anwendung ist. Mit einem guten Failover-System (System zur Ausfallsicherung) mit replizierten Slaves können Sie die Ausfallzeiten drastisch verringern. Failover behandeln wir ebenfalls in Kapitel 9. Testen von MySQL-Upgrades Es ist üblich, einen Slave-Server mit einer aufgerüsteten MySQL-Version auszustatten und mit seiner Hilfe zu überprüfen, ob die Abfragen erwartungsgemäß funktionieren, bevor man alle anderen Server umrüstet.
Wie die Replikation funktioniert Bevor wir uns detailliert damit befassen, wie man die Replikation einrichtet, wollen wir uns anschauen, wie MySQL tatsächlich Daten repliziert. Global gesehen, handelt es sich bei der Replikation um einen einfachen dreiteiligen Vorgang: 3 Obwohl, wie wir später sehen werden, die zeilenbasierte Replikation, die in MySQL 5.1 eingeführt wurde, viel mehr Bandbreite benutzen könnte als die traditionellere, anweisungsbasierte Replikation.
Replikation im Überblick | 375
1. Der Master zeichnet die Änderungen an seinen Daten in seinem Binärlog auf. (Diese
Aufzeichnungen werden als Binärlog-Events bezeichnet.) 2. Der Slave kopiert die Binärlog-Events des Masters in sein Relay-Log. 3. Der Slave spielt die Ereignisse im Relay-Log noch einmal ab und wendet damit die
Änderungen auf seine eigenen Daten an. Das ist nur der Überblick – die einzelnen Schritte sind relativ komplex. Abbildung 8-1 verdeutlicht die Replikation im Detail. Master
Slave Ein/AusgabeThread
Datenänderungen
SQL-Thread
Lesen Lesen
Schreiben Binärlog
Wiederabspielen RelayLog
Abbildung 8-1: Wie die MySQL-Replikation funktioniert
Der erste Teil dieses Vorgangs ist das Schreiben in das Binärlog auf dem Master (wir zeigen Ihnen später, wie Sie das einrichten). Direkt, bevor eine Transaktion, die Daten auf dem Master aktualisiert, fertig wird, schreibt der Master die Änderungen in sein Binärlog. MySQL schreibt die Transaktionen seriell in das Binärlog, auch wenn die Anweisungen in den Transaktionen während der Ausführung verschachtelt wurden. Nachdem die Events in das Binärlog geschrieben wurden, weist der Master die Storage-Engine(s) an, die Transaktionen zu bestätigen. Im nächsten Schritt muss der Slave das Binärlog des Masters auf seine eigene Festplatte kopieren, und zwar in das sogenannte Relay-Log. Zuerst startet er einen Arbeits-Thread, den Ein-/Ausgabe-Slave-Thread. Der Ein-/Ausgabe-Thread öffnet eine normale Clientverbindung zum Master und startet dann einen speziellen Binlog-Dump-Prozess (es gibt dazu keinen SQL-Befehl). Der Binlog-Dump-Prozess liest die Events aus dem Binärlog des Masters. Er fragt nicht nach Events. Wenn er mit dem Master fertig ist, wird er zurückgestellt und wartet auf das Signal des Masters, das besagt, dass es wieder neue Events gibt. Der Ein-/Ausgabe-Thread schreibt die Events in das Relay-Log des Slaves.
376 | Kapitel 8: Replikation
Vor MySQL 4.0 funktionierte die Replikation in vielen Aspekten ganz anders. Zum Beispiel verwendete die erste Replikationsfunktionalität von MySQL kein Relay-Log, so dass bei der Replikation nur zwei Threads zum Einsatz kamen und nicht drei. Die meisten Leute verwenden inzwischen aber neuere MySQL-Versionen, wir werden uns daher in diesem Kapitel nicht über die sehr alten Versionen von MySQL auslassen.
Der SQL-Slave-Thread erledigt den letzten Teil des Vorgangs. Dieser Thread liest die Events aus dem Relay-Log und spielt sie wieder ab, wobei er die Daten des Slaves aktualisiert, damit sie schließlich denen des Masters entsprechen. Solange dieser Thread mit dem Ein-/Ausgabe-Thread Schritt hält, bleibt das Relay-Log normalerweise im Cache des Betriebssystems, Relay-Logs verursachen also einen sehr geringen Overhead. Die Events, die der SQL-Thread ausführt, können optional in das Slave-eigene Binärlog übernommen werden. Wir kommen später auf Szenarien zurück, bei denen sich das als sinnvoll erweist. Abbildung 8-1 zeigte nur zwei Replikations-Threads auf dem Slave. Es gibt aber außerdem einen Thread auf dem Master: Wie jede Verbindung zu einem MySQL-Server startet die Verbindung, die der Slave zum Master öffnet, einen Thread auf dem Master. Diese Replikationsarchitektur koppelt die Vorgänge des Holens und des Abspielens von Events auf dem Slave voneinander ab, wodurch sie asynchron ausgeführt werden können. Das heißt, der Ein-/Ausgabe-Thread kann unabhängig vom SQL-Thread arbeiten. Sie erlegt dem Replikationsprozess außerdem Beschränkungen auf, von denen die wichtigste lautet, dass die Replikation auf dem Slave serialisiert wird. Das bedeutet, dass Aktualisierungen, die auf dem Master möglicherweise parallel (in unterschiedlichen Threads) ausgeführt wurden, auf dem Slave nicht parallelisiert werden können. Wie wir später sehen werden, ist das für viele Lasten ein möglicher Performance-Engpass.
Die Replikation einrichten Das Einrichten der Replikation ist in MySQL ein relativ einfacher Vorgang, es gibt für die grundlegenden Schritte allerdings viele Variationen, die vom jeweiligen Szenario abhängen. Das einfachste Szenario sind frisch installierte Master und Slaves. Auf einer höheren Ebene sieht das Vorgehen so aus: 1. einrichten der Replikations-Accounts auf jedem Server 2. konfigurieren von Master und Slave 3. den Slave anweisen, eine Verbindung zum Master herzustellen und ihn zu replizieren
Hier wird davon ausgegangen, dass viele Standardeinstellungen ausreichen, was in der Tat zutrifft, wenn Sie den Master und den Slave gerade installiert haben und sie die gleichen Daten aufweisen (die vorgegebene mysql-Datenbank). Wir zeigen Ihnen, wie Sie die einzelnen Schritte ausführen. Dabei nehmen wir an, dass Ihre Server server1 (IP-Adresse 192.168.0.1) und server2 (IP-Adresse 192.168.0.2) heißen. Anschließend erläutern wir, wie man einen Slave von einem Server aus initialisiert, der bereits läuft, und untersuchen die empfohlene Replikationskonfiguration.
Die Replikation einrichten | 377
Replikations-Accounts anlegen MySQL verfügt über einige besondere Berechtigungen, die den Replikationsprozessen die Ausführung erlauben. Der Slave-Ein-/Ausgabe-Thread, der auf dem ReplikationsSlave-Server läuft, stellt eine TCP/IP-Verbindung zum Master her. Sie müssen also einen Benutzer-Account auf dem Master anlegen und diesem die richtigen Berechtigungen verleihen, damit der Ein-/Ausgabe-Thread sich als dieser Benutzer anmelden und das Binärlog des Masters lesen kann. Wir erzeugen hier einen Benutzer-Account namens repl: mysql> GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* -> TO repl@'192.168.0.%' IDENTIFIED BY 'p4ssword';
Diesen Account legen wir sowohl auf dem Master als auch auf dem Slave an. Beachten Sie, dass wir den Benutzer auf das lokale Netzwerk beschränkt haben, weil der Replikations-Account unsicher ist. (In Kapitel 12 erfahren Sie mehr über die Sicherheit.) Der Replikationsbenutzer benötigt eigentlich nur die Berechtigung REPLICATION CLIENT auf dem Master; die Berechtigung REPLICATION SLAVE auf beiden Servern ist nicht erforderlich. Weshalb also gewähren wir diese Berechtigungen auf beiden Servern? Das hat zwei Gründe: • Der Account, den Sie benutzen, um die Replikation zu überwachen und zu verwalten, braucht die Berechtigung REPLICATION SLAVE. Es ist einfacher, den gleichen Account für beide Zwecke einzusetzen, anstatt einen eigenen Benutzer für diese Aufgabe anzulegen. • Wenn Sie den Account auf dem Master einrichten und dann den Slave vom Master klonen, wird der Slave korrekterweise so eingerichtet, dass er als Master agiert, falls Sie irgendwann einmal die Rollen von Master und Slave vertauschen wollen.
Master und Slave konfigurieren Der nächste Schritt besteht darin, einige Einstellungen auf dem Master zu aktivieren, der bei uns den Namen server1 tragen soll. Sie müssen das Binär-Logging einschalten und eine Server-ID angeben. Setzen Sie die folgenden Zeilen in die my.cnf-Datei des Masters (oder überprüfen Sie, ob diese Zeilen vorhanden sind): log_bin server_id
= mysql-bin = 10
Die exakten Werte müssen Sie selbst einsetzen. Wir nehmen hier den einfachsten Weg, Sie können natürlich etwas Raffinierteres verwenden. Sie müssen explizit eine eindeutige Server-ID festlegen. Wir haben beschlossen, 10 anstelle von 1 zu nehmen, weil 1 die Vorgabe ist, die ein Server typischerweise wählt, wenn kein Wert angegeben wurde. (Das ist versionsabhängig; manche MySQL-Versionen funktionieren dann einfach nicht.) Die 1 kann deshalb leicht zu Verwirrung und Konflikten mit Servern führen, die keine expliziten Server-IDs besitzen. Oft wird das letzte Oktett der IPAdresse des Servers benutzt, vorausgesetzt natürlich, dass die Adresse sich nicht ändert und eindeutig ist (d.h., dass die Server nur zu einem Subnetz gehören).
378 | Kapitel 8: Replikation
Wenn das Binär-Logging noch nicht in der Konfigurationsdatei des Masters angegeben war, müssen Sie MySQL neu starten. Um sicherzustellen, dass die Binärlog-Datei auf dem Master angelegt wurde, führen Sie SHOW MASTER STATUS aus und überprüfen, ob Sie eine Ausgabe erhalten, die der folgenden Ausgabe ähnelt. (MySQL hängt einige Ziffern an den Dateinamen an, Sie sehen daher bei der Datei nicht exakt den Namen, den Sie angegeben haben.) mysql> SHOW MASTER STATUS; +------------------+----------+--------------+------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | +------------------+----------+--------------+------------------+ | mysql-bin.000001 | 98 | | | +------------------+----------+--------------+------------------+ 1 row in set (0.00 sec)
Der Slave verlangt in seiner my.cnf-Datei eine Konfiguration ähnlich der des Masters, außerdem müssen Sie MySQL auf dem Slave neu starten: log_bin server_id relay_log log_slave_updates read_only
= = = = =
mysql-bin 2 mysql-relay-bin 1 1
Technisch gesehen, sind einige dieser Optionen nicht erforderlich, und für einige geben wir einfach nur explizit die Vorgabewerte an. Tatsächlich wird auf einem Slave nur der Parameter server_id verlangt, wir haben aber auch log_bin aktiviert und der BinärlogDatei einen expliziten Namen gegeben. Standardmäßig wird sie nach dem Hostnamen des Servers benannt, das kann aber zu Problemen führen, wenn sich der Hostname ändert. Außerdem wollen wir, dass die Logs jedes Servers gleich heißen, um eine einfache Slave-zu-Master-Umwandlung zu erlauben. Das heißt, nicht nur der Replikationsbenutzer auf Master und Slave ist gleich, sondern auch die Einstellungen für beide. Wir haben außerdem zwei weitere optionale Konfigurationsparameter hinzugefügt: relay_log (um die Lage und den Namen des Relay-Logs anzugeben) und log_slave_ updates (damit der Slave die replizierten Events in sein eigenes Binärlog schreiben kann).
Die zweite Option verursacht auf den Slaves zusätzliche Arbeit, doch wie Sie später sehen werden, haben wir gute Gründe dafür, diese optionalen Einstellungen auf jedem Slave hinzuzufügen. Manche Leute aktivieren nur das Binärlog und nicht log_slave_updates, so dass sie es merken, ob irgendetwas, wie etwa eine fehlkonfigurierte Anwendung, Daten auf dem Slave modifiziert. Falls möglich sollten Sie die Konfigurationseinstellung read_only benutzen, die verhindert, dass andere als die besonders berechtigten Threads Daten ändern. (Gewähren Sie Ihren Benutzern nicht mehr Berechtigungen als nötig!) Allerdings ist read_only oft nicht praktisch, vor allem nicht für Anwendungen, die in der Lage sein müssen, Tabellen auf Slaves zu erzeugen.
Die Replikation einrichten | 379
Setzen Sie die Replikationskonfigurationsoptionen, wie etwa master_host und master_port, nicht in die my.cnf-Datei des Slaves. Diese alte Methode der Konfiguration eines Slaves wird nicht mehr empfohlen. Sie kann Probleme verursachen und bringt keine Vorteile mit sich.
Den Slave starten Der nächste Schritt besteht darin, dass man dem Slave mitteilt, wie er sich mit dem Server verbinden und seine Binärlogs abspielen soll. Benutzen Sie dafür nicht die Datei my.cnf, sondern nehmen Sie die Anweisung CHANGE MASTER TO. Diese Anweisung ersetzt die entsprechenden my.cnf-Einstellungen komplett. Sie erlaubt es Ihnen auch, den Slave in Zukunft auf einen anderen Master zu richten, ohne den Server zu stoppen. Hier ist die Anweisung, die Sie zum Start der Replikation auf dem Slave ausführen müssen: mysql> -> -> -> ->
CHANGE MASTER TO MASTER_HOST='server1', MASTER_USER='repl', MASTER_PASSWORD='p4ssword', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=0;
Der Parameter MASTER_LOG_POS ist auf 0 gesetzt, weil dies der Anfang des Logs ist. Nachdem Sie dies ausgeführt haben, sollten Sie anhand der Ausgabe von SHOW SLAVE STATUS feststellen können, dass die Einstellungen des Slaves korrekt sind: mysql> SHOW SLAVE STATUS\G *************************** 1. row *************************** Slave_IO_State: Master_Host: server1 Master_User: repl Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000001 Read_Master_Log_Pos: 4 Relay_Log_File: mysql-relay-bin.000001 Relay_Log_Pos: 4 Relay_Master_Log_File: mysql-bin.000001 Slave_IO_Running: No Slave_SQL_Running: No ...omitted... Seconds_Behind_Master: NULL
Die Spalten Slave_IO_State, Slave_IO_Running und Slave_SQL_Running zeigen, dass die Slave-Prozesse nicht laufen. Scharfsinnige Leser werden auch bemerken, dass die LogPosition 4 anstelle von 0 ist. Das liegt daran, dass 0 eigentlich keine Log-Position ist; sie bedeutet nur »am Anfang der Log-Datei«. MySQL weiß, dass das erste Event tatsächlich an Position 4 kommt.4
4 Wie Sie an der früheren Ausgabe von SHOW MASTER STATUS erkennen können, befindet es sich tatsächlich an Position 98. Master und Slave machen das miteinander aus, sobald der Slave sich mit dem Master verbunden hat, was noch nicht geschehen ist.
380 | Kapitel 8: Replikation
Um die Replikation zu starten, führen Sie folgenden Befehl aus: mysql> START SLAVE;
Dieser Befehl erzeugt keine Fehlermeldungen oder Ausgaben. Untersuchen Sie jetzt noch einmal SHOW SLAVE STATUS: mysql> SHOW SLAVE STATUS\G *************************** 1. row *************************** Slave_IO_State: Waiting for master to send event Master_Host: server1 Master_User: repl Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000001 Read_Master_Log_Pos: 164 Relay_Log_File: mysql-relay-bin.000001 Relay_Log_Pos: 164 Relay_Master_Log_File: mysql-bin.000001 Slave_IO_Running: Yes Slave_SQL_Running: Yes ...omitted... Seconds_Behind_Master: 0
Sie sehen, dass sowohl die Slave-Ein-/Ausgabe- als auch die SQL-Threads laufen und dass Seconds_Behind_Master nicht mehr NULL ist (wir untersuchen später, was Seconds_Behind_Master bedeutet). Der Ein-/Ausgabe-Thread wartet auf ein Event vom Master, was bedeutet, dass er alle Binärlogs des Masters geholt hat. Die Log-Positionen haben sich erhöht, es wurden also einige Events geholt und ausgeführt (Ihre Ergebnisse werden anders aussehen). Wenn Sie auf dem Master eine Änderung vornehmen, sollten Sie sehen, dass sich die verschiedenen Datei- und Positionseinstellungen auf dem Slave erhöhen. Sie sollten auch Änderungen an den Datenbanken auf dem Slave feststellen können! Sie erkennen auch die Replikations-Threads in den jeweiligen Prozesslisten auf dem Master und dem Slave. Auf dem Master sollten Sie eine Verbindung sehen, die vom Ein-/Ausgabe-Thread des Slaves erzeugt wurde: mysql> SHOW PROCESSLIST\G *************************** 1. row *************************** Id: 55 User: repl Host: slave1.webcluster_1:54813 db: NULL Command: Binlog Dump Time: 610237 State: Has sent all binlog to slave; waiting for binlog to be updated Info: NULL
Auf dem Slave müssen Sie zwei Threads sehen. Einer ist der Ein-/Ausgabe-Thread, der andere ist der SQL-Thread:
Die Replikation einrichten | 381
mysql> SHOW PROCESSLIST\G *************************** Id: 1 User: system user Host: db: NULL Command: Connect Time: 611116 State: Waiting for master Info: NULL *************************** Id: 2 User: system user Host: db: NULL Command: Connect Time: 33 State: Has read all relay Info: NULL
1. row ***************************
to send event 2. row ***************************
log; waiting for the slave I/O thread to update it
Die gezeigte Beispielausgabe stammt von Servern, die schon lange laufen, weshalb die Time-Spalten der Ein-/Ausgabe-Threads auf dem Master und dem Slave große Werte zeigen. Der SQL-Thread auf dem Slave war 33 Sekunden lang untätig, was bedeutet, dass 33 Sekunden lang keine Events eingespielt wurden. Diese Prozesse laufen immer unter dem Benutzer-Account »system user«, die anderen Spalten zeigen bei Ihnen dagegen andere Werte. Wenn z.B. der SQL-Thread ein Event auf dem Slave abspielt, zeigt die Info-Spalte, dass die Abfrage ausgeführt wird. Falls Sie ein bisschen mit der MySQL-Replikation herumspielen wollen, dann kann Giuseppe Maxias MySQL-Sandbox-Skript (http://sourceforge. net/projects/mysql-sandbox/) eine »Spielinstallation« aus einer frisch heruntergeladenen MySQL-tar-Datei starten. Es sind nur einige Tastendrücke und etwa 15 Sekunden nötig, um einen Master und zwei Slaves zum Laufen zu bringen: $ ./set_replication.pl ~/mysql-5.0.45-linux-x86_64-glibc23.tar.gz
Einen Slave von einem anderen Server aus initialisieren Die gerade gezeigten Hinweise zur Einrichtung gingen davon aus, dass Sie Master und Slave nach einer Neuinstallation mit den vorgegebenen Anfangsdaten gestartet haben, so dass Sie implizit die gleichen Daten auf beiden Servern hatten und die Binärlog-Koordinaten des Masters kannten. Das ist nicht unbedingt der typische Fall. Normalerweise werden Sie bereits einen Master am Laufen haben und wollen einen neu installierten Slave mit diesem synchronieren, obwohl dieser nicht die Daten des Masters besitzt. Es gibt verschiedene Möglichkeiten, einen Slave von einem anderen Server aus zu initialisieren oder zu »klonen«. Dazu gehört das Kopieren der Daten vom Master, das Klonen eines Slaves von einem anderen Slave aus und das Starten eines Slaves aus einem aktuellen Backup. Sie brauchen drei Dinge, um einen Slave mit einem Master zu synchronisieren:
382 | Kapitel 8: Replikation
• Einen Schnappschuss der Daten des Masters zu einem bestimmten Zeitpunkt. • Die aktuelle Log-Datei des Masters und den Byte-Offset innerhalb dieses Logs zu dem exakten Zeitpunkt, zu dem Sie den Schnappschuss erzeugt haben. Wir bezeichnen diese beiden Werte als Log-Datei-Koordinaten, weil sie zusammen eine Binärlog-Position kennzeichnen. Sie können die Log-Datei-Koordinaten des Masters mit dem Befehl SHOW MASTER STATUS ermitteln. • Die Binärlog-Dateien des Masters von diesem Zeitpunkt bis zu Gegenwart. Dies sind Methoden, um einen Slave von einem anderen Server zu klonen: Mit einer kalten Kopie Eine der einfachsten Methoden, um einen Slave zu starten, besteht darin, den potenziellen Master herunterzufahren und seine Dateien auf den Slave zu kopieren (in Anhang A erfahren Sie, wie Sie Dateien effizient kopieren). Sie können dann den Master wieder starten, der dann ein neues Binärlog beginnt, und CHANGE MASTER TO einsetzen, um den Slave am Anfang dieses Binärlogs zu starten. Der Nachteil dieser Technik ist offensichtlich: Sie müssen den Master herunterfahren, während Sie die Kopie anlegen. Mit einer warmen Kopie Wenn Sie nur MyISAM-Tabellen benutzen, können Sie die Dateien mit mysqlhotcopy kopieren, während der Server läuft. Näheres erfahren Sie in Kapitel 11. Mittels mysqldump Falls Sie nur InnoDB-Tabellen verwenden, können Sie den folgenden Befehl verwenden, um alles vom Master zu speichern, es in den Slave zu laden und die Koordinaten des Slaves auf die entsprechende Position im Binärlog des Masters zu ändern: $ mysqldump --single-transaction --all-databases --master-data=1 --host=server1 | mysql --host=server2
Die Option --single-transaction veranlasst den Dump, die Daten so zu lesen, wie sie am Anfang der Transaktion vorlagen. Diese Option kann auch bei anderen transaktionsfähigen Storage-Engines funktionieren, wir haben sie aber nicht getestet. Falls Sie keine transaktionsfähigen Tabellen verwenden, können Sie die Option --lock-alltables einsetzen, um einen konsistenten Dump aller Tabellen zu bekommen. Mit einem LVM-Schnappschuss oder einem Backup Wenn Sie die entsprechenden Binärlog-Koordinaten kennen, können Sie einen LVM-Schnappschuss vom Master oder einem Backup benutzen, um den Slave zu initialisieren (falls Sie ein Backup verwenden, dann verlangt diese Methode, dass Sie alle Binärlogs des Masters seit dem Zeitpunkt des Backups aufbewahrt haben). Stellen Sie einfach das Backup oder den Schnappschuss auf dem Slave wieder her, und benutzen Sie dann die passenden Binärlog-Koordinaten in CHANGE MASTER TO. Mehr dazu erfahren Sie in Kapitel 11. InnoDB Hot Backup, das ebenfalls in Kapitel 11 behandelt wird, ist eine gute Methode, um einen Slave zu initialisieren, falls Sie nur InnoDB-Tabellen benutzen.
Die Replikation einrichten | 383
Von einem anderen Slave Mit einer der erwähnten Schnappschuss- oder Klontechniken können Sie einen Slave aus einem anderen klonen. Falls Sie allerdings mysqldump verwenden, funktioniert die Option --master-data nicht. Anstatt SHOW MASTER STATUS zu benutzen, um die Binärlog-Koordinaten des Masters zu erhalten, müssen Sie SHOW SLAVE STATUS einsetzen, damit Sie die Position finden, an der der Slave auf dem Master ausgeführt wurde, als Sie den Schnappschuss erzeugt haben. Der größte Nachteil beim Klonen eines Slaves von einem anderen Slave besteht darin, dass Sie schlechte Daten klonen, falls Ihr Slave aus irgendeinem Grund nicht synchron mit dem Master ist. Benutzen Sie nicht LOAD DATA FROM MASTER oder LOAD TABLE FROM MASTER! Diese Befehle sind veraltet, langsam und sehr gefährlich. Außerdem funktionieren sie nur mit MyISAM.
Machen Sie sich mit der Technik vertraut, für die Sie sich letztendlich entscheiden, und dokumentieren Sie sie, oder schreiben Sie sich ein Skript. Sie werden diesen Vorgang mit hoher Wahrscheinlichkeit mehr als einmal durchführen und müssen dazu in der Lage sein, ihn zu wiederholen, falls etwas schiefgeht.
Die empfohlene Replikationskonfiguration Es gibt viele Replikationsparameter, und die meisten von ihnen haben wenigstens eine gewisse Wirkung auf die Datensicherheit und die Performance. Wir erläutern später, welche Regeln Sie wann brechen sollten. In diesem Abschnitt zeigen wir eine empfohlene, »sichere« Replikationskonfiguration, die die Gelegenheiten zum Auftreten von Problemen minimiert. Die wichtigste Einstellung für das Führen eines Binärlogs auf dem Master ist sync_binlog: sync_binlog=1
Sie veranlasst MySQL, den Inhalt des Binärlogs bei jedem Bestätigen einer Transaktion auf die Festplatte zu synchronisieren, damit Sie im Falle eines Absturzes keine LogEvents verlieren. Wenn Sie diese Option deaktivieren, hat der Server zwar etwas weniger Arbeit, aber die Binärlog-Einträge könnten nach einem Serverabsturz beschädigt werden oder verlorengehen. Auf einem Slave, der nicht als Master auftreten muss, verursacht diese Option unnötigen Overhead. Sie gilt nur für das Binärlog, nicht für das Relay-Log. Wir empfehlen außerdem die Verwendung von InnoDB, falls Sie beschädigte Tabellen nach einem Absturz nicht tolerieren können. MyISAM ist in Ordnung, falls die Beschädigung der Tabelle keine große Sache ist, MyISAM-Tabellen dagegen gelangen wahrscheinlich in einen inkonsistenten Zustand, wenn ein Slave-Server abstürzt. Es kommt mit großer Sicherheit dazu, dass eine Anweisung unvollständig auf eine oder mehrere Tabel-
384 | Kapitel 8: Replikation
len angewandt wurde und die Daten auch dann inkonsistent bleiben, nachdem Sie die Tabellen repariert haben. Falls Sie InnoDB benutzen, empfehlen wir Ihnen unbedingt, die folgenden Optionen auf dem Master einzustellen: innodb_flush_logs_at_trx_commit=1 # Uebertraegt alle Log-Schreibvorgaenge innodb_support_xa=1 # Nur MySQL 5.0 und neuere Versionen innodb_safe_binlog # Nur MySQL 4.1, ist in etwa aequivalent # zu innodb_support_xa
Das sind die Standardeinstellungen in MySQL 5.0. Auf dem Slave sollten Sie die folgenden Konfigurationsoptionen aktivieren: skip_slave_start read_only
Die Option skip_slave_start verhindert, dass der Slave nach einem Absturz automatisch wieder startet, wodurch Sie die Möglichkeit haben, einen Server zu reparieren, falls er Probleme hat. Wenn der Slave nach einem Absturz automatisch startet und in einem inkonsistenten Zustand ist, kann er weitere Schäden anrichten, so dass Sie unter Umständen seine Daten wegwerfen und von vorn beginnen müssen. Selbst wenn Sie alle empfohlenen Optionen aktiviert haben, kann ein Slave nach einem Absturz noch kaputtgehen, weil die Relay-Logs und die Datei master.info nicht absturzsicher sind. Sie werden nicht einmal auf die Festplatte übertragen, und es gibt keine Konfigurationsoption, mit der man dieses Verhalten kontrollieren könnte. (Die Google-Patches, auf die wir später noch kommen, befassen sich mit diesem Problem.) Die Option read_only hält die meisten Benutzer davon ab, nichttemporäre Tabellen zu ändern. Ausgenommen sind der Slave-SQL-Thread und Threads mit der Berechtigung SUPER. Dies ist einer der Gründe dafür, weshalb Sie Ihren normalen Accounts nicht die Berechtigung SUPER geben sollten (mehr zu Berechtigungen erfahren Sie in Kapitel 12). Wenn ein Slave weit hinter seinem Master zurückliegt, dann kann der Slave-Ein-/Ausgabe-Thread viele Relay-Logs schreiben. Der Slave-SQL-Thread entfernt sie, sobald er damit fertig ist, sie abzuspielen (das können Sie mit der Option relay_log_purge ändern). Falls er aber weit hinterher ist, füllt der Ein-/Ausgabe-Thread möglicherweise die Festplatte. Die Lösung für dieses Problem ist die Konfigurationsvariable relay_log_space_ limit. Übersteigt die Gesamtgröße aller Relay-Logs die Größe dieser Variablen, stoppt der Ein-/Ausgabe-Thread und wartet darauf, dass der SQL-Thread etwas mehr Festplattenplatz freigibt. Das klingt zwar alles gut und schön, kann aber insgeheim problematisch sein. Wenn der Slave noch nicht alle Relay-Logs vom Master geholt hat, sind diese Logs vielleicht für immer verloren, falls der Master abstürzt. Es ist sicher keine schlechte Idee, wenn Sie den Slave so viel Platz wie nötig für die Relay-Logs benutzen lassen (es sei denn, Sie haben mit dem Festplattenplatz etwas Besseres vor). Deswegen haben wir die Einstellung relay_log_space_limit nicht in unsere empfohlene Konfiguration aufgenommen.
Die Replikation einrichten | 385
Replikation näher betrachtet Nachdem wir einige der Grundlagen der Replikation erläutert haben, wollen wir sie uns genauer anschauen. Wir untersuchen, wie Replikation wirklich funktioniert, welche Stärken und Schwächen sie mitbringt und welche erweiterten Konfigurationsoptionen es für die Replikation gibt.
Anweisungsbasierte Replikation MySQL 5.0 und frühere Versionen unterstützen nur anweisungsbasierte Replikation (auch als logische Replikation bezeichnet). Das ist in der Datenbankwelt ungewöhnlich. Bei der anweisungsbasierten Replikation wird die Abfrage aufgezeichnet, die die Daten auf dem Master geändert hat. Wenn der Slave das Event aus dem Relay-Log liest und es ausführt, führt er die tatsächliche SQL-Abfrage noch einmal aus, die der Master ausgeführt hat. Dieses Vorgehen bringt sowohl Vor- als auch Nachteile mit sich. Der offensichtlichste Vorteil besteht darin, dass es relativ einfach zu implementieren ist. Durch das einfache Aufzeichnen und erneute Abspielen aller Anweisungen, die Daten ändern, bleibt der Slave theoretisch synchron mit dem Master. Ein weiterer Vorteil der anweisungsbasierten Replikation besteht darin, dass die Binärlog-Events in der Regel ausgesprochen kompakt sind. Anweisungsbasierte Replikation benötigt also relativ wenig Bandbreite – eine Abfrage, die Gigabytes an Daten aktualisiert, belegt möglicherweise nur einige Dutzend Bytes im Binärlog. Darüber hinaus funktioniert das erwähnte Werkzeug mysqlbinlog mit anweisungsbasiertem Logging am besten. In der Praxis ist die anweisungsbasierte Replikation jedoch nicht ganz so einfach, wie es scheinen mag, weil viele Änderungen auf dem Master noch von anderen Faktoren als nur dem Abfragetext abhängen können. Beispielsweise werden die Anweisungen auf dem Master und dem Slave zu mehr oder weniger unterschiedlichen Zeiten ausgeführt. Daraus folgt, dass das Binärlog-Format von MySQL mehr als nur den Abfragetext enthält; es überträgt auch mehrere Bits mit Metadaten, wie etwa den aktuellen Zeitstempel. Außerdem gibt es Anweisungen, die MySQL nicht korrekt replizieren kann, wie etwa Abfragen mit der Funktion CURRENT_USER( ). Auch gespeicherte Routinen und Trigger werfen bei der anweisungsbasierten Replikation Probleme auf. Ein weiteres Problem bei der anweisungsbasierten Replikation besteht darin, dass die Modifikationen serialisierbar sein müssen. Das erfordert erhebliche Mengen an Spezialcode, Konfigurationseinstellungen und zusätzliche Serverfunktionen, einschließlich der Next-Key-Locks von InnoDB und automatisch inkrementierender Sperren. Nicht alle Storage-Engines funktionieren mit anweisungsbasierter Replikation, obwohl es diejenigen tun, die in der offiziellen MySQL-Serverdistribution bis einschließlich MySQL 5.1 enthalten sind. Im MySQL-Handbuch finden Sie im Kapitel über die Replikation eine vollständige Liste mit den Nachteilen der anweisungsbasierten Replikation.
386 | Kapitel 8: Replikation
Zeilenbasierte Replikation MySQL 5.1 bietet Unterstützung für zeilenbasierte Replikation, bei der die tatsächlichen Datenänderungen im Binärlog aufgezeichnet werden. Ihre Implementierung ähnelt der anderer Datenbankprodukte. Dieses Vorgehen bringt einige Vor- und Nachteile mit sich. Der größte Vorteil besteht darin, dass MySQL jede Anweisung korrekt replizieren kann; einige Anweisungen werden sogar noch effizienter repliziert. Die wichtigsten Nachteile sind, dass das Binärlog viel größer werden kann und dass nicht so klar ist, welche Anweisungen die Daten aktualisiert haben, so dass sich das Binärlog nicht für eine Prüfung mit mysqlbinlog eignet. Zeilenbasiertes Logging ist nicht abwärtskompatibel. Das Dienstprogramm mysqlbinlog, das mit MySQL 5.1 vertrieben wird, kann Binärlogs lesen, die Events im zeilenbasierten Format aufzeichnen (sie sind nicht vom Menschen lesbar, der MySQL-Server kann sie allerdings interpretieren). mysqlbinlog-Versionen aus früheren MySQL-Distributionen dagegen erkennen solche Log-Events nicht und beenden sich mit einer Fehlermeldung, wenn sie sie bemerken.
MySQL kann manche Änderungen mithilfe der zeilenbasierten Replikation effizienter replizieren, weil der Slave die Abfragen nicht noch einmal wiedergeben muss, mit denen die Zeilen auf dem Master geändert wurden. Das erneute Abspielen einiger Abfragen kann sehr teuer sein. Hier sehen Sie z.B. eine Abfrage, die Daten aus einer sehr großen Tabelle in einer kleineren Tabelle zusammenfasst: mysql> -> -> ->
INSERT INTO summary_table(col1, col2, sum_col3) SELECT col1, col2, sum(col3) FROM enormous_table GROUP BY col1, col2;
Stellen Sie sich vor, dass es nur drei eindeutige Kombinationen aus col1 und col2 in der enormous_table-Tabelle gibt. Diese Abfrage scannt viele Zeilen in der Quelltabelle, ergibt aber nur drei Zeilen in der Zieltabelle. Durch das Replizieren dieses Events muss der Slave die ganze Arbeit wiederholen, um nur wenige Zeilen zu generieren. Eine zeilenbasierte Replikation ist dagegen auf dem Slave unwahrscheinlich billig und damit viel effizienter. Andererseits lässt sich das folgende Event mit einer anweisungsbasierten Replikation billiger replizieren: mysql> UPDATE enormous_table SET col1 = 0;
Die Verwendung der zeilenbasierten Replikation wäre in diesem Fall viel teurer, weil sie jede Zeile ändert: Jede Zeile müsste in das Binärlog geschrieben werden, wodurch dieses außerordentlich anwachsen würde. Sowohl durch das Logging als auch durch die Replikation würde die Last auf dem Master stark ansteigen, und das langsamere Logging würde in der Folge die Nebenläufigkeit herabsetzen.
Replikation näher betrachtet | 387
Da kein Format für alle Situationen perfekt geeignet ist, wechselt MySQL 5.1 dynamisch zwischen anwendungsbasierter und zeilenbasierter Replikation. Standardmäßig verwendet es die anwendungsbasierte Replikation. Wenn es allerdings ein Event bemerkt, das mit einer Anweisung nicht korrekt repliziert werden kann, geht es zur zeilenbasierten Replikation über. Sie können das Format bei Bedarf auch steuern, indem Sie die Sitzungsvariable binlog_format einstellen. Mit einem Binärlog, das Events im zeilenbasierten Format enthält, ist eine punktgenaue Wiederherstellung schwieriger zu realisieren, aber nicht unmöglich. Ein Log-Server kann dabei helfen – mehr dazu später. Theoretisch löst die zeilenbasierte Replikation verschiedene Probleme, auf die wir später noch kommen. In der Praxis setzen jedoch die meisten Leute, von denen wir wissen, dass sie MySQL 5.1 im täglichen Betrieb benutzen, weiterhin auf die anweisungsbasierte Replikation. Es ist deshalb noch zu früh, etwas Abschließendes über die zeilenbasierte Replikation zu sagen.
Replikationsdateien Schauen wir uns einige der Dateien an, die bei der Replikation verwendet werden. Sie kennen bereits das Binärlog und das Relay-Log. Es gibt aber noch mehr Dateien. Wo MySQL sie ablegt, hängt hauptsächlich von Ihren Konfigurationseinstellungen ab. Üblicherweise legen unterschiedliche MySQL-Versionen sie in unterschiedliche Verzeichnisse. Sie finden sie entweder im Datenverzeichnis oder in dem Verzeichnis, das die .pidDatei des Servers enthält (auf Unix-artigen Systemen wahrscheinlich /var/run/mysqld/). Hier sind sie: mysql-bin.index Ein Server, bei dem das Binär-Logging aktiviert ist, besitzt auch eine Datei, die genauso heißt wie die Binärlogs, allerdings mit dem Suffix .index. Diese Datei verfolgt die Binärlog-Dateien, die auf der Festplatte existieren. Es handelt sich nicht um einen Index im Sinne eines Tabellenindex. Stattdessen enthält jede Zeile in der Datei den Dateinamen einer Binärlog-Datei. Vielleicht glauben Sie jetzt, dass diese Datei redundant ist und gelöscht werden kann (schließlich könnte MySQL einfach auf der Festplatte nachschauen, um seine Dateien zu finden), das sollten Sie aber unterlassen. MySQL verlässt sich auf diese Indexdatei und kann eine Binärlog-Datei nicht erkennen, wenn sie hier nicht erwähnt wird. mysql-relay-bin.index Diese Datei dient dem gleichen Zweck wie die Binärlog-Indexdatei, allerdings für die Relay-Logs. master.info Diese Datei enthält die Informationen, die ein Slave-Server benötigt, um eine Verbindung zu seinem Master herzustellen. Das Format ist einfacher Text (ein Wert pro Zeile) und kann zwischen den MySQL-Versionen variieren. Löschen Sie diese Datei 388 | Kapitel 8: Replikation
nicht, da Ihr Slave sonst nicht weiß, wie er sich nach einem Neustart mit dem Master verbinden soll. Diese Datei enthält das Passwort des Replikationsbenutzers im Klartext, so dass Sie ihre Berechtigungen einschränken müssen. relay-log.info Diese Datei enthält die aktuellen Binärlog- und Relay-Log-Koordinaten des Slaves (d.h. die Position des Slaves auf dem Master). Löschen Sie sie nicht, da der Slave sonst nach einem Neustart vergisst, woher er repliziert hat, und möglicherweise versucht, Anweisungen noch einmal abzuspielen, die er bereits ausgeführt hat. Diese Dateien bilden eine ziemlich plumpe Methode, den Replikations- und LoggingZustand von MySQL aufzuzeichnen. Leider werden sie nicht synchron geschrieben. Falls also bei Ihrem Server der Strom ausfällt und die Dateien noch nicht auf die Festplatte übertragen waren, könnten sie nach einem Neustart fehlerhaft sein. Standardmäßig tragen die Binärlogs den Hostnamen des Servers mit einem zusätzlichen numerischen Suffix, allerdings ist es besser, ihnen in my.cnf explizit einen Namen zu geben: log_bin # Nicht so, sonst werden die Dateien mit dem Hostnamen benannt log_bin = mysql-bin # Das ist sicher
Das ist wichtig, weil sonst die Replikation fehlschlagen könnte, wenn sich der Hostname des Servers ändert. Wir empfehlen darüber hinaus, nicht den Hostnamen für die Benennung zu verwenden – klopfen Sie also die Vorgaben nicht auch noch fest. Legen Sie stattdessen einen Namen für Ihre Binärlogs fest, und benutzen Sie ihn allgemein. Dadurch wird es viel einfacher, die Datei eines Servers auf eine andere Maschine zu verschieben und das Failover zu automatisieren. Sie sollten auch die Relay-Logs (die standardmäßig ebenfalls nach dem Hostnamen des Servers benannt werden) und die entsprechenden .index-Dateien explizit mit Namen versehen. Hier sind unsere empfohlenen my.cnf-Einstellungen für all diese Optionen: log_bin log_bin_index relay_log relay_log_index
= = = =
mysql-bin mysql-bin.index mysql-relay-bin mysql-relay-bin.index
Die .index-Dateien erben ihre Namen eigentlich von den Log-Dateien, allerdings schadet es nichts, wenn man sie explizit benennt. Die .index-Dateien arbeiten auch mit einer anderen Einstellung zusammen, nämlich expire_logs_days, die angibt, wie MySQL abgelaufene Binärlogs aufräumen soll. Wenn die mysql-bin.index-Dateien Dateien erwähnen, die es auf der Festplatte gar nicht gibt, funktioniert die automatische Reinigung nicht, ja, nicht einmal die Anweisung PURGE MASTER LOGS funktioniert. Die Lösung für dieses Problem besteht im Allgemeinen darin, die Binärlogs vom MySQL-Server verwalten zu lassen, damit dieser nicht durcheinanderkommt. Sie müssen explizit eine Log-Reinigungsstrategie implementieren, entweder mit expire_logs_days oder auf andere Weise, da MySQL ansonsten die Festplatte mit BinärReplikation näher betrachtet | 389
logs füllt. Denken Sie in diesem Zusammenhang gleich über Ihre Backup-Regelungen nach. Mehr über das Binärlog erfahren Sie in »Das Binärlogformat« auf Seite 532.
Replikations-Events an andere Slaves senden Mit der Option log_slave_updates können Sie einen Slave als Master für andere Slaves verwenden. Sie weist MySQL an, die Events, die der Slave-SQL-Thread ausführt, in sein eigenes Binärlog zu schreiben, das andere Slaves sich dann holen und ausführen können. Abbildung 8-2 verdeutlicht das. Master
Slave
Slave
Ein/AusgabeThread
Lesen
Datenänderungen
Mit log_slave_ SQL-Thread updates
Lesen
Lesen
Schreiben
Binärlog
Ein/AusgabeThread
Wiederabspielen RelayLog
BinaryLog
SQL-Thread
Lesen
Wiederabspielen
RelayLog
Abbildung 8-2: Ein Replikations-Event an weitere Slaves übergeben
In diesem Szenario sorgt eine Änderung auf dem Master dafür, dass ein Event in sein Binärlog geschrieben wird. Der erste Slave holt sich das Event und führt es aus. An dieser Stelle wäre das Leben des Events normalerweise vorbei. Da aber log_slave_updates aktiviert ist, schreibt der Slave das Event stattdessen in sein eigenes Binärlog. Jetzt kann der zweite Slave das Event in sein Relay-Log übertragen und ausführen. Diese Konfiguration sorgt also dafür, dass Änderungen auf dem ursprünglichen Master an Slaves weiterverteilt werden können, die nicht direkt an den Master angeschlossen sind. Wir setzen log_slave_updates immer, weil Sie dadurch einen Slave anschließen können, ohne den Server neu starten zu müssen. Wenn der erste Slave ein Binärlog-Event vom Master in sein eigenes Binärlog schreibt, befindet sich das Event mit hoher Wahrscheinlichkeit an einer anderen Position als auf dem Master – d.h., es könnte in einer anderen Log-Datei stehen oder an einer anderen numerischen Position innerhalb der Log-Datei. Sie dürfen also nicht davon ausgehen, dass alle Server, die sich an der gleichen logischen Stelle in der Replikation befinden, die gleichen Log-Koordinaten aufweisen. Wie wir später sehen werden, wird dadurch die Erledigung mancher Aufgaben verkompliziert, etwa das Wechseln der Slaves zu einem anderen Master oder das Ankündigen eines Slaves als Master. 390 | Kapitel 8: Replikation
Falls Sie nicht sorgfältig darauf geachtet haben, jedem Server eine eindeutige Server-ID zu geben, könnte die Konfiguration eines Slaves auf diese Weise zu schleichenden Fehlern führen oder sogar dafür sorgen, dass sich die Replikation beschwert und stoppt. Eine der am häufigsten auftretenden Fragen in Bezug auf die Konfiguration der Replikation lautet, weshalb man die Server-ID angeben muss. Müsste MySQL nicht in der Lage sein, Anweisungen zu replizieren, ohne zu wissen, woher sie stammen? Wieso kümmert sich MySQL darum, ob die Server-ID global eindeutig ist? Die Antwort auf diese Frage ist darin zu suchen, wie MySQL eine Endlosschleife bei der Replikation verhindert. Wenn der SlaveSQL-Thread das Relay-Log liest, dann verwirft er alle Events, deren Server-ID seiner eigenen entspricht. Dadurch werden Endlosschleifen bei der Replikation unterbrochen. Das Verhindern von Endlosschleifen ist für einige der nützlichsten Replikationstopologien wichtig, etwa für die Master-Master-Replikation. Falls Sie Probleme damit haben, die Replikation in Gang zu bekommen, dann sollten Sie als eines der ersten Dinge die Server-ID überprüfen. Es reicht nicht, nur die @@server_id-Variable zu untersuchen. Diese besitzt einen Vorgabewert, allerdings funktioniert die Replikation nur, wenn dieser Vorgabewert ausdrücklich gesetzt ist, entweder in my.cnf oder über einen SET-Befehl. Wenn Sie einen SET-Befehl verwenden, dann denken Sie daran, auch die Konfigurationsdatei zu aktualisieren, da Ihre Einstellungen sonst einen Serverneustart nicht überleben.
Replikationsfilter Die Möglichkeiten zur Replikationsfilterung erlauben es Ihnen, nur einen Teil der Serverdaten zu replizieren. Es gibt zwei Arten von Replikationsfiltern: solche, die Events aus dem Binärlog auf dem Master filtern, und solche, die Events filtern, die aus dem RelayLog auf dem Slave kommen. Abbildung 8-3 illustriert die beiden Arten. Master
Slave Ein/AusgabeThread
SQL-Thread Lesen binlog_do_db binlog_ignore_db
Wiederabspielen Schreiben Lesen
Bin rlog
RelayLog
replicate_do_db replicate_do_table replicate_ignore_db replicate_ignore_table replicate_rewrite_db replicate_wild_do_table replicate_wild_ignore_table
Abbildung 8-3: Möglichkeiten zur Replikationsfilterung
Replikation näher betrachtet | 391
Die Optionen zur Filterung des Binärlogs heißen binlog_do_db und binlog_ignore_db. Normalerweise sollten Sie sie nicht aktivieren, wie wir gleich erklären. Auf dem Slave filtern die replicate_*-Optionen Events, während der Slave-SQL-Thread sie aus dem Relay-Log liest. Sie können eine oder mehrere Datenbanken replizieren oder ignorieren, eine Datenbank in eine andere Datenbank umschreiben und Tabellen auf Basis einer LIKE-Mustervergleichssyntax replizieren oder ignorieren. Sie müssen vor allen Dingen verstehen, dass die Optionen *_do_db und *_ignore_db sowohl auf dem Master als auch auf dem Slave nicht so funktionieren, wie Sie es vielleicht erwarten. Vermutlich glauben Sie, dass die Optionen auf der Datenbank des angegebenen Objekts filtern, dabei filtern sie tatsächlich auf der aktuellen Standarddatenbank. Das heißt, wenn Sie die Anweisungen mysql> USE test; mysql> DELETE FROM sakila.film;
auf dem Master ausführen, filtern die Parameter *_do_db und *_ignore_db die DELETEAnweisung auf test, nicht auf sakila. Normalerweise ist das nicht das, was Sie wollen, und kann dazu führen, dass die falschen Anweisungen repliziert oder ignoriert werden. Es gibt Anwendungen für die Parameter *_do_db und *_ignore_db, allerdings sind diese begrenzt und selten, und Sie sollten sie sehr vorsichtig einsetzen. Wenn Sie diese Parameter verwenden, kann es leicht vorkommen, dass die Replikation in eine Schieflage gerät. Die Optionen binlog_do_db und binlog_ignore_db haben nicht nur das Potenzial, die Replikation zu stören, sondern machen es auch noch unmöglich, eine punktgenaue Wiederherstellung aus einem Backup durchzuführen. Sie sollten sie in den meisten Situationen nicht verwenden. Wir zeigen Ihnen weiter hinten in diesem Kapitel einige sichere Methoden, die Replikation mit Blackhole-Tabellen zu filtern.
Mit Replikationsfiltern möchte man normalerweise verhindern, dass GRANT- und REVOKEAnweisungen auf Slaves repliziert werden.5 Oft kommt es vor, dass ein Administrator einem Benutzer mit GRANT bestimmte Schreibberechtigungen auf dem Master gewährt hat und dann feststellt, dass diese auf den Slave weiterverbreitet wurden, wo der Benutzer eigentlich keine Daten ändern dürfte. Die folgenden Replikationsoptionen auf dem Slave verhindern das: replicate_ignore_table=mysql.columns_priv replicate_ignore_table=mysql.db replicate_ignore_table=mysql.host replicate_ignore_table=mysql.procs_priv replicate_ignore_table=mysql.tables_priv replicate_ignore_table=mysql.user
5 Eine bessere Möglichkeit, die Berechtigungen auf Slaves zu beschränken, besteht darin, read_only zu benutzen und auf dem Master und den Slaves die gleichen Berechtigungen einzusetzen.
392 | Kapitel 8: Replikation
Vielleicht haben Sie den Tipp erhalten, einfach alle Tabellen in der mysql-Datenbank mit einer solchen Regel auszufiltern: replicate_wild_ignore_table=mysql.%
Sicher, damit wird verhindert, dass GRANT-Anweisungen repliziert werden, allerdings werden jetzt auch keine Events und Routinen repliziert. Solche unvorhergesehenen Folgen sind der Grund dafür, weshalb wir gesagt haben, dass Sie mit Filtern vorsichtig sein müssen. Es ist sicher besser, wenn Sie verhindern, dass bestimmte Anweisungen repliziert werden. Üblicherweise erreichen Sie das mit SET SQL_LOG_BIN=0, obwohl auch dieses Vorgehen Gefahren birgt. Im Allgemeinen sollten Sie Replikationsfilter sehr vorsichtig einsetzen und auch nur, wenn Sie sie wirklich brauchen, weil es leicht ist, mit ihnen die anweisungsbasierte Replikation zu stören. (Zeilenbasierte Replikation könnte einige dieser Probleme lösen, allerdings ist das noch nicht vollständig bewiesen.) Die Filteroptionen sind im MySQL-Handbuch gut dokumentiert, weshalb wir die Einzelheiten hier nicht wiederholen wollen.
Replikationstopologien Sie können die MySQL-Replikation für fast jede Konfiguration aus Mastern und Slaves einrichten. Als Einschränkung gilt nur, dass eine Instanz eines MySQL-Slaves nur einen Master haben darf. Es sind viele komplexe Topologien möglich, doch selbst die einfachen können sehr flexibel sein. Eine einzige Topologie kann viele unterschiedliche Anwendungszwecke haben. Behalten Sie dies im Hinterkopf, wenn Sie unsere Beschreibungen lesen, weil wir nur die einfachen Anwendungen beschreiben. Die Vielfalt der Möglichkeiten könnte leicht ein ganzes Buch füllen. Wir haben bereits gesehen, wie man einen Master mit einem einzigen Slave einrichtet. In diesem Abschnitt schauen wir uns weitere gebräuchliche Topologien an und diskutieren deren Stärken und Beschränkungen. Merken Sie sich diese Grundregeln: • Eine MySQL-Slave-Instanz kann nur einen Master haben. • Jeder Slave muss eine eindeutige Server-ID besitzen. • Ein Master kann viele Slaves haben (bzw. ein Slave kann entsprechend viele Geschwister aufweisen). • Ein Slave kann Änderungen von seinem Master weiterverbreiten und der Master anderer Slaves sein, falls Sie log_slave_updates aktivieren.
Ein Master und mehrere Slaves Neben der bereits erwähnten, zwei Server umfassenden Master-Slave-Anordnung ist dies die einfachste Replikationstopologie. Sie ist im Prinzip genauso einfach wie die Grundanordnung, weil die Slaves überhaupt nichts miteinander zu tun haben; sie sind jeweils nur mit dem Master verbunden. Abbildung 8-4 zeigt dieses Arrangement.
Replikationstopologien | 393
Master
Slave
Slave
Slave
Abbildung 8-4: Ein Master mit mehreren Slaves
Diese Konfiguration ist am sinnvollsten, wenn Sie wenige Schreib- und viele Leseoperationen haben. Sie können die Lesevorgänge über eine beliebige Anzahl Slave-Server verteilen, und zwar bis zu dem Punkt, an dem die Slaves zu viel Last auf den Master legen oder die Netzwerkbandbreite vom Master zu den Slaves zu einem Problem wird. Sie können viele Slaves auf einmal einrichten oder sie bei Bedarf hinzufügen. Dabei gehen Sie genauso vor, wie wir es bereits gezeigt haben. Obwohl diese Topologie sehr einfach ist, genügt sie vielen Anforderungen. Hier sind nur einige Anregungen: • Verwenden Sie unterschiedliche Slaves für unterschiedliche Rollen (z.B. für unterschiedliche Indizes oder Storage-Engines). • Richten Sie einen der Slaves als Reserve-Master ein, zu dem es keinen weiteren Verkehr als die Replikation gibt. • Setzen Sie einen der Slaves als Notfallreserve in ein entferntes Rechenzentrum. • Richten Sie einen oder mehrere Slaves mit Zeitverzögerung ein, so dass er als Notfallreserve dienen kann. • Nutzen Sie einen der Slaves für Backups, zu Trainingszwecken oder als Entwicklungs- oder Staging-Server. Einer der Gründe für die Beliebtheit dieser Topologie besteht darin, dass sie viele der Komplexitäten vermeidet, die andere Konfigurationen mit sich bringen. Hier ein Beispiel: Es ist leicht, einen Slave in Bezug auf die Binärlog-Positionen auf dem Master mit einem anderen Slave zu vergleichen, weil sie gleich sind. Mit anderen Worten: Wenn Sie alle Slaves an der gleichen logischen Position in der Replikation stoppen, dann lesen sie alle von der gleichen physischen Stelle im Log des Masters. Das ist eine hübsche Eigenschaft, die viele administrative Aufgaben vereinfacht, wie etwa die Beförderung eines Slaves zum Master. Diese Eigenschaft gilt allerdings nur zwischen »verschwisterten« Slaves. Zwischen Servern, die sich nicht in einer direkten Master-Slave- oder Geschwisterbeziehung befinden, lassen sich Log-Positionen nicht so einfach vergleichen. Viele der Topologien, die wir
394 | Kapitel 8: Replikation
später vorstellen, wie etwa die Baumreplikation oder Distribution-Master, erlauben es nicht so einfach festzustellen, von welcher Stelle in der logischen Abfolge der Events ein Slave tatsächlich repliziert.
Master-Master im Aktiv-Aktiv-Modus Die Master-Master-Replikation (auch als Dual-Master- oder bidirektionale Replikation bezeichnet) umfasst zwei Server, die jeweils als Master und als Slave des anderen Servers konfiguriert sind – mit anderen Worten: ein Paar aus Co-Mastern. Abbildung 8-5 verdeutlicht dies.
Master
Master
Abbildung 8-5: Master-Master-Replikation
MySQL unterstützt keine Multimaster-Replikation Wir verwenden den Begriff Multimaster-Replikation ganz speziell, um einen Slave mit mehr als einem Master zu beschreiben. Unabhängig davon, was man Ihnen erzählt hat, unterstützt MySQL (im Gegensatz zu einigen anderen Datenbankservern) momentan die Konfiguration, die in Abbildung 8-6 gezeigt wird, nicht. Wir zeigen Ihnen jedoch weiter hinten in diesem Kapitel einige Möglichkeiten, die Multimaster-Replikation zu emulieren. Leider benutzen viele Leute diesen Begriff gelegentlich, um eine Anordnung zu beschreiben, bei der es mehr als einen Master in der gesamten Replikationstopologie gibt, wie etwa die »Baum«-Topologie, auf die wir noch kommen. Andere Leute beschreiben damit die sogenannte Master-Master-Replikation, bei der die Server gegenseitig als Master und Slave auftreten. Diese Probleme mit der Terminologie verursachen viel Verwirrung und sogar Streit. Deshalb ist es unserer Meinung nach am besten, sorgfältig mit den Namen umzugehen. Stellen Sie sich vor, wie schwierig es wird, sich zu verständigen, wenn MySQL auf einmal Unterstützung für einen Slave mit zwei Mastern bietet! Welchen Begriff würden Sie dafür benutzen, wenn Sie »Multimaster-Replikation« nicht reserviert haben?
Es gibt Anwendungen für eine Master-Master-Replikation im Aktiv-Aktiv-Modus, allerdings sind diese üblicherweise sehr speziell. Ein möglicher Anwendungszweck ist für geografisch getrennte Büros, bei denen jedes Büro seine eigene lokal schreibbare Kopie der Daten benötigt.
Replikationstopologien | 395
Master
Master
Slave
Abbildung 8-6: MySQL unterstützt keine Multimaster-Replikation.
Das größte Problem mit einer solchen Konfiguration besteht darin, wie mit widersprüchlichen Änderungen umgegangen wird. Die Liste der möglichen Probleme, die dadurch verursacht werden, dass man zwei schreibbare Co-Master hat, ist sehr lang. Probleme treten normalerweise auf, wenn eine Abfrage gleichzeitig auf beiden Mastern die gleiche Zeile ändert oder zum gleichen Zeitpunkt auf beiden Servern etwas in eine Tabelle mit einer AUTO_INCREMENT-Spalte einfügt. Seit MySQL 5.0 gibt es einige Replikationsfunktionen, die diese Art der Replikation ein wenig sicherer machen: die Einstellungen auto_increment_increment und auto_increment_ offset. Diese Einstellungen erlauben es Servern, automatisch nicht-widersprüchliche Werte für INSERT-Abfragen zu generieren. Es ist aber immer noch gefährlich, auf beiden Servern Schreiboperationen zu erlauben. Updates, die auf den beiden Maschinen in unterschiedlicher Reihenfolge geschehen, können weiterhin dafür sorgen, dass die Daten stillschweigend in Konflikt geraten. Stellen Sie sich z.B. vor, dass Sie eine Tabelle mit einer Spalte und einer Zeile haben, die den Wert 1 enthält. Nehmen Sie nun an, dass diese beiden Anweisungen gleichzeitig ausgeführt werden: • Auf dem ersten Co-Master: mysql> UPDATE tbl SET col=col + 1;
• Auf dem zweiten: mysql> UPDATE tbl SET col=col * 2;
Das Ergebnis? Ein Server hat den Wert 4, der andere den Wert 3. Und dennoch gibt es keine Replikationsfehler. Dass Daten durcheinanderkommen, ist nur der Anfang. Was passiert, wenn die normale Replikation mit einem Fehler stoppt, die Anwendung aber weiter auf beide Server schreibt? Sie können nicht einfach einen der Server von dem anderen klonen, weil beide von ihnen Änderungen enthalten, die Sie auf den anderen kopieren müssen. Es ist vermutlich ziemlich schwer, dieses Problem zu lösen. Wenn Sie eine Master-Master-Aktiv-Aktiv-Konfiguration sorgfältig einrichten, möglicherweise mit gut partitionierten Daten und Berechtigungen, können Sie einige dieser
396 | Kapitel 8: Replikation
Probleme vermeiden.6 Es ist jedoch schwierig, das wirklich gut hinzubekommen, und normalerweise gibt es eine bessere Methode, das gewünschte Ziel zu erreichen. Im Allgemeinen bringt es mehr Ärger, als dass es nützt, wenn man Schreiboperationen auf beiden Servern zulässt. Eine Aktiv-Passiv-Konfiguration dagegen ist ganz sinnvoll, wie Sie im nächsten Abschnitt sehen werden.
Master-Master im Aktiv-Passiv-Modus Es gibt eine Variante der Master-Master-Replikation, mit der man die gerade besprochenen Nachteile vermeidet und die in der Tat eine sehr leistungsfähige Methode darstellt, um fehlertolerante und hochverfügbare Systeme zu entwickeln. Der wesentliche Unterschied besteht darin, dass einer der Server ein schreibgeschützter »passiver« Server ist, wie Abbildung 8-7 zeigt.
Aktiv
Passiv
Abbildung 8-7: Master-Master-Replikation im Aktiv-Passiv-Modus
Mit dieser Konfiguration können Sie die aktiven und passiven Serverrollen leicht wechseln, da die Konfigurationen der Server symmetrisch sind. Dadurch werden Failover und Failback erleichtert. Außerdem können Sie Wartungsarbeiten, Tabellenoptimierungen, Betriebssystem-(oder Anwendungs- oder Hardware-)Upgrades und andere Aufgaben ohne Ausfallzeiten durchführen. So wird z.B. durch das Ausführen einer ALTER TABLE-Anweisung die gesamte Tabelle gesperrt, d.h., Lese- und Schreiboperationen werden blockiert. Dieser Zustand kann lange anhalten und den Service unterbrechen. Die Master-Master-Konfiguration erlaubt es Ihnen jedoch, die Slave-Threads auf dem aktiven Server zu stoppen, so dass keine Aktualisierungen vom passiven Server mehr verarbeitet werden. Ferner können Sie bei einer Master-Master-Konfiguration die Tabelle auf dem passiven Server verändern, die Rollen vertauschen und den Slave-Prozess auf dem vormals aktiven Server neu starten.7 Dieser Server liest dann sein Relay-Log und führt die gleiche ALTER TABLE-Anweisung aus. Auch dies kann lange dauern, was aber egal ist, da der Server aktuell keine Abfragen bedient.
6 Einige, aber nicht alle – wir können den Advocatus Diaboli spielen und Ihnen in praktisch jeder vorstellbaren Anordnung Schwachstellen zeigen. 7 Sie können das Binär-Logging zeitweise mit SET SQL_LOG_BIN=0 deaktivieren, anstatt die Replikation zu stoppen. Manche Befehle, wie etwa OPTIMIZE TABLE, unterstützen auch die Optionen LOCAL oder NO_WRITE_TO_BINLOG, die das Logging verhindern.
Replikationstopologien | 397
Mit der Aktiv-Passiv-Master-Master-Topologie umgehen Sie viele weitere Probleme und Beschränkungen in MySQL. Sie können sich beim Einrichten und Verwalten eines solchen Systems vom MySQL-Master-Master Replication Manager (http://code.google.com/ p/mysql-master-master/) unterstützen lassen. Dieses Programm automatisiert viele knifflige Aufgaben, wie etwa das Wiederherstellen und Neusynchronisieren der Replikation, das Einrichten neuer Slaves usw. Schauen wir uns an, wie man ein Master-Master-Paar konfiguriert. Führen Sie diese Schritte auf beiden Servern aus, damit die Konfigurationen symmetrisch bleiben: 1. Aktivieren Sie das Binär-Logging, wählen Sie eindeutige Server-IDs, und legen Sie
Accounts für die Replikation an. 2. Aktivieren Sie das Logging von Slave-Updates. Das ist entscheidend für Failover und
Failback, wie Sie später sehen werden. 3. Konfigurieren Sie optional den passiven Server als schreibgeschützt, um Änderun-
gen zu verhindern, die mit Änderungen auf dem aktiven Server in Konflikt geraten könnten. 4. Stellen Sie sicher, dass die Server exakt die gleichen Daten enthalten. 5. Starten Sie die MySQL-Instanz auf allen Servern. 6. Konfigurieren Sie die einzelnen Server als Slave des jeweils anderen Servers, wobei
Sie mit dem neu angelegten Binärlog beginnen. Wir wollen nun verfolgen, was geschieht, wenn es eine Änderung auf dem aktiven Server gibt. Die Änderung wird in sein Binärlog geschrieben und gelangt durch die Replikation in das Relay-Log des passiven Servers. Der passive Server führt die Abfrage aus und schreibt das Event in sein eigenes Binärlog, da Sie log_slave_updates eingeschaltet haben. Der aktive Server holt dann die gleiche Änderung über die Replikation in sein eigenes Relay-Log, ignoriert sie jedoch, weil die Server-ID des Events seiner eigenen entspricht. In »Die Master wechseln« auf Seite 415 erfahren Sie, wie Sie die Rollen vertauschen. Das Einrichten einer Aktiv-Passiv-Master-Master-Topologie ist ein bisschen wie das Anlegen eines Hot-Spare, nur dass Sie hier das »Spare« verwenden können, um die Performance anzukurbeln. Sie können es für Leseabfragen, Backups, »Offline«-Wartungsarbeiten, Upgrades usw. einsetzen – Dinge, die mit einem echten Hot-Spare nicht möglich sind. Allerdings erzielen Sie hiermit keine bessere Schreib-Performance als mit einem einzelnen Server (mehr dazu später). Wenn wir weitere Szenarien und Anwendungen für die Replikation vorstellen, kommen wir auf diese Konfiguration zurück. Es ist eine sehr wichtige und verbreitete Replikationstopologie.
Master-Master mit Slaves Bei einer verwandten Konfiguration werden jedem Co-Master ein oder mehrere Slaves hinzugefügt (siehe Abbildung 8-8). 398 | Kapitel 8: Replikation
Master
Master
Slave
Slave
Abbildung 8-8: Master-Master-Replikation mit Slaves
Vorteilhaft an dieser Konfiguration ist die zusätzliche Redundanz. In einer geografisch verteilten Replikationstopologie vermeidet sie die eine Schwachstelle jedes Standorts. Wir üblich können Sie außerdem leseintensive Abfragen auf die Slaves abschieben. Wenn Sie lokal für ein schnelles Failover eine Master-Master-Topologie einsetzen, kann Ihnen diese Konfiguration dennoch helfen. Es ist möglich, aber auch etwas komplexer, einen der Slaves zu befördern, um einen ausgefallenen Master zu ersetzen. Das gilt auch für das Verschieben eines der Slaves derart, dass er auf einen anderen Master verweist. Die zusätzliche Komplexität ist ein wichtiger Gesichtspunkt.
Ring Die Dual-Master-Konfiguration ist in Wirklichkeit ein Sonderfall8 der Ring-Replikationskonfiguration, die in Abbildung 8-9 zu sehen ist. Ein Ring umfasst drei oder mehr Master. Jeder Server ist ein Slave des vorhergehenden Servers im Ring und ein Master des nachfolgenden Servers. Diese Topologie wird auch als kreisförmige Replikation bezeichnet.
Master
Master
Master
Abbildung 8-9: Eine Replikationsringtopologie
8 Ein etwas vernünftigerer Sonderfall, wie wir hinzufügen wollen.
Replikationstopologien | 399
Ringe verfügen nicht über die wichtigsten Vorteile einer Master-Master-Anordnung, wie etwa symmetrische Konfiguration und einfaches Failover. Darüber hinaus sind sie vollständig davon abhängig, dass jeder Knoten im Ring verfügbar ist, wodurch sich die Wahrscheinlichkeit stark erhöht, dass das gesamte System ausfällt. Und falls Sie einen der Knoten aus dem Ring entfernen, können alle Replikations-Events, die von diesem Knoten stammten, in eine Endlosschleife gelangen. Sie kreisen für immer durch die Topologie, weil der einzige Server, der sie anhand ihrer Server-ID filtern könnte, der Server ist, der sie erzeugt hat. Ringe sind im Allgemeinen eigenwillig und sollten am besten vermieden werden. Sie können einige der Risiken einer ringförmigen Replikationsanordnung mildern, indem Sie Slaves hinzufügen, die an jedem Standort für Redundanz sorgen (siehe Abbildung 8-10). Das schützt allerdings nur vor dem Risiko eines Serverausfalls. Ein Stromausfall oder ein anderes Problem, das die Verbindung zwischen den Standorten beeinflusst, unterbricht weiterhin den gesamten Ring.
Slave
Slave
Master
Master
Master
Slave
Abbildung 8-10: Ein Replikationsring mit Slaves an jedem Standort
Master, Distribution-Master und Slaves Wir haben erwähnt, dass Slaves eine ziemliche Last auf einen Master laden können, wenn es genügend von ihnen gibt. Jeder Slave erzeugt auf dem Master einen neuen Thread, der den speziellen binlog dump-Befehl ausführt. Dieser Befehl liest die Daten aus dem Binärlog und schickt sie an den Slave. Die Arbeit wird für jeden Slave-Thread wiederholt; die Threads teilen sich die Ressourcen nicht, die für einen Binlog-Dump benötigt werden.
400 | Kapitel 8: Replikation
Wenn es viele Slaves und ein besonders großes Binärlog-Event, wie ein riesiges LOAD DATA INFILE, gibt, kann die Last auf dem Master deutlich ansteigen. Dem Master geht unter Umständen sogar der Speicher aus, und er stürzt ab, weil alle Slaves gleichzeitig das gleiche riesige Event anfordern. Falls andererseits die Slaves unterschiedliche Binlog-Events anfordern, die sich nicht mehr im Dateisystem-Cache befinden, werden vermutlich viele Festplattensuchen verursacht, die sich ebenfalls zuungunsten der Leistung des Servers auswirken. Aus diesem Grund bietet es sich an, die Last vom Server zu nehmen und einen Distribution-Master einzusetzen, wenn Sie viele Slaves benötigen. Ein Distribution-Master ist ein Slave, dessen einziger Zweck darin besteht, die Binärlogs vom Master zu lesen und anzubieten. Mit dem Distribution-Master können sich viele Slaves verbinden, wodurch der ursprüngliche Master von der Last abgeschirmt wird. Um zu vermeiden, dass die Abfragen tatsächlich auf dem Distribution-Master ausgeführt werden, sollten Sie seine Tabellen in die Blackhole-Storage-Engine überführen, wie es in Abbildung 8-11 gezeigt ist.
Master
Distribution-Master mit Blackhole-Storage-Engine
Viele Slaves
Abbildung 8-11: Ein Master, ein Distribution-Master und viele Slaves
Es ist schwer, genau anzugeben, wie viele Slaves ein Master bedienen kann, bevor er einen Distribution-Master braucht. Als Faustregel gilt: Wenn ein Master mit fast voller Kapazität läuft, dann sollten Sie ihm nicht mehr als 10 Slaves zuordnen. Gibt es nur eine geringe Schreibaktivität oder replizieren Sie nur einen Bruchteil der Tabellen, dann kann der Master wahrscheinlich viel mehr Slaves bedienen. Außerdem müssen Sie sich nicht auf einen Distribution-Master beschränken. Sie können mehrere verwenden, wenn Sie auf eine wirklich große Zahl von Slaves replizieren müssen. Sogar eine Pyramide aus Distribution-Mastern wäre möglich. Der Distribution-Master lässt sich auch für andere Aufgaben benutzen, wie etwa das Filtern und das Umschreiben von Regeln für die Binärlog-Events. Das ist effizienter, als wenn Sie das Logging, Umschreiben und Filtern auf jedem Slave wiederholen.
Replikationstopologien | 401
Falls Sie auf dem Distribution-Master mit Blackhole-Tabellen arbeiten, kann dieser mehr Slaves bedienen als üblich. Der Distribution-Master führt die Abfragen aus, die ausgesprochen preiswert sind, weil die Blackhole-Tabellen keine Daten enthalten. Häufig taucht die Frage auf, wie man sicherstellt, dass alle Tabellen auf dem Distribution-Master die Blackhole-Storage-Engine benutzen. Was passiert, wenn jemand auf dem Master eine neue Tabelle anlegt und eine andere Storage-Engine festlegt? Das Problem tritt übrigens auch auf, wenn Sie auf einem Slave eine andere Storage-Engine einsetzen wollen. Normalerweise setzt man die storage_engine-Option des Servers: storage_engine = blackhole
Dies beeinflusst nur CREATE TABLE-Anweisungen, die nicht explizit eine Storage-Engine angeben. Falls Sie eine Anwendung haben, die Sie nicht kontrollieren können, dann ist diese Topologie eventuell empfindlich. Sie können InnoDB mit der Option skip_innodb deaktivieren und die Tabellen auf MyISAM zurückgreifen lassen, Sie können jedoch die Engines MyISAM oder Memory nicht ausschalten. Der andere wesentliche Nachteil ist die Schwierigkeit, den Master durch einen der (tatsächlichen) Slaves zu ersetzen. Es ist schwierig, einen der Slaves an seine Stelle zu befördern, weil der dazwischenliegende Master dafür sorgt, dass er fast immer andere Binärlog-Koordinaten besitzt als der ursprüngliche Master.
Baum oder Pyramide? Falls Sie einen Master auf eine sehr große Anzahl von Slaves replizieren – egal, ob Sie die Daten geografisch verteilen oder lediglich versuchen, die Lesekapazität zu erhöhen –, kann es praktischer sein, ein pyramidenförmiges Design zu verwenden (siehe Abbildung 8-12).
Master
Slave
Slave
Slave
Slave
Slave
Abbildung 8-12: Eine pyramidenförmige Replikationstopologie
402 | Kapitel 8: Replikation
Slave
Der Vorteil dieses Aufbaus besteht darin, dass er die Last auf dem Master mildert – genau wie der Distribution-Master im vorherigen Abschnitt. Nachteilig ist, dass jeder Ausfall in einer Zwischenebene mehrere Server beeinträchtigt. Wären die Slaves jeweils direkt an den Master angeschlossen, würde dies nicht geschehen. Je mehr Zwischenebenen Sie außerdem haben, umso schwieriger und komplizierter wird der Umgang mit Ausfällen.
Eigene Replikationslösungen Die MySQL-Replikation ist so flexibel, dass man oft sogar eigene Lösungen für die Anforderungen einer Anwendung entwickeln kann. Typischerweise nutzt man eine Kombination aus Filterung, Verteilung und Replikation auf unterschiedliche Storage-Engines. Sie können natürlich auch »hacken«, also etwa auf und von Servern replizieren, die die Blackhole-Storage-Engine einsetzen (wie in »Master, Distribution-Master und Slaves« auf Seite 400 besprochen). Ihr Entwurf kann so ausgeklügelt sein, wie Sie wollen. Die größten Einschränkungen betreffen das, was Sie vernünftigerweise überwachen und administrieren können, sowie die Grenzen Ihrer Ressourcen (Netzwerkbandbreite, CPULeistung usw.).
Selektive Replikation Um die Lokalitätseigenschaft auszunutzen und Ihren Arbeitssatz für Leseoperationen im Speicher zu behalten, können Sie auf jeden der vielen Slaves eine kleine Menge Daten replizieren. Wenn jeder Slave einen Bruchteil der Daten des Masters enthält und Sie Leseoperationen an die Slaves umleiten, nutzen Sie den Speicher auf den einzelnen Slaves viel besser aus. Jeder Slave übernimmt auch nur einen Bruchteil der Schreiblast des Masters, so dass der Master viel leistungsfähiger wird, ohne dass die Slaves zurückfallen. Dieses Szenario ähnelt in gewisser Hinsicht der horizontalen Datenpartitionierung, auf die wir im nächsten Kapitel ausführlicher eingehen werden, besitzt aber den Vorteil, dass immer noch ein Server alle Daten vorhält – der Master. Das bedeutet, dass Sie für eine Schreibabfrage niemals auf mehr als einen Server schauen müssen. Gibt es Leseabfragen, die Daten benötigen, die nicht alle auf einem einzigen Slave-Server vorliegen, dann haben Sie die Möglichkeit, diese Leseoperationen auf dem Master ausführen zu lassen. Selbst wenn Sie nicht alle Leseoperationen auf den Slaves durchführen können, sollten Sie in der Lage sein, viele von ihnen vom Master abzuziehen. Am einfachsten geht das, indem Sie die Daten auf verschiedene Datenbanken auf dem Master aufteilen und dann jede Datenbank auf einen anderen Slave-Server replizieren. Falls Sie z.B. Daten für jede Abteilung in Ihrem Unternehmen auf einen anderen Slave replizieren wollen, legen Sie Datenbanken namens sales, marketing, procurement usw. an. Jeder Slave muss dann eine replicate_wild_do_table-Konfigurationsoption enthalten, die seine Daten auf die angegebene Datenbank beschränkt. Hier ist die Konfigurationsoption für die sales-Datenbank: replicate_wild_do_table = sales.%
Replikationstopologien | 403
Das Filtern mit einem Distribution-Master ist ebenfalls sinnvoll. Falls Sie z.B. nur einen Teil eines stark ausgelasteten Servers über ein langsames oder sehr teures Netzwerk replizieren wollen, dann nutzen Sie einfach einen lokalen Distribution-Master mit BlackholeTabellen und Filterregeln. Der Distribution-Master kann Replikationsfilter enthalten, die unerwünschte Einträge aus seinen Logs entfernen. Auf diese Weise vermeiden Sie gefährliche Loggingeinstellungen auf dem Master, und außerdem müssen Sie nicht alle Logs über das Netzwerk auf die entfernten Slaves übertragen.
Funktionen trennen Viele Anwendungen enthalten einen Mix aus OLTP- (Online Transaction Processing) und OLAP-(Online Analytical Processing-)Abfragen. OLTP-Abfragen sind meist kurz und transaktionsfähig. OLAP-Abfragen dagegen sind normalerweise groß und langsam und verlangen keine absolut aktuellen Daten. Die zwei Arten von Abfragen beanspruchen den Server völlig unterschiedlich. Daher werden sie am besten auf Servern ausgeführt, die verschieden konfiguriert sind und vielleicht sogar unterschiedliche StorageEngines und Hardware verwenden. Eine gebräuchliche Lösung für dieses Problem besteht darin, die Daten des OLTP-Servers auf Slaves zu replizieren, die speziell für die OLAP-Last entworfen wurden. Diese Slaves können eine andere Hardware, andere Konfigurationen, Indizes und/oder Storage-Engines besitzen. Wenn Sie einen Slave für OLAP-Abfragen vorsehen, dann könnten Sie auf diesem Slave auch eine gewisse Verzögerung bei der Replikation oder eine anderweitig verminderte Service-Qualität tolerieren. Das bedeutet möglicherweise, dass Sie ihn für Aufgaben einsetzen, die auf einem nicht speziell zugewiesenen Slave zu einer unakzeptablen Performance führen würden, etwa für das Ausführen sehr lange laufender Abfragen. Es ist keine spezielle Replikationsanordnung erforderlich. Allerdings erreichen Sie vielleicht deutliche Einsparungen, wenn Sie nicht alle Daten des Masters auf dem Slave haben. Denken Sie daran: Selbst wenn Sie nur einen kleinen Teil der Daten mit Replikationsfiltern im Relay-Log herausfiltern können, verringern Sie die Ein-/Ausgabe- und Cache-Aktivität.
Daten archivieren Sie können Daten auf einem Slave-Server archivieren – d.h., sie auf dem Slave behalten, aber vom Master entfernen –, indem Sie Löschabfragen auf dem Master ausführen und dafür sorgen, dass diese Abfragen nicht auf dem Slave ausgeführt werden. Dazu sind zwei Methoden üblich: Entweder Sie deaktivieren selektiv das Binär-Logging auf dem Master, oder Sie setzen auf dem Slave replicate_ignore_db-Regeln ein. Die erste Methode verlangt das Ausführen von SET SQL_LOG_BIN=0 in dem Prozess, der die Daten auf dem Master aufräumt. Anschließend werden die Daten aufgeräumt. Auf dem Slave ist in diesem Fall keine besondere Replikationskonfiguration erforderlich. Und da die Anweisungen auch nicht in das Binärlog des Masters gelangen, ist diese Methode
404 | Kapitel 8: Replikation
sogar noch ein bisschen effizienter. Der größte Nachteil besteht darin, dass Sie das Binärlog auf dem Master nicht mehr dazu verwenden können, die punktgenaue Wiederherstellung zu überwachen, da es nicht mehr jede Modifikation enthält, die Sie an den Daten des Masters vorgenommen haben. Außerdem ist für diese Methode die SUPER-Berechtigung nötig. Bei der zweiten Technik wird eine bestimmte Datenbank mit USE auf dem Master eingesetzt, bevor die Anweisungen ausgeführt werden, die die Daten aufräumen. Sie können z.B. eine Datenbank namens purge anlegen und dann in der my.cnf-Datei des Slaves replicate_ignore_db=purge festlegen. Anschließend starten Sie den Slave neu. Der Slave ignoriert Anweisungen, die diese Datenbank mit USE benutzen. Dieser Ansatz hat nicht die Schwächen der ersten Technik, bringt allerdings den (kleinen) Nachteil mit sich, dass der Slave veranlasst wird, Binärlog-Events zu holen, die er gar nicht braucht. Potenziell könnte außerdem jemand versehentlich Abfragen in der purge-Datenbank ausführen, die nicht zum Aufräumen der Daten gedacht sind, was den Slave dazu bringt, gewünschte Events nicht wieder abzuspielen. Das Maakit-Programm mk-archiver unterstützt beide Methoden. Eine dritte Möglichkeit besteht darin, mit binlog_ignore_db ReplikationsEvents herauszufiltern. Wie wir aber bereits angemerkt haben, halten wir das in den meisten Situationen für gefährlich.
Slaves für Volltextsuchen einsetzen Viele Anwendungen verlangen eine Kombination aus Transaktionen und Volltextsuchen. Allerdings sind nur in MyISAM-Tabellen Fähigkeiten zur Volltextsuche eingebaut, und MyISAM unterstützt keine Transaktionen. Eine Lösung wäre, einen Slave für Volltextsuchen zu konfigurieren, indem die Storage-Engine für bestimmte Tabellen auf dem Slave auf MyISAM geändert wird. Sie können Volltextindizes hinzufügen und Volltextsuchabfragen auf dem Slave ausführen. Dadurch werden potenzielle Replikationsprobleme mit transaktionsfähigen und nichttransaktionsfähigen Storage-Engines in derselben Abfrage auf dem Master vermieden, und der Master muss nicht noch zusätzlich die Volltextindizes pflegen.
Schreibgeschützte Slaves In vielen Unternehmen zieht man es vor, die Slaves nur zum Lesen zuzulassen, damit ungewollte Änderungen nicht die Replikation unterbrechen. Dazu verwendet man die Konfigurationsvariable read_only. Sie deaktiviert die meisten Schreiboperationen: Ausnahmen bilden die Slave-Prozesse, Benutzer mit der Berechtigung SUPER und temporäre Tabellen. Diese Lösung ist perfekt, solange Sie nicht normalen Benutzern die Berechtigung SUPER gewähren, was Sie sowieso nie tun sollten.
Replikationstopologien | 405
Multimaster-Replikation emulieren MySQL bietet momentan keine Unterstützung für die Multimaster-Replikation (d.h. einen Slave mit mehr als einem Master). Sie können diese Topologie jedoch emulieren, indem Sie einen Slave so konfigurieren, dass er abwechselnd auf unterschiedliche Master verweist. Zum Beispiel verweisen Sie den Slave auf Master A und lassen ihn eine Weile laufen, richten ihn dann für eine Weile auf Master B und wechseln dann wieder zurück auf Master A. Wie gut das funktioniert, hängt von Ihren Daten ab sowie davon, wie viel Arbeit die beiden Master für den einen Slave verursachen. Ist die Last auf Ihren Mastern relativ leicht und kommen sich ihre Updates nicht ins Gehege, dann könnte das ganz gut funktionieren. Sie müssen die Binärlog-Koordinaten für die einzelnen Master im Auge behalten. Außerdem sollten Sie sicherstellen, dass der Ein-/Ausgabe-Thread des Slaves nicht mehr Daten holt, als er in jedem Durchlauf ausführen soll; ansonsten erhöhen Sie unter Umständen den Netzwerkverkehr deutlich, indem Sie in jedem Zyklus viele Daten holen und verwerfen. Es gibt für diesen Zweck ein fertiges Skript: http://code.google.com/p/mysql-mmre/. Sie können die Multimaster-Replikation mithilfe einer Master-Master- (oder Ring-) Replikation und der Blackhole-Storage-Engine mit einem Slave emulieren, wie Abbildung 8-13 zeigt. Co-Master1
Co-Master2
DB1
DB1
DB2
DB2
DB1
Blackhole-Storage-Engines DB2
Slave1 Abbildung 8-13: Emulation der Multimaster-Replikation mit zwei Mastern und der Blackhole-StorageEngine
In dieser Konfiguration besitzen die beiden Master jeweils ihre eigenen Daten. Sie enthalten außerdem die Tabellen vom anderen Master, benutzen aber die Blackhole-StorageEngine, um zu vermeiden, dass sie tatsächlich Daten in diesen Tabellen speichern. Ein Slave ist an einen der Co-Master angeschlossen – es spielt keine Rolle, an welchen. Dieser Slave verwendet nicht die Blackhole-Storage-Engine, so dass er im Prinzip der Slave beider Master ist.
406 | Kapitel 8: Replikation
Eigentlich ist dafür keine Master-Master-Topologie erforderlich. Sie können einfach von server1 auf server2 auf den Slave replizieren. Wenn server2 die Blackhole-StorageEngine für Tabellen benutzt, die von server1 repliziert wurden, enthält er keine Daten von server1, wie in Abbildung 8-14 zu sehen ist.
Master 1
DB1
DB1 Master 2 DB2
DB1 Slave DB2
Abbildung 8-14: Eine weitere Methode, um die Multimaster-Replikation zu emulieren
Beide Konfigurationen können unter den üblichen Problemen leiden, wie etwa widersprüchlichen Aktualisierungen und CREATE TABLE-Anweisungen, die explizit eine StorageEngine angeben.
Einen Log-Server erzeugen Eines der Dinge, die Sie mit der MySQL-Replikation erreichen können, ist die Erzeugung eines »Log-Servers« ohne Daten, dessen einzige Aufgabe darin besteht, das Abspielen und/oder Filtern von Binärlog-Events zu erleichtern. Wie Sie später erkennen werden, nützt das, wenn man die Replikation nach einem Absturz neu starten muss, und hilft bei der punktgenauen Wiederherstellung, die wir in Kapitel 11 vorstellen. Stellen Sie sich vor, Sie haben eine Gruppe von Binärlogs oder Relay-Logs – vielleicht aus einem Backup, vielleicht von einem abgestürzten Server – und wollen die darin enthaltenen Events wieder abspielen. Sie könnten die Events mit mysqlbinlog extrahieren, bequemer und effizienter ist es jedoch, eine MySQL-Instanz ohne Daten einzurichten und ihr vorzugaukeln, dass die Binärlogs ihre eigenen wären. Sie können das MySQL SandboxSkript von http://sourceforge.net/projects/mysql-sandbox/ benutzen, um den Log-Server einzurichten, falls Sie ihn nur zeitweise benötigen. Der Log-Server benötigt keine Daten, weil er keine Logs ausführen wird – er dient nur den Logs der anderen Server. (Er braucht allerdings einen Replikationsbenutzer.) Schauen wir uns an, wie diese Technik funktioniert (Anwendungen dafür zeigen wir später). Nehmen Sie an, die Logs heißen somelog-bin.000001, somelog-bin.000002 usw.
Replikationstopologien | 407
Legen Sie diese Dateien in das Binärlogverzeichnis Ihres Log-Servers. Wir nehmen einmal an, das ist /var/log/mysql. Bevor Sie den Log-Server starten, bearbeiten Sie seine my.cnfDatei: log_bin = /var/log/mysql/somelog-bin log_bin_index = /var/log/mysql/somelog-bin.index
Der Server entdeckt die Log-Dateien nicht automatisch, Sie müssen also die Log-Indexdatei des Servers aktualisieren. Nutzen Sie dazu auf Unix-artigen Systemen folgenden Befehl:9 # /bin/ls -1 /var/log/mysql/somelog-bin.[0-9]* > /var/log/mysql/somelog-bin.index
Sorgen Sie dafür, dass der Benutzer-Account, unter dem MySQL läuft, die Log-Indexdatei lesen und schreiben kann. Jetzt können Sie Ihren Log-Server starten und mit SHOW MASTER LOGS überprüfen, ob er die Log-Dateien sieht. Wieso ist es für die Wiederherstellung besser, einen Log-Server anstelle von mysqlbinlog einzusetzen? Dafür gibt es mehrere Gründe: • Es ist schneller, weil jetzt die Anweisungen nicht mehr aus dem Log extrahiert und an mysql geleitet werden müssen. • Sie können den Fortgang leicht überblicken. • Sie können Fehlern leicht begegnen. Es ist z.B. möglich, Anweisungen zu überspringen, die nicht repliziert werden können. • Sie können leicht Replikations-Events filtern. • Manchmal ist mysqlbinlog aufgrund der Änderungen des Logging-Formats nicht in der Lage, das Binärlog zu lesen.
Replikation und Kapazitätsplanung Meist sind Schreiboperationen die Engstelle bei der Replikation. Es ist schwierig, Schreiboperationen mit der Replikation zu skalieren. Sie müssen richtig rechnen, wenn Sie planen, welche Kapazität die Slaves Ihrem System insgesamt hinzufügen. Es kommt in diesem Zusammenhang leicht zu Fehlern. Stellen Sie sich z.B. vor, Ihre Arbeitsbelastung besteht zu 20 % aus Schreib- und zu 80 % aus Lesevorgängen. Um die Berechnungen zu erleichtern, wollen wir vereinfachen und Folgendes annehmen: • Lese- und Schreibabfragen erfordern die gleiche Menge Arbeit. • Alle Server sind genau gleich und haben die Kapazität, exakt 1.000 Abfragen pro Sekunde zu erledigen.
9 Wir benutzen explizit /bin/ls, um zu verhindern, dass gebräuchliche Aliase aufgerufen werden, die TerminalEscape-Codes für eine farbige Darstellung hinzufügen.
408 | Kapitel 8: Replikation
• Slaves und Master besitzen die gleichen Leistungsmerkmale. • Sie können alle Leseabfragen auf die Slaves verschieben. Falls Sie momentan einen Server haben, der 1.000 Abfragen pro Sekunde erledigt, wie viele Slaves müssen Sie dann hinzufügen, um das Doppelte Ihrer aktuellen Last zu verarbeiten und alle Leseabfragen auf die Slaves zu verschieben? Es sieht so aus, als könnten Sie 2 Slaves hinzufügen und die 1.600 Leseoperationen zwischen ihnen aufteilen. Vergessen Sie jedoch nicht, dass Ihre Schreiblast sich auf 400 Abfragen pro Sekunde erhöht hat und nicht zwischen Mastern und Slaves aufgeteilt werden kann. Jeder Slave muss 400 Schreiboperationen pro Sekunde durchführen. Das bedeutet, dass jeder Slave zu 40 % mit Schreiboperationen belastet wird und nur 600 Leseoperationen pro Sekunde bedienen kann. Daher brauchen Sie nicht zwei, sondern drei Slaves, um den doppelten Verkehr zu erledigen. Was ist, wenn Ihr Verkehr sich wieder verdoppelt? Es gibt dann 800 Schreiboperationen pro Sekunde, der Master kommt also noch hinterher. Die Slaves sind dann aber auch zu 80 % mit Schreiboperationen belastet, so dass Sie 16 Slaves brauchen, um die 3.200 Leseabfragen pro Sekunde zu verarbeiten. Und wenn sich der Verkehr jetzt noch ein wenig erhöht, ist es für den Master zu viel. Das ist weit von einer linearen Skalierbarkeit entfernt: Sie benötigen 17-mal so viele Server, um die vierfache Anzahl an Abfragen zu verarbeiten. Dies verdeutlicht, dass Sie schnell einen Punkt abnehmender Erträge erreichen, wenn Sie Slaves zu einem einzigen Master hinzufügen. Und das gilt sogar bei unseren unrealistischen Annahmen, die z.B. die Tatsache ignorieren, dass eine anweisungsbasierte Single-Thread-Replikation normalerweise dafür sorgt, dass die Slaves eine niedrigere Kapazität als der Master aufweisen. Eine echte Replikationsanordnung ist wahrscheinlich sogar noch schlechter als unsere theoretische.
Weshalb die Replikation das Skalieren von Schreiboperationen nicht unterstützt Das grundlegende Problem mit dem miesen Verhältnis von Server zu Kapazität besteht darin, dass Sie die Schreibvorgänge im Gegensatz zu den Lesevorgängen nicht gleichermaßen zwischen den Maschinen verteilen können. Leseoperationen lassen sich also skalieren, Schreiboperationen nicht. Sie werden sich fragen, ob es eine Möglichkeit gibt, mittels Replikation die Schreibkapazität zu erhöhen. Die Antwort lautet Nein – nicht einmal ein bisschen. Das Aufteilen (Partitionieren) Ihrer Daten ist die einzige Methode, mit der Sie Schreiboperationen skalieren können. Näheres dazu finden Sie im nächsten Kapitel. Manche Leser denken jetzt vielleicht darüber nach, eine Master-Master-Topologie zu verwenden (siehe »Master-Master im Aktiv-Aktiv-Modus« auf Seite 395) und auf beide Master zu schreiben. Diese Konfiguration kann im Vergleich zu einer Master-Slave-Topologie
Replikation und Kapazitätsplanung | 409
etwas mehr Schreiboperationen erledigen, weil Sie die Nachteile durch die Serialisierung gleichmäßig zwischen den beiden Servern aufteilen können. Wenn Sie auf jedem Server 50 % der Schreiboperationen erledigen, dann müssen nur die 50 %, die via Replikation vom anderen Server erfolgen, serialisiert werden. Theoretisch ist das besser, als 100 % der Schreiboperationen parallel auf einer Maschine (dem Master) durchzuführen und 100 % der Schreiboperationen seriell auf der anderen Maschine (dem Slave). Das scheint attraktiv zu sein. Eine solche Konfiguration kann allerdings trotzdem nicht so viele Schreiboperationen durchführen wie ein einzelner Server. Ein Server, dessen Schreiblast zu 50 % serialisiert ist, ist langsamer als ein einzelner Server, der alle seine Schreibvorgänge parallel erledigt. Deshalb eignet sich diese Taktik nicht zum Skalieren von Schreiboperationen. Es ist nur eine Methode, um den Nachteil serialisierten Schreibens auf zwei Server zu verteilen, damit das »schwächste Glied in der Kette« nicht ganz so schwach ist. Sie bietet nur eine relativ kleine Verbesserung gegenüber einer Aktiv-Passiv-Anordnung, das zusätzliche Risiko ist jedoch unverhältnismäßig viel größer – und, wie Sie im nächsten Abschnitt erfahren, Sie haben eigentlich gar nichts davon.
Seien Sie großzügig und verschwenderisch Wenn Sie Ihre Server absichtlich teilweise ungenutzt lassen, können Sie auf clevere und kosteneffektive Weise eine große Anwendung aufbauen, speziell, wenn Sie Replikation einsetzen. Server, die freie Kapazitäten aufweisen, tolerieren Lastspitzen besser, haben mehr Potenzial, um langsame Abfragen und Wartungsaufgaben zu erledigen (wie etwa OPTIMIZE TABLE-Operationen), und halten besser mit der Replikation Schritt. Es ist normalerweise der falsche Ansatz zu versuchen, die Replikationsnachteile auszugleichen, indem man in einer Master-Master-Topologie auf beide Knoten schreibt. Sie sollten ein Master-Master-Paar üblicherweise zu weniger als 50 % mit Leseoperationen belasten, denn wenn die Last größer ist, reicht die Kapazität nicht aus, falls einer der Server ausfällt. Wenn beide Server die Last auch allein bewältigen könnten, dann müssen Sie sich wahrscheinlich nicht um die Nachteile bei der Single-Thread-Replikation sorgen. Das Anlegen einer übertriebenen Kapazität stellt darüber hinaus eines der besten Mittel dar, um Hochverfügbarkeit zu erreichen, obwohl es dafür auch andere Methoden gibt. So könnten Sie Ihre Anwendung auch in einem »geschwächten« Modus ausführen, wenn es einen Ausfall gegeben hat. Im nächsten Kapitel untersuchen wir das genauer.
Replikationsadministration und -wartung Vermutlich werden Sie sich nicht ständig mit dem Einrichten der Replikation befassen, zumindest, wenn Sie nicht sehr viele Server haben. Sobald Sie allerdings mit der Einrichtung fertig sind, gehören Überwachung und Administration Ihrer Replikationstopologie zu Ihren regelmäßigen Aufgaben, egal, wie viele Server Sie haben.
410 | Kapitel 8: Replikation
Sie sollten versuchen, diese Arbeit nach Möglichkeit zu automatisieren. Allerdings müssen Sie dazu keine eigenen Werkzeuge schreiben: In Kapitel 14 stellen wir verschiedene Programme für MySQL vor, von denen viele bereits mit Überwachungsmöglichkeiten oder Plugins ausgestattet sind. Zu den nützlichsten Angeboten gehören Nagios, MySQL Enterprise Monitor und MonYOG.
Die Replikation überwachen Die Replikation erhöht die Komplexität der MySQL-Überwachung. Obwohl die Replikation sowohl auf dem Master als auch auf dem Slave stattfindet, wird die meiste Arbeit auf dem Slave erledigt. Dort treten auch die meisten Probleme auf. Replizieren wirklich alle Slaves? Hat ein Slave Fehler gezeigt? Wie weit liegt der langsamste Slave zurück? MySQL bietet die meisten Informationen, die Sie benötigen, um diese Fragen zu beantworten. Ihnen bleibt es allerdings überlassen, den Vorgang zu überwachen und die Replikation zu stabilisieren. Auf dem Master können Sie mit dem Befehl SHOW MASTER STATUS die aktuelle BinärlogPosition des Masters und die Konfiguration ermitteln (siehe »Master und Slave konfigurieren« auf Seite 378). Sie können den Master auch fragen, welche Binärlogs auf der Festplatte vorliegen: mysql> SHOW MASTER LOGS; +------------------+-----------+ | Log_name | File_size | +------------------+-----------+ | mysql-bin.000220 | 425605 | | mysql-bin.000221 | 1134128 | | mysql-bin.000222 | 13653 | | mysql-bin.000223 | 13634 | +------------------+-----------+
Diese Informationen helfen Ihnen dabei, festzustellen, welche Parameter Sie dem Befehl PURGE MASTER LOGS übergeben müssen. Mit dem Befehl SHOW BINLOG EVENTS können Sie sich Replikations-Events im Binärlog anschauen. Nachdem wir z.B. den vorangegangenen Befehl ausgeführt hatten, erzeugten wir eine Tabelle auf einem ansonsten unbenutzten Server. Da wir wussten, dass dies die einzige Anweisung war, die irgendwelche Daten ändert, wussten wir, dass der Offset der Anweisung im Binärlog 13634 war. Wir konnten sie uns also folgendermaßen anschauen: mysql> SHOW BINLOG EVENTS IN 'mysql-bin.000223' FROM 13634\G *************************** 1. row *************************** Log_name: mysql-bin.000223 Pos: 13634 Event_type: Query Server_id: 1 End_log_pos: 13723 Info: use `test`; CREATE TABLE test.t(a int)
Replikationsadministration und -wartung | 411
Den Rückstand des Slaves messen Zu den Dingen, die Sie am häufigsten überwachen müssen, gehört der Rückstand, den ein Slave gegenüber dem Master hat. Die Spalte Seconds_behind_master in SHOW SLAVE STATUS zeigt zwar theoretisch den Rückstand des Slaves, dieser Wert ist allerdings aus verschiedenen Gründen nicht immer exakt: • Der Slave berechnet Seconds_behind_master, indem er den aktuellen Zeitstempel des Servers mit dem Zeitstempel vergleicht, der im Binärlog-Event aufgezeichnet ist, so dass der Slave seinen Rückstand erst dann melden kann, wenn er eine Abfrage verarbeitet. • Der Slave gibt normalerweise NULL zurück, wenn die Slave-Prozesse nicht laufen. • Manche Fehler (z.B. falsch angepasste max_allowed_packet-Einstellungen zwischen dem Master und dem Slave oder ein instabiles Netzwerk) können die Replikation unterbrechen und/oder die Slave-Threads stoppen. Seconds_behind_master gibt jedoch eine 0 zurück, anstatt einen Fehler anzuzeigen. • Der Slave kann manchmal die Verzögerung nicht berechnen, obwohl die Slave-Prozesse laufen. Wenn dies geschieht, meldet der Slave entweder 0 oder NULL. • Eine sehr lange Transaktion kann dafür sorgen, dass der berichtete Rückstand schwankt. Falls Sie z.B. eine Transaktion haben, die Daten aktualisiert, stundenlang offen bleibt und dann bestätigt wird, gelangt die Aktualisierung ungefähr eine Stunde, nachdem sie tatsächlich aufgetreten ist, in das Binärlog. Wenn der Slave die Anweisung verarbeitet, berichtet er zeitweise, dass er eine Stunde hinter dem Master zurückliegt, und springt dann wieder auf einen Rückstand von null Sekunden. • Wenn ein Distribution-Master zurückfällt und selbst Slaves besitzt, berichten die Slaves, dass sie null Sekunden zurückliegen, wenn sie zum Distribution-Master aufgeholt haben, obwohl es relativ zum eigentlichen Master einen wirklichen Rückstand gibt. Ignorieren Sie deshalb Seconds_behind_master, und messen Sie die Verzögerung des Slaves mit etwas, das Sie direkt überwachen und messen können. Eine gute Lösung wäre ein Heartbeat Record, also ein Zeitstempel, den Sie einmal pro Sekunde auf dem Master aktualisieren können. Um den Rückstand zu berechnen, subtrahieren Sie einfach den Heartbeat vom aktuellen Zeitstempel auf dem Slave. Diese Methode ist immun gegenüber all den gerade erwähnten Problemen und hat den zusätzlichen Vorteil, dass ein praktischer Zeitstempel erzeugt wird, der zeigt, an welchem Punkt in der Zeit sich die Daten des Slaves momentan befinden. Das mk-heartbeat-Skript, das in Maatkit enthalten ist, ist eine Implementierung eines Replikations-Heartbeats. Keines der genannten Maße für den Rückstand bietet Ihnen einen Anhaltspunkt dafür, wie lange es dauert, bis ein Slave tatsächlich seinen Master eingeholt hat. Das hängt von vielen Faktoren ab, etwa von der Leistungsfähigkeit des Slaves und der Anzahl der Schreibabfragen, die der Master verarbeitet.
412 | Kapitel 8: Replikation
Feststellen, ob Slaves konsistent mit dem Master sind In einer perfekten Welt würde ein Slave immer eine exakte Kopie seines Masters sein. Tatsächlich aber sorgen Fehler bei der Replikation dafür, dass die Daten des Slaves von denen des Masters »wegdriften«. Selbst wenn offensichtlich keine Fehler auftreten, können Slaves abweichen, weil es MySQL-Eigenschaften gibt, die nicht korrekt repliziert werden, weil Bugs in MySQL, Netzwerkschäden, Abstürze, unsanftes Herunterfahren oder andere Ausfälle auftreten.10 Unserer Erfahrung nach ist das die Regel, nicht die Ausnahme, was bedeutet, dass es eigentlich eine Routineaufgabe sein sollte, Ihre Slaves auf Konsistenz mit ihren Mastern zu prüfen. Das ist besonders wichtig, wenn Sie die Replikation für Backups benutzen, weil Sie natürlich keine Backups von einem beschädigten Slave nehmen wollen. Die erste Ausgabe dieses Buches enthielt ein Beispielskript, mit dem man die Anzahl der Zeilen in den Tabellen auf dem Master und dem Slave vergleichen konnte. Damit kann man sicher einige Unterschiede feststellen, allerdings ist die Zeilenanzahl keine echte Gewähr für identische Daten. Was Sie wirklich brauchen, ist eine effiziente Methode, um die eigentlichen Inhalte der Tabellen zu vergleichen. In MySQL ist keine Methode integriert, mit der sich feststellen ließe, ob ein Server die gleichen Daten enthält wie ein anderer Server. Es besitzt einige Grundbausteine, um Prüfsummen von Tabellen und Daten zu erzeugen, wie etwa CHECKSUM TABLE. Es ist jedoch nicht trivial, einen Slave mit seinem Master zu vergleichen, während die Replikation im Gange ist. Maatkit enthält ein Werkzeug namens mk-table-checksum, das dieses und etliche andere Probleme löst. Das Werkzeug besitzt mehrere Funktionen, einschließlich schneller paralleler Vergleiche vieler Server auf einmal. Seine Hauptfunktion ist allerdings, zu verifizieren, ob die Daten eines Slaves synchron zu denen seines Masters sind. Dazu führt es INSERT ... SELECT-Abfragen auf dem Master ab. Diese Abfragen erzeugen Prüfsummen der Daten und fügen die Ergebnisse in eine Tabelle ein. Die Anweisungen laufen durch die Replikation und werden erneut auf dem Slave ausgeführt. Sie können die Ergebnisse auf dem Master dann mit den Ergebnissen auf dem Slave vergleichen und feststellen, ob sich die Daten unterscheiden. Da dieser Vorgang durch die Replikation geht, erhalten Sie konsistente Ergebnisse, ohne dass Sie die Tabellen auf beiden Servern gleichzeitig sperren müssen. Typischerweise wird dieses Werkzeug auf dem Master ausgeführt, und zwar mit solchen Parametern: $ mk-table-checksum --replicate=test.checksum --chunksize 100000 --sleepcoef=2 <master_host>
10 Wenn Sie eine nichttransaktionsfähige Storage-Engine benutzen, dann ist das Herunterfahren des Servers, ohne zuerst STOP SLAVE auszuführen, unsanft.
Replikationsadministration und -wartung | 413
Dieser Befehl erzeugt aus allen Tabellen Prüfsummen, wobei er versucht, die Tabellen in Gruppen von ungefähr 100.000 Zeilen zu verarbeiten, und fügt die Ergebnisse in die test.checksum-Tabelle ein. Zwischen den einzelnen Verarbeitungsgruppen pausiert er und schläft doppelt so lange, wie es dauerte, die Prüfsumme für die letzte Gruppe zu erzeugen. Damit wird sichergestellt, dass die Abfragen den normalen Datenbankbetrieb nicht blockieren. Sobald die Abfragen auf den Slaves repliziert wurden, kann eine einfache Abfrage den Slave auf Unterschiede zum Master prüfen. mk-table-checksum sucht automatisch die Slaves eines Servers, führt die Abfrage auf den einzelnen Slaves aus und gibt die Ergebnisse aus. Der folgende Befehl steigt, jeweils beim gleichen Master-Server beginnend, bis auf eine Tiefe von 10 in der Slave-Hierarchie hinab und gibt Tabellen aus, die sich vom Master unterscheiden: $ mk-table-checksum --replicate=test.checksum --replcheck 10 <master_host>
Bei MySQL AB plant man, irgendwann eine ähnliche Funktion im Server selbst zu implementieren. Das ist dann wahrscheinlich besser als ein externes Skript, aber momentan ist mk-table-checksum das einzige Werkzeug, mit dem man zuverlässig und einfach die Daten eines Slaves mit denen seines Masters vergleichen kann.
Einen Slave wieder mit dem Master synchronisieren Sie werden vermutlich mehr als einmal in Ihrer beruflichen Laufbahn mit einem Slave in Berührung kommen, dessen Daten nicht mehr mit denen des Masters übereinstimmen. Vielleicht haben Sie mit der Prüfsummentechnik Unterschiede entdeckt, vielleicht wissen Sie auch, dass der Slave eine Abfrage übersprungen hat oder dass jemand die Daten auf dem Slave geändert hat. Der herkömmliche Rat zum Reparieren eines nichtsynchronisierten Slaves lautet, ihn zu stoppen und wieder vom Master zu klonen. Wenn ein inkonsistenter Slave ein ernsthaftes Problem darstellt, dann sollten Sie ihn wahrscheinlich anhalten und aus dem Betrieb entfernen, sobald Sie ihn gefunden haben. Anschließend können Sie den Slave erneut klonen oder aus einem Backup wiederherstellen. Nachteilig an diesem Ansatz ist seine Unbequemlichkeit, vor allem, wenn Sie viele Daten haben. Wenn Sie feststellen können, welche Daten sich unterscheiden, dann können Sie es vermutlich effizienter erledigen, als durch das erneute Klonen des gesamten Servers. Und wenn die entdeckte Inkonsistenz nicht bedeutsam ist, dann lassen Sie ihn einfach in Betrieb und synchronisieren nur die betroffenen Daten. Am einfachsten ist es, nur die betroffenen Daten mit mysqldump zu speichern und erneut zu laden. Das funktioniert ganz gut, wenn sich die Daten währenddessen nicht ändern. Sie sperren die Tabelle auf dem Master, stellen einen Dump der Tabelle her, warten darauf, dass der Slave den Master einholt, und importieren dann die Tabelle auf den Slave. (Sie müssen auf den Slave warten, damit Sie in den anderen Tabellen keine weiteren Inkonsistenzen einführen, wie etwa solche, die dann wiederum in Joins mit der nichtsynchronisierten Tabelle auftreten.)
414 | Kapitel 8: Replikation
Das geht zwar in vielen Fällen gut, ist aber auf einem stark belasteten Server oft unmöglich. Es hat außerdem den Nachteil, dass die Daten des Slaves außerhalb der Replikation geändert werden. Das Ändern der Slave-Daten durch die Replikation (indem Änderungen auf dem Master vorgenommen werden) ist normalerweise die sicherste Technik, da hässliche Race-Conditions und andere Überraschungen vermieden werden. Wenn die Tabelle sehr groß oder die Netzwerkbandbreite begrenzt ist, dann sind das Speichern in einem Dump und das Neuladen unerschwinglich teuer. Was ist, wenn sich jede tausendste Zeile in einer Tabelle mit Millionen von Zeilen unterscheidet? Das Speichern und Neuladen der gesamten Tabelle wäre in diesem Fall eine Verschwendung. mk-table-sync ist ein weiteres Werkzeug von Maatkit, das einige dieser Probleme löst. Es kann Unterschiede zwischen Tabellen effizient finden und auflösen. Außerdem kann es durch die Replikation agieren, indem es den Slave durch das Ausführen von Abfragen auf dem Master neu synchronisiert – also: keine Race-Conditions. Es funktioniert jedoch nicht in allen Szenarien: Es erfordert, dass die Replikation läuft, um einen Master und einen Slave korrekt zu synchronisieren. Das Werkzeug funktioniert also nicht, wenn bei der Replikation ein Fehler auftritt. mk-table-sync wurde mit Blick auf Effizienz geschaffen, ist bei extrem großen Datenmengen jedoch unpraktisch. Der Vergleich von einem Terabyte Daten auf dem Master und dem Slave verursacht zwangsläufig zusätzliche Arbeit für beide Server. In Fällen, in denen es jedoch funktioniert, können Sie eine Menge Zeit und Aufwand sparen.
Die Master wechseln Früher oder später müssen Sie einen Slave auf einen neuen Master richten. Vielleicht tauschen Sie die Server aufgrund eines Upgrades, vielleicht gab es ja auch einen Ausfall und Sie müssen einen Slave zum Master machen, oder vielleicht teilen Sie einfach nur neue Kapazitäten zu. Egal, welchen Grund es gibt, Sie müssen den Slave über seinen neuen Master informieren. Wenn der Prozess geplant ist, dann ist es leicht (oder zumindest leichter als im Krisenfall). Sie rufen auf dem Slave einfach den CHANGE MASTER TO-Befehl mit den passenden Werten auf. Die meisten der Werte sind optional; Sie müssen nur diejenigen angeben, die Sie ändern. Der Slave verwirft seine aktuelle Konfiguration und die Relay-Logs und beginnt damit, vom neuen Master zu replizieren. Er aktualisiert außerdem die master.info-Datei mit den neuen Parametern, damit die Änderung einen Neustart des Slaves überdauert. Der schwierigste Teil dieses Vorgangs ist das Ermitteln der gewünschten Position auf dem neuen Master, damit der Slave an der gleichen logischen Position beginnt, an der er auf dem alten Master gestoppt hat. Das Befördern eines Slaves zum neuen Master ist etwas schwieriger. Es gibt zwei Grundszenarien, wie man einen Master durch einen seiner Slaves ersetzt. Beim ersten handelt es sich um eine geplante Beförderung, beim zweiten um eine ungeplante.
Replikationsadministration und -wartung | 415
Geplante Beförderungen Einen Slave zum Master zu befördern, ist vom Konzept her einfach. Es sind folgende Schritte erforderlich: 1. Schreiboperationen auf dem alten Master werden gestoppt. 2. Optional müssen seine Slaves bei der Replikation aufholen (dies vereinfacht die
nachfolgenden Schritte). 3. Ein Slave muss zum neuen Master konfiguriert werden. 4. Slaves und Schreibverkehr müssen auf den neuen Master gerichtet werden, anschließend müssen Schreiboperationen auf ihm zugelassen werden. Der Teufel steckt jedoch im Detail. Es sind mehrere Szenarien möglich, je nach Ihrer Replikationstopologie. So unterscheiden sich die Schritte bei einer Master-Master-Topologie leicht von denen bei einer Master-Slave-Anordnung. Hier sind, etwas ausführlicher, die Schritte, die Sie wahrscheinlich in den meisten Anordnungen unternehmen müssen: 1. Stoppen Sie alle Schreiboperationen auf dem aktuellen Master. Falls möglich soll-
2.
3.
4. 5. 6.
7. 8. 9.
ten Sie sogar alle Clientprogramme (nicht die Replikationsverbindungen) zwingen, sich zu beenden. Es hilft, wenn Sie Ihre Clientprogramme mit einem »Do not run«Flag erstellt haben, das Sie setzen können. Wenn Sie virtuelle IP-Adressen benutzen, können Sie einfach ihre Verarbeitung ausschalten und dann alle Clientverbindungen beenden, um deren offenen Transaktionen zu schließen. Stoppen Sie optional mit FLUSH TABLES WITH READ LOCK alle Schreibaktivitäten auf dem Master. Sie können auf dem Master mit der Option read_only auch einen Schreibschutz setzen. Von diesem Augenblick an sollten Sie alle Schreibvorgänge auf dem zu ersetzenden Master verbieten. Da er jetzt kein Master mehr ist, würden Sie ansonsten Daten verlieren, wenn Sie auf ihm schreiben lassen! Wählen Sie einen der Slaves als neuen Master aus, und stellen Sie sicher, dass er in der Replikation vollständig aufgeholt hat (d.h., lassen Sie ihn das Ausführen aller Relay-Logs beenden, die er vom alten Master geholt hat). Überprüfen Sie optional, dass der neue Master die gleichen Daten enthält wie der alte Master. Führen Sie STOP SLAVE auf dem neuen Master aus. Führen Sie auf dem neuen Master CHANGE MASTER TO MASTER_HOST='' aus, gefolgt von RESET SLAVE, um die Verbindung zum alten Master zu trennen und die Verbindungsinformationen in seiner master.info-Datei zu verwerfen. (Das funktioniert nicht richtig, wenn die Verbindungsinformationen in my.cnf angegeben sind, weshalb wir auch davon abraten, sie dorthin zu legen.) Holen Sie sich die Binärlog-Koordinaten des neuen Masters mit SHOW MASTER STATUS. Sorgen Sie dafür, dass alle anderen Slaves auf den neuesten Stand kommen. Fahren Sie den alten Master herunter.
416 | Kapitel 8: Replikation
10. Aktivieren Sie in MySQL-Versionen ab 5.1 Events auf dem neuen Master, falls erfor-
derlich. 11. Erlauben Sie es den Clients, eine Verbindung zum neuen Master herzustellen. 12. Rufen Sie auf jedem Slave einen CHANGE MASTER TO-Befehl auf, mit dem Sie den jeweili-
gen Slave auf den neuen Master richten. Verwenden Sie dabei die Binärlog-Koordinaten, die Sie mit SHOW MASTER STATUS gesammelt haben. Wenn Sie einen Slave in einen Master umwandeln, dann achten Sie darauf, dass Sie ihn aus allen Slave-spezifischen Datenbanken, Tabellen und Berechtigungen entfernen. Sie müssen außerdem alle Slave-spezifischen Konfigurationsparameter ändern, wie etwa eine innodb_flush_log_ at_trx_commit-Option. Falls Sie andererseits einen Master zum Slave zurückstufen, müssen Sie ihn ebenfalls entsprechend umkonfigurieren. Falls Sie Ihre Master und Slaves identisch konfigurieren, müssen Sie nichts ändern.
Ungeplante Beförderungen Es ist nicht ganz so leicht, einen Slave zu befördern, um einen Master zu ersetzen, wenn dieser abstürzt. Gibt es nur einen Slave, dann nehmen Sie einfach diesen. Gibt es jedoch mehrere Slaves, dann müssen Sie einige zusätzliche Schritte ausführen, um einen Slave in den neuen Master zu verwandeln. Dazu kommt noch das Problem potenziell verloren gegangener Replikations-Events. Es ist möglich, dass einige Updates, die auf dem Master vorgenommen wurden, noch nicht auf alle seine Slaves repliziert wurden. Es kann sogar sein, dass eine Anweisung ausgeführt und dann auf dem Master zurückgenommen wurde, nicht jedoch auf dem Slave – so dass der Slave der logischen Replikationsposition des Masters sogar voraus sein könnte.11 Falls Sie die Daten des Masters an irgendeinem Punkt wiederherstellen können, dann sind Sie möglicherweise in der Lage, die verloren gegangenen Anweisungen wiederzubekommen und manuell anzuwenden. Achten Sie in den folgenden Schritten darauf, die Werte Master_Log_File und Read_ Master_Log_Pos in Ihren Berechnungen einzusetzen. Hier ist das Vorgehen beim Befördern eines Slaves in einer Master-und-Slaves-Topologie: 1. Stellen Sie fest, welcher Slave die neuesten Daten besitzt. Prüfen Sie die Ausgabe von SHOW SLAVE STATUS auf jedem Slave, und wählen Sie denjenigen, dessen Master_Log_ File/Read_Master_Log_Pos-Koordinaten am neuesten sind.
2. Erlauben Sie allen Slaves, das Ausführen der Relay-Logs zu beenden, die sie vom
alten Master geholt hatten, bevor er abgestürzt ist. Wenn Sie den Master eines Slaves wechseln, bevor er mit dem Ausführen des Relay-Logs fertig ist, wirft er alle verbleibenden Log-Events weg, und Sie wissen nicht, wo er gestoppt hat. 11 Das ist tatsächlich möglich, obwohl MySQL Events erst dann in das Log schreibt, wenn die Transaktion bestätigt wurde. Näheres erfahren Sie in »Transaktionsfähige und nichttransaktionsfähige Tabellen mischen« auf Seite 425.
Replikationsadministration und -wartung | 417
3. Führen Sie die Schritte 5 bis 7 der Liste aus dem vorangegangenen Abschnitt aus. 4. Vergleichen Sie die Master_Log_File/Read_Master_Log_Pos-Koordinaten der einzel-
nen Slaves mit denen des neuen Masters. 5. Führen Sie die Schritte 10 bis 12 der Liste aus dem vorangegangenen Abschnitt aus.
Wir gehen davon aus, dass Sie log_bin und log_slave_updates auf allen Slaves aktiviert haben, wie wir Ihnen zu Beginn dieses Kapitels empfohlen haben. Wenn Sie dieses Logging aktivieren, haben Sie die Möglichkeit, alle Slaves zu einem konsistenten Zeitpunkt wiederherzustellen, was ansonsten nicht zuverlässig möglich wäre.
Die gewünschten Log-Positionen suchen Befindet sich einer der Slaves nicht an der gleichen Position wie der neue Master, müssen Sie die Position in den Binärlogs des neuen Masters suchen, die dem letzten Event entspricht, das der Slave repliziert hat, und für CHANGE MASTER TO einsetzen. Mit dem Programm mysqlbinlog können Sie die letzte Abfrage untersuchen, die der Slave ausgeführt hat, und diese Abfrage im Binärlog des neuen Masters suchen. Ein paar kleine Berechnungen sind auch oft ganz hilfreich. Um das zu verdeutlichen, wollen wir annehmen, dass Log-Events ansteigende ID-Nummern haben und dass der Slave, der auf dem neuesten Stand ist – der neue Master –, gerade Event 100 erreicht hatte, als der alte Master abstürzte. Nun wollen wir noch annehmen, dass es zwei weitere Slaves gibt, nämlich slave2 und slave3. slave2 hatte Event 99 und slave3 Event 98 erreicht. Wenn Sie beide Slaves auf die aktuelle Binärlog-Position des neuen Masters verweisen, beginnen sie bei Event 101 mit der Replikation, sind also nicht mehr synchron. Solange jedoch das Binärlog des neuen Masters mit log_slave_updates eingeschaltet war, können Sie die Events 99 und 100 im Binärlog des neuen Masters finden, um die Slaves wieder zurück in einen konsistenten Zustand zu überführen. Aufgrund von Serverneustarts, unterschiedlichen Konfigurationen, Log-Wechseln oder FLUSH LOGS-Befehlen können die gleichen Events auf unterschiedlichen Servern bei unter-
schiedlichen Byte-Offsets existieren. Das Suchen der Events nervt und geht langsam vonstatten, ist aber normalerweise nicht schwer. Untersuchen Sie einfach das letzte Event, das auf dem jeweiligen Slave ausgeführt wurde, indem Sie im Binärlog oder im Relay-Log des Slaves mysqlbinlog ausführen. Suchen Sie anschließend – ebenfalls mit mysqlbinlog – die gleiche Abfrage im Binärlog des neuen Masters. Das Programm gibt den Byte-Offset der Abfrage aus. Sie können diesen Offset dann in der CHANGE MASTER TO-Abfrage einsetzen. Sie beschleunigen diesen Vorgang, indem Sie die Byte-Offsets, an denen der neue Master und der Slave gestoppt haben, voneinander abziehen und auf diese Weise die Differenz in ihren Byte-Positionen ermitteln. Falls Sie dann diesen Wert von der aktuellen BinärlogPosition des neuen Masters subtrahieren, ist die Wahrscheinlichkeit ziemlich hoch, dass Sie die gewünschte Abfrage an dieser Position gefunden haben. Sie müssen nun nur noch verifizieren, dass sie es tatsächlich ist, und haben jetzt die Position, an der Sie den Slave starten müssen.
418 | Kapitel 8: Replikation
Schauen wir uns ein konkretes Beispiel an. Stellen Sie sich vor, server1 ist der Master von server2 und server3 und stürzt ab. Entsprechend Master_Log_File/Read_Master_Log_Pos in SHOW SLAVE STATUS hat es server2 geschafft, alle Events zu replizieren, die sich im Binärlog von server1 befanden; server3 dagegen ist nicht so aktuell. Abbildung 8-15 verdeutlicht dieses Szenario (die Log-Events und Byte-Offsets dienen nur zur Demonstration).
Server1
mysql-bin.000001 1450 1493 1582
Server2
Master_Log_File = mysql-bin.000001 Read_Master_Log_Pos = 1582
UPDATE DELETE INSERT
Server3
Master_Log_File = mysql-bin.000001 Read_Master_Log_Pos = 1493
mysql-bin.000009 8035 8078 8167
UPDATE DELETE INSERT
Binärlog-Datei der Klarheit wegen weggelassen
Abbildung 8-15: Als server1 abstürzte, holte server2 auf, aber server3 hinkte hinter der Replikation hinterher.
Wie Abbildung 8-15 verdeutlicht, können wir sicher sein, dass server2 alle Events im Binärlog des Masters repliziert hat, weil seine Master_Log_File- und Read_Master_Log_PosWerte den letzten Positionen auf server1 entsprachen. Daher können wir server2 zum neuen Master befördern und server3 zu seinem Slave machen. Doch welche Parameter sollten wir im CHANGE MASTER TO-Befehl auf server3 verwenden? Hier müssen wir ein bisschen rechnen und überlegen. server3 stoppte bei Offset 1493, was 89 Byte hinter Offset 1582 liegt, dem letzten Befehl, den server2 ausgeführt hat. server2 schreibt momentan an Position 8167 in sein Binärlog. 8167 – 89 = 8078, theoretisch müssen wir also server3 auf diesen Offset in den Logs von server2 verweisen. Es ist allerdings keine schlechte Idee, die Log-Events um diese Position herum näher zu untersuchen und zu verifizieren, dass server2 tatsächlich die richtigen Events an diesem Offset in seinen Logs hat. Es könnte nämlich dort auch etwas anderes stehen, weil z.B. eine Datenaktualisierung stattgefunden hat, die sich auf server2 beschränkte.
Replikationsadministration und -wartung | 419
Wenn sich bei der Untersuchung herausgestellt hat, dass es tatsächlich die gleichen Events waren, machen wir mit dem folgenden Befehl server3 zum Slave von server2: server2> CHANGE MASTER TO MASTER_HOST="server2", MASTER_LOG_FILE="mysqlbin.000009", MASTER_LOG_POS=8078;
Was ist, wenn server1 beim Absturz ein weiteres Event hinter Offset 1582 ausgeführt und im Log verzeichnet hatte? Da server2 die Events nur bis zum Offset 1582 gelesen und ausgeführt hatte, haben Sie womöglich ein Event für immer verloren. Falls jedoch die Festplatte des alten Masters nicht beschädigt wurde, können Sie das fehlende Event mit mysqlbinlog oder mit einem Log-Server aus seinem Binärlog wiederherstellen. Wir empfehlen Ihnen, fehlende Events vom alten Master wiederherzustellen, nachdem Sie den neuen Master befördert, aber bevor Sie Clientverbindungen zu diesem Server zugelassen haben. Auf diese Weise müssen Sie die fehlenden Events nicht auf jedem Slave ausführen, das übernimmt die Replikation für Sie. Ist der ausgefallene Master dagegen vollständig unerreichbar, müssen Sie wahrscheinlich warten und diese Arbeit später erledigen. Eine Variante dieses Vorgehens besteht darin, dass Sie eine zuverlässige Methode zur Speicherung der Binärlog-Dateien des Masters einsetzen, etwa ein SAN oder ein Distributed Replicated Block Device (DRBD). Selbst wenn der Master komplett ausgefallen ist, haben Sie immer noch seine Binärlog-Dateien. Sie können einen Log-Server einrichten, die Slaves auf ihn richten und diese dann bis zu der Stelle aufholen lassen, an der der Master ausgefallen ist. Anschließend ist es trivial, einen der Slaves zum neuen Master zu befördern – das geht im Prinzip genauso wie bei einer geplanten Beförderung. Wir besprechen diese Speicheroptionen im nächsten Kapitel genauer. Wenn Sie einen Slave zum neuen Master befördern, dann ändern Sie seine Server-ID nicht auf die des alten Masters. In diesem Fall sind Sie nämlich nicht mehr in der Lage, einen Log-Server einzusetzen, um die Events vom alten Master wieder abzuspielen. Dies ist einer der vielen Gründe dafür, weshalb man Server-IDs als fest betrachten sollte.
Rollentausch in einer Master-Master-Konfiguration Einer der Vorteile der Master-Master-Replikation besteht darin, dass Sie aufgrund der symmetrischen Konfiguration ganz einfach die aktiven und passiven Rollen vertauschen können. In diesem Abschnitt zeigen wir, wie Sie diesen Tausch vollziehen. Wenn man die Rollen in einer Master-Master-Konfiguration vertauscht, dann ist es am wichtigsten, sicherzustellen, dass zu einem Zeitpunkt nur auf einen der Co-Master geschrieben wird. Überschneiden sich Schreiboperationen von einem Master mit Schreiboperationen vom anderen Master, können sie in einen Konflikt geraten. Mit anderen Worten: Der passive Server darf keine Binärlog-Events vom aktiven Server empfangen, nachdem die Rollen getauscht wurden. Sie können garantieren, dass dies nicht passiert, indem Sie sicherstellen, dass der Slave-Thread des passiven Servers zum aktiven Server aufholt, bevor Sie ihn schreibbar machen. 420 | Kapitel 8: Replikation
Mithilfe der folgenden Schritte vertauschen Sie die Rollen, ohne dass Sie Gefahr laufen, widersprüchliche Aktualisierungen zu erhalten: 1. Stoppen Sie alle Schreiboperationen auf dem aktiven Server. 2. Führen Sie SET @@global.read_only := 1 auf dem aktiven Server aus, und setzen Sie
aus Sicherheitsgründen für den Fall eines Neustarts die Option read_only in seiner Konfigurationsdatei. Bedenken Sie, dass Benutzer mit der SUPER-Berechtigung immer noch Änderungen vornehmen könnten. Falls Sie Änderungen von allen Benutzern verhindern wollen, verwenden Sie FLUSH TABLES WITH READ LOCK. Tun Sie dies nicht, müssen Sie alle Clientverbindungen beenden, damit es wirklich keine lange laufenden Anweisungen oder unbestätigten Transaktionen gibt. 3. Führen Sie SHOW MASTER STATUS auf dem aktiven Server aus, und merken Sie sich die
Binärlog-Koordinaten. 4. Führen Sie SELECT MASTER_POS_WAIT( ) auf dem passiven Server mit den Binärlog-
Koordinaten des aktiven Servers aus. Dieser Befehl blockiert, bis die Slave-Prozesse zum aktiven Server aufgeholt haben. 5. Führen Sie SET @@global.read_only := 0 auf dem passiven Server aus, und machen Sie
ihn damit zum aktiven Server. 6. Konfigurieren Sie Ihre Anwendungen neu, damit diese auf den neuen aktiven Server
schreiben. Je nach der Konfiguration Ihrer Anwendung müssen Sie möglicherweise noch weitere Aufgaben ausführen, wie etwa die IP-Adressen auf den beiden Servern ändern. Dies besprechen wir im nächsten Kapitel.
Replikationsprobleme und Lösungen Es ist nicht schwer, die MySQL-Replikation zu unterbrechen. Die einfache Implementierung, die die Einrichtung so sehr erleichtert, bietet auch viele Möglichkeiten, sie zu stoppen, zu verwirren oder anderweitig zu stören. In diesem Abschnitt zeigen wir Ihnen häufig auftretende Probleme, wie sie sich äußern und wie Sie sie lösen oder sogar vermeiden können.
Durch Datenverfälschung oder -verlust verursachte Fehler Aus vielerlei Gründen ist die MySQL-Replikation nicht besonders belastbar, wenn es zu Abstürzen, Stromausfällen und Beschädigungen durch Festplatten-, Speicher- oder Netzwerkfehler kommt. Sie müssen mit hoher Sicherheit irgendwann einmal die Replikation aufgrund dieser Probleme neu starten. Die meisten Probleme mit der Replikation nach einem unerwarteten Herunterfahren stammen von einem der Server, der seine Daten nicht mehr auf die Festplatte zurückspeichern konnte. Rechnen Sie mit folgenden Dingen:
Replikationsprobleme und Lösungen | 421
Unerwartetes Herunterfahren des Masters Wenn der Master nicht mit sync_binlog konfiguriert wurde, hat er vor dem Absturz möglicherweise seine letzten paar Binärlog-Events nicht auf die Festplatte übertragen. Der Ein-/Ausgabe-Thread des Slaves war möglicherweise gerade dabei, ein Event einzulesen, das es nie auf die Festplatte geschafft hat. Wenn der Master neu startet, baut der Slave wieder eine Verbindung auf und versucht, dieses Event erneut zu lesen. Der Master antwortet nun jedoch, dass es kein solches Binlog-Offset gibt. Der Binlog-Dump-Prozess geht typischerweise fast sofort los, so dass dies nicht ungewöhnlich ist. Die Lösung dieses Problem sieht so aus, dass der Slave angewiesen wird, mit dem Lesen am Anfang des nächsten Binärlogs zu beginnen. Manche Log-Events werden allerdings für immer verloren sein. Das hätte man vermeiden können, wenn der Master mit sync_binlog konfiguriert worden wäre. Doch auch wenn Sie sync_binlog konfiguriert haben, können MyISAM-Daten bei einem Absturz beschädigt werden, genau wie InnoDB-Daten, wenn innodb_flush_ logs_at_trx_commit nicht auf 1 gesetzt ist. Unerwartetes Herunterfahren des Slaves Wenn der Slave nach einem unerwarteten Herunterfahren wieder startet, liest er seine master.info-Datei, um festzustellen, wo er die Replikation gestoppt hat. Leider wird diese Datei nicht auf die Festplatte synchronisiert, so dass die Informationen, die sie enthält, wahrscheinlich falsch sind. Der Slave versucht wahrscheinlich, einige Binärlog-Events erneut auszuführen. Das könnte einige einmalige Indexstörungen verursachen. Solange Sie nicht feststellen, wo der Slave wirklich angehalten hat, was unwahrscheinlich ist, haben Sie keine andere Wahl, als die resultierenden Fehler zu überspringen. Dabei kann Ihnen das in Maatkit enthaltene mk-slave-restart-Werkzeug helfen. Falls Sie alle InnoDB-Tabellen verwenden, können Sie nach dem Neustart des Slaves in das MySQL-Fehler-Log schauen. Der InnoDB-Wiederherstellungsprozess gibt die Binärlog-Koordinaten bis zu der Stelle an, ab der wiederhergestellt wird; Sie können mit ihrer Hilfe dann feststellen, wohin der Slave auf dem Master verweisen muss. Zusätzlich zu den Datenverlusten, die daher rühren, dass MySQL unsauber beendet wurde, werden oft auch die Binärlogs oder Relay-Logs auf der Festplatte beschädigt. Dies sind einige der häufiger auftretenden Szenarien: Binärlogs auf dem Master sind beschädigt Wenn das Binärlog auf dem Master beschädigt ist, haben Sie keine andere Wahl, als zu versuchen, den beschädigten Teil zu überspringen. Sie können auf dem Master FLUSH LOGS ausführen, damit er eine neue Log-Datei anfängt, und den Slave auf den Anfang des neuen Logs richten. Oder Sie versuchen, das Ende des beschädigten Bereichs zu finden. Manchmal können Sie mit SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1 ein einzelnes beschädigtes Event überspringen. Wenn es mehr als ein beschädigtes Event gibt, wiederholen Sie den Vorgang, bis Sie alle übersprungen haben. 422 | Kapitel 8: Replikation
Relay-Logs auf dem Slave sind beschädigt Wenn die Binärlogs auf dem Master intakt sind, können Sie die beschädigten RelayLogs mit CHANGE MASTER TO verwerfen und sie neu holen. Verweisen Sie den Slave einfach an die gleiche Position, von der er momentan repliziert (Relay_Master_Log_ File/Exec_Master_Log_Pos). Dadurch verwirft er alle Relay-Logs auf der Festplatte. Das Binärlog ist nicht mehr synchron zum InnoDB-Transaktions-Log Stürzt der Master ab, könnte InnoDB eine Transaktion als bestätigt aufzeichnen, obwohl sie gar nicht in das Binärlog auf der Festplatte geschrieben wurde. Es gibt keine Möglichkeit, die fehlende Transaktion wiederherzustellen, es sei denn, sie befindet sich im Relay-Log eines Slaves. Sie vermeiden diesen Fehler mit dem sync_binlog-Parameter in MySQL 5.0 oder den Parametern sync_binlog und safe_binlog in MySQL 4.1. Es hängt von der Art der Beschädigung des Binärlogs ab, wie viele Daten Sie wiederherstellen können. Es können verschiedene Arten auftreten: Bytes sind geändert, aber das Event ist immer noch gültiges SQL Leider kann MySQL diese Art der Beschädigung gar nicht erkennen. Aus diesem Grund ist es keine schlechte Idee, wenn man regelmäßig überprüft, ob Ihre Slaves die richtigen Daten enthalten. Bytes sind geändert, und das Event ist ungültiges SQL Wahrscheinlich können Sie das Event mit mysqlbinlog extrahieren und sich die verstümmelten Daten anschauen: UPDATE tbl SET col?????????????????
Versuchen Sie, den Anfang des nächsten Events zu finden und es auszugeben. Dazu addieren Sie den Offset und die Länge. Vielleicht können Sie dieses Event einfach überspringen. Bytes wurden weggelassen, und/oder die Länge des Events ist falsch In diesem Fall bricht mysqlbinlog manchmal mit einem Fehler ab oder stürzt ab, weil es das Event nicht lesen und den Anfang des nächsten Events nicht finden kann. Mehrere Events sind beschädigt oder wurden überschrieben, oder Offsets haben sich verschoben, und das nächste Event beginnt bei einem falschen Offset Auch hier nützt Ihnen mysqlbinlog nicht viel. Wenn die Beschädigung so schlimm ist, dass mysqlbinlog die Log-Events nicht lesen kann, müssen Sie auf Hex-Editing oder andere seltsame Techniken zurückgreifen, um die Grenzen zwischen den Log-Events zu finden. Das ist normalerweise gar nicht so schwer, weil die Events durch deutlich erkennbare Markierungen getrennt werden. Hier ist ein Beispiel. Wir wollen uns zuerst die Offsets eines Log-Events aus einem Beispiel-Log anschauen, das mysqlbinlog ausgegeben hat: $ mysqlbinlog mysql-bin.000113 | egrep '^# at ' # at 4 # at 98
Replikationsprobleme und Lösungen | 423
# # # #
at at at at
185 277 369 447
Sie finden die Offsets in dem Log, indem Sie die Offsets mit der Ausgabe des folgenden strings-Befehls vergleichen: $ strings -n 2 -t d mysql-bin.000113 1 binpC'G 25 5.0.38-Ubuntu_0ubuntu1.1-log 99 C'G 146 std 156 test 161 create table test(a int) 186 C'G 233 std 243 test 248 insert into test(a) values(1) 278 C'G 325 std 335 test 340 insert into test(a) values(2) 370 C'G 417 std 427 test 432 drop table test 448 D'G 474 mysql-bin.000114
Es gibt ein gut erkennbares Muster, das es Ihnen erlauben sollte, die Anfänge der Events zu finden. Beachten Sie, dass die Strings, die mit 'G enden, sich ein Byte hinter dem Anfang des Log-Events befinden. Sie gehören zu dem Log-Event-Header, der eine feste Länge besitzt. Der exakte Wert variiert von Server zu Server, Ihre Ergebnisse hängen also vom Server ab, dessen Log Sie untersuchen. Mit ein bisschen Herumsuchen sollten Sie in der Lage sein, das Muster in Ihrem Binärlog zu finden und den Offset des nächsten intakten Log-Events zu ermitteln. Sie können dann versuchen, mit dem Argument --start-position für mysqlbinlog hinter das schlechte Event bzw. die schlechten Events zu springen. Oder Sie benutzen den Parameter MASTER_LOG_POS für CHANGE MASTER TO.
Nichttransaktionsfähige Tabellen verwenden Wenn alles gut geht, funktioniert die anweisungsbasierte Replikation normalerweise gut mit nichttransaktionsfähigen Tabellen. Tritt jedoch ein Fehler bei der Aktualisierung einer nichttransaktionsfähigen Tabelle auf (wird etwa eine Anweisung abgebrochen, bevor sie vollständig ausgeführt wurde), dann kommt es auf dem Master und dem Slave zu unterschiedlichen Daten.
424 | Kapitel 8: Replikation
Nehmen Sie z.B. an, Sie aktualisieren eine MyISAM-Tabelle mit 100 Zeilen. Was geschieht, wenn die Anweisung 50 der Zeilen aktualisiert und jemand sie dann abbricht? Die eine Hälfte der Zeilen wurde geändert, die andere hingegen nicht. Als Ergebnis muss die Replikation aus dem Takt geraten, weil die Anweisung auf dem Slave erneut abgespielt wird und alle 100 Zeilen ändert. (MySQL bemerkt dann, dass die Anweisung auf dem Master einen Fehler verursacht hat, auf dem Slave aber nicht, und stoppt die Replikation mit einer Fehlermeldung.) Wenn Sie MyISAM-Tabellen einsetzen, dann führen Sie auf jeden Fall STOP SLAVE aus, bevor Sie den MySQL-Server stoppen, da ansonsten alle laufenden Abfragen abgebrochen werden (einschließlich unvollständiger Aktualisierungsanweisungen). Transaktionsfähige Storage-Engines haben dieses Problem nicht. Bei transaktionsfähigen Tabellen wird eine fehlgeschlagene Aktualisierung auf dem Master zurückgenommen und nicht im Binärlog verzeichnet.
Transaktionsfähige und nichttransaktionsfähige Tabellen mischen Wenn Sie eine transaktionsfähige Storage-Engine benutzen, dann schreibt MySQL die Anweisungen, die Sie ausführen, erst dann in das Binärlog, wenn die Transaktionen bestätigt wurden. Das heißt, wenn eine Transaktion zurückgenommen wird, zeichnet MySQL die Anweisungen nicht auf, sie werden also auch nicht auf dem Slave abgespielt. Falls Sie jedoch transaktionsfähige und nichttransaktionsfähige Tabellen mischen und es zu einem Rollback kommt, dann kann MySQL die Änderungen an den transaktionsfähigen Tabellen zurücknehmen; die nichttransaktionsfähigen Tabellen dagegen werden dauerhaft verändert. Solange keine Fehler auftreten, indem etwa eine Aktualisierung auf halbem Wege abgebrochen wird, stellt das kein Problem dar: Anstatt die Anweisungen nicht zu protokollieren, schreibt MySQL zuerst die Anweisungen in sein Binärlog und anschließend eine ROLLBACK-Anweisung. In der Folge werden auf dem Slave die gleichen Anweisungen ausgeführt, und alles ist gut. Das ist nicht ganz so effizient, weil der Slave auch etwas tun muss, theoretisch jedoch bleibt der Slave weiterhin synchron mit dem Master. So weit, so gut. Problematisch wird es, wenn es auf dem Slave ein Deadlock gibt, das auf dem Master nicht aufgetreten ist. Für die Tabellen, die eine transaktionsfähige StorageEngine benutzen, gibt es auf dem Slave ein Rollback, für die nichttransaktionsfähigen Tabellen ist das aber nicht möglich. Es kommt dazu, dass die Daten des Slaves sich von denen des Masters unterscheiden. Sie können dieses Problem nur verhindern, indem Sie das Mischen transaktionsfähiger und nichttransaktionsfähiger Tabellen vermeiden. Falls Sie das Problem bemerken, dann haben Sie nur die Möglichkeit, den Fehler auf dem Slave zu überspringen und die beteiligten Tabellen neu zu synchronisieren. Im Prinzip sollte dieses Problem bei der zeilenbasierten Replikation nicht auftreten. Bei der zeilenbasierten Replikation werden Änderungen an den Zeilen aufgezeichnet, nicht an den SQL-Anweisungen. Wenn eine Anweisung Zeilen in einer MyISAM-Tabelle und einer
Replikationsprobleme und Lösungen | 425
InnoDB-Tabelle ändert, es dann auf dem Master zu einem Deadlock kommt und die InnoDB-Tabelle wieder zurückgeändert wird, werden die Änderungen an der MyISAMTabelle dennoch im Binärlog aufgezeichnet und auf dem Slave abgespielt. Wir haben einfache Fälle getestet und festgestellt, dass dies korrekt funktioniert; allerdings hatten wir zu dem Zeitpunkt, als dieses Buch entstand, noch nicht genügend Erfahrungen mit der zeilenbasierten Replikation, um sicher sagen zu können, ob Probleme komplett vermieden werden, die durch das Mischen transaktionsfähiger und nichttransaktionsfähiger Tabellen auftreten können.
Nichtdeterministische Anweisungen Jede Anweisung, die Daten auf nichtdeterministische Weise ändert, kann dafür sorgen, dass die Daten eines Slaves sich von denen seines Masters unterscheiden. Zum Beispiel hängt ein UPDATE mit einem LIMIT von der Reihenfolge ab, in der die Anweisung die Zeilen in der Tabelle findet. Solange die Reihenfolge auf dem Master und dem Slave nicht garantiert gleich ist – indem z.B. die Zeilen nach dem Primärschlüssel sortiert sind –, könnte die Anweisung unterschiedliche Zeilen auf diesen beiden Servern ändern. Solche Probleme sind unter Umständen subtil und schwer zu bemerken, weshalb manchmal die Regel gilt, dass LIMIT nie mit einer Anweisung benutzt werden darf, die Daten ändert. Achten Sie außerdem auf Anweisungen, die INFORMATION_SCHEMA-Tabellen einbeziehen. Diese können sich zwischen Master und Slave unterscheiden, so dass auch die Ergebnisse unterschiedlich sind. Denken Sie schließlich noch daran, dass die meisten Servervariablen, wie etwa @@server_id und @@hostname in Versionen vor MySQL 5.1 nicht korrekt repliziert werden. Die zeilenbasierte Replikation unterliegt nicht diesen Beschränkungen.
Unterschiedliche Storage-Engines auf Master und Slave Wie wir in diesem Kapitel schon angemerkt haben, ist es oft praktisch, wenn man auf einem Slave andere Storage-Engines benutzt. Unter gewissen Umständen erzeugt allerdings die anweisungsbasierte Replikation auf einem Slave mit anderen Storage-Engines unterschiedliche Ergebnisse. Beispielsweise verursachen nichtdeterministische Anweisungen (wie die im vorherigen Abschnitt erwähnten) mit größerer Wahrscheinlichkeit Probleme, wenn sich die Storage-Engines auf dem Slave unterscheiden. Falls Sie feststellen, dass die Daten Ihres Slaves in bestimmten Tabellen nicht mit denjenigen Ihres Masters übereinstimmen, dann sollten Sie die Storage-Engines untersuchen, die auf beiden Servern benutzt werden, sowie die Abfragen, die diese Tabellen aktualisieren.
426 | Kapitel 8: Replikation
Datenänderungen auf dem Slave Die anweisungsbasierte Replikation ist darauf angewiesen, dass der Slave die gleichen Daten enthält wie der Master. Sie dürfen daher Änderungen auf dem Slave weder erlauben noch vornehmen (mit der Konfigurationsvariablen read_only können Sie dieses Ziel durchsetzen). Schauen Sie sich die folgende Anweisung an: mysql> INSERT INTO table1 SELECT * FROM table2;
Wenn table2 auf dem Slave andere Daten enthält, bekommt table1 ebenfalls unterschiedliche Daten. Mit anderen Worten: Datenunterschiede pflegen sich von Tabelle zu Tabelle auszubreiten. Das passiert mit allen Arten von Abfragen, nicht nur mit INSERT ... SELECT-Abfragen. Es gibt zwei mögliche Folgen: Sie erhalten einen Fehler wie eine doppelte Indexverletzung, oder Sie bekommen überhaupt keinen Fehler. Seien Sie froh, wenn Sie einen Fehler erhalten, weil Sie damit wenigstens alarmiert werden, dass Ihre Daten auf dem Slave nicht gleich sind. Bleiben unterschiedliche Daten unentdeckt, können sie insgeheim alle möglichen Schäden anrichten. Die einzige Lösung für dieses Problem besteht darin, die Daten neu vom Master zu synchronisieren.
Nichteindeutige Server-IDs Dies gehört zu den schwerer fassbaren Problemen, die bei der Replikation auftreten können. Falls Sie versehentlich zwei Slaves mit derselben Server-ID konfigurieren, scheinen sie auf den ersten Blick gut zu funktionieren. Schauen Sie sich jedoch deren Fehler-Logs an oder beobachten Sie den Master mit innotop, werden Ihnen eigenartige Dinge auffallen. Auf dem Master sehen Sie, dass immer nur einer der beiden Slaves verbunden ist. (Normalerweise besteht zu allen Slaves eine Verbindung, so dass sie replizieren können.) Auf dem Slave finden Sie im Fehler-Log viele Fehlermeldungen über das Trennen und erneute Aufbauen von Verbindungen, aber keinen Hinweis auf eine fehlkonfigurierte Server-ID. Je nach der verwendeten MySQL-Version replizieren die Slaves korrekt, aber langsam. Vielleicht replizieren sie aber auch gar nicht korrekt, sondern verpassen Binärlog-Events oder wiederholen sie fälschlicherweise, wodurch Fehler mit duplizierten Schlüsseln (oder Datenverfälschungen) auftreten. Es kann auch zu Abstürzen oder beschädigten Daten auf dem Master selbst kommen, weil die Slaves, die gegeneinander kämpfen, eine erhöhte Last verursachen. Und wenn die Slaves einander sehr stark bekämpfen, können die Fehler-Logs in kurzer Zeit sehr stark anwachsen. Um diese Probleme zu vermeiden, kommen Sie nicht umhin, beim Einrichten der Slaves sehr sorgfältig vorzugehen. Möglicherweise hilft es Ihnen, eine Liste mit Slave-zu-ServerID-Zuordnungen anzulegen, damit Sie nicht den Überblick darüber verlieren, welche ID zu einem Slave gehört.12 Falls sich Ihre Slaves alle in einem Teilnetz Ihres Netzwerks 12 Vielleicht wollen Sie sie in einer Datenbanktabelle speichern? Wir scherzen … aber nicht nur: Sie können in der ID-Spalte einen eindeutigen Index hinzufügen.
Replikationsprobleme und Lösungen | 427
befinden, können Sie eindeutige IDs wählen, indem Sie nur das letzte Oktett der IPAdressen der einzelnen Maschinen nutzen.
Undefinierte Server-IDs Falls Sie die Server-ID nicht in der my.cnf-Datei definieren, scheint MySQL die Replikation mit CHANGE MASTER TO einzurichten, erlaubt Ihnen aber nicht, den Slave zu starten: mysql> START SLAVE; ERROR 1200 (HY000): The server is not configured as slave; fix in config file or with CHA NGE MASTER TO
Dieser Fehler ist besonders verwirrend, weil Sie gerade CHANGE MASTER TO benutzt und Ihre Einstellungen mit SHOW SLAVE STATUS verifiziert haben. Sie erhalten möglicherweise einen Wert von SELECT @@server_id, aber das ist nur ein Vorgabewert. Sie müssen den Wert explizit setzen.
Abhängigkeiten von nichtreplizierten Daten Haben Sie auf dem Master Datenbanken oder Tabellen, die nicht auf dem Slave existieren, dann ist es ziemlich einfach, die Replikation absichtlich zu unterbrechen. Nehmen Sie an, dass es eine scratch-Datenbank auf dem Master gibt, die auf dem Slave nicht vorliegt. Beziehen sich irgendwelche Datenaktualisierungen auf dem Master auf eine Tabelle in dieser Datenbank, dann wird die Replikation abgebrochen, wenn der Slave versucht, die Aktualisierungen abzuspielen. Man kann dieses Problem nicht umgehen. Sie können höchstens auf dem Master das Anlegen von Tabellen vermeiden, die auf dem Slave nicht existieren. Wie wird eine solche Tabelle erzeugt? Es gibt viele Möglichkeiten, von denen sich einige schwerer vermeiden lassen als andere. Nehmen Sie z.B. an, dass Sie eine scratch-Datenbank auf dem Slave erzeugt haben, die auf dem Master nicht vorhanden war, und dann aus irgendeinem Grund Master und Slave vertauscht haben. Vielleicht haben Sie in diesem Fall vergessen, die scratch-Datenbank und ihre Berechtigungen zu entfernen. Jetzt könnte jemand eine Verbindung zu dem neuen Master herstellen und eine Abfrage in dieser Datenbank ausführen, oder ein regelmäßig ablaufender Job entdeckt die Tabellen und führt in ihnen jeweils OPTIMIZE TABLE aus. Daran müssen Sie unter anderem denken, wenn Sie einen Slave zum Master befördern oder wenn Sie entscheiden, wie die Slaves konfiguriert werden sollen. Alles, was die Slaves anders macht als die Master oder umgekehrt, könnte künftig zu einem Problem werden. Die zeilenbasierte Replikation sollte einige dieser Probleme lösen, allerdings ist es noch zu früh, um sich sicher zu sein.
428 | Kapitel 8: Replikation
Fehlende temporäre Tabellen Für manche Einsatzzwecke sind temporäre Tabellen ganz praktisch, allerdings sind sie leider inkompatibel zur anweisungsbasierten Replikation. Wenn ein Slave abstürzt oder wenn Sie ihn herunterfahren, dann verschwinden alle temporären Tabellen, die der Slave-Thread benutzt. Nach einem Neustart des Slaves schlagen alle weiteren Anweisungen fehl, die sich auf die fehlenden temporären Tabellen beziehen. Es gibt keine sichere Methode, um temporäre Tabellen auf dem Master zusammen mit anweisungsbasierter Replikation zu benutzen. Viele Leute mögen die temporären Tabellen, so dass sie vermutlich schwer davon zu überzeugen sind, aber es stimmt. Egal, wie kurz sie existieren, temporäre Tabellen machen es potenziell unmöglich, Slaves zu stoppen und zu starten und sie nach Abstürzen wiederherzustellen. Das gilt sogar dann, wenn Sie sie nur innerhalb einer einzigen Transaktion benutzen. (Es ist etwas weniger problematisch, temporäre Tabellen auf einem Slave einzusetzen, wo sie sehr bequem sein können; ist der Slave aber selbst ein Master, dann existiert das Problem weiterhin.) Falls die Replikation stoppt, weil der Slave eine temporäre Tabelle nach einem Neustart nicht finden kann, gibt es wirklich nur wenig, was Sie tun können. Überspringen Sie die auftretenden Fehler, oder legen Sie manuell eine Tabelle mit dem gleichen Namen und der gleichen Struktur wie die verschwundene Tabelle an. Wie auch immer Sie vorgehen, Ihre Daten werden auf dem Slave wahrscheinlich anders aussehen, wenn Schreibabfragen sich auf die temporäre Tabelle beziehen. Es ist nicht so schwer, wie es scheint, temporäre Tabellen zu eliminieren. Die beiden nützlichsten Eigenschaften einer temporären Tabelle sind folgende: • Sie sind nur für die Verbindung sichtbar, die sie erzeugt hat, kommen also nicht den temporären Tabellen anderer Verbindungen in die Quere, die denselben Namen tragen. • Sie verschwinden, wenn die Verbindung geschlossen wird. Sie müssen sie also nicht explizit entfernen. Sie können diese Eigenschaften leicht emulieren, indem Sie eine Datenbank exklusiv für pseudotemporäre Tabellen reservieren, wo Sie stattdessen permanente Tabellen anlegen. Sie müssen nur eindeutige Namen für sie wählen. Zum Glück geht das ganz leicht: Hängen Sie einfach die Verbindungs-ID an den Tabellennamen an. Wo Sie z.B. CREATE TEMPORARY TABLE top_users(...) ausgeführt haben, führen Sie nun CREATE TABLE temp.top_users_ 1234(...) aus, wobei 1234 der Wert ist, der von CONNECTION_ID( ) zurückgeliefert wird. Nachdem Ihre Anwendung mit der pseudotemporären Tabelle fertig ist, können Sie sie entweder wegwerfen oder sie von einem Aufräumprozess entfernen lassen. Da die Verbindungs-ID im Namen steht, kann man leicht feststellen, welche Tabellen nicht mehr in Benutzung sind – Sie erhalten eine Liste der aktiven Verbindungen mit SHOW PROCESSLIST und müssen sie nur mit den Verbindungs-IDs in den Tabellennamen vergleichen.13 13 mk-find – noch ein weiteres Werkzeug in Maatkit – kann pseudotemporäre Tabellen leicht mit den Optionen --pid und --sid entfernen.
Replikationsprobleme und Lösungen | 429
Die Verwendung von echten Tabellen anstelle von temporären Tabellen hat noch andere Vorteile. Zum Beispiel lassen sich damit leichter Fehler in Ihren Anwendungen aufspüren, weil Sie die Daten sehen können, die die Anwendungen von einer anderen Verbindung aus manipulieren. Mit einer temporären Tabelle wäre das nicht so einfach möglich. Echte Tabellen bringen jedoch einen gewissen Overhead mit, den Sie bei temporären Tabellen nicht haben: Es geht langsamer, sie zu erzeugen, weil die .frm-Dateien, die mit diesen Tabellen verknüpft sind, auf die Festplatte synchronisiert werden müssen. Sie können die sync_frm-Option deaktivieren, um das zu beschleunigen, aber das ist gefährlicher. Falls Sie temporäre Tabellen einsetzen, müssen Sie sicherstellen, dass die Statusvariable Slave_open_temp_tables 0 ist, bevor sie einen Slave herunterfahren. Wenn sie nicht 0 ist,
bekommen Sie wahrscheinlich Probleme damit, den Slave neu zu starten. Das richtige Vorgehen besteht darin, STOP SLAVE auszuführen, die Variable zu untersuchen und erst dann den Slave herunterzufahren. Wenn Sie die Variable untersuchen, bevor Sie die Slave-Prozesse stoppen, riskieren Sie eine Race-Condition.
Es werden nicht alle Updates repliziert Falls Sie SET SQL_LOG_BIN=0 missbrauchen oder die Replikationsfilterregeln nicht verstehen, ignoriert Ihr Slave möglicherweise Aktualisierungen, die auf dem Master stattgefunden haben. Manchmal wollen Sie das zu Archivierungszwecken, normalerweise jedoch geschieht das versehentlich und hat schlimme Folgen. Nehmen Sie z.B. an, dass Sie eine replicate_do_db-Regel haben, um nur die sakilaDatenbank auf einen Ihrer Slaves zu replizieren. Wenn Sie die folgenden Befehle auf dem Master ausführen, werden die Daten des Slaves anders als die Daten auf dem Master: mysql> USE test; mysql> UPDATE sakila.actor ...
Andere Arten von Anweisungen können aufgrund von nichtreplizierten Abhängigkeiten sogar dafür sorgen, dass eine Replikation mit einem Fehler fehlschlägt.
Lock-Contention aufgrund von sperrenden InnoDB-Selects Die InnoDB-SELECT-Anweisungen sind normalerweise nichtsperrend, besorgen sich aber in bestimmten Fällen Sperren. Speziell INSERT ... SELECT sperrt standardmäßig alle Zeilen, aus denen es in der Quelltabelle liest. MySQL braucht die Sperren um sicherzustellen, dass die Anweisungen beim Ausführen auf dem Slave das gleiche Ergebnis erzeugen. Im Prinzip serialisieren die Sperren die Anweisung auf dem Master, was der Art und Weise entspricht, wie der Slave sie ausführt. Ihnen könnten aufgrund dieses Designs Lock-Contention, Blockaden und Timeouts infolge des Wartens auf Sperren begegnen. Um diese Probleme zu mildern, sollte man eine Transaktion nicht länger offen halten als nötig, damit die Sperren weniger Blockaden verursachen. Sie können die Sperren freigeben, indem Sie die Transaktion so bald wie möglich auf dem Master bestätigen.
430 | Kapitel 8: Replikation
Es ist außerdem hilfreich, wenn die Anweisungen kurz gehalten werden, indem man große Anweisungen in kleinere unterteilt. Das ist sehr effektiv, um Lock-Contention zu reduzieren. Es lohnt sich meist, auch wenn es nicht leicht durchzuführen ist. Andere Lösungen bestehen darin, INSERT ... SELECT-Anweisungen durch eine Kombination aus SELECT INTO OUTFILE, gefolgt von LOAD DATA INFILE auf dem Master zu ersetzen. Das geht schnell und erfordert keine Sperren. Zugegebenermaßen ist das ein Hack, der aber dennoch manchmal hilft. Die größten Probleme sind die Wahl eines eindeutigen Namens für die Ausgabedatei, die noch nicht existieren darf, und das Aufräumen der Ausgabedatei, wenn Sie damit fertig sind. Sie können die CONNECTION_ID( )-Technik einsetzen, die wir in »Fehlende temporäre Tabellen« auf Seite 429 besprochen haben, um sicherzustellen, dass der Dateiname eindeutig ist, und Sie können einen regelmäßig ausgeführten Job benutzen (crontab unter Unix, GEPLANTE TASKS unter Windows), um ungenutzte Ausgabedateien aufzuräumen, nachdem die Verbindungen, die sie erzeugt haben, mit ihnen fertig sind. Sie sind vielleicht versucht, die Sperren zu deaktivieren, anstatt eine der genannten Lösungen einzusetzen. Es gibt zwar eine Methode, um das zu erreichen, es ist aber meist keine gute Idee, weil dadurch der Slave stillschweigend hinter den Master zurückfallen könnte. Außerdem wird dadurch das Binärlog nutzlos für die Wiederherstellung eines Servers. Falls Sie jedoch beschließen, dass die Vorteile die Risiken aufwiegen, wird die Konfigurationsänderung folgendermaßen erreicht: innodb_locks_unsafe_for_binlog = 1
Dadurch können die Ergebnisse einer Anweisung von Daten abhängen, die sie nicht sperrt. Wenn eine zweite Anweisung die Daten modifiziert und sie vor der ersten Anweisung bestätigt, dann kann es passieren, dass die beiden Anweisungen beim Abspielen des Binärlogs nicht dieselben Ergebnisse erzeugen. Das gilt sowohl für die Replikation als auch für die punktgenaue Wiederherstellung. Um einmal zu sehen, wie durch das Sperren von Leseoperationen Chaos vermieden wird, stellen Sie sich vor, dass Sie zwei Tabellen haben: eine ohne Zeilen und eine, deren einzige Zeile den Wert 99 enthält. Die Daten werden durch zwei Transaktionen aktualisiert. Transaktion 1 fügt den Inhalt der zweiten Tabelle in die erste Tabelle ein, und Transaktion 2 aktualisiert die zweite (Quell-)Tabelle, wie in Abbildung 8-16 gezeigt ist. Schritt 2 in dieser Folge von Ereignissen ist sehr wichtig. Darin versucht Transaktion 2, die Quelltabelle zu aktualisieren, was es nötig macht, eine exklusive (Schreib-)Sperre auf die Zeilen zu legen, die aktualisiert werden sollen. Eine exklusive Sperre ist inkompatibel zu jeder anderen Sperre, einschließlich des Shared Lock, das Transaktion 1 auf diese Zeile gelegt hat, so dass Transaktion 2 gezwungen ist zu warten, bis Transaktion 1 bestätigt wird. Die Transaktionen werden im Binärlog in der Reihenfolge serialisiert, in der sie bestätigt werden, so dass sich beim erneuten Abspielen dieser Transaktionen in der Binärlog-(Commit-)Reihenfolge die gleichen Ergebnisse erzielen lassen.
Replikationsprobleme und Lösungen | 431
Tbl1 0 Ausgangszustand
1 Txn 1 INSERT/SELECT mit einem Shared Lock
2 Txn 2 UPDATE der Blöcke mit einem exklusiven Lock 3 Txn 1 wird bestätigt und gibt Sperren frei
4 Txn 2 fährt fort und wird bestätigt
Tbl2
Binlog
99
99 99
SET = 100
99
99
99
SET = 100 99
99
Txn 1
99 100
Txn 1 Txn 2
Abbildung 8-16: Zwei Transaktionen aktualisieren Daten; mittels gemeinsam genutzter Sperren (Shared Lock) wird die Aktualisierung serialisiert.
Falls andererseits Transaktion 1 kein Shared Lock auf die Zeilen legt, die es für das INSERT liest, gibt es keine solche Garantie. Betrachten Sie Abbildung 8-17, in der eine mögliche Folge von Ereignissen ohne das Lock zu sehen ist. Ohne Sperren werden die Transaktionen in einer Reihenfolge in das Binärlog geschrieben, die andere Ergebnisse erzeugt, wenn dieses Log erneut abgespielt wird, wie Sie in der Abbildung erkennen. MySQL zeichnet Transaktion 2 zuerst auf, so dass die Ergebnisse von Transaktion 1 auf dem Slave beeinflusst werden. Auf dem Master ist das nicht passiert. In der Folge enthält der Slave andere Daten als der Master. Wir raten Ihnen dringend, in den meisten Fällen die Konfigurationsvariable innodb_ locks_unsafe_for_binlog auf 0 gesetzt zu lassen.
In einer Master-Master-Replikation auf beide Master schreiben Auf beide Master zu schreiben, ist im Allgemeinen keine gute Idee. Falls Sie versuchen, sicher auf beide Master gleichzeitig zu schreiben, dann gibt es für einige der Probleme Lösungen, für andere jedoch nicht. In MySQL 5.0 gibt es zwei Server-Konfigurationsvariablen, mit denen man dem Problem der widersprüchlichen AUTO_INCREMENT-Primärschlüssel begegnen kann. Diese Variablen
432 | Kapitel 8: Replikation
Tbl1 1 Txn 1 INSERT/SELECT ohne Locking
2 Txn 2 macht weiter und wird bestätigt
3 Txn 1 wird bestätigt
Tbl2 99
99
SET = 100
99 100
Txn 2
100
Txn 2
99
99
Txn 1
Später, auf dem Slave... 1 Txn 2 UPDATE
2 Txn 1 INSERT/SELECT
Binlog
SET = 100
99 100
100 100
Abbildung 8-17: Zwei Transaktionen aktualisieren Daten, allerdings ohne dass ein Shared Lock die Aktualisierungen serialisiert.
sind auto_increment_increment und auto_increment_offset. Mit ihnen können Sie die Zahlen, die die Server generieren, »staffeln«, so dass sie sich verschachteln und nicht miteinander kollidieren. Das löst jedoch nicht alle Probleme, die mit zwei schreibbaren Mastern auftreten. Es löst nur das Autoinkrement-Problem, das wahrscheinlich nur einen kleinen Teil der widersprüchlichen Schreiboperationen ausmacht, mit denen Sie es zu tun bekommen könnten. Um genau zu sein, werden dadurch mehrere neue Probleme eingeführt: • Dieses Verfahren erschwert es, Server in der Replikationstopologie zu verschieben. • Es verschwendet Schlüsselplatz, indem potenziell Lücken zwischen den Zahlen eingeführt werden. • Es hilft nur, wenn alle Ihre Tabellen AUTO_INCREMENT-Primärschlüssel besitzen, und dabei ist es nicht unbedingt eine gute Idee, AUTO_INCREMENT-Primärschlüssel allgemein einzusetzen. Sie können eigene, nicht widersprüchliche Primärschlüsselwerte generieren. Eine Möglichkeit wäre, einen mehrspaltigen Primärschlüssel zu erzeugen und die Server-ID für die erste Spalte zu benutzen. Das funktioniert gut, vergrößert allerdings die Primärschlüssel, was eine schlimme Wirkung auf die Sekundärschlüssel in InnoDB hat.
Replikationsprobleme und Lösungen | 433
Es ist auch möglich, einen einspaltigen Primärschlüssel einzusetzen und die »hohen Bits« des Integers für die Server-ID zu nehmen. Das erreichen Sie durch eine einfache Linksverschiebung (oder Multiplikation) und Addition. Falls Sie z.B. die 8 höchstwertigen Bits einer vorzeichenlosen BIGINT-(64-Bit-)Spalte benutzen, um die Server-ID aufzunehmen, können Sie den Wert 11 auf Server 15 folgendermaßen einfügen: mysql> INSERT INTO test(pk_col, ...) VALUES( (15 << 56) + 11, ...);
Wenn Sie das Ergebnis in die Basis 2 konvertieren und es auf 64 Bit Länge auffüllen, ist der Effekt besser zu erkennen: mysql> SELECT LPAD(CONV(pk_col, 10, 2), 64, '0') FROM test; +------------------------------------------------------------------+ | LPAD(CONV(pk_col, 10, 2), 64, '0') | +------------------------------------------------------------------+ | 0000111100000000000000000000000000000000000000000000000000001011 | +------------------------------------------------------------------+
Das Problem bei dieser Methode besteht darin, dass Sie eine externe Möglichkeit benötigen, um die Schlüsselwerte zu generieren, weil AUTO_INCREMENT das nicht für Sie erledigen kann. Benutzen Sie nicht @@server_id anstelle der Konstante 15 in dem INSERT, da Sie ansonsten ein anderes Ergebnis auf dem Slave erhalten. Sie können sich auch mit einer Funktion wie MD5( ) oder UUID( ) für pseudozufällige Werte entscheiden, allerdings sind diese unter Umständen schlecht für die Performance – sie sind groß, und sie sind prinzipiell zufällig, was speziell für InnoDB nicht so gut ist. (Benutzen Sie UUID( ) nur dann, wenn Sie die Werte in der Anwendung generieren, weil UUID( ) mit der anweisungsbasierten Replikation nicht korrekt repliziert wird.) Es ist ein schwieriges Problem, und wir empfehlen im Normalfall, die Anwendung so umzugestalten, dass man nur noch einen schreibbaren Master hat.
Übermäßiger Rückstand bei der Replikation Ein Rückstand bei der Replikation ist ein häufig auftretendes Problem. Nichtsdestotrotz ist es keine schlechte Idee, die Anwendungen so zu entwerfen, dass sie einen gewissen Rückstand der Slaves tolerieren. Wenn das System mit verzögerten Slaves nicht funktionieren kann, dann ist die Replikation vermutlich nicht die richtige Architektur für Ihre Anwendung. Sie können jedoch einige Schritte unternehmen, um den Slaves zu helfen, mit dem Master Schritt zu halten. Die Single-Thread-Natur der MySQL-Replikation bedeutet, dass sie auf dem Slave relativ ineffizient ist. Selbst ein schneller Slave mit vielen Festplatten, CPUs und Speicher kann ganz leicht hinter einem Master zurückfallen, weil der eine Thread des Slaves üblicherweise nur eine CPU und eine Festplatte effizient nutzt. Um genau zu sein, muss jeder Slave typischerweise mindestens so leistungsfähig sein wie der Master. Auch Sperren auf dem Slave stellen ein Problem dar. Andere Abfragen, die auf einem Slave laufen, könnten Sperren setzen, die den Replikations-Thread blockieren. Da die
434 | Kapitel 8: Replikation
Replikation nur mit einem Thread erfolgt, könnte der Replikations-Thread keine weitere Arbeit verrichten, während er wartet. Meist fällt die Replikation auf zwei Arten zurück: mit Verzögerungspitzen, die in der Folge aufgeholt werden, oder indem der Slave ständig hinterherbummelt. Die erste Art wird üblicherweise von Abfragen verursacht, die schon lange laufen; die zweite Form dagegen kann sogar dann auftauchen, wenn es keine langen Abfragen gibt. Leider gibt es momentan nur eine Möglichkeit, um festzustellen, wie dicht der Slave an seine Kapazitätsgrenzen kommt: indem man empirische Beweise untersucht. Wenn Ihre Last immer absolut gleichförmig wäre, würden Ihre Slaves bei 99 % Kapazität fast genauso gut funktionieren wie bei 10 % Kapazität. Wenn sie 100 % Kapazität erreichen, würden sie abrupt zurückfallen. In der Realität ist die Last kaum so stetig. Wenn also ein Slave fast seine volle Schreibkapazität erreicht hat, würden Sie wahrscheinlich während Lastspitzen eine erhöhte Replikationsverzögerung bemerken. Das ist ein Warnsignal! Es bedeutet vermutlich, dass Sie gefährlich nahe daran sind, Ihre Slaves überzubeanspruchen, so dass sie auch zwischen den eigentlichen Lastspitzen nicht aufholen können. Ein seltsamer Test dafür, wie dicht Sie an der Grenze sind, besteht darin den SQL-Thread eines Slaves absichtlich für eine Weile zu stoppen, ihn dann neu zu starten und festzustellen, wie lange es dauert, bis er wieder aufgeholt hat. Die Patches, die Google veröffentlicht hat (siehe »Synchrone MySQL-Replikation« auf Seite 492), enthalten ebenfalls einen SHOW USER STATISTICS-Befehl, der die Busy_time des Replikationsbenutzers zeigen kann. Dabei handelt es sich um den prozentualen Zeitanteil, den der Slave-Thread mit dem Verarbeiten von Abfragen verbracht hat – eine weitere gute Methode, um die »Kopffreiheit« des Slave-SQL-Threads zu ermitteln. Wenn Sie merken, dass Slaves nicht hinterherkommen, dann ist es am besten, die Abfragen auf einem Slave zu protokollieren und mit einem entsprechenden Analysewerkzeug festzustellen, was da so langsam ist. Verlassen Sie sich bei dieser Frage nicht auf Ihren Instinkt, und gehen Sie nicht davon aus, wie die Abfragen auf dem Master funktionieren, da Slaves und Master ganz unterschiedliche Leistungsprofile aufweisen. Für diese Analyse aktivieren Sie am besten das Slow-Query-Log auf dem Slave. Das normale MySQLSlow-Query-Log zeichnet nicht die langsamen Abfragen auf, die der Slave-Thread ausführt, so dass Sie beim Replizieren nicht sehen können, welche Abfragen langsam sind. Der Mikrosekunden-Patch löst dieses Problem. Mehr über das Slow-Query-Log und diesen Patch erfahren Sie in »MySQL-Profiling« auf Seite 68. Es gibt nicht viel, was Sie auf einem Slave einstellen oder verändern können, der nicht Schritt hält, abgesehen vom Erwerb schnellerer Festplatten und CPUs. Meist soll man auf dem Slave Dinge deaktivieren, die zusätzliche Arbeit verursachen, und auf diese Weise versuchen, die Last auf dem Slave zu verringern. Man könnte z.B. InnoDB so konfigurieren, dass es Änderungen weniger oft auf die Festplatte überträgt, damit Transaktionen schneller bestätigt werden. Das erreichen Sie, indem Sie innodb_flush_log_at_trx_commit auf 2 setzen. Sie können auch das Binär-Logging auf dem Slave deaktivieren, innodb_locks_unsafe_for_binlog auf 1 und delay_key_write auf ALL für MyISAM setzen.
Replikationsprobleme und Lösungen | 435
Diese Einstellungen tauschen allerdings Sicherheit gegen Geschwindigkeit ein. Wenn Sie einen Slave zum Master befördern, dann denken Sie daran, diese Einstellungen auf sichere Werte zurückzusetzen.
Verdoppeln Sie den teuren Teil des Schreibens nicht Der Umbau Ihrer Anwendung und/oder die Optimierung Ihrer Abfragen sind oft die besten Methoden, um Slaves auf die Sprünge zu helfen. Versuchen Sie, die Arbeitsmenge zu reduzieren, die durch Ihr System dupliziert werden muss. Alle Schreiboperationen, die auf dem Master teuer sind, werden auf allen Slaves noch einmal abgespielt. Falls Sie die Arbeit vom Master auf einen Slave delegieren können, muss nur einer der Slaves die Arbeit erledigen. Sie können dann die Schreibergebnisse wieder auf den Master schieben, z.B. mit LOAD DATA INFILE. Betrachten wir ein Beispiel. Nehmen Sie an, Sie haben eine sehr große Tabelle, die Sie für eine häufig vorkommende Verarbeitung in einer kleineren Tabelle zusammenfassen: mysql> REPLACE INTO main_db.summary_table (col1, col2, ...) -> SELECT col1, sum(col2, ...) -> FROM main_db.enormous_table GROUP BY col1;
Falls Sie diese Operation auf dem Master durchführen, muss jeder Slave die riesige GROUP BY-Abfrage wiederholen. Kommt das oft vor, werden die Slaves irgendwann nicht mehr mithalten können. Es hilft wahrscheinlich, das Zahlenschieben auf einen der Slaves abzuwälzen. Auf dem Slave, vielleicht in einer speziellen Datenbank, die extra reserviert wurde, um Konflikte mit den Daten zu vermeiden, die vom Master repliziert wurden, können Sie Folgendes ausführen: mysql> REPLACE INTO summary_db.summary_table (col1, col2, ...) -> SELECT col1, sum(col2, ...) -> FROM main_db.enormous_table GROUP BY col1;
Jetzt können Sie SELECT INTO OUTFILE, gefolgt von LOAD DATA INFILE, auf dem Master ausführen, um die Ergebnisse zurück auf den Master zu verschieben. Voilà – die doppelte Arbeit wurde auf ein einfaches LOAD DATA INFILE reduziert. Bei N Slaves haben Sie gerade N – 1 riesige GROUP BY-Abfragen gespart. Das Problem bei dieser Strategie ist der Umgang mit alten Daten. Manchmal ist es schwer, konsistente Ergebnisse zu erhalten, indem man auf dem Slave liest und auf den Master schreibt (ein Problem, mit dem wir uns im nächsten Kapitel befassen). Wenn es schwer ist, das Lesen auf dem Slave durchzuführen, können Sie die Abfrage vereinfachen und Ihren Slaves eine Menge Arbeit ersparen. Wenn Sie die REPLACE- und SELECT-Teile der Abfrage trennen, können Sie die Ergebnisse in Ihre Anwendung holen und sie dann wieder auf den Master einfügen. Führen Sie zuerst die folgende Abfrage auf dem Master durch: mysql> SELECT col1, sum(col2, ...) FROM main_db.enormous_table GROUP BY col1;
Sie fügen dann die Ergebnisse wieder in die Summary-Tabelle ein, indem Sie die folgende Abfrage für jede Zeile in der Ergebnismenge wiederholen: mysql> REPLACE INTO main_db.summary_table (col1, col2, ...)
436 | Kapitel 8: Replikation
VALUES (?, ?, ...);
Auch hier haben Sie den Slaves den großen GROUP BY-Anteil der Abfrage erspart; das Trennen des SELECT vom REPLACE bedeutet, dass der SELECT-Teil der Abfrage nicht auf jedem Slave wieder abgespielt wird. Diese allgemeine Strategie – den Slaves den teuren Teil einer Schreiboperation zu ersparen – kann in vielen Fällen helfen, in denen Sie Abfragen haben, deren Ergebnisse aufwendig zu berechnen, aber nach der Berechnung einfach zu handhaben sind.
Schreiboperationen außerhalb der Replikation parallel durchführen Eine andere Taktik, um übermäßige Verzögerungen bei den Slaves zu vermeiden, sieht vor, die Replikation ganz und gar zu umgehen. Alle Schreiboperationen, die Sie auf dem Master durchführen, müssen auf dem Slave serialisiert werden, so dass es sinnvoll ist, sich die »serialisierten Schreiboperationen« als knappe Ressource vorzustellen. Müssen alle Ihre Schreibvorgänge vom Master zum Slave laufen? Wie können Sie die beschränkte serialisierte Schreibkapazität Ihres Slaves für solche Schreiboperationen aufsparen, die wirklich mittels Replikation erledigt werden müssen? Wenn Sie es in diesem Licht betrachten, können Sie vielleicht die Prioritäten für die Schreiboperationen neu setzen. Falls Sie im Speziellen Schreiboperationen finden, die sich leicht außerhalb der Replikation durchführen lassen, können Sie Schreiboperationen parallelisieren, die ansonsten wertvolle Schreibkapazitäten auf dem Slave beanspruchen würden. Ein großartiges Beispiel ist die Datenarchivierung, die wir weiter vorn in diesem Kapitel diskutiert haben. OLTP-Archivierungsabfragen sind oft einfache Einzeilenoperationen. Wenn Sie einfach nur nicht benötigte Zeilen von einer Tabelle in eine andere verschieben, besteht eigentlich kein Grund, diese Schreiboperationen auf den Slaves zu replizieren. Stattdessen können Sie das Binär-Logging für die Archivierungsanweisungen deaktivieren und dann separate, aber identische Prozesse auf den Mastern und den Slaves ausführen. Es klingt vielleicht verrückt, die Daten selbst auf einen anderen Server zu kopieren, anstatt das durch die Replikation erledigen zu lassen, das kann aber tatsächlich für manche Anwendungen sinnvoll sein. Das gilt vor allem dann, wenn eine Anwendung die einzige Aktualisierungsquelle für eine bestimmte Gruppe von Tabellen darstellt. Flaschenhälse in der Replikation sammlen sich oft um eine kleine Gruppe von Tabellen, und falls Sie diese Tabellen außerhalb der Replikation verarbeiten können, haben Sie die Möglichkeit, diese deutlich zu beschleunigen.
Den Cache für den Slave-Thread vorbereiten Bei einer entsprechenden Last könnten Sie von einer Parallelisierung der Ein-/Ausgaben auf den Slaves profitieren, indem Sie die Daten bereits vorab in den Speicher holen. Diese Technik ist nicht sehr verbreitet, wir kennen aber große Anwendungen, die sie bereits erfolgreich einsetzen.
Replikationsprobleme und Lösungen | 437
Dabei wird ein Programm benutzt, das bereits kurz vor dem Slave-SQL-Thread in den Relay-Logs liest und die Abfragen als SELECT-Anweisungen ausführt. Dadurch wird der Server veranlasst, einige der Daten von der Festplatte in den Speicher zu holen. Wenn dann der Slave-SQL-Thread die Anweisung aus dem Relay-Log ausführt, muss er nicht warten, bis die Daten von der Festplatte geholt wurden. Im Prinzip parallelisiert das SELECT die Ein-/Ausgabe, die der Slave-SQL-Thread normalerweise seriell ausführen muss. Während eine Anweisung Daten ändert, werden die Daten der nächsten Anweisung von der Festplatte in den Speicher geholt. Wie weit das Programm vor dem Slave-SQL-Thread bleiben soll, ist ganz unterschiedlich. Sie können einige Sekunden oder eine entsprechende Anzahl von Byte im Relay-Log ausprobieren. Sind Sie zu weit voraus, werden die Daten, die Sie in die Caches holen, schon wieder verschwunden sein, wenn der SQL-Thread sie braucht. Schauen wir uns an, wie man Anweisungen umschreiben muss, um diesen Ansatz verfolgen zu können. Nehmen Sie einmal die folgende Abfrage: mysql> UPDATE sakila.film SET rental_duration=4 WHERE film_id=123;
Das folgende SELECT bezieht die gleichen Zeilen und Spalten: mysql> SELECT rental_duration FROM sakila.film WHERE film_id=123;
Diese Technik funktioniert nur mit den richtigen Lasteigenschaften und der passenden Hardware-Konfiguration. Folgende Bedingungen könnten darauf hindeuten, dass sie funktioniert: • Der Slave-SQL-Thread ist ein-/ausgabegebunden, aber der Slave-Server insgesamt ist es nicht. Ein vollständig ein-/ausgabegebundener Server würde vom Vorabholen der Daten nichts haben, weil er keine untätigen Festplatten hat, mit denen dies erfolgen könnte. • Der Arbeitssatz ist viel größer als der Speicher (weshalb der SQL-Thread viel Zeit mit dem Warten auf die Ein-/Ausgaben verbringt). Wenn Ihr Arbeitssatz in den Speicher passt, hilft das Vorabholen nicht, weil Ihr Server sich nach einer Weile »aufwärmt« und nur noch selten auf Ein-/Ausgaben warten muss. • Der Slave besitzt viele Festplattenlaufwerke. Die Leute, von denen wir wissen, dass sie diese Taktik einsetzen, haben acht oder mehr Laufwerke pro Slave. Es könnte mit weniger funktionieren, aber Sie brauchen wenigstens zwei bis vier Laufwerke. • Sie nutzen eine Storage-Engine mit Row-Level-Locking, wie etwa InnoDB. Wenn Sie dies auf einer MyISAM-Tabelle versuchen, werden der Slave-SQL-Thread und der Thread, der die Daten vorab holt, wahrscheinlich in einen Wettstreit um die Tabellen-Locks verfallen, wodurch sich die Dinge weiter verlangsamen. (Wenn Sie jedoch viele Tabellen haben und die Schreiboperationen zwischen ihnen verteilt sind, könnte sich theoretisch auch bei MyISAM-Tabellen mit dieser Technik die Replikation beschleunigen lassen.)
438 | Kapitel 8: Replikation
Eine Beispiellast, die vom Vorabholen der Daten profitiert, ist eine mit vielen weit verstreuten, einzeiligen UPDATE-Anweisungen, die typischerweise auf dem Master stark nebenläufig sind. DELETE-Anweisungen kommt dies ebenfalls zugute. INSERT-Anweisungen haben weniger von diesem Ansatz – vor allem, wenn die Zeilen sequenziell eingefügt werden –, weil das Ende des Index bereits von vorangegangenen Einfügevorgängen »heiß« ist. Wenn eine Tabelle viele Indizes besitzt, ist es eventuell nicht möglich, alle Daten vorher zu holen, die eine Anweisung modifizieren wird. Die UPDATE-Anweisung ändert vielleicht jeden Index, das SELECT liest aber im besten Fall typischerweise nur den Primärschlüssel und einen sekundären Index. Das UPDATE muss dennoch weitere Indizes zur Modifikation holen. Die Effektivität dieser Taktik ist daher in Tabellen mit vielen Indizes deutlich eingeschränkt. Sie können mit iostat feststellen, ob Sie freie Festplatten haben, die solche Anforderungen zum Vorabholen von Daten erledigen könnten. Schauen Sie sich die Warteschlangengröße und die Service-Zeit an (Beispiele finden Sie im vorangegangenen Kapitel). Eine kleine Warteschlange zeigt, dass etwas auf einer höheren Ebene Anforderungen serialisiert. Eine große Warteschlange zeigt eine hohe Last – die Art, die Sie typischerweise nicht von einem Slave-SQL-Thread erhalten werden, wenn Sie viele Festplatten haben. Eine hohe Service-Zeit bedeutet, dass zu viele Anforderungen auf einmal an die Festplatte übermittelt werden. Diese Technik ist keine Wunderwaffe. Es gibt viele Gründe, weshalb sie bei Ihnen nicht funktionieren oder vielleicht sogar weitere Probleme verursachen könnte. Sie sollten sie nur dann versuchen, wenn Sie Ihre Hardware und Ihr Betriebssystem gut kennen. Wir kennen Leute, bei denen dieser Ansatz die Replikationsgeschwindigkeit um 300 % bis 400 % vergrößert hat. Wir haben es aber selbst ausprobiert und festgestellt, dass es nicht immer funktioniert hat. Es ist wichtig, die Parameter richtig einzustellen. Allerdings gibt es nicht immer eine richtige Kombination aus Parametern. Manchmal verhindern auch Verhaltensweisen des Dateisystems und/oder des Kernels eine parallele Ein-/Ausgabe. Sie müssen es selbst ausprobieren! Das Werkzeug mk-slave-prefetch, ein Teil von Maatkit, ist eine Implementierung der Ideen, die wir in diesem Abschnitt beschrieben haben.
Übergroße Pakete vom Master Ein weiteres schwer zu findendes Problem bei der Replikation kann auftreten, wenn die max_allowed_packet-Größe des Masters nicht der des Slaves entspricht. In diesem Fall kann der Master ein Paket im Log aufzeichnen, das der Slave als zu groß ansieht. Wenn der Slave dann das Binärlog-Event bezieht, treten bei ihm dann verschiedene Probleme auf. Dazu gehören eine Endlosschleife aus Fehlern und erneuten Versuchen oder eine Beschädigung im Relay-Log.
Replikationsprobleme und Lösungen | 439
Beschränkte Replikationsbandbreite Wenn Sie über eine eingeschränkte Bandbreite replizieren, können Sie die Option slave_compressed_protocol auf dem Slave aktivieren (verfügbar seit MySQL 4.0). Verbindet sich der Slave mit dem Master, fordert er eine komprimierte Verbindung an – mit der gleichen Komprimierung, die jede MySQL-Clientverbindung nutzen kann. Die verwendete Komprimierungs-Engine ist zlib, und unsere Tests haben gezeigt, dass sie textuelle Daten auf etwa ein Drittel ihrer Originalgröße komprimiert. Allerdings wird für die Komprimierung auf dem Master und die Dekomprimierung auf dem Slave zusätzliche CPUZeit benötigt. Bei einer langsamen Verbindung mit einem Master auf der einen Seite und vielen Slaves auf der anderen Seite, könnten Sie einen Distribution-Master auf der Slave-Seite unterbringen. Auf diese Weise ist über die langsame Leitung nur ein Server mit dem Master verbunden, wodurch sich die Last auf der Verbindung sowie die CPU-Last auf dem Master verringern.
Kein Festplattenplatz Die Replikation kann Ihre Festplatten tatsächlich mit Binärlogs, Relay-Logs oder temporären Tabellen füllen, vor allem, wenn Sie viele LOAD DATA INFILE-Abfragen auf dem Master durchführen und log_slave_updates auf dem Slave aktiviert haben. Je weiter ein Slave zurückfällt, umso mehr Festplattenplatz benutzt er wahrscheinlich für die Relay-Logs, die vom Master geholt, aber noch nicht ausgeführt wurden. Sie können diese Fehler vermeiden, indem Sie die Festplattenauslastung überwachen und die Konfigurationsvariable relay_log_space setzen.
Beschränkungen der Replikation Die MySQL-Replikation kann aufgrund der ihr innewohnenden Beschränkungen ausfallen oder – mit oder ohne Fehler – aus dem Takt geraten. Es gibt eine ziemlich lange Liste an SQL-Funktionen und Programmierpraktiken, die einfach nicht zuverlässig repliziert werden (wir haben viele von ihnen in diesem Kapitel erwähnt). Es ist schwer sicherzustellen, dass keine von ihnen den Weg in Ihren Produktionscode findet, vor allem, wenn Ihre Anwendung oder Ihr Team sehr groß ist.14 Ein weiteres Problem sind Bugs im Server. Wir wollen nicht negativ klingen, aber die meisten großen Versionen des MySQL-Servers haben in der Vergangenheit Bugs in der Replikation besessen, vor allem in den ersten Ausgaben der Hauptversion. Neue Eigenschaften, wie etwa gespeicherte Prozeduren, haben normalerweise weitere Probleme verursacht.
14 Leider besitzt MySQL keine forbid_operations_unsafe_for_replication-Option.
440 | Kapitel 8: Replikation
Für die meisten Benutzer stellt dies keinen Grund dar, neue Funktionen zu vermeiden, sondern bietet lediglich Veranlassung, besonders sorgfältig zu testen, vor allem wenn Sie Ihre Anwendung oder MySQL aufrüsten. Auch die Überwachung ist wichtig; Sie müssen wissen, wenn etwas ein Problem verursacht. Die MySQL-Replikation ist kompliziert, und je komplizierter Ihre Anwendung ist, umso sorgfältiger müssen Sie sein. Wenn Sie jedoch lernen, damit umzugehen, funktioniert sie wirklich gut.
Wie schnell ist die Replikation? Häufig wird in Bezug auf die Replikation die Frage gestellt: »Wie schnell ist sie?« Sie ist, kurz gesagt, im Allgemeinen sehr schnell und läuft so schnell, wie MySQL die Events vom Master kopieren und wieder abspielen kann. Wenn Sie ein langsames Netzwerk und sehr große Binärlog-Events haben, dann ist die Verzögerung zwischen der Aufzeichnung im Binärlog und der Ausführung auf dem Slave vielleicht spürbar. Brauchen Ihre Abfragen lange für die Ausführung und haben Sie ein schnelles Netzwerk, dann können Sie meist davon ausgehen, dass die Abfragezeit auf dem Slave einen größeren Anteil an der Zeit für die Replikation eines Events hat. Um eine umfassendere Antwort geben zu können, müsste jeder Schritt des Vorgangs gemessen und anschließend entschieden werden, welche Schritte die meiste Zeit in Ihrer Anwendung erfordern. Für manche Leser ist es vermutlich nur wichtig, dass es normalerweise nur eine kleine Verzögerung zwischen dem Aufzeichnen der Events auf dem Master und dem Kopieren der Events in das Relay-Log auf dem Slave gibt. Falls Sie es genauer wissen wollen, schauen Sie sich unser kleines Experiment an. Wir bauten auf dem Prozess auf, der in der ersten Ausgabe dieses Buches beschrieben wurde, sowie auf den Methoden von Giuseppe Maxia,15 um die Replikationsgeschwindigkeit mit hoher Präzision zu messen. Wir erstellten eine nichtdeterministische benutzerdefinierte Funktion, die die Systemzeit mit Mikrosekundengenauigkeit zurückliefert (den Quellcode finden Sie in »Benutzerdefinierte Funktionen« auf Seite 248): mysql> SELECT NOW_USEC( ) +----------------------------+ | NOW_USEC( ) | +----------------------------+ | 2007-10-23 10:41:10.743917 | +----------------------------+
Dies erlaubt es uns, die Replikationsgeschwindigkeit zu messen, indem wir den Wert von NOW_USEC( ) in eine Tabelle auf dem Master einfügten und sie dann mit dem Wert auf dem Slave vergleichten.
15 Siehe http://datacharmer.blogspot.com/2006/04/measuring-replication-speed.html.
Wie schnell ist die Replikation?
| 441
Wir maßen die Verzögerung, indem wir zwei Instanzen von MySQL auf dem gleichen Server einrichteten, um Ungenauigkeiten zu vermeiden, die durch den Takt verursacht wurden. Wir konfigurierten eine Instanz als Slave der anderen und führten dann die folgenden Abfragen auf der Master-Instanz aus: mysql> CREATE TABLE test.lag_test( -> id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -> now_usec VARCHAR(26) NOT NULL -> ); mysql> INSERT INTO test.lag_test(now_usec) VALUES( NOW_USEC( ) );
Wir verwendeten eine VARCHAR-Spalte, weil die in MySQL eingebauten Typen Zeiten mit Auflösungen von unter einer Sekunde nicht speichern können (obwohl einige seiner Zeitfunktionen in der Lage sind, Berechnungen im Bereich unter einer Sekunde durchzuführen). Nun musste nur noch die Differenz zwischen dem Slave und dem Master verglichen werden. Dazu eignet sich eine Federated-Tabelle. Auf dem Slave führten wir Folgendes aus: mysql> CREATE TABLE test.master_val ( -> id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -> now_usec VARCHAR(26) NOT NULL -> ) ENGINE=FEDERATED -> CONNECTION='mysql://user:[email protected]/test/lag_test';
Ein einfaches Join und die Funktion TIMESTAMPDIFF( ) zeigen den Rückstand in Mikrosekunden zwischen der Ausführungszeit der Abfrage auf dem Master und dem Slave: mysql> SELECT m.id, TIMESTAMPDIFF(FRAC_SECOND, m.now_usec, s.now_usec) AS usec_lag -> FROM test.lag_test as s -> INNER JOIN test.master_val AS m USING(id); +----+----------+ | id | usec_lag | +----+----------+ | 1 | 476 | +----+----------+
Wir fügten mit einem Perl-Skript 1.000 Zeilen auf dem Master ein. Zwischen den einzelnen Einfügungen ließen wir eine Verzögerung von 10 Millisekunden, um zu verhindern, dass die Master- und Slave-Instanzen begannen, um die CPU-Zeit zu kämpfen. Anschließend erzeugten wir eine temporäre Tabelle, die den Rückstand des jeweiligen Events enthielt: mysql> CREATE TABLE test.lag AS > SELECT TIMESTAMPDIFF(FRAC_SECOND, m.now_usec, s.now_usec) AS lag -> FROM test.master_val AS m -> INNER JOIN test.lag_test as s USING(id);
Als Nächstes gruppierten wir die Ergebnisse nach Rückstandszeit, um festzustellen, welches die am häufigsten auftretenden Rückstandszeiten waren: mysql> -> -> ->
SELECT ROUND(lag / 1000000.0, 4) * 1000 AS msec_lag, COUNT(*) FROM lag GROUP BY msec_lag ORDER BY msec_lag;
442 | Kapitel 8: Replikation
+----------+----------+ | msec_lag | COUNT(*) | +----------+----------+ | 0.1000 | 392 | | 0.2000 | 468 | | 0.3000 | 75 | | 0.4000 | 32 | | 0.5000 | 15 | | 0.6000 | 9 | | 0.7000 | 2 | | 1.3000 | 2 | | 1.4000 | 1 | | 1.8000 | 1 | | 4.6000 | 1 | | 6.6000 | 1 | | 24.3000 | 1 | +----------+----------+
Die Ergebnisse zeigen, dass die meisten kleinen Abfragen weniger als 0,3 Millisekunden für die Replikation brauchen, also von der Ausführungszeit auf dem Master bis zur Ausführungszeit auf dem Slave. Hier wird allerdings nicht gemessen, wie schnell ein Event beim Slave ankommt, nachdem es im Binärlog auf dem Master aufgezeichnet wurde. Es wäre schön, diesen Wert zu kennen, denn je schneller der Slave das Log-Event empfängt, umso besser ist es. Wenn der Slave das Event empfangen hat, kann er eine Kopie bereitstellen, falls der Master abstürzt. Obwohl Ihre Messungen nicht exakt zeigen, wie lang dieser Teil des Vorgangs dauert, sollte er theoretisch außerordentlich schnell ablaufen (d.h. nur durch die Netzwerkgeschwindigkeit eingeschränkt). Der MySQL-Binlog-Dump-Prozess fragt nicht den Master nach Events ab, weil das ineffizient und langsam wäre. Stattdessen benachrichtigt der Master den Slave über Events. Das Lesen eines Binärlog-Events vom Master ist ein blockierender Netzwerkaufruf, der praktisch sofort damit beginnt, Daten zu senden, nachdem der Master das Event im Log aufgezeichnet hat. Man kann also mit hoher Sicherheit sagen, dass das Event den Slave so schnell erreicht, wie der Slave-Thread aufwachen und das Netzwerk die Daten übertragen kann.
Die Zukunft der MySQL-Replikation Die MySQL-Replikation hat eine Reihe von Schwächen, die MySQL AB künftig beheben will. Von dritter Seite wurden bereits einige Funktionen und Problemlösungen bereitgestellt. Zum Beispiel hat Google eine Reihe von eigenen Patches für den MySQL-Server veröffentlicht, die ihn um Fähigkeiten zur quasisynchronen Replikation sowie weitere Eigenschaften erweitern (siehe »Synchrone MySQL-Replikation« auf Seite 492). Ein weiterer möglicher Zusatz ist die Unterstützung für die Multimaster-Replikation – d.h. einen Slave mit mehr als einem Master. Dieses Problem ist wahrscheinlich schwierig zu lösen und erfordert vermutlich Fähigkeiten zur Konflikterkennung und -auflösung.
Die Zukunft der MySQL-Replikation | 443
Die zeilenbasierte Replikation in MySQL 5.1 ist ein Schritt in diese Richtung. Zeilenbasierte Replikation könnte es künftig auch erlauben, durch mehrere Threads Events auf den Slave anwenden zu lassen, wodurch der Flaschenhals verschwindet, der durch die Single-Thread-Regelung auftritt. Es gibt darüber hinaus Pläne, die Online-Backup-API in die Replikation zu integrieren und es einem MySQL-Server zu erlauben, sich automatisch selbst als Slave eines anderen Servers zu konfigurieren. Momentan gibt es in MySQL keine Garantien für die Konsistenz und Korrektheit von Daten. Entsprechend einer Umfrage auf der MySQL-Website ist die am meisten gewünschte Funktion ein Online-Konsistenztest, mit dem geprüft werden kann, ob ein Slave die gleichen Daten enthält wie sein Master. MySQL AB betreibt für diese Aufgabe ein Worklog mit einer grundlegenden Beschreibung für die Lösung dieses Problems. Viele Leute haben außerdem Erweiterungen für das Binärlog-Format gefordert, um sicherzustellen, dass Beschädigungen erkannt werden können, und MySQL AB hat auch hier bestätigt, dass es sich um eine wichtige Aufgabe handelt. Diese und weitere Verbesserungen dürften dafür sorgen, dass die MySQL-Replikation in der Zukunft noch leistungsfähiger und zuverlässiger wird. Es ist ermutigend, wenn man auf die letzten Jahre zurückblickt und die Änderungen sieht, die in dieser Zeit geschehen sind. Es soll dennoch angemerkt werden, dass die meisten Funktionen, die in der ersten Ausgabe dieses Buches vorhergesagt wurden, niemals umgesetzt wurden, obwohl einige von ihnen teilweise implementiert wurden – wie z.B. die ausfallsichere Replikation, die zwar in der MySQL-Codebasis vorhanden ist, aber als Projekt nie zu Ende geführt wurde.
444 | Kapitel 8: Replikation
KAPITEL 9
Skalierung und Hochverfügbarkeit
In diesem Kapitel zeigen wir Ihnen, wie Sie eine MySQL-Architektur aufbauen, die sehr groß werden kann, dabei aber dennoch schnell und zuverlässig bleibt. Meist geht Skalierungsproblemen keine Warnung voraus; sie tauchen eines Tages einfach auf. Falls Sie nicht vorhaben, Ihre Anwendung zu skalieren, müssen Sie wahrscheinlich viel dafür tun, damit sie reaktionsfähig bleibt. Unternehmen, die ihre Anwendungen nicht skalieren können, fallen damit oft völlig aus dem Rennen. Ironie des Schicksals: zu viel Erfolg kann das Geschäft zum Erliegen bringen. Sie müssen unter allen Umständen sicherstellen, dass Ihre Anwendung in Betrieb bleibt. Viele Dinge können sie beeinträchtigen, meist jedoch liegen die Probleme in ordinären Hardware- und Softwareausfällen begründet. Ihre Anwendung sollte diese als Routine betrachten und vorzugsweise automatisch damit klarkommen. Die Forderungen nach Skalierung und nach Hochverfügbarkeit gehen oft Hand in Hand. Hochverfügbarkeit ist bei einer kleinen Anwendung aus verschiedenen Gründen nicht so wichtig: Sie läuft normalerweise nur auf einem einzigen Server, so dass ein Serverausfall weniger wahrscheinlich ist; weil sie klein ist, kosten Ausfallzeiten meist nicht so viel Geld und werden durch die kleinere Benutzerbasis eher toleriert. Wenn Sie jedoch zehnmal so viele Server haben, steigt die Wahrscheinlichkeit eines Serverausfalls auch auf das Zehnfache, und wahrscheinlich haben Sie auch viel mehr Benutzer mit viel höheren Erwartungen. Mit der richtigen Architektur und der passenden Implementierung können Sie dafür sorgen, dass MySQL gut skaliert. Das gilt auch für die Gewähr von Hochverfügbarkeit. In diesem Kapitel wollen wir diese Konzepte so gut wie möglich auseinandernehmen, damit wir sie getrennt betrachten können. Wir beginnen mit einem Überblick über die Terminologie und gehen dann nacheinander Skalierung und Hochverfügbarkeit an (in dem Zusammenhang befassen wir uns auch mit Lastausgleich). Wir beginnen jeden Teil mit einem Abschnitt über die Anforderungen, weil es für den erfolgreichen Betrieb einer großen Anwendung sehr wichtig ist, so früh wie möglich die wesentlichen Geschäftsanforderungen zu definieren. Diese Anforderungen haben große Auswirkungen auf den Entwurf und die Architektur der Anwendung. Anschließend diskutieren wir mögliche Techniken und Lösungen, wobei wir uns die jeweiligen Vor- und Nachteile anschauen. | 445
Terminologie Zunächst einmal ist es wichtig, die Konzepte klar zu verstehen. Oft werden Begriffe wie »Skalierbarkeit« und »Leistung« im normalen Gespräch synonym verwendet, dabei sind sie wirklich verschieden. Hier sind unsere vier Definitionen der Schlüsselbegriffe, die wir in diesem Kapitel verwenden: Leistung Die Fähigkeit der Anwendung, ein bestimmtes Ziel zu erreichen, wie etwa eine bestimmte Antwortzeit, einen Durchsatz oder ein anderes der Maße, die wir in Kapitel 2 besprochen haben. Kapazität Die Gesamtlast, die die Anwendung verarbeiten kann. Wir kommen weiter hinten in diesem Abschnitt darauf, was »Last« bedeutet. Skalierbarkeit Die Fähigkeit der Anwendung, die Leistung beizubehalten, wenn sie um ein bestimmtes Maß anwächst (z.B. mehr Server umfasst). Wenn wir im weiteren Sinne des Wortes von Leistung sprechen, dann meinen wir Kapazität und Skalierbarkeit zusammen. Verfügbarkeit Der prozentuale Zeitanteil, in dem die Anwendung in der Lage ist, auf Anforderungen zu reagieren. Dies wird normalerweise in »Neunen« angegeben: Zum Beispiel bedeutet »fünf Neunen«, dass die Anwendung 99,999 % der Zeit verfügbar ist, was eine ungefähre Ausfallzeit von fünf Minuten pro Jahr ergibt. (Für die meisten Anwendungen ist das ein sehr hoher Wert.) Fehlertoleranz Die Fähigkeit der Anwendung und des Gesamtsystems, mit Ausfällen annehmbar klarzukommen. Selbst ein System, das auf Hochverfügbarkeit ausgelegt wurde, kann ausfallen. In diesem Fall kann eine fehlertolerante Anwendung weiterhin so viel Funktionalität wie möglich bereitstellen, anstatt einfach komplett unerreichbar zu werden. Skalierbarkeit ist das Konzept, das am schwierigsten zu erklären ist. Hier ist eine Analogie: • Leistung drückt aus, wie schnell das Auto ist. • Kapazität gibt die Geschwindigkeitsbegrenzung und die Anzahl der Spuren auf der Autobahn an. • Skalierbarkeit bezeichnet den Grad, in dem Sie Autos und Autobahnen hinzufügen können, ohne dass der Verkehr langsamer wird. • Verfügbarkeit gibt an, wie oft eine Autobahn oder eine Spur befahrbar ist.
446 | Kapitel 9: Skalierung und Hochverfügbarkeit
In dieser Analogie hängt die Skalierbarkeit von solchen Faktoren ab wie der Gestaltung der Abfahrten, der Anzahl der Autos, die Unfälle oder Pannen haben, und davon, ob oft überholt wird – aber im Allgemeinen nicht davon, wie leistungsstark die Motoren sind. Mit anderen Worten: Skalierbarkeit ist die Fähigkeit, die Kapazität bei Bedarf zu erhöhen, ohne die Leistung zu reduzieren. Der wesentliche Punkt ist: »die Fähigkeit, zu erhöhen«. Selbst wenn Ihre MySQL-Architektur skalierbar ist, muss es Ihre Anwendung noch lange nicht sein. Wenn es aus irgendeinem Grund schwierig ist, die Kapazität zu erhöhen, dann ist Ihre Anwendung nicht skalierbar. Die Fehlertoleranz hängt von der Fähigkeit der Anwendung ab, auch dann teilweise zu funktionieren, wenn eine Komponente ausfällt. Fehlertolerant zu sein ist nicht dasselbe wie selbstheilend zu sein, was sich auf die Fähigkeit einer Anwendung bezieht, sich im Fehlerfall selbst wiederherzustellen oder die vollständige Funktionalität zu gewährleisten. Fehlertoleranz ist oft eine wichtige Komponente der Skalierbarkeit, weil Sie sie bei der Gestaltung Ihrer Anwendung mit vorsehen müssen. Wenn Sie die Komponenten nicht so entwerfen, dass Sie sie leicht deaktivieren können und dass sie mit dem Ausfall anderer Komponenten zurechtkommen, könnte ein Problem dafür sorgen, dass ein größerer Teil Ihrer Anwendung ausfällt als nötig. Fehlertoleranz verlangt darüber hinaus eine saubere Trennung zwischen den Komponenten, was schwierig zu erreichen ist, wenn Sie das nicht von Anfang an eingeplant haben. Wenn Skalierbarkeit die Fähigkeit ist, die Kapazität zu erhöhen, und Kapazität angibt, wie viel Last eine Anwendung verarbeiten kann, dann ist Skalierbarkeit auch die Fähigkeit, mehr und mehr Last zu verarbeiten. »Last« ist in der Tat ein kompliziertes Konzept, weil sie von der Anwendung abhängt. Hier sind einige gebräuchliche Lastmetriken für eine typische »Social-Networking-Site« (eine Anwendung, die ein gutes Beispiel für viele der Konzepte darstellt, die wir diskutieren): Datenmenge Die schiere Menge an Daten, die Ihre Anwendung sammeln kann, bildet eine der größten Herausforderungen für die Skalierung. Das ist vor allem für viele der heutigen Webanwendungen, die nie irgendwelche Daten löschen, ein wesentlicher Faktor. Auf Social-Networking-Sites werden z.B. üblicherweise nie alte Nachrichten oder Kommentare gelöscht. Anzahl der Benutzer Selbst wenn jeder Benutzer nur eine geringe Menge an Daten hat, kommt bei einer entsprechend großen Anzahl an Benutzern viel zusammen – und die Datengröße kann unproportional schneller wachsen als die Anzahl der Benutzer. Viele Benutzer bedeutet im Allgemeinen auch viele Transaktionen, und die Anzahl der Transaktionen muss sich ebenfalls nicht proportional zur Anzahl der Benutzer verhalten. Schließlich können viele Benutzer auch zunehmend komplexe Abfragen bedeuten, vor allem, wenn die Abfragen von der Anzahl der Beziehungen der Benutzer untereinander abhängen. (Die Anzahl der Beziehungen wird begrenzt durch ( N * (N–1) ) / 2, wobei N die Anzahl der Benutzer ist.)
Terminologie | 447
Benutzeraktivität Nicht alle Benutzeraktivitäten sind gleich, und die Benutzeraktivität ist nicht konstant. Wenn Ihre Benutzer plötzlich aktiver werden, weil sie z.B. eine neue Funktion mögen, dann kann Ihre Last deutlich ansteigen. Die Benutzeraktivität ist allerdings nicht nur eine Frage der Page-Views – die gleiche Anzahl an Page-Views kann mehr Arbeit verursachen, wenn Teile der Site, die aufwendiger zu generieren sind, an Beliebtheit zunehmen. Außerdem sind manche Benutzer aktiver als andere: Sie haben vielleicht mehr Freunde, Nachrichten oder Fotos als der durchschnittliche Benutzer. Größe der verwandten Datensätze Wenn es Beziehungen zwischen Benutzergruppen gibt, muss die Anwendung möglicherweise Abfragen und Berechnungen auf ganzen Gruppen von miteinander in Beziehung stehenden Benutzern durchführen. Das ist komplexer, als wenn nur mit einzelnen Benutzern und ihren Daten gearbeitet wird. Social-Networking-Sites stehen aufgrund von beliebten Gruppen oder Benutzern mit vielen Freunden oft großen Herausforderungen gegenüber.
MySQL skalieren Alle Daten Ihrer Anwendung in eine einzige MySQL-Instanz zu setzen, ist ein Ansatz, der nicht besonders gut skaliert. Früher oder später werden Sie mit Leistungsengpässen konfrontiert werden, die durch eine zunehmende Last auf dem Server verursacht werden. Die traditionelle Lösung sieht bei vielen Anwendungen so aus, dass leistungsfähigere Server gekauft werden. Man bezeichnet das als »vertikale Skalierung«. Der entgegengesetzte Ansatz besteht darin, die Arbeit auf viele Computer aufzuteilen, was man üblicherweise »horizontale Skalierung« nennt. Bei den meisten Anwendungen gibt es darüber hinaus Daten, die selten oder nie benötigt werden und die daher ausgelagert oder archiviert werden können (»Scaling Back«). Manche Datenbankprodukte schließlich unterstützen eine Skalierung durch Federation, die es Ihnen erlaubt, auf entfernte Daten genauso zuzugreifen, als wären sie lokal. MySQL bietet dafür nur eingeschränkte Unterstützung. Das Traumszenario für die Skalierung ist eine einzige logische Datenbank, die so viele Daten enthalten, so viele Abfragen bedienen und so groß werden kann, wie Sie wollen. Viele Leute denken zuerst daran, einen »Cluster« oder ein »Netz« zu erzeugen, das dies nahtlos erledigt, ohne dass die Anwendung Dreckarbeit verrichten oder wissen muss, dass die Daten sich tatsächlich nicht nur auf einem, sondern auf vielen Servern befinden. Die MySQL-Technik NDB Cluster unterstützt das bis zu einem gewissen Grad, funktioniert für die meisten Webanwendungen aber nicht besonders gut und unterliegt einigen Beschränkungen. Aus diesem Grund werden die meisten großen Anwendungen, die auf MySQL aufbauen, auf andere Weise skaliert. Wir besprechen die Skalierung durch Cluster am Ende dieses Teils des Kapitels.
448 | Kapitel 9: Skalierung und Hochverfügbarkeit
Skalierbarkeit einplanen Das typische Symptom für eine schlechte Skalierbarkeit ist die Schwierigkeit, mit einer zunehmenden Last mitzuhalten. Normalerweise äußert sich das in einer verminderten Leistung in Form von langsamen Abfragen, einer Verschiebung der Belastung von CPUgebunden zu ein-/ausgabegebunden, im Wettstreit zwischen gleichzeitig auftretenden Abfragen und in einer zunehmenden Latenz. Der Übeltäter ist oft eine zunehmende Komplexität der Abfragen oder ein Teil der Daten bzw. ein Index, der früher in den Speicher gepasst hat, dies nun aber nicht mehr tut. Bei bestimmten Arten von Abfragen fallen Ihnen vielleicht Änderungen auf, bei anderen dagegen nicht. Lange oder komplexe Abfragen zeigen z.B. die Belastung oft eher als kleinere Abfragen. Wenn Ihre Anwendung skalierbar ist, können Sie einfach mehr Server anschließen, um die Last zu bewältigen. Die Leistungsprobleme verschwinden dann. Ist sie dagegen nicht skalierbar, müssen Sie sich auf die Leistungsprobleme konzentrieren, versuchen, die Server anzupassen usw. Das bedeutet eine Konzentration auf die Symptome, nicht auf die eigentlichen Ursachen. Sie können dies vermeiden, indem Sie Skalierbarkeit einplanen. Der schwierigste Teil bei der Planung der Skalierbarkeit ist es abzuschätzen, wie viel Last Sie bewältigen müssen. Es ist nicht nötig, dass Sie absolut richtig liegen, Sie sollten sich aber zumindest in der richtigen Größenordnung bewegen. Wenn Sie die Last überschätzen, dann verschwenden Sie Ressourcen; unterschätzen Sie sie dagegen, werden Sie auf die Last nicht richtig vorbereitet sein. Sie müssen auch Ihren Zeitplan richtig einschätzen – d.h., Sie müssen wissen, wie der Zeithorizont aussieht. Bei manchen Anwendungen könnte ein einfacher Prototyp einige Monate lang gut funktionieren, wodurch Sie die Möglichkeit haben, Geld aufzutreiben und eine besser skalierbare Architektur aufzubauen. Bei anderen Anwendungen müssen Sie dafür sorgen, dass die aktuelle Architektur genug Kapazität für zwei Jahre bietet. Hier sind einige Fragen, die Sie sich stellen sollten, wenn Sie die Skalierbarkeit planen: • Wie vollständig ist die Funktionalität Ihrer Anwendung? Viele der Skalierungslösungen, die wir vorschlagen, erschweren es unter Umständen, bestimmte Funktionen zu implementieren. Wenn Sie einige der Kernfunktionen Ihrer Anwendung noch nicht umgesetzt haben, ist es möglicherweise schwer festzustellen, wie Sie sie in eine skalierte Anwendung integrieren können. Genauso könnte es schwierig sein, sich für eine Skalierungslösung zu entscheiden, bevor Sie gesehen haben, wie diese Funktionen tatsächlich funktionieren. • Wie sieht Ihre erwartete Spitzenlast aus? Ihre Anwendung muss auch bei Spitzenbelastung funktionieren. Was wäre, wenn es Ihre Site auf die Startseite von Heise News, Slashdot oder Spiegel schafft? Selbst wenn Ihre Anwendung keine ausgesprochen populäre Website ist, können Lastspitzen vorkommen. Wenn Sie z.B. ein Online-Händler sind, stellen Feiertage Zeiten mit Spitzenlast dar – etwa vor Weihnachten oder anderen Feiertagen.
MySQL skalieren | 449
• Was ist, wenn Teile Ihres Systems ausfallen, obwohl Sie darauf angewiesen sind, dass jeder Teil des Systems mithilft, die Last zu bewältigen? Kommen Sie z.B. noch hinterher, falls Sie sich darauf verlassen, dass Replikations-Slaves die Leselast verteilen, aber einer von ihnen ausfällt? Müssen Sie dafür einen Teil der Funktionalität deaktivieren? Sie könnten eine gewisse zusätzliche Kapazität einbauen, um diesen Problemen zu begegnen.
Vor der Skalierung In einer perfekten Welt wäre es schön, wenn man vorausplanen könnte, genügend Entwickler hätte, niemals Geldprobleme hätte usw. In der Realität ist es meist etwas komplizierter, und Sie müssen Kompromisse eingehen, wenn Sie eine Anwendung skalieren. Sie müssen im Speziellen möglicherweise große Änderungen der Anwendung für eine Weile verschieben. Bevor wir uns näher mit den Einzelheiten der Skalierung von MySQL befassen, wollen wir hier einige Dinge aufzählen, die Sie bereits erledigen können, bevor Sie sich der Mühe der Skalierung unterziehen: Optimieren der Leistung Oft können Sie mit relativ einfachen Änderungen deutliche Leistungssteigerungen erzielen, etwa indem Sie Tabellen korrekt indizieren oder eine andere StorageEngine einsetzen. Falls Ihnen jetzt Leistungsgrenzen begegnen, sollten Sie als Erstes das Slow-Query-Log aktivieren und analysieren und feststellen, welche Abfragen Sie optimieren können. Mehr zu diesem Thema erfahren Sie in »Logging von Abfragen« auf Seite 69. Es gibt einen Punkt der abnehmenden Erträge. Nachdem Sie die wichtigsten Probleme behoben haben, wird es immer schwerer, die Leistung mithilfe von Abfrageoptimierungen zu verbessern. Neue Optimierungen bringen immer weniger, erfordern einen immer größeren Aufwand und verkomplizieren Ihre Anwendung einfach nur noch. Kaufen Sie leistungsstärkere Hardware Manchmal hilft es, Ihre Server aufzurüsten oder weitere Server anzuschließen. Vor allem, wenn sich eine Anwendung erst am Anfang ihres Lebenszyklus befindet, ist es oft keine schlechte Idee, ein paar weitere Server zu kaufen. Die Alternative wäre es zu versuchen, die Anwendung weiterhin auf einem einzigen Server zu betreiben. Obwohl ein wunderbares, elegantes Design dies ermöglichen könnte, ist es vielleicht praktischer, weitere Server zu kaufen, wenn drei Leute schon einen Monat brauchen, um dieses Design umzusetzen. Das gilt vor allem, wenn Zeit wichtig und Entwickler rar sind. Neue Hardware zu kaufen funktioniert gut, wenn Ihre Anwendung entweder klein oder so gestaltet ist, dass sie weitere Hardware gut ausnutzen kann. Für neue Anwendungen ist das normal, da sie meist sehr klein oder vernünftig entworfen sind. Für größere, ältere Anwendungen muss dieser Ansatz nicht funktionieren oder ist möglicherweise zu teuer. Es ist z.B. keine große Sache, von einem auf drei Server umzusteigen, anders sieht es
450 | Kapitel 9: Skalierung und Hochverfügbarkeit
jedoch aus, wenn von 100 auf 300 übergegangen werden soll – das ist sehr teuer. An dieser Stelle lohnt es sich, Zeit und Mühe zu investieren, um so viel Leistung wie möglich aus Ihren existierenden Systemen herauszuholen.
Vertikal skalieren Die vertikale Skalierung, also die Vergrößerung Ihres Systems, kann eine Weile funktionieren, klappt aber nicht mehr, wenn Ihre Anwendung sehr groß wird. Der erste Grund ist das Geld. Ungeachtet der Software, die Sie auf dem Server ausführen, ist die Vergrößerung des Systems irgendwann finanziell nicht mehr tragbar. Es gibt einen bestimmten Bereich an Hardware, der das beste Preis-Leistungs-Verhältnis bietet. Außerhalb dieses Bereichs wird die Hardware zunehmend proprietär und ungewöhnlich und entsprechend auch teurer. Das bedeutet, dass es eine praktische Grenze gibt, bis zu der Sie sich die Skalierung überhaupt leisten können. Abgesehen von den wirtschaftlichen Aspekten neigt MySQL selbst dazu, sich nicht besonders gut vertikal skalieren zu lassen, weil man es schlecht dazu bewegen kann, viele CPUs und Festplatten effektiv auszunutzen. Wie viel Hardware genau Sie verwenden können, hängt von der Last, der Art der eingesetzten Hardware und dem Betriebssystem ab. Wir denken, dass ungefähr 8 CPUs und 14 Festplatten für aktuelle Versionen von MySQL die Grenze darstellen.1 Viele Leute haben schon mit weniger Hardware Probleme. Selbst wenn Ihr Master-Server viele CPUs effektiv benutzen kann, besteht nur eine geringe Wahrscheinlichkeit, dass Sie einen Slave-Server aufbauen können, der leistungsfähig genug ist, um Schritt zu halten. Ein stark ausgelasteter Master kann leicht mehr Arbeit verrichten als ein Slave-Server mit der gleichen Hardware, weil der ReplikationsThread des Slaves nicht in der Lage ist, mehrere CPUs und Festplatten effizient einzusetzen. Darüber hinaus können Sie das System nicht unendlich vergrößern, weil selbst die leistungsstärksten Computer Grenzen haben. Single-Server-Anwendungen stoßen zuerst beim Lesen an ihre Grenzen, vor allem, wenn sie komplizierte Leseabfragen durchführen. Solche Abfragen werden innerhalb von MySQL in einem einzigen Thread ausgeführt, so dass sie nur eine CPU benutzen. Und auch mit viel Geld kann man ihnen nicht mehr Leistung kaufen. Die schnellsten Server-CPUs, die Sie kaufen können, sind nur ein paarmal schneller als handelsübliche CPUs. Viele zusätzliche CPUs oder CPU-Kerne lassen langsame Abfragen auch nicht schneller laufen. Der Server stößt auch in Bezug auf seinen Speicher an Grenzen, wenn die Daten zu groß werden, um sie effektiv im Cache abzulegen. Das äußert sich normalerweise in einer intensiven Festplattenbenutzung. Und die Festplatten sind schließlich die langsamsten Teile moderner Computer. 1 Große Mengen Speicher stellen kein großes Problem dar, solange Sie ein 64-Bit-Betriebssystem und entsprechende Hardware benutzen. Es gibt immer noch einige Einschränkungen, aber sie sind nicht so streng und offensichtlich. Wir haben die Speicherausnutzung ausführlich in Kapitel 7 besprochen.
MySQL skalieren | 451
Auch die Skalierbarkeit der Anwendung stellt oft ein Problem dar. Anwendungsspezifische Designentscheidungen oder Beschränkungen, die durch die Arbeitsbelastung auferlegt werden, können sich negativ darauf auswirken, wie viel Hardware Sie effektiv benutzen können. Aus diesen Gründen empfehlen wir Ihnen, nicht vertikal zu skalieren oder zumindest Ihr System nicht unendlich zu vergrößern. Wenn Sie wissen, dass Ihre Anwendung sehr groß werden wird, dann ist es sicher in Ordnung, einen leistungsfähigeren Server zu kaufen, während Sie an einer anderen Lösung arbeiten. Im Allgemeinen jedoch müssen Sie schließlich horizontal skalieren, was uns zu unserem nächsten Thema bringt.
Horizontal skalieren Die einfachste und am weitesten verbreitete Methode, um horizontal zu skalieren, besteht darin, Ihre Daten mittels Replikation über mehrere Server zu verteilen und dann die Slaves für die Leseabfragen zu nutzen. Diese Technik kann gut für eine Anwendung eingesetzt werden, in der viel gelesen werden muss. Sie hat Nachteile, wie etwa die Cache-Doppelung, aber selbst die sind kein so ernstes Problem, wenn die Datengröße beschränkt ist. Wir haben im vorangegangenen Kapitel bereits über diese Probleme geschrieben und kommen später noch einmal darauf zurück. Bei der anderen verbreiteten Methode zum horizontalen Skalieren wird die Arbeitslast über mehrere »Knoten« verteilt, also partitioniert. Wie genau Sie diese Partitionierung vornehmen, ist eine schwierige Entscheidung. Denken Sie an unser »Traumsystem« zurück, das automatisch unsichtbar und unendlich skaliert – das ist nicht das, was Leute normalerweise mit MySQL machen. Die meisten großen MySQL-Anwendungen automatisieren die Partitionierung nicht, zumindest nicht vollständig. In diesem Abschnitt schauen wir uns einige der Möglichkeiten für die Partitionierung an und untersuchen ihre Stärken und Schwächen. Ein Knoten ist die funktionale Einheit in Ihrer MySQL-Architektur. Wenn Sie bei der Planung nicht auf Redundanz und Hochverfügbarkeit achten, dann könnte ein Knoten einfach aus einem Server bestehen. Entwerfen Sie dagegen ein redundantes System mit Failover, dann sieht ein Knoten meist aus wie eine der folgenden Anordnungen: • eine Master-Master-Replikationstopologie mit einem aktiven Server und einem passiven Replikations-Slave • ein Master und viele Slaves • ein aktiver Server, der ein Distributed Replicated Block Device (DRBD) als Reserve nutzt • ein SAN-basierter »Cluster« In den meisten Fällen sollten alle Server innerhalb eines Knotens die gleichen Daten aufweisen. Wir nehmen gern die Master-Master-Replikationsarchitektur für Zwei-ServerAktiv-Passiv-Knoten. Mehr über diese Topologie erfahren Sie in »Master-Master im Aktiv-Passiv-Modus« auf Seite 397. 452 | Kapitel 9: Skalierung und Hochverfügbarkeit
Funktionelle Partitionierung Funktionelle Partitionierung – oder Trennung der Aufgaben – bedeutet, dass unterschiedliche Knoten unterschiedlichen Aufgaben zugewiesen werden. Wir haben bereits einige ähnliche Ansätze erwähnt; z.B. beschrieben wir im vorangegangenen Kapitel, wie man unterschiedliche Server für OLTP- und OLAP-Lasten entwirft. Bei der funktionellen Partitionierung wird diese Strategie normalerweise noch weiter getrieben, indem man einzelne Server oder Knoten verschiedenen Anwendungen zuordnet, so dass jeder nur die Daten enthält, die seine jeweilige Anwendung benötigt. Wir verwenden das Wort »Anwendung« hier in einem etwas weiteren Sinne: Wir meinen damit nicht ein einzelnes Computerprogramm, sondern eine Gruppe verwandter Programme, die leicht von anderen Programmen getrennt werden kann, die nichts mit ihr zu tun haben. Falls Sie z.B. eine Website mit getrennten Abschnitten haben, die keine Daten gemeinsam nutzen, können Sie eine Partitionierung anhand der funktionalen Bereiche der Website vornehmen. Oft findet man Portale, die unterschiedliche Bereiche miteinander verbinden; vom Portal aus können Sie den Nachrichtenabschnitt der Site, die Foren, den Support-Bereich, die Wissensdatenbank usw. durchsuchen. Die Daten für die einzelnen funktionellen Bereiche könnten sich auf einem entsprechend zugeordneten MySQLServer befinden. Abbildung 9-1 verdeutlicht diese Anordnung.
Clients
Webserver
Foren
News
Support
Abbildung 9-1: Ein Portal und Knoten, die funktionellen Bereichen zugeordnet wurden
Wenn die Anwendung sehr groß ist, kann jeder funktionelle Bereich seinen eigenen speziellen Webserver besitzen, allerdings ist das weniger verbreitet. Ein weiterer möglicher Ansatz zur funktionellen Partitionierung besteht darin, die Daten einer einzelnen Anwendung aufzusplitten, indem Sie Gruppen von Tabellen bestimmen, die Sie niemals durch ein Join miteinander verbinden. Wenn es notwendig wird, können Sie normalerweise einige solcher Joins entweder in der Anwendung oder mit FederatedTabellen vornehmen, falls sie nicht entscheidend für die Leistung sind. Es gibt für diesen
MySQL skalieren | 453
Ansatz einige Variationen, die allerdings die gemeinsame Eigenschaft besitzen, dass jede Datenart nur auf einem einzigen Knoten zu finden ist. Diese Methode zum Partitionieren von Daten ist nicht sehr verbreitet, da sie nur schwer effektiv durchzusetzen ist und keine Vorteile gegenüber den anderen Methoden bietet. Letztlich ist es dennoch nicht möglich, die funktionelle Partitionierung unendlich zu skalieren, da jeder funktionelle Bereich vertikal skaliert werden muss, wenn er an einen einzelnen MySQL-Knoten gebunden ist. Irgendwann wird eine der Anwendungen oder einer der funktionellen Bereiche zu stark anwachsen, wodurch Sie gezwungen sind, eine andere Strategie zu suchen. Und wenn Sie die funktionelle Partitionierung zu weit treiben, dann kann es schwierig sein, später auf ein besser skalierbares Design umzusteigen.
Datenzerlegung (Sharding) Das Daten-Sharding2 ist der am weitesten verbreitete und erfolgreichste Ansatz zum Skalieren der heutigen, sehr großen MySQL-Anwendungen. Sie zerlegen die Daten, indem Sie sie in kleinere Segmente unterteilen und auf unterschiedlichen Knoten speichern. Das Sharding funktioniert auch gut im Zusammenhang mit einer funktionellen Partitionierung. Die meisten zerlegten Systeme haben auch irgendwelche »globalen« Daten, die nicht zerlegt werden (z.B. Listen mit Städten). Diese globalen Daten werden üblicherweise auf einem einzelnen Knoten abgelegt, oft hinter einem Cache wie memcached. Im Prinzip zerlegen die meisten Anwendungen nur die Daten, für die das nötig ist – meist die Teile der Datenmenge, die sehr groß werden. Nehmen Sie an, Sie bauen einen Blogging-Dienst auf. Wenn Sie 10 Millionen Benutzer erwarten, müssen Sie die Benutzerregistrierungsinformationen noch nicht zerlegen, weil Sie immer noch alle Benutzer (oder zumindest die aktive Untermenge) komplett in den Speicher bekommen. Erwarten Sie dagegen 500 Millionen Benutzer, dann sollten Sie diese Daten wahrscheinlich zerlegen. Der von den Benutzern generierte Inhalt, wie etwa Nachrichten und Kommentare, muss vermutlich sowieso zerlegt werden, weil diese Datensätze viel größer sind und es sehr viel mehr von ihnen gibt. Große Anwendungen haben sicherlich mehrere verschiedene logische Datenmengen, die Sie unterschiedlich zerlegen können. Sie können sie auf unterschiedlichen Servergruppen speichern, müssen es aber nicht. Sie können die gleichen Daten auch auf verschiedene Arten zerlegen, je nachdem, wie Sie darauf zugreifen. Wir zeigen Ihnen später ein Beispiel für diesen Ansatz. Das Sharding unterscheidet sich drastisch von der Art, wie die meisten Anwendungen ursprünglich gestaltet sind. Es kann schwierig sein, eine Anwendung von einem monolithischen Datenspeicher in eine zerlegte Architektur zu überführen. Deshalb ist es viel einfacher, eine Anwendung mit einem zerlegten Datenspeicher von Anfang an gezielt aufzubauen, wenn Sie damit rechnen, dass Sie dies später brauchen werden. 2 Das Sharding wird auch als »Zersplitterung« und »Partitionierung« bezeichnet. Wir bleiben jedoch beim Begriff Sharding, um Verwirrung zu vermeiden. Google nennt es so, und wenn es gut genug für Google ist, dann ist es auch gut genug für uns.
454 | Kapitel 9: Skalierung und Hochverfügbarkeit
Die meisten Anwendungen, die das Sharding nicht von Anfang an einbauen, durchlaufen mehrere Phasen, wenn sie größer werden. Beispielsweise können Sie die Leseabfragen in Ihrem Blogging-Dienst mittels Replikation skalieren, bis das nicht mehr geht. Anschließend unterteilen Sie den Dienst in drei Teile: Benutzer, Nachrichten und Kommentare. Sie können diese auf unterschiedliche Server legen (funktionelle Partitionierung) und die Joins in der Anwendung durchführen. Abbildung 9-2 zeigt die Entwicklung von einem einzelnen Server zur funktionellen Partitionierung. Benutzer Nachrichten Kommentare
Einzelne Instanz
Benutzer Nachrichten Kommentare
Benutzer globale Daten
Master und Slaves
Nachrichten
Kommentare
Funktionelle Partitionierung
Abbildung 9-2: Von einer einzelnen Instanz zu einem funktionell partitionierten Datenspeicher
Schließlich können Sie die Nachrichten und Kommentare anhand der Benutzer-ID zerlegen und die Benutzerinformationen auf einen einzelnen Knoten legen. Wenn Sie für den globalen Knoten eine Master-Slave-Konfiguration vorhalten und die Master-MasterPaare für die zerlegten Knoten benutzen, könnte der endgültige Datenspeicher schließlich so aussehen wie in Abbildung 9-3. Benutzer globale Daten
Nachrichten
Kommentare
Zerlegter Datenspeicher
Abbildung 9-3: Ein Datenspeicher mit einem globalen Knoten und sechs Master-Master-Knoten
Falls Sie vorher wissen, dass Sie sehr hoch skalieren müssen und die Grenzen der funktionellen Partitionierung kennen, könnten Sie die Schritte in der Mitte überspringen und direkt von einem einzelnen Knoten zu einem zerlegten Datenspeicher übergehen.
MySQL skalieren | 455
Zerlegte Anwendungen haben oft eine Database Abstraction Library, die die Kommunikation zwischen der Anwendung und dem zerlegten Datenspeicher erleichtert. Solche Bibliotheken verbergen normalerweise das Sharding nicht komplett, da die Anwendung meist etwas über eine Abfrage weiß, der Datenspeicher jedoch nicht. Zu viel Abstraktion kann Ineffizienzen verursachen, wie etwa das Abfragen aller Knoten nach Daten, die sich auf einem einzigen Knoten befinden. Das ist einer der Gründe, weshalb die MySQL-NDB-Cluster-Storage-Engine für Webanwendungen gar nicht so gut geeignet ist: Sie verbirgt die Tatsache, dass sie viele Knoten abfragen muss, und lässt es so aussehen, als gäbe es nur einen einzigen Server. Ein zerlegter Datenspeicher mag wie eine elegante Lösung aussehen, ist aber schwer aufzubauen. Weshalb sollte man sich dann für diese Architektur entscheiden? Die Antwort ist einfach: Wenn Sie Ihre Schreibkapazität skalieren wollen, müssen Sie Ihre Daten partitionieren. Sie können die Schreibkapazität nicht skalieren, wenn Sie nur einen einzigen Master haben, egal wie viele Slaves es gibt. Sharding stellt, bei allen Nachteilen, unsere bevorzugte Lösung für dieses Problem dar. Eine vollständig automatisierte, leistungsstarke, transparente Methode, um die Daten zu partitionieren und es so aussehen zu lassen, als würden sie sich alle auf einem einzigen Server befinden, wäre wunderbar, existiert aber noch nicht. In Zukunft könnte die MySQL-NDB-Cluster-Storage-Engine schnell und robust genug sein, um für diesen Zweck eingesetzt zu werden.
Einen Partitionierungsschlüssel wählen Die wichtigste Herausforderung beim Sharding ist das Suchen und Abrufen der Daten. Wie Sie die Daten finden, hängt davon ab, wie Sie sie zerlegen. Es gibt dafür viele Methoden, von denen einige besser sind als andere. Das Ziel besteht darin, Ihre wichtigsten und am häufigsten auftretenden Abfragen so wenig wie möglich zu zerlegen. Der wichtigste Teil dieses Vorgangs ist die Wahl eines (oder mehrerer) Partitionierungsschlüssel(s) für Ihre Daten. Der Partitionierungsschlüssel legt fest, welche Zeilen in welchen zerlegten Teil kommen sollen. Wenn Sie den Partitionierungsschlüssel eines Objekts kennen, können Sie zwei Fragen beantworten: • Wo muss ich diese Daten ablegen? • Wo kann ich die Daten finden, die ich holen muss? Wir zeigen Ihnen später verschiedene Möglichkeiten, um einen Partitionierungsschlüssel zu wählen und zu benutzen. Jetzt wollen wir uns erst einmal ein Beispiel anschauen. Nehmen Sie an, wir machen es so wie der NDB Cluster von MySQL und benutzen einen Hash des Primärschlüssels der einzelnen Tabellen, um die Daten über alle Speicher zu verteilen. Dieser Ansatz ist sehr einfach, skaliert aber nicht besonders gut, weil es oft erforderlich ist, in allen Datenspeichern nach den gewünschten Daten zu suchen. Wo können Sie z.B. die Blog-Nachrichten von Benutzer 3 finden? Wahrscheinlich sind sie
456 | Kapitel 9: Skalierung und Hochverfügbarkeit
gleichmäßig über alle Speicher verstreut, weil sie nach dem Primärschlüssel und nicht nach dem Benutzer partitioniert wurden. Abfragen über alle Shards sind schlechter als Abfragen in einem Shard, allerdings nicht zu schlimm, solange Sie nicht zu viele Shards anfassen müssen. Der schlimmste Fall ist, wenn Sie überhaupt keine Idee haben, wo sich die gewünschten Daten befinden, und jedes Shard durchsuchen müssen, um sie zu finden. Ein guter Partitionierungsschlüssel ist normalerweise die ID einer sehr wichtigen Entity in der Datenbank. Diese IDs identifizieren die Einheit des Sharding. Falls Sie Ihre Daten z.B. nach der Benutzer-ID oder einer Client-ID zerlegen, dann ist die Sharding-Einheit der Benutzer oder der Client. Ein guter Ausgangspunkt ist es, wenn Sie Ihr Datenmodell mit einem Entity-Relationship-Diagramm oder einem äquivalenten Werkzeug aufzeichnen, das alle Entities und deren Beziehungen zeigt. Versuchen Sie, das Diagramm so anzulegen, dass die verwandten Entities dicht beieinander liegen. Oft findet man durch bloßes Anschauen des Diagramms Kandidaten für Partitionierungsschlüssel, die man ansonsten nicht sehen würde. Schauen Sie sich jedoch nicht nur das Diagramm an, sondern betrachten Sie auch die Abfragen Ihrer Anwendungen. Selbst wenn zwei Entities in irgendeiner Weise verwandt sind, können Sie die Beziehung unterbrechen, um das Sharding zu implementieren, falls Sie sie in dieser Beziehung selten oder nie zusammenführen. Manche Datenmodelle lassen sich, je nach dem Grad der Konnektivität im Entity-Relationship-Diagramm, einfacher zerlegen als andere. Abbildung 9-4 zeigt ein leicht zu zerlegendes Datenmodell auf der linken und ein schwieriger zu zerlegendes auf der rechten Seite.3
= Partitionierungsschlüssel
Abbildung 9-4: Zwei Datenmodelle: Eines ist leicht und das andere schwer zu zerlegen.3
3 Danke an das HiveDB-Projekt und Britt Crawford für diese eleganten Diagramme.
MySQL skalieren | 457
Das linke Datenmodell lässt sich einfach zerlegen, weil es viele verbundene Untergraphen enthält, die hauptsächlich aus Knoten mit nur einer Verbindung bestehen. Sie können die Verbindungen zwischen den Untergraphen relativ leicht »durchschneiden«. Das Modell auf der rechten Seite lässt sich dagegen nur schwer zerlegen, weil es solche Untergraphen dort nicht gibt. Die meisten Datenmodelle sehen eher so aus wie das links dargestellte Diagramm. Mehrere Partitionierungsschlüssel: Komplizierte Datenmodelle erschweren das Daten-Sharding. Viele Anwendungen besitzen mehr als einen Partitionierungsschlüssel, vor allem wenn es zwei oder mehr wichtige »Dimensionen« in den Daten gibt. Mit anderen Worten: Die Anwendung muss möglicherweise eine effiziente, schlüssige Sicht der Daten aus unterschiedlichen Blickwinkeln sehen. Das bedeutet, dass Sie unter Umständen wenigstens einige der Daten doppelt im System speichern müssen. So müssen Sie z.B. vielleicht die Daten Ihrer Blogging-Anwendung sowohl nach der Benutzer-ID als auch nach der Nachrichten-ID zerlegen, da dies zwei gebräuchliche Möglichkeiten darstellt, wie die Anwendung die Daten betrachtet. Stellen Sie es sich so vor: Sie wollen häufig alle Nachrichten eines Benutzers sowie alle Kommentare für eine Nachricht sehen. Das Zerlegen nach dem Benutzer hilft Ihnen jedoch nicht dabei, die Kommentare für eine Nachricht zu finden. Das Zerlegen nach der Nachricht wiederum nützt Ihnen nichts, um die Nachrichten für einen Benutzer zu finden. Falls beide Arten von Abfragen nur ein einziges Shard anfassen müssen, müssen Sie auf beide Arten zerlegen. Nur weil Sie mehrere Partitionierungsschlüssel benötigen, müssen Sie nicht gleich zwei völlig redundante Datenspeicher entwerfen. Schauen wir uns ein anderes Beispiel an: die Website eines Social-Networking-Buchclubs, auf der die Benutzer der Site Kommentare zu den Büchern hinterlassen können. Die Website kann alle Kommentare für ein Buch anzeigen sowie die Bücher, die ein Benutzer gelesen und kommentiert hat. Sie könnten einen zerlegten Datenspeicher für die Benutzerdaten und einen weiteren für die Buchdaten anlegen. Kommentare besitzen sowohl eine Benutzer-ID als auch eine Nachrichten-ID, so dass sie die Grenzen zwischen den Shards überschreiten. Anstatt die Kommentare vollständig zu duplizieren, können Sie sie zusammen mit den Benutzerdaten ablegen. Mit den Buchdaten müssen Sie dann nur die Überschrift eines Kommentars und seine ID speichern. Das sollte reichen, um die meisten Sichten der Kommentare eines Buchs darzustellen, ohne auf beide Datenspeicher zuzugreifen. Falls Sie den kompletten Kommentartext brauchen, holen Sie ihn aus dem Benutzerdatenspeicher.
Abfragen in mehreren Shards Die meisten zerlegten Anwendungen enthalten wenigstens einige Abfragen, die Daten aus mehreren Shards sammeln oder zusammenführen müssen. Falls z.B. die BuchclubSite die beliebtesten oder aktivsten Benutzer anzeigt, muss sie laut Definition auf jedes Shard zugreifen. Solche Abfragen so zu gestalten, dass sie gut funktionieren, ist der schwierigste Teil der Implementierung des Daten-Sharding, weil das, was die Anwendung als eine einzige Abfrage ansieht, aufgeteilt und parallel in so vielen Anwendungen 458 | Kapitel 9: Skalierung und Hochverfügbarkeit
ausgeführt werden muss, wie es Shards gibt. Eine gute Datenbankabstraktionsebene kann dabei helfen, aber selbst dann sind solche Abfragen noch viel langsamer und teurer als Abfragen innerhalb von Shards, so dass normalerweise außerdem ein aggressives Caching erforderlich ist. Manche Sprachen, wie etwa PHP, bieten keine gute Unterstützung für die parallele Ausführung mehrerer Abfragen. Um dennoch zurechtzukommen, kann man eine HelperAnwendung schreiben, etwa in C oder Java, um die Abfragen auszuführen und die Ergebnisse zu sammeln. Die PHP-Anwendung fragt dann die Helper-Anwendung ab, bei der es sich oft um einen Webservice handelt. Shard-übergreifende Abfragen profitieren von Summary-Tabellen. Sie können diese erzeugen, indem Sie alle Shards durchlaufen und die Ergebnisse nach Abschluss redundant in jedem Shard speichern. Wenn das Duplizieren der Daten in jedem Shard zu viel Platz verschwenden würde, können Sie die Summary-Tabellen in einem weiteren Datenspeicher zusammenfügen, so dass sie nur einmal vorhanden sind. Nicht zerlegte Daten befinden sich oft im »globalen« Knoten, wobei starkes Caching sie von der Last abschirmt. Manche Anwendungen nutzen hauptsächlich zufälliges Sharding, wenn eine absolut gleichmäßige Datenverteilung entscheidend ist oder wenn es keinen guten Partitionierungsschlüssel gibt. Ein gutes Beispiel dafür ist eine verteilte Suchanwendung. In diesem Fall sind Shard-übergreifende Abfragen und Datensammlung die Norm und nicht die Ausnahme. Nicht nur das Shard-übergreifende Abfragen ist mit Sharding schwieriger, sondern auch das Wahren der Datenkonsistenz. Fremdschlüssel funktionieren mit Shards nicht, so dass die normale Lösung darin besteht, bei Bedarf in der Anwendung nachzuschauen. Man kann XA-Transaktionen einsetzen, in der Praxis ist das allerdings aufgrund des großen Aufwands unüblich. Mehr dazu finden Sie in »Verteilte (XA-) Transaktionen« auf Seite 283. Sie können auch Aufräumprozesse entwerfen, die periodisch ausgeführt werden. Falls z.B. der Buchclubzugang eines Benutzers ausläuft, müssen Sie ihn nicht sofort löschen. Weisen Sie einen regelmäßig durchgeführten Job an, die Kommentare des Benutzers aus dem Buch-Shard zu löschen. Außerdem können Sie ein Prüfskript schreiben, das regelmäßig läuft und die Konsistenz der Daten in den Shards sicherstellt.
Daten, Shards und Knoten zuweisen Shards und Knoten stehen nicht in einer Eins-zu-eins-Beziehung zueinander. Oft ist es gar keine schlechte Idee, ein Shard viel kleiner zu machen, als es die Kapazität eines Knotens erlaubt, damit Sie mehrere Shards auf einem einzigen Knoten speichern können. Indem die einzelnen Shards klein gehalten werden, bleiben die Daten handhabbar. Datenbank-Backups und -Wiederherstellungen werden erleichtert. Sind die Tabellen klein, können auch leicht Schemaänderungen vorgenommen werden. Nehmen Sie z.B. an, Sie haben eine 100 GByte große Tabelle, die Sie entweder so speichern können, wie sie ist, oder in 100 Shards aus 1 GByte großen Tabellen aufteilen können, die Sie auf einem einzigen Knoten speichern. Nehmen Sie nun noch an, Sie wollen zu der bzw. den
MySQL skalieren | 459
Tabelle(n) einen Index hinzufügen. Auf einer 100-GByte-Tabelle würde das viel länger dauern als kombiniert in allen 1-GByte-Shards, weil die 1-GByte-Teile komplett in den Speicher passen. Möglicherweise wollen Sie außerdem noch, dass die Daten während der Ausführung von ALTER TABLE nicht erreichbar sind – und es ist besser, 1 GByte an Daten zu blockieren als 100 GByte. Kleinere Shards lassen sich darüber hinaus leichter verschieben. Dadurch können Kapazitäten leicht neu zugewiesen und zwischen den Knoten ausgeglichen werden. Das Verschieben eines Shards ist im Allgemeinen kein effizienter Vorgang. Typischerweise müssen Sie das betroffene Shard in den schreibgeschützten Modus bringen (eine Eigenschaft, die Sie in Ihre Anwendung einbauen müssen), die Daten extrahieren und auf einen anderen Knoten verschieben. Dazu muss in der Regel mysqldump die Daten exportieren, und mysql muss sie dann neu laden. (Wenn Sie MyISAM benutzen, können Sie die Dateien einfach kopieren; mehr dazu folgt in Kapitel 11.) Neben dem Verschieben von Shards zwischen Knoten müssen Sie möglicherweise auch das Verschieben von Daten zwischen Shards in Betracht ziehen, vorzugsweise, ohne den Dienst für die gesamte Anwendung zu unterbrechen. Wenn Ihre Shards groß sind, ist es schwieriger, die Kapazität auszugleichen, indem Sie ganze Shards verschieben, so dass Sie wahrscheinlich eine Methode benötigen, mit der Sie einzelne Datenbits (z.B. einen einzelnen Benutzer) zwischen den Shards bewegen. Das Verschieben von Daten zwischen Shards ist normalerweise viel komplizierter als das Verschieben der Shards, so dass Sie dies nach Möglichkeit vermeiden sollten. Aus diesem Grund empfehlen wir, die Größe der Shards in einem Bereich zu halten, der noch handhabbar ist. Die relative Größe Ihrer Shards hängt von den Anforderungen der Anwendung ab. Eine »handhabbare Größe« ist für uns im Normalfall eine Größe, bei der die Tabellen klein genug sind, um normale Wartungsarbeiten wie ALTER TABLE, CHECK TABLE oder OPTIMIZE TABLE in 5 oder 10 Minuten zu erledigen. Wenn Sie die Shards zu klein machen, erhalten Sie möglicherweise zu viele Tabellen, was zu Problemen mit dem Dateisystem oder den internen Strukturen von MySQL führen könnte. Mehr dazu finden Sie in »Der Tabellen-Cache« auf Seite 301. Kleine Shards erhöhen darüber hinaus eventuell die Zahl der erforderlichen Shard-übergreifenden Abfragen. Shards auf Knoten anordnen: Sie müssen entscheiden, wie Sie die Shards auf einem Knoten anordnen wollen. Hier sind einige gebräuchliche Methoden: • Benutzen Sie eine Datenbank pro Shard, und verwenden Sie für die Datenbank jedes Knotens den gleichen Namen. Diese Methode ist typisch, wenn Sie wollen, dass jedes Shard die Struktur der ursprünglichen Anwendung widerspiegelt. Sie kann gut funktionieren, wenn Sie viele Instanzen der Anwendung herstellen, die jeweils nur ein Shard kennen. • Platzieren Sie Tabellen aus mehreren Shards in eine Datenbank, und fügen Sie dem Namen der einzelnen Tabellen die Nummer des Shards hinzu (z.B. bookclub.comments_23). In dieser Konfiguration kann eine einzige Datenbank mehrere Shards aufnehmen.
460 | Kapitel 9: Skalierung und Hochverfügbarkeit
• Benutzen Sie eine Datenbank pro Shard, und fügen Sie alle Tabellen der Anwendung in die Datenbank ein. Nehmen Sie die Nummer des Shards in den Datenbanknamen auf, nicht jedoch in den Tabellennamen (d.h., die Tabellen könnten bookclub_23.comments, bookclub_23.users usw. heißen). Diese Methode ist üblich, wenn eine Anwendung mit einer einzigen Datenbank verbunden ist und in keiner ihrer Abfragen den Datenbanknamen angibt. Der Vorteil besteht darin, dass Sie die Abfragen nicht an die einzelnen Shards anpassen müssen. Außerdem wird für eine Anwendung, die nur eine Datenbank verwendet, der Übergang zu einer zerlegten Struktur erleichtert. • Benutzen Sie eine einzige Datenbank pro Shard, und fügen Sie die Nummer des Shards sowohl in den Datenbank- als auch in den Tabellennamen ein (d.h., der Tabellenname wäre bookclub_23.comments_23). Wenn Sie die Nummer des Shards in den Tabellennamen aufnehmen, brauchen Sie eine Methode, um die Shard-Nummer in die Abfrage-Templates einzubinden. Typischerweise benutzt man dazu besondere »magische« Platzhalterwerte in Abfragen, Formatierungsspezifikationen im sprintf( )-Stil, wie etwa %s, und Stringinterpolation mit Variablen. Hier ist eine Möglichkeit, um Abfrage-Templates in PHP zu erzeugen: $sql = "SELECT book_id, book_title FROM bookclub_%d.comments_%d... "; $res = mysql_query(sprintf($sql, $shardno, $shardno), $conn);
Oder mit Stringinterpolation: $sql = "SELECT book_id, book_title FROM bookclub_$shardno.comments_$shardno ..."; $res = mysql_query($sql, $conn);
Das lässt sich leicht in eine neue Anwendung einbauen, bei bestehenden Anwendungen ist es dagegen etwas schwieriger. Wenn wir neue Anwendungen erstellen und AbfrageTemplates kein Problem darstellen, dann benutzen wir gern eine Datenbank pro Shard, wobei die Nummer des Shards sowohl im Datenbank- als auch im Tabellennamen vorkommen soll. Die Komplexität für Jobs wie ALTER TABLE in Skripten erhöht sich zwar, es hat aber auch Vorteile: • Sie können ein Shard leicht mit mysqldump verschieben, wenn es vollständig in einer einzigen Datenbank enthalten ist. • Dass die Datenbank sich in einem Verzeichnis des Dateisystems befindet, erleichtert die Verwaltung der Dateien des Shards. • Wenn das Shard nicht mit anderen Shards vermischt ist, kann man leicht ihre Größe feststellen. • Die global eindeutigen Tabellennamen helfen, Fehler zu verhindern. Wenn die Tabellennamen überall gleich sind, kann man leicht versehentlich das falsche Shard abfragen, weil man mit dem falschen Knoten verbunden ist, oder die Daten eines Shards in die Tabellen eines anderen Shards importieren. Überlegen Sie, ob die Daten Ihrer Anwendung zum Sharding neigen. Möglicherweise hilft es Ihnen, wenn Sie bestimmte Shards »nahe« aneinander platzieren (auf demselben Ser-
MySQL skalieren | 461
ver, im selben Teilnetz, im selben Rechenzentrum oder auf demselben Switch), um die Ähnlichkeit in den Datenzugriffsmustern auszunutzen. Zum Beispiel könnten Sie nach dem Benutzer zerlegen und dann Benutzer aus demselben Land in Shards auf denselben Knoten ablegen. Wenn man eine bestehende Anwendung um die Unterstützung zum Sharding erweitert, erhält man oft ein Shard pro Knoten. Diese Vereinfachung erlaubt es Ihnen einzuschränken, wie stark die Abfragen der Anwendung geändert werden müssen. Das Sharding ist normalerweise eine ziemlich krasse Änderung für eine Anwendung, so dass man es sinnvollerweise vereinfachen sollte. Wenn Sie so zerlegen, dass jeder Knoten wie eine Miniaturkopie der Daten der gesamten Anwendung aussieht, müssen Sie vermutlich nicht die meisten der Abfragen ändern oder sich darum kümmern, wie Sie die Abfragen an den gewünschten Knoten leiten.
Feste Allozierung Es gibt zwei wesentliche Methoden, um die Daten zu den Shards zuzuweisen: die festen und die dynamischen Allozierungsstrategien. Beide verlangen eine Partitionierungsfunktion, die den Partitionierungsschlüssel einer Zeile als Eingabe entgegennimmt und das Shard zurückliefert, das die Zeile enthält.4 Bei der festen Allozierung wird eine Partitionierungsfunktion benutzt, die nur vom Wert des Partitionierungsschlüssels abhängt. Hash-Funktionen und Modulo sind gute Beispiele. Diese Funktionen bilden jeden Wert des Partitionierungsschlüssels auf eine begrenzte Anzahl von »Buckets« ab, die die Daten aufnehmen können. Nehmen Sie an, Sie haben 100 Buckets und möchten herausfinden, wo Sie Benutzer 111 hinlegen sollen. Wenn Sie den Modulo verwenden, dann ist die Antwort einfach: 111 Modulo 100 ist 11, Sie sollten den Benutzer also am besten in Shard 11 legen. Falls Sie andererseits die CRC32( )-Hash-Funktion benutzen, lautet die Antwort 81: mysql> SELECT CRC32(111) % 100; +------------------+ | CRC32(111) % 100 | +------------------+ | 81 | +------------------+
Die wichtigsten Vorteile einer festen Strategie sind Einfachheit und geringer Overhead. Außerdem können Sie sie fest in die Anwendung einbauen. Allerdings bringt eine feste Allozierungsstrategie auch Nachteile mit sich: • Wenn die Shards groß sind und es nur wenige von ihnen gibt, könnte es schwierig sein, die Last zwischen den Shards auszugleichen. 4 Wir benutzen »Funktion« hier im mathematischen Sinn, um eine Zuordnung von der Eingabe (Domain) auf die Ausgabe (Bereich) zu bezeichnen. Sie können eine solche Funktion auf vielerlei Weise erzeugen, z.B. mithilfe einer Lookup-Tabelle in Ihrer Datenbank.
462 | Kapitel 9: Skalierung und Hochverfügbarkeit
• Die feste Allozierung erlaubt es Ihnen nicht zu entscheiden, wo Sie die einzelnen Daten speichern, was für Anwendungen wichtig ist, die keine gleichförmige Last auf der Sharding-Einheit haben. Manche Daten sind viel aktiver als andere, und falls viele von ihnen in dasselbe Shard kommen, dann erlaubt es Ihnen eine feste Allozierungsstrategie nicht, die Belastung zu verringern, indem Sie einige von ihnen in ein anderes Shard verschieben. Das ist bei vielen kleinen Datenschnipseln in jedem Shard kein so großes Problem, weil dann das Gesetz der großen Zahlen zur Anwendung kommt und die Dinge wieder ausgleicht. • Es ist normalerweise schwieriger, das Sharding zu ändern, weil dabei vorhandene Daten neu zugewiesen werden müssen. Falls Sie z.B. nach einer Hash-Funktion Modulo 10 zerlegt haben, haben Sie 10 Shards. Wächst die Anwendung und werden die Shards zu groß, wollen Sie die Anzahl der Shards vielleicht auf 20 anheben. Dazu müssen neue Hash-Werte berechnet, viele Daten aktualisiert und Daten zwischen den Shards verschoben werden. Aufgrund dieser Beschränkungen bevorzugen wir für neue Anwendungen normalerweise eine dynamische Allozierung. Wenn Sie allerdings eine vorhandene Anwendung zerlegen, wird es Ihnen vermutlich leichter fallen, eine feste Allozierungsstrategie einzubauen, weil sie einfacher ist als eine dynamische Strategie. Manchmal setzen wir sogar für neue Projekte auf eine feste Allozierung. Ein Beispiel, bei dem das gut funktioniert hat, ist der BoardReader (http://www.boardreader.com), eine Forensuchmaschine, die einige der Autoren hergestellt haben. Diese Site indiziert eine sehr große Menge an Daten. Wir waren versucht, die Foren anhand eines Hashs der SiteID zu zerlegen. Dadurch wären alle Foren der Site in ein Shard gelangt, was gut für Abfragen gewesen wäre, die auf Daten aus vielen der Foren der Site zugreifen – wie z.B. die Abfrage, die die beliebtesten Foren einer Site sucht. Manche Sites jedoch haben Tausende von Foren mit Nachrichten, deren Anzahl in die Zehn- oder Hundertmillionen geht. Die Shards würden zu groß werden, um mit diesem Schema noch handhabbar zu bleiben, so dass wir uns dafür entschieden haben, stattdessen das Sharding anhand eines Hashs der Forums-ID vorzunehmen.
Dynamische Allozierung Die Alternative zur festen Allozierung ist eine dynamische Allozierung, die Sie separat, als eine Pro-Sharding-Einheit-Zuordnung speichern. Ein Beispiel ist eine zweispaltige Tabelle mit Benutzer-IDs und Shard-IDs: CREATE TABLE user_to_shard ( user_id INT NOT NULL, shard_id INT NOT NULL, PRIMARY KEY (user_id) );
Die Tabelle selbst ist die Partitionierungsfunktion. Wird ein Wert des Partitionierungsschlüssels (das ist die Benutzer-ID) vorgegeben, können Sie die Shard-ID finden. Wenn
MySQL skalieren | 463
die Zeile nicht existiert, können Sie das gewünschte Shard nehmen und zur Tabelle hinzufügen. Sie können sie auch später ändern – dadurch wird dies zu einer dynamischen Allozierungsstrategie. Die dynamische Allozierung verursacht zusätzlichen Aufwand für die Partitionierungsfunktion, weil sie einen Aufruf an eine externe Ressource erfordert, wie etwa einen Directory-Server (einen Datenspeicherknoten, der die Zuordnung speichert). Um effizient zu sein, benötigt eine solche Architektur oft weitere Ebenen. Zum Beispiel können Sie ein verteiltes Caching-System verwenden, um die Daten des Directory-Servers im Speicher abzulegen, weil diese sich in der Praxis gar nicht so sehr ändern. Der größte Vorteil der dynamischen Allozierung ist die genaue Kontrolle darüber, wo die Daten gespeichert werden. Dies macht es einfacher, die Daten gleichmäßig zu den Shards zuzuordnen, und bietet Ihnen die Flexibilität, Änderungen Rechnung zu tragen, die Sie nicht vorhersehen. Eine dynamische Zuordnung erlaubt es Ihnen darüber hinaus, mehrere Ebenen aus Sharding-Strategien auf die einfache Schlüssel-zu-Shard-Zuordnung aufzubauen. So können Sie z.B. eine duale Zuordnung erstellen, die jede Sharding-Einheit einer Gruppe (z.B. einer Gruppe von Benutzern im Buchclub) zuweist und dann die Gruppen nach Möglichkeit in einem Shard zusammenhält. Damit nutzen Sie die Vorteile der Sharding-Neigung und können Shard-übergreifende Abfragen vermeiden. Mit einer dynamischen Allozierungsstrategie können Sie unausgeglichene Shards haben. Das kann sich als nützlich erweisen, wenn Ihre Server nicht alle gleich leistungsfähig sind oder wenn Sie einige von ihnen für andere Zwecke, wie etwa archivierte Daten, einsetzen wollen. Falls Sie außerdem die Fähigkeit haben, Shards jederzeit neu auszugleichen, können Sie eine Eins-zu-eins-Zuordnung der Shards zu Knoten pflegen, ohne Kapazität zu verschwenden. Manche Leute bevorzugen die Einfachheit von einem Shard pro Knoten. (Aber denken Sie daran, dass es Vorteile bringt, die Shards klein zu halten.) Dynamische Allozierung und der clevere Einsatz der Sharding-Neigung können verhindern, dass die Shard-übergreifenden Abfragen bei einer Skalierung nach oben anwachsen. Stellen Sie sich eine Shard-übergreifende Abfrage in einem Datenspeicher mit vier Knoten vor. Bei einer festen Allozierung könnte es passieren, dass eine bestimmte Abfrage verlangt, alle Shards anzufassen. Eine dynamische Allozierung dagegen erlaubt es Ihnen, die gleiche Abfrage nur auf drei der Knoten auszuführen. Das scheint nicht viel anders zu sein, aber überlegen Sie einmal, was passiert, wenn Ihr Datenspeicher auf 400 Shards anwächst: Die feste Allozierung verlangt, dass 400 Shards abgefragt werden, während die dynamische Allozierung vermutlich weiterhin das Abfragen von nur drei Shards erfordert. Mit der dynamischen Allozierung können Sie Ihre Sharding-Strategie so komplex machen, wie Sie wollen; die feste Allozierung dagegen bietet Ihnen nicht so viele Möglichkeiten.
464 | Kapitel 9: Skalierung und Hochverfügbarkeit
Dynamische und feste Allozierung mischen: Sie können einen Mix aus fester und dynamischer Allozierung einsetzen, was oft hilfreich und manchmal erforderlich ist. Die dynamische Allozierung funktioniert gut, wenn die Verzeichniszuordnung nicht zu groß ist. Bei vielen Sharding-Einheiten funktioniert sie unter Umständen nicht so gut. Nehmen Sie zum Beispiel ein System, das dazu gedacht ist, Links zwischen Websites zu speichern. Solch eine Site muss viele Milliarden Zeilen speichern können. Der Partitionierungsschlüssel ist eine Kombination aus Quell- und Ziel-URLs. (Schon eine der beiden URLs kann Hunderte Millionen Links besitzen, so dass keine der beiden URLs von sich aus selektiv genug ist.) Es ist jedoch nicht möglich, alle Quell- und Ziel-URL-Kombinationen in der Zuordnungstabelle zu speichern, weil es sehr viele sind und jede URL eine Menge Speicherplatz benötigt. Eine Lösung besteht darin, die URLs zu verketten und sie in eine feste Anzahl von »Buckets« zu packen, die Sie dann dynamisch zu den Shards zuordnen können. Wenn die Anzahl der Buckets groß genug ist – z.B. eine Million –, werden Sie in der Lage sein, ziemlich viele von ihnen in jedes Shard zu legen. Sie profitieren also vom dynamischen Sharding, ohne dass Sie eine riesige Zuordnungstabelle brauchen.
Explizite Allozierung Eine dritte Allozierungsstrategie erlaubt es der Anwendung, das gewünschte Sharding für jede Zeile explizit zu wählen, wenn sie die Zeile erzeugt. Das ist mit vorhandenen Daten schwieriger zu erreichen, so dass diese Technik nicht unbedingt verbreitet ist, wenn eine Anwendung um Sharding erweitert wird. Manchmal kann sie jedoch ganz hilfreich sein. Der Grundgedanke ist es, die Shard-Nummer in die ID aufzunehmen, etwa vergleichbar zu der Technik, mit der sich duplizierte Schlüsselwerte in der Master-Master-Replikation vermeiden lassen. (Näheres finden Sie unter »In einer Master-Master-Replikation auf beide Master schreiben« auf Seite 432.) Nehmen Sie z.B. an, Ihre Anwendung möchte Benutzer 3 erzeugen und ihn zu Shard 11 zuweisen. Sie haben die 8 höchstwertigen Bits einer BIGINT-Spalte für die Shard-Nummer reserviert. Der resultierende ID-Wert lautet (11 << 56) + 3 oder 792633534417207299. Die Anwendung kann später ganz leicht die Benutzer-ID und die Shard-ID extrahieren. Hier ist ein Beispiel: mysql> SELECT (792633534417207299 >> 56) AS shard_id, -> 792633534417207299 & ~(11 << 56) AS user_id; +----------+---------+ | shard_id | user_id | +----------+---------+ | 11 | 3 | +----------+---------+
Nehmen Sie nun noch an, Sie wollen für diesen Benutzer einen Kommentar anlegen und im gleichen Shard speichern. Die Anwendung kann die Kommentar-ID 5 für den Benutzer zuweisen und den Wert 5 mit der Shard-ID 11 auf dieselbe Weise kombinieren.
MySQL skalieren | 465
Der Vorteil dieses Ansatzes besteht darin, dass die ID jedes Objekts ihren Partitionierungsschlüssel mit sich führt, während andere Ansätze normalerweise ein Join oder eine andere Suche erfordern, um den Partitionierungsschlüssel zu finden. Falls Sie einen bestimmten Kommentar aus der Datenbank holen wollen, müssen Sie wissen, zu welchem Benutzer er gehört; die ID des Objekts teilt Ihnen dann mit, wo Sie ihn finden. Wurde das Objekt dynamisch anhand der Benutzer-ID zerlegt, müssen Sie den Benutzer des Kommentars suchen und dann den Directory-Server fragen, in welchem Shard Sie weitersuchen sollen. Eine weitere Lösung wäre es, den Partitionierungsschlüssel zusammen mit dem Objekt in separaten Spalten zu speichern. Sie würden sich z.B. nie auf Kommentar 5 beziehen, sondern immer auf Kommentar 5, der zu Benutzer 3 gehört. Dieser Ansatz macht möglicherweise einige Leute glücklicher, weil er die erste Normalform nicht verletzt; allerdings verursacht die zusätzliche Spalte weitere Kodierung, Unannehmlichkeiten und Overhead. (Dies ist einer der Fälle, in denen wir es als vorteilhaft empfinden, zwei Werte in einer einzigen Spalte zu speichern.) Nachteilig an der expliziten Allozierung ist, dass das Sharding fest ist und dass es schwieriger ist, Shards auszugleichen. Andererseits funktioniert dieser Ansatz gut mit einer Kombination aus fester und dynamischer Allozierung. Anstatt die Hashs aus einer festen Anzahl von Buckets zu erzeugen und sie auf die Knoten abzubilden, kodieren Sie das Bucket als Teil jedes Objekts. Damit erhält die Anwendung die Kontrolle darüber, wo sich die Daten befinden, so dass sie verwandte Daten zusammen in das gleiche Shard packen kann. BoardReader benutzt eine Variante dieser Technik: Es kodiert den Partitionierungsschlüssel in der Sphinx-Dokument-ID. Auf diese Weise können die verwandten Daten der jeweiligen Suchergebnisse leicht in dem zerlegten Datenspeicher gefunden werden. In Anhang C erfahren Sie mehr über Sphinx. Wir haben die gemischte Allozierung beschrieben, weil wir Fälle kennengelernt haben, in denen sie sinnvoll ist. Normalerweise raten wir jedoch davon ab. Wir setzen nach Möglichkeit eine dynamische Allozierung ein und vermeiden eine explizite Allozierung.
Shards neu ausgleichen Falls es notwendig sein sollte, können Sie Daten auf andere Shards verschieben, um die Last neu auszugleichen. So haben z.B. bestimmt schon viele Leser gehört, wie Entwickler von großen Foto-Sharing-Sites oder beliebten Social-Networking-Sites über ihre Werkzeuge zum Verschieben von Benutzern auf andere Shards sprechen. Die Fähigkeit, Daten zwischen Shards zu verschieben, hat ihre Vorteile. Sie kann Ihnen z.B. helfen, Ihre Hardware aufzurüsten, da Sie Benutzer von dem alten Shard auf das neue Shard verschieben können, ohne das gesamte Shard abzuschalten oder es mit einem Schreibschutz zu versehen.
466 | Kapitel 9: Skalierung und Hochverfügbarkeit
Wir versuchen jedoch nach Möglichkeit, das Neuausgleichen von Shards zu vermeiden, weil dadurch der Service für die Benutzer unterbrochen werden könnte. Das Verschieben von Daten zwischen Shards erschwert darüber hinaus das Hinzufügen von Funktionen zur Anwendung, weil die neuen Funktionen unter Umständen ein Upgrade für das Ausgleichsskript enthalten müssen. Wenn Sie Ihre Shards klein genug halten, ist das wahrscheinlich nicht nötig; Sie können die Last häufig schon dadurch neu ausgleichen, dass Sie komplette Shards verschieben, was einfacher ist, als ein Shard nur teilweise zu bewegen (außerdem ist es in Bezug auf die Kosten pro Datenzeile effizienter). Eine Strategie, die gut funktioniert, ist das zufällige Zuweisen von neuen Daten zu Shards. Wenn ein Shard voll genug ist, können Sie ein Flag setzen, das die Anwendung anweist, diesem Shard keine neuen Daten mehr zu übergeben. Später nehmen Sie das Flag einfach wieder zurück, wenn dieses Shard doch mehr Daten erhalten soll. Nehmen Sie an, dass Sie einen neuen MySQL-Knoten installieren und 100 Shards darauf setzen. Zunächst setzen Sie deren Flags auf 1, damit die Anwendung weiß, dass sie zur Aufnahme neuer Daten bereit sind. Sobald sie jeweils genug Daten enthalten (z.B. jeweils 10.000 Benutzer), setzen Sie ihre Flags auf 0. Wenn der Knoten dann irgendwann nicht mehr ausgelastet ist, weil Accounts frei werden, können Sie einige der Shards wieder öffnen und auf ihnen neue Benutzer hinzufügen. Falls Sie die Anwendung aufrüsten und Funktionen hinzufügen, die die Abfragelast der einzelnen Shards erhöhen, oder falls Sie die Last falsch berechnet haben, können Sie einige der Shards auf neue Knoten verschieben, um die Last zu mildern. Nachteilig ist, dass ein ganzes Shard währenddessen schreibgeschützt oder offline sein könnte. Sie und Ihre Benutzer müssen in diesem Fall entscheiden, ob das akzeptabel ist.
Global eindeutige IDs generieren Wenn Sie ein System in einen zerlegten Datenspeicher umwandeln, müssen Sie oft auf vielen Maschinen global eindeutige IDs generieren. Ein monolithischer Datenspeicher setzt für diesen Zweck häufig AUTO_INCREMENT-Spalten ein, allerdings ist die AUTO_ INCREMENT-Funktion standardmäßig dafür gedacht, auf einem einzigen Server zu laufen, der leicht Eindeutigkeit garantieren kann. Es gibt verschiedene Möglichkeiten, dieses Problem zu lösen: Verwenden Sie auto_increment_increment und auto_increment_offset Diese beiden Servereinstellungen weisen MySQL an, AUTO_INCREMENT-Spalten um einen gewünschten Wert zu erhöhen und von einem vorgegebenen Offset aus eine Nummerierung vorzunehmen. Im einfachsten Fall mit zwei Servern können Sie z.B. die Server so konfigurieren, dass sie um zwei erhöhen, wobei Sie den Offset des einen Servers auf eins, den des anderen Servers auf zwei setzen (es ist nicht möglich, einen der Werte auf null zu setzen). Jetzt werden die Spalten des einen Servers immer gerade Zahlen enthalten und die des anderen ungerade. Die Einstellung gilt für alle Tabellen auf dem Server.
MySQL skalieren | 467
Diese Technik bildet aufgrund ihrer Einfachheit und ihrer Unabhängigkeit von einem zentralen Knoten eine beliebte Methode, um Werte zu generieren, verlangt allerdings eine gewisse Sorgfalt bei den Serverkonfigurationen. Es ist leicht, die Server versehentlich so zu konfigurieren, dass sie doppelte Nummern generieren, vor allem, wenn Sie ihnen nach dem Hinzufügen weiterer Server unterschiedliche Rollen zuweisen oder wenn Sie sie nach einem Ausfall wiederherstellen. Erzeugen Sie eine Tabelle im globalen Knoten Sie können in Ihrem globalen Datenbankknoten eine Tabelle mit einer AUTO_ INCREMENT-Spalte erzeugen, die die Anwendungen dann benutzen, um eindeutige Nummern zu generieren. Benutzen Sie memcached Es gibt eine incr( )-Funktion in der memcached-API, die eine Nummer atomar inkrementieren und das Ergebnis zurückliefern kann. Reservieren Sie die Zahlen schubweise Die Anwendung kann gleich einen ganzen »Stoß« Nummern von einem globalen Knoten anfordern, sie aufbrauchen und dann weitere anfordern. Benutzen Sie eine Kombination aus Werten Um die Werte der einzelnen Server eindeutig zu machen, können Sie eine Kombination aus Werten benutzen, wie etwa die Shard-ID und eine stets zunehmende Zahl. Lesen Sie die Beschreibung dieser Technik im vorangegangenen Abschnitt. Benutzen Sie zweispaltige AUTO_INCREMENT-Schlüssel Das funktioniert nur bei MyISAM-Tabellen: mysql> CREATE TABLE inc_test( -> a INT NOT NULL, -> b INT NOT NULL AUTO_INCREMENT, -> PRIMARY KEY(a, b) -> ) ENGINE=MyISAM; mysql> INSERT INTO inc_test(a) VALUES(1), (1), (2), (2); mysql> SELECT * FROM inc_test; +---+---+ | a | b | +---+---+ | 1 | 1 | | 1 | 2 | | 2 | 1 | | 2 | 2 | +---+---+
Benutzen Sie GUID-Werte Sie können mit der UUID( )-Funktion global eindeutige Werte erzeugen. Seien Sie jedoch vorsichtig: Diese Funktion wird nicht korrekt repliziert, obwohl sie gut funktioniert, wenn die Anwendung den Wert in ihren eigenen Speicher lädt und ihn dann als Literal in Anweisungen einsetzt. GUID-Werte sind groß und nichtsequenziell, so dass sie sich nicht gut als Primärschlüssel für InnoDB-Tabellen eignen (siehe »Mit InnoDB Zeilen in Primärschlüsselreihenfolge einfügen« auf Seite 125).
468 | Kapitel 9: Skalierung und Hochverfügbarkeit
Die MySQL-Entwickler haben eine UUID_SHORT( )-Funktion hergestellt, die kürzere, sequenzielle Werte zurückliefert, die sich besser als Primärschlüssel eignen. Möglicherweise enthalten künftige Versionen des MySQL-Servers diesen Code; bisher wurde er aber noch nicht veröffentlicht. Wenn Sie einen globalen Zuweiser zum Generieren von Werten verwenden, dann achten Sie darauf, dass dieser Schwachpunkt keinen Flaschenhals für Ihre Anwendung erzeugt. Der memcached-Ansatz kann zwar ziemlich schnell sein (Zehntausende von Werten pro Sekunde), er ist aber nicht persistent. Jedes Mal, wenn Sie den memcached-Dienst neu starten, müssen Sie den Wert im Cache initialisieren. Das könnte dazu führen, dass Sie den Maximalwert ermitteln müssen, der in allen Shards gilt, was sehr langsam sein könnte und einzeln schwer zu erreichen ist. Falls Sie eine Tabelle in MySQL benutzen, dann könnten Sie eine einzeilige MyISAMTabelle mit einer AUTO_INCREMENT-Spalte anlegen, auf die Sie aus Geschwindigkeitsgründen außerhalb einer Transaktion zugreifen. Sie können die Tabelle entweder wachsen lassen, wenn Sie Zeilen hinzufügen, oder sie auf eine Zeile beschränken, indem Sie REPLACE verwenden: CREATE TABLE single_row ( col1 int NOT NULL AUTO_INCREMENT, col2 int NOT NULL, PRIMARY KEY(col1), UNIQUE KEY(col2) ) ENGINE=MyISAM;
Mit dieser Tabelle generieren Sie dann die Werte: mysql> REPLACE INTO single_row(col2) VALUES(1);
Wenn diese Anweisung ausgeführt wurde, können Sie mit der mysql_insert_id( )-Funktion der MySQL-API den generierten Wert holen. Wie Sie das tun, hängt von der verwendeten Sprache ab; hier ist ein Beispiel in Perl: my $sth = $dbh->prepare('REPLACE INTO single_row(col2) VALUES(1)'); while ( my $item = @work_to_do ) { $sth->execute( ); my $id = $dbh->{mysql_insert_id}; # Hier wird die Arbeit erledigt... }
Sie dürfen keine andere Abfrage, wie z.B. SELECT LAST_INSERT_ID( ), einsetzen, um den Wert zu holen. Dies würde nämlich einen weiteren Serverkontakt erfordern, was das Ganze weniger effizient macht.
Werkzeuge für das Sharding Eine der ersten Aufgaben, die Sie erledigen müssen, wenn Sie eine zerlegte Anwendung entwerfen, ist das Schreiben von Code zum Abfragen mehrerer Datenquellen.
MySQL skalieren | 469
Im Allgemeinen gilt es als schlechtes Design, wenn man der Anwendung ohne irgendwelche Abstraktion mehrere Datenquellen präsentiert, weil sich dadurch die Komplexität des Codes deutlich erhöht. Besser ist es, die Datenquellen hinter einer Abstraktionsebene zu verbergen. Diese Ebene könnte die folgenden Aufgaben ausführen: • Herstellen der Verbindung zum richtigen Shard und Abfragen des Shards • Verteilte Konsistenzüberprüfungen • Sammeln der Ergebnisse von den Shards • Shard-übergreifende Joins • Locking- und Transaktionsverwaltung • Erzeugen von Shards (oder zumindest das sofortige Erkennen neuer Shards) und Ausgleichen von Shards, falls Sie es schaffen, das zu implementieren Sicher müssen Sie nicht eine von Grund auf neue Sharding-Infrastruktur aufbauen. Es gibt verschiedene Werkzeuge und Systeme, die entweder einen Teil der erforderlichen Funktionalität anbieten oder speziell dafür ausgelegt sind, eine zerlegte Architektur zu implementieren. Auf der einfachsten Ebene können Sie ein Werkzeug wie MySQL Proxy einsetzen, um einen Teil der Komplexität der mehrfachen Datenquellen zu abstrahieren. Je nachdem, wie sich MySQL Proxy künftig entwickelt, könnte es ein wesentlicher Bestandteil vieler zerlegter Datenspeicher werden. Eine bereits existierende Datenbankabstraktionsebene mit Sharding-Unterstützung ist Hibernate Shards (http://shards.hibernate.org), eine Erweiterung der Open-Source-ORM(Object-Relational Mapping-)Bibliothek Hibernate, die in Java geschrieben wurde. Google hat Hibernate Shards als eines seiner berühmten 20%-Projekte geschrieben und dann den Code der Gemeinschaft zur Verfügung gestellt. Es bietet Shard-fähige Implementierungen der Hibernate-Core-Schnittstellen, damit Anwendungen nicht unbedingt umgestaltet werden müssen, um einen zerlegten Datenspeicher benutzen zu können; sie müssen unter Umständen nicht wissen, dass sie einen verwenden. Hibernate Shards bietet eine gute Möglichkeit, um transparent Daten über viele Server hinweg zu speichern und anzufordern, enthält aber manche Funktionen nicht, wie etwa das Neuausgleichen von Shards und das Sammeln von Abfrageergebnissen. Es benutzt eine feste Allozierungsstrategie, um Daten zu den Shards zuzuweisen. Ein weiteres Sharding-System ist HiveDB (http://www.hivedb.org), ein Open-SourceFramework zum Sharding von MySQL, das versucht, die wesentlichen Ideen des Shardings auf eine klare und präzise Weise zu implementieren. HiveDB ist in Java geschrieben und wurde von Grund auf neu entwickelt, um einen zerlegten Datenspeicher zu erzeugen, zu benutzen und zu verwalten. Es besitzt einige Eigenschaften, die andere Systeme nicht unterstützen, wie etwa das Erzeugen von Shards und das Verschieben von Daten zwischen Shards (Ausgleichen). HiveDB benutzt eine dynamische Allozierung und bezeichnet das Sharding als »horizontale Partitionierung«. Sphinx ist eine Volltextsuchmaschine, kein zerlegtes Datenspeicher- und -abfragesystem, eignet sich aber dennoch für einige Arten von Abfragen über zerlegte Datenspeicher. Es
470 | Kapitel 9: Skalierung und Hochverfügbarkeit
kann parallel entfernte Systeme abfragen und die Ergebnisse sammeln, was bei einem zerlegten Datenspeicher zu den schwierigeren Dingen gehört. (Mehr über Sphinx erfahren Sie in Anhang C.)
Den Datenumfang verringern Eine der einfacheren Methoden für den Umgang mit einer wachsenden Datengröße und Belastung ist die Archivierung und Entfernung nicht benötigter Daten. Je nach Ihrer Belastung werden Sie einen deutlichen Nutzen aus der Archivierung der Daten ziehen, die Sie nicht brauchen. Dadurch wird natürlich nicht der Bedarf an einer horizontalen Skalierung ersetzt, es kann aber Teil einer kurzfristigen Strategie sein, mit der Sie Zeit gewinnen können, und sollte wahrscheinlich Teil einer langfristigen Strategie sein, um mit großen Datenmengen zurechtzukommen. Folgende Dinge müssen Sie beachten, wenn Sie Archivierungs- und Säuberungsstrategien entwerfen: Einfluss auf die Anwendung Eine gut gestaltete Archivierungsstrategie kann Daten von einem stark belasteten OLTP-Server entfernen, ohne dass die Transaktionsverarbeitung merklich beeinträchtigt wird. Der Trick daran ist, effizient die Zeilen zu finden, die entfernt werden müssen, und sie in kleinen Gruppen zu entfernen. Normalerweise müssen Sie die Anzahl der Zeilen, die Sie auf einmal archivieren, mit der Größe der Transaktion ausgleichen, um einen guten Kompromiss zwischen dem Wettstreit um die Sperren und dem transaktionalen Aufwand zu finden. Gestalten Sie Ihre Archivierungsjobs so, dass sie notfalls der Transaktionsverarbeitung weichen. Welche Zeilen sollen archiviert werden? Sie können Daten säubern oder archivieren, sobald Sie wissen, dass Sie sich nie wieder auf sie beziehen werden. Sie können Ihre Anwendung aber auch so gestalten, dass sie Daten archiviert, auf die selten zugegriffen wird. Sie haben die Möglichkeit, die archivierten Daten gleich neben den Kerntabellen zu speichern und auf sie über Sichten zuzugreifen oder sie sogar ganz auf einen anderen Server zu verschieben. Zuerst Tiefe oder zuerst Breite? Die Beziehungen der Daten untereinander verkomplizieren das Archivieren und Säubern. Ein gut gestalteter Archivierungsjob hält die Daten logisch konsistent oder zumindest so konsistent, wie die Anwendung verlangt, ohne dass mehrere Tabellen an riesigen Transaktionen beteiligt sind. Wenn es Beziehungen zwischen den Tabellen gibt, ist es immer eine Herausforderung zu entscheiden, welche Tabellen zuerst archiviert werden müssen. Sie müssen bei der Archivierung den Einfluss »verwaister« oder »verwitweter« Zeilen beachten. Meist geht es darum, zu entscheiden, ob Fremdschlüssel verletzt werden müssen (Sie können InnoDB-Fremdschlüsselbeschränkungen mit SET FOREIGN_KEY_CHECKS=0 deaktivieren) oder ob man Zeiger zeitweise einfach so »herumhängen« lässt. Welches Vorgehen zu bevorzugen ist, hängt davon ab, wie Ihre Anwendung die Daten
MySQL skalieren | 471
ansieht. Betrachtet die Anwendung eine bestimmte Gruppe verwandter Tabellen von oben nach unten, sollten Sie sie wahrscheinlich in der gleichen Reihenfolge archivieren. Falls z.B. Ihre Anwendung immer die Bestellungen vor den Rechnungen untersucht, dann archivieren Sie die Bestellungen zuerst; Ihre Anwendung sollte die verwaisten Rechnungen nicht sehen, und Sie können sie als Nächstes archivieren. Datenverlust vermeiden Falls Sie zwischen Servern archivieren, sollten Sie besser keine verteilten Transaktionen durchführen; außerdem archivieren Sie ja vielleicht in MyISAM oder eine andere nicht transaktionsfähige Storage-Engine. Um daher einen Datenverlust zu vermeiden, nehmen Sie das Einfügen in das Ziel vor, bevor Sie aus der Quelle löschen. Es ist darüber hinaus keine schlechte Idee, archivierte Daten während des Vorgangs in eine Datei zu schreiben. Gestalten Sie Ihre Archivierungsjobs so, dass Sie sie nach Belieben beenden und neu starten können, ohne Inkonsistenzen oder Indexbeschädigungen zu verursachen. Aus dem Archiv entfernen (Dearchivieren) Sie bekommen viel mehr Daten unter, wenn Sie mit einer Dearchivierungsstrategie archivieren. Diese erlaubt es Ihnen, Daten zu archivieren, von denen Sie nicht wissen, ob Sie sie brauchen, und Sie können sich die Möglichkeit offen lassen, die Daten später wieder zurückzuholen. Falls Sie Einstiegspunkte festlegen können, an denen Ihr System überprüfen kann, ob es archivierte Daten wieder zurückholen muss, lässt sich eine solche Strategie relativ leicht implementieren. Wenn Sie z.B. möglicherweise inaktive Benutzer archivieren, dann ist der Einstiegspunkt der Login-Vorgang. Wenn ein Login fehlschlägt, weil es einen solchen Benutzer nicht gibt, können Sie das Archiv überprüfen und nachschauen, ob der Benutzer dort existiert, den Benutzer aus dem Archiv holen und das Login verarbeiten. Maatkit enthält ein Werkzeug, das Ihnen bei der effizienten Archivierung und/oder Säuberung von MySQL-Tabellen hilft. Es bietet allerdings keine Unterstützung für das Dearchivieren.
Aktive Daten getrennt halten Selbst wenn Sie veraltete Daten nicht auf einen anderen Server verschieben müssen, profitieren viele Anwendungen von einer Trennung der aktiven und inaktiven Datensätze. Die Effizienz des Caches erhöht sich, und Sie haben die Möglichkeit, unterschiedliche Arten von Hardware oder Anwendungsarchitekturen für die aktiven und inaktiven Daten zu benutzen. Hier sind einige Methoden, mit denen Sie dies erreichen können: Tabellen in mehrere Teile aufspalten Oft ist es eine kluge Entscheidung, Tabellen aufzuspalten, vor allem, wenn die ganze Tabelle nicht in den Speicher passt. Sie können z.B. die Tabelle users in active_users und inactive_users aufteilen. Vielleicht glauben Sie, dass das nicht notwendig ist, weil die Datenbank »heiße« Daten sowieso im Cache ablegt, aber das
472 | Kapitel 9: Skalierung und Hochverfügbarkeit
hängt von Ihrer Storage-Engine ab. Wenn Sie InnoDB benutzen, funktioniert das Caching für eine Seite auf einmal. Falls 100 Benutzer auf eine Seite passen und nur 10 % Ihrer Benutzer aktiv sind, macht das aus der Sicht von InnoDB wahrscheinlich jede Seite »heiß« – dennoch sind 90 % jeder »heißen« Seite verschwendet. Durch das Aufspalten der Tabelle in zwei Teile könnte sich die Speicherauslastung drastisch verbessern. Die Storage-Engine Falcon setzt auf zeilenbasiertes Caching, was den Cache noch effizienter machen sollte. Das bedeutet jedoch nicht, dass Falcon-Tabellen nicht von einer Aktiv/inaktiv-Aufteilung profitieren. Falcon legt seine Indizes seitenweise im Cache ab; eine Mischung aus aktiven und inaktiven Daten macht deshalb den Index-Cache weniger effizient. MySQL-Partitionierung MySQL 5.1 bietet selbst schon partitionierte Tabellen, was dabei hilft, die neuesten Daten im Speicher zu halten. In »Merge-Tabellen und Partitionierung« auf Seite 273 erfahren Sie mehr über die Partitionierung. Zeitbasierte Datenpartitionierung Wenn Ihre Anwendung kontinuierlich neue Daten erhält, ist es sehr wahrscheinlich, dass die neuesten Daten viel aktiver sind als die älteren Daten. Wir kennen z.B. einen Blog-Dienst, dessen Datenaufkommen hauptsächlich aus Nachrichten und Kommentaren stammt, die in den jeweils letzten sieben Tagen erzeugt wurden. Die meisten seiner Aktualisierungen erfolgen an diesen Datensätzen. Daraus folgt, dass diese Daten komplett im Speicher gehalten werden und die Replikation dafür sorgt, dass eine wiederherstellbare Kopie auf der Platte existiert, die bei einem Ausfall eingesetzt werden kann. Die restlichen Daten leben dagegen an einer anderen Stelle fort. Wir haben darüber hinaus Designs gesehen, die die Daten der einzelnen Benutzer in Shards auf zwei Knoten speichern. Neue Daten kommen auf den »aktiven« Knoten, der viel Speicher und schnelle Festplatten besitzt. Diese Daten werden für einen sehr schnellen Zugriff optimiert. Der andere Knoten speichert die älteren Daten und besitzt sehr große (aber langsamere) Festplatten. Die Anwendung geht davon aus, dass sie die älteren Daten wahrscheinlich nicht benötigt. Das ist für viele Anwendungen eine gute Annahme, die wahrscheinlich mehr als 90 % der Anforderungen aus lediglich den neuesten 10 % Daten befriedigen können. Diese Sharding-Regelung lässt sich leicht mit dynamischem Sharding umsetzen. Die Tabellendefinition Ihres Sharding-Verzeichnisses könnte z.B. so aussehen: CREATE TABLE users ( user_id int unsigned not null, shard_new int unsigned not null, shard_archive int unsigned not null, archive_timestamp timestamp, PRIMARY KEY (user_id) );
MySQL skalieren | 473
Ein Archivierungsskript kann ältere Daten vom aktiven Knoten auf den Archivknoten verschieben, wobei die archive_timestamp-Spalte aktualisiert wird, wenn die Daten eines Benutzers auf den Archivknoten verschoben werden. Die shard_newund shard_archive-Spalten teilen Ihnen mit, welche Shard-Nummern die Daten enthalten.
Mit Clustern skalieren Clustering stellt eine weitere Methode zur Skalierung dar. Dabei wird die Last über viele Server verteilt. Der Begriff »Clustering« ist im Bereich der Computer mit verschiedenen Bedeutungen überfrachtet, im Allgemeinen jedoch besteht ein System mit Clustern aus mehreren Hosts in einem lokalen Netzwerk, die so konfiguriert sind, dass sie wie ein einziger Server auftreten. Eine Variante des Clusterings ist Federation – d.h., auf entfernte Server wird so zugegriffen, als wären sie lokal, wodurch ein gigantischer »virtueller Server« erzeugt wird, der als Proxy für viele Server auftritt.
Clustering Die MySQL-Storage-Engine NDB Cluster ist eine verteilte, im Speicher vorgehaltene Shared-Nothing-Storage-Engine mit synchroner Replikation und einer automatischen Datenpartitionierung über die Knoten. Ihr Leistungsprofil unterscheidet sich völlig von dem anderer MySQL-Storage-Engines, und sie funktioniert am besten mit spezialisierter Hardware. Obwohl es sich für einige Anwendungen um eine sehr leistungsstarke Methode handelt, die Daten zu speichern, ist es für die meisten Webanwendungen keine besonders gute Lösung, um High Performance zu erzielen. NDB Cluster eignet sich gut für Anwendungen mit relativ wenigen Daten und einfachen Abfragen. Gute Einsatzfälle sind z.B. die Speicherung von Website-Sessions, Metadaten usw. Mit komplexen Abfragen und Joins kommt sie nicht besonders gut zurecht. Im Prinzip verlangt jede Abfrage, die keine Suche in einem einzigen Tabellenindex ist, eine Kommunikation über Knotengrenzen hinweg und ist daher langsam. NDB Cluster ist ein transaktionsfähiges System, bietet jedoch keine MVCC-Unterstützung. Beim Lesen werden Sperren gesetzt. Es enthält außerdem keine Deadlock-Erkennung. Ein auftretender Deadlock wird von NDB mit einem Timeout aufgelöst. Die Kombination aus sperrenden Leseoperationen und Timeout-basierter Deadlock-Auflösung bedeutet, dass es sich nicht gut für interaktive Mehrbenutzeranwendungen oder Webanwendungen eignet. Man kann eine Vielzahl von Clustering-Lösungen auf, vor oder unterhalb von MySQL implementieren. Ein Beispiel ist Continuent (http://www.continuent.com),5 das eine synchrone Replikation, Lastausgleich und Failover für MySQL über eine MiddlewareSchicht bietet. 5 Oder das Open-Source-Angebot seiner Schöpfer, Sequoia, verfügbar unter http://sequoia.continuent.org.
474 | Kapitel 9: Skalierung und Hochverfügbarkeit
Federation Federation ist ein weiterer Begriff mit vielen Bedeutungen. In der Datenbankwelt heißt das im Allgemeinen, auf die Daten eines Servers von einem anderen Server aus zuzugreifen. Die verteilten Sichten des Microsoft SQL Server sind ein Beispiel. MySQL bietet über die Federated-Storage-Engine eine eingeschränkte Unterstützung für Federation. Genau wie NDB Cluster funktioniert diese Storage-Engine am besten bei sehr einfachen Lookups, obwohl sie auch eine akzeptable Methode darstellt, INSERTAbfragen auf einem anderen Server durchzuführen. Ihre aktuelle Architektur macht DELETE- und UPDATE-Abfragen weniger effizient – im schlimmsten Fall sogar viel weniger. Die Federated-Engine funktioniert bei Joins und großen SELECT-Abfragen sehr schlecht. So bezieht z.B. eine GROUP BY-Abfrage alle Daten aus einer Tabelle und benutzt mysql_ store_result6, um diese Daten vom entfernten Server in den Speicher des lokalen Servers zu holen. Wächst die Datengröße der Anwendung, kann das zu einer Menge Ärger führen. Federated-Tabellen verkomplizieren darüber hinaus die Replikation, weil eine einzige Aktualisierung auf mehreren Servern ausgeführt werden kann.
Lastausgleich Der Grundgedanke hinter dem Lastausgleich ist einfach: Man versucht, die Arbeitsbelastung so gleichmäßig wie möglich über eine Ansammlung von Servern zu verteilen. Normalerweise wird dazu ein Load Balancer (Lastverteiler; oft eine spezielle Hardware) vor die Server gesetzt. Der Load Balancer leitet die eingehenden Verbindungen dann an den am wenigsten ausgelasteten Server weiter. Abbildung 9-5 zeigt ein typisches Lastausgleichsszenario für eine große Website; es gibt einen Load Balancer für den HTTP-Verkehr und einen weiteren für den MySQL-Verkehr. Mit dem Lastausgleich werden fünf Ziele verfolgt: Skalierbarkeit Wenn Sie Ihre Systeme richtig entworfen haben, dann können Sie deren Kapazität erweitern, indem Sie zu einem Knoten mehr Server hinzufügen. Sie müssen allerdings in diesem Fall die Last zwischen den Servern ausgleichen. Effizienz Lastausgleich hilft Ihnen dabei, die Ressourcen effizienter einzusetzen, weil Sie die Kontrolle darüber haben, wie die Anforderungen geleitet werden. Das ist vor allem dann wichtig, wenn Ihre Server nicht alle gleich leistungsfähig sind: Sie können mehr Arbeit an die leistungsfähigen Maschinen leiten. Verfügbarkeit Eine kluge Lösung für den Lastausgleich nutzt die Server aus, die in einem bestimmten Augenblick verfügbar sind. 6 In »Das MySQL-Client/Server-Protokoll« auf Seite 173 erfahren Sie mehr über mysql_store_result.
Lastausgleich | 475
Transparenz Clients müssen nichts über die Maßnahmen zum Lastausgleich wissen. Sie müssen sich nicht darum kümmern, wie viele Maschinen sich hinter dem Load Balancer befinden oder wie sie heißen; der Load Balancer sorgt dafür, dass die Clients nur einen einzigen virtuellen Server sehen. Konsistenz Wenn Ihre Anwendung zustandsbehaftet ist (Datenbanktransaktionen, WebsiteSessions usw.), sollte der Load Balancer verwandte Anforderungen an einen einzigen Server leiten, damit der Zustand zwischen den Anforderungen nicht verlorengeht. Auf diese Weise muss die Anwendung nicht selbst darauf achten, mit welchem Server sie verbunden ist.
Client-Netzwerk
Load Balancer
Webservers
Leseabfragen Load Balancer Leseabfragen
Schreibabfragen MySQLMaster
Replikation
MySQLSlaves
Abbildung 9-5: Eine typische Lastausgleichsarchitektur für eine leseintensive Website
In der MySQL-Welt sind Lastausgleichsarchitekturen oft eng mit Sharding und Replikation verknüpft. Sie können Lastausgleichs- und Hochverfügbarkeitslösungen mischen und aufeinander abstimmen und sie an die passenden Stellen in Ihrer Anwendung setzen. So können Sie z.B. die Last zwischen mehreren Knoten in einem MySQL-Cluster ausgleichen. Oder Sie führen einen Lastausgleich zwischen Rechenzentren durch und erstellen
476 | Kapitel 9: Skalierung und Hochverfügbarkeit
innerhalb der einzelnen Rechenzentren eine Sharding-Architektur, bei der jeder Knoten eigentlich ein Master-Master-Replikationspaar mit vielen Slaves ist, deren Last wiederum ausgeglichen wird. Gleiches funktioniert auch für Hochverfügbarkeitsstrategien: Sie können in einer Architektur mehrere Failover-Stufen haben. Lastausgleich zeigt viele Nuancen. Eine der Herausforderungen ist z.B. die Verwaltung der Lese-/Schreibstrategien. Manche Techniken zum Lastausgleich erledigen dies selbst, andere verlangen, dass die Anwendung selbst erkennt, welche Knoten les- und schreibbar sind. Sie müssen diese Faktoren beachten, wenn Sie entscheiden, wie Sie den Lastausgleich implementieren. Es gibt eine Vielzahl von Lastausgleichslösungen, von Peer-basierten Implementierungen wie Wackamole (http://www.backhand.org/wackamole/) bis Domain Name System (DNS), LVS (Linux Virtual Server; http://www.linuxvirtualserver.org), Hardware-Load-Balancern, MySQL Proxy und der Organisation des Lastausgleichs in der Anwendung.
Direkt verbinden Manche Leute assoziieren Lastausgleich automatisch mit einem zentralen System, das zwischen die Anwendung und die MySQL-Server eingefügt wird. Das ist allerdings nicht die einzige Möglichkeit, den Lastausgleich durchzuführen. Sie können die Last ausgleichen und trotzdem eine direkte Verbindung von der Anwendung zu den MySQL-Servern haben. Um genau zu sein, funktionieren zentralisierte Lastausgleichssysteme normalerweise nur dann gut, wenn es einen Pool aus Servern gibt, die die Anwendung als untereinander austauschbar betrachten kann. Wenn die Anwendung entscheiden muss, ob es sicher ist, von einem Slave-Server zu lesen, muss sie sich üblicherweise direkt mit dem Server verbinden. Abgesehen von der Ermöglichung von Sonderfällen kann das Entscheiden über den Lastausgleich in der Anwendung sehr effizient sein. Bei z.B. zwei identischen Slaves beschließen Sie, dass einer von ihnen für alle Abfragen verwendet wird, die bestimmte Shards berühren, während der andere für Abfragen an die anderen Shards dient. Dadurch wird der Speicher der Slaves gut ausgenutzt, weil jeder von ihnen nur einen Teil der Daten von seinen Festplatten im Cache vorhalten muss. Fällt einer der Slaves aus, hat der andere dennoch alle Daten, die erforderlich sind, um die Abfragen an beide Shards zu bedienen. Im folgenden Abschnitt geht es um Methoden, um direkte Verbindungen von der Anwendung herzustellen, sowie um einige der Dinge, die Sie beachten müssen, wenn Sie die jeweiligen Möglichkeiten beurteilen.
Lese- und Schreiboperationen bei der Replikation trennen Die MySQL-Replikation liefert Ihnen mehrere Kopien Ihrer Daten und erlaubt es Ihnen zu wählen, ob eine Abfrage auf dem Master oder einem Slave ausgeführt werden soll. Die hauptsächliche Schwierigkeit besteht in der Frage, wie mit veralteten Daten auf dem
Lastausgleich | 477
Slave umgegangen werden soll, da die Replikation asynchron ist. Sie sollten Slaves darüber hinaus nur zum Lesen verwenden; der Master dagegen darf Lese- und Schreibabfragen verarbeiten. Normalerweise müssen Sie Ihre Anwendung so modifizieren, dass sie sich dieser Belange bewusst ist.7 Die Anwendung kann dann den Master für die Schreiboperationen einsetzen und die Leseoperationen zwischen dem Master und den Slaves aufteilen: Falls veraltete Daten keine große Rolle spielen, kann sie die Slaves verwenden, und der Master wird für Daten eingesetzt, die auf dem neuesten Stand sein müssen. Für ein Master-Master-Paar mit einem aktiven und einem passiven Master gelten die gleichen Überlegungen. In dieser Konfiguration sollte jedoch nur der aktive Server Schreibabfragen empfangen. Leseabfragen können an den passiven Server gehen, wenn es in Ordnung ist, dass potenziell veraltete Daten gelesen werden. Das größte Problem besteht darin, Artefakte zu vermeiden, die durch das Lesen veralteter Daten verursacht werden. Das klassische Artefakt ist, wenn ein Benutzer etwas ändert, z.B. einen Kommentar zu einem Blog-Artikel verfasst, dann die Seite neulädt und anschließend die Änderung nicht sieht, weil die Anwendung veraltete Daten von einem Slave gelesen hat. Hier sind einige der gebräuchlichsten Methoden für das Aufspalten von Lese- und Schreiboperationen: Abfragebasierte Aufteilung Bei der einfachsten Aufteilung werden alle Schreiboperationen sowie alle Leseoperationen, die veraltete Daten nicht tolerieren können, an den aktiven oder Master-Server geleitet. Alle anderen Leseoperationen gehen an den Slave oder passiven Server. Diese Strategie lässt sich leicht implementieren, nutzt in der Praxis aber nicht so oft die Slaves, wie sie könnte, da nur sehr wenige Leseabfragen immer veraltete Daten tolerieren können. Aufteilung der veralteten Daten Dies ist eine kleine Verbesserung der abfragebasierten Aufteilungsstrategie. Es ist relativ wenig zusätzliche Arbeit erforderlich, damit die Anwendung den Rückstand des Slaves überprüfen und entscheiden kann, ob die Daten zum Lesen schon zu alt sind. Viele Reporting-Anwendungen können diese Strategie einsetzen: Solange die nächtliche Datenlast fertig auf den Slave repliziert wurde, ist es egal, ob ihre Daten zu 100 % synchron mit dem Master sind. Sitzungsbasierte Aufteilung Eine etwas raffiniertere Methode, um zu entscheiden, ob eine Leseoperation an einen Slave gerichtet werden kann oder nicht, besteht darin, festzustellen, ob der Benutzer irgendwelche Daten geändert hat. Der Benutzer muss die neuesten Daten von anderen Benutzern nicht sehen, sollte aber seine eigenen Änderungen sehen 7 Wenn Sie MySQL Proxy einsetzen können, um Ihre Abfragen aufzuteilen, müssen Sie wahrscheinlich die Anwendung nicht ändern.
478 | Kapitel 9: Skalierung und Hochverfügbarkeit
können. Sie implementieren dies auf der Sitzungsebene, indem Sie die Sitzung mit einem Flag kennzeichnen, das besagt, dass eine Änderung vorgenommen wurde, um die Leseabfragen des Benutzers hinterher für einen bestimmten Zeitraum an den Master weiterzuleiten. Sie können dies mit einer Überwachung des Replikationsrückstands kombinieren; wenn der Benutzer die Daten vor 10 Sekunden geändert hat und kein Slave mehr als fünf Sekunden zurückliegt, dann ist es sicher, von einem Slave zu lesen. Es bietet sich an, einen der Slaves auszuwählen und für die ganze Sitzung zu benutzen, da der Benutzer ansonsten merkwürdige Effekte beobachten könnte, die daher rühren, dass einige der Slaves weiter zurückliegen als andere. Versionsbasierte Aufteilung Diese ähnelt der sitzungsbasierten Aufteilung: Sie verfolgen die Versionsnummern und/oder Zeitstempel von Objekten und lesen die Version oder den Zeitstempel des Objekts vom Slave, um festzustellen, ob seine Daten frisch genug sind, um sie zu benutzen. Sind die Daten des Slaves zu alt, können Sie die frischen Daten vom Master lesen. Sie können auch die Versionsnummer des obersten Elements inkrementieren, wenn das Objekt selbst sich nicht ändert. Dadurch wird die Altersüberprüfung erleichtert (Sie müssen nur an einer Stelle nachschauen – auf dem obersten Element). Sie können z.B. die Version des Benutzers aktualisieren, wenn er einen neuen Blog-Eintrag schreibt. Leseabfragen gehen dann an den Master. Der Overhead erhöht sich, wenn die Version des Objekts vom Slave gelesen wird. Diesen Effekt können Sie durch den Einsatz des Cache verringern. Caching und Objektversionierung werden im nächsten Kapitel ausführlicher behandelt. Globale Versions-/Sitzungsaufteilung Dies ist eine Variante der versions- und sitzungsbasierten Aufteilungen. Wenn die Anwendung eine Schreiboperation durchführt, führt sie nach dem Bestätigen der Transaktion SHOW MASTER STATUS aus. Sie speichert die Log-Koordinaten des Masters als Versionsnummer des modifizierten Objekts und/oder der Sitzung im Cache. Verbindet sich die Anwendung dann mit dem Slave, führt sie SHOW SLAVE STATUS aus und vergleicht die Koordinaten des Slaves mit der gespeicherten Version. Ist der Slave wenigstens schon bis zu der Stelle vorgedrungen, an der der Master die Transaktion bestätigt hat, dann kann der Slave sicher zum Lesen benutzt werden. Die meisten Lösungen zur Lese-/Schreibaufteilung erfordern eine Überwachung des Rückstands des Slaves, um zu entscheiden, wohin Leseoperationen gerichtet werden. Bedenken Sie in diesem Fall, dass die Seconds_behind_master-Spalte aus SHOW SLAVE STATUS keine zuverlässige Methode darstellt, um den Rückstand des Slaves zu überwachen. Mehr dazu erfahren Sie in »Den Rückstand des Slaves messen« auf Seite 412. Falls Ihr Ziel reine Skalierbarkeit ist und Sie sich nicht darum kümmern müssen, wie viel Hardware das erfordert, können Sie die Dinge vereinfachen und entweder keine Replikation einsetzen oder sie nur für die Zwecke der Hochverfügbarkeit und nicht für den Lastausgleich benutzen. Auf diese Art vermeiden Sie möglicherweise die Komplexität, die das
Lastausgleich | 479
Aufteilen der Leseoperationen zwischen den Mastern und den Slaves mit sich bringt. Manche Leute halten das für sinnvoll, andere für eine Verschwendung von Hardware. Diese Trennung spiegelt unterschiedliche Ziele wider: Wollen Sie nur Skalierbarkeit oder sowohl Skalierbarkeit als auch Effizienz? Falls es Ihnen auch um die Effizienz geht und Sie die Slaves auch für andere Aufgaben als zum Ablegen einer Kopie der Daten benutzen wollen, müssen Sie mit einer erhöhten Komplexität rechnen.
Die Anwendungskonfiguration ändern Eine Möglichkeit zur Verteilung der Last ist die Neukonfiguration Ihrer Anwendung. Sie können z.B. mehrere Maschinen so konfigurieren, dass sie sich die Belastung bei der Erzeugung großer Berichte teilen. Die Konfiguration kann die jeweilige Maschine anweisen, sich mit einem anderen MySQL-Slave zu verbinden und Berichte für jeden N-ten Benutzer oder für jede N-te Site zu generieren. Dieses System lässt sich im Allgemeinen sehr leicht implementieren, wird allerdings spröde und schwerfällig, wenn Codeänderungen – einschließlich Änderungen an den Konfigurationsdateien – erforderlich werden. Alles Festkodierte, was Sie auf jedem Server oder an einer zentralen Stelle ändern und dann über Dateikopien oder UpdateBefehle »veröffentlichen« müssen, ist von Natur aus beschränkt. Wenn Sie die Konfiguration in der Datenbank und/oder einem Cache speichern, können Sie es vermeiden, Codeänderungen zu veröffentlichen.
DNS-Namen ändern Eine plumpe Lastausgleichstechnik, aber eine, die bei manchen einfachen Anwendungen gut funktioniert, ist das Erzeugen von DNS-Namen für verschiedene Zwecke. Sie können einen regelmäßig ablaufenden Job schreiben, der die MySQL-Server überwacht, und die Namen je nach Bedarf an unterschiedliche Server verweisen. Bei der einfachsten Implementierung hat man einen DNS-Namen für die schreibgeschützten Server und einen für den schreibbaren Server. Wenn die Slaves mit dem Master Schritt halten, können Sie den »schreibgeschützten« DNS-Namen so ändern, dass er auf die Slaves verweist; fallen sie zurück, verweisen Sie ihn wieder zurück auf den Master. Die DNS-Technik lässt sich leicht implementieren, hat aber viele Nachteile. Das größte Problem besteht darin, dass das DNS nicht vollständig Ihrer Kontrolle unterliegt: • DNS-Änderungen erfolgen nicht sofort. Es kann lange dauern, bis DNS-Änderungen sich über ein Netzwerk oder zwischen Netzwerken ausgebreitet haben. • DNS-Daten werden an verschiedenen Stellen in Caches abgelegt; die Verfallszeiten sind nur Hinweise und nicht obligatorisch. • DNS-Änderungen können erfordern, dass eine Anwendung oder ein Server neu gestartet werden, um voll wirksam zu werden.
480 | Kapitel 9: Skalierung und Hochverfügbarkeit
• Es ist keine gute Idee, mehrere IP-Adressen für einen DNS-Namen zu benutzen und sich dann auf das Round-Robin-Verhalten zu verlassen, um Anforderungen auszugleichen. Das Round-Robin-Verhalten ist nicht immer vorhersehbar. • DNS-Änderungen sind nicht atomar. • Der Datenbankadministrator hat nicht unbedingt direkten Zugriff auf das DNS. Solange die Anwendung nicht sehr einfach ist, ist es gefährlich, sich auf ein System zu verlassen, das man nicht kontrollieren kann. Sie können Ihre Kontrolle ein wenig verbessern, indem Sie Änderungen an /etc/hosts anstatt am DNS vornehmen. Wenn Sie eine Änderung an dieser Datei veröffentlichen, wissen Sie, dass die Änderung wirksam geworden ist. Das ist immerhin besser, als darauf zu warten, dass ein im Cache gespeicherter DNS-Eintrag verfällt – ideal ist es dennoch nicht. Wir empfehlen üblicherweise, sich beim Entwurf des Systems nicht auf das DNS zu verlassen. Am besten vermeiden Sie es schon bei einfachen Anwendungen, weil Sie niemals wissen, wie groß Ihre Anwendungen werden.
IP-Adressen verschieben Manche Lösungen zum Lastausgleich beruhen darauf, dass virtuelle IP-Adressen8 zwischen Servern verschoben werden, was ganz gut funktionieren kann. Das klingt vielleicht ähnlich wie das Durchführen von Änderungen am DNS, ist aber nicht das Gleiche. Server lauschen nicht auf Netzwerkverkehr zu einem DNS-Namen, sondern zu einer speziellen IP-Adresse. Das Verschieben von IP-Adressen erlaubt es also, die DNS-Namen statisch zu lassen. Sie können über ARP-Befehle (Address Resolution Protocol) erzwingen, dass Änderungen der IP-Adresse sehr schnell bemerkt werden. Zwei Systeme, die diese Technik einsetzen, sind Wackamole und LVS. Sie erlauben es Ihnen z.B., eine einzelne IP-Adresse mit einer Rolle wie »schreibgeschützt« zu verknüpfen, und kümmern sich bei Bedarf um das Verschieben der IP-Adressen zwischen den Maschinen. Wackamole kann viele IP-Adressen verwalten und sorgt dafür, dass immer nur eine Maschine aus dem Pool an jeder Adresse lauscht. Das Einmalige an Wackamole ist, dass es Peer-basiert arbeitet, wodurch die Schwachstelle eliminiert wird. Eine praktische Technik besteht darin, jedem physischen Server eine feste IP-Adresse zuzuweisen. Diese IP-Adresse definiert den Server selbst und ändert sich nie. Sie können dann für jeden logischen »Dienst« eine virtuelle IP-Adresse benutzen. Diese kann leicht zwischen den Servern verschoben werden, was es erleichtert, Dienste und Anwendungsinstanzen zu verlagern, ohne die Anwendung neu konfigurieren zu müssen. Das ist eine hübsche Eigenschaft, selbst wenn Sie die IP-Adressen zum Lastausgleich oder zum Absichern der Hochverfügbarkeit nicht oft verschieben.
8 Virtuelle IP-Adressen sind nicht mit bestimmten Computern oder Netzwerkschnittstellen verbunden; sie »fließen« zwischen den Computern hin und her.
Lastausgleich | 481
Einen Vermittler einführen Bisher gehen alle Techniken, die wir vorgestellt haben, davon aus, dass Ihre Anwendung direkt mit den MySQL-Servern kommuniziert. Viele Lösungen zum Lastausgleich führen allerdings einen Vermittler ein, der als Proxy für den Netzwerkverkehr auftritt. Der Vermittler nimmt auf der einen Seite den gesamten Verkehr entgegen und leitet ihn auf der anderen Seite an den gewünschten Server weiter; die Antworten schickt er dann zurück an die Ausgangsmaschine. Manchmal handelt es sich bei dem Vermittler um eine bestimmte Hardware, manchmal um Software.9 Abbildung 9-6 verdeutlicht diese Architektur. Solche Lösungen funktionieren im Allgemeinen sehr gut; wenn der Vermittler selbst allerdings nicht redundant ist, führt man an seiner Stelle eine Sollbruchstelle ein.
Load Balancer Anwendungsserver
Datenbank-Server
Abbildung 9-6: Ein Load Balancer, der als Vermittler auftritt
Load Balancer Es gibt auf dem Markt eine Vielzahl von Hardware- und Software-Produkten zum Lastausgleich, allerdings sind nur wenige der Angebote speziell dafür ausgelegt, die Last auf MySQL-Servern auszugleichen.10 Webserver benötigen viel häufiger einen Lastausgleich, so dass viele der allgemeinen Lastausgleichsgeräte besondere Funktionen für HTTP enthalten und nur wenige einfache Funktionen für alles andere. Eine Ausnahme bildet MySQL Proxy, das eine gute Möglichkeit bietet, Lese- und Schreiboperationen zu trennen. Mit ihm erhöhen sich zwar Komplexität und Aufwand, aber auch die Flexibilität verbessert sich, und Sie können Skripte für eigene Lese-/Schreibaufteilungen einsetzen. MySQL Proxy ist relativ neu, es gibt dafür aber schon viele Anleitungen und Beispiele für einen selbst erstellten Lastausgleich. Da dieses Werkzeug in den Datenstrom hineinschauen kann, den es weiterleitet, kann man damit potenziell sehr komplizierte Abfragen leiten.
9 Sie können LVS so konfigurieren, dass es nur beteiligt ist, wenn eine Anwendung eine neue Verbindung erzeugen muss, und danach ist es kein Vermittler. 10 Wir haben bereits einige der Software-Implementierungen (Sequoia, Continuent) erwähnt; es gibt außerdem noch DBIx::DBCluster für Perl sowie SQL Relay (http://sqlrelay.sourceforge.net) für eine sprachunabhängige Lösung.
482 | Kapitel 9: Skalierung und Hochverfügbarkeit
MySQL-Verbindungen sind normale TCP/IP-Verbindungen; Sie können also für MySQL ganz allgemeine Load Balancer verwenden. Durch das Fehlen MySQL-spezifischer Eigenschaften ergeben sich allerdings einige Einschränkungen: • Solange sich der Load Balancer nicht der wahren Last von MySQL bewusst ist, wird man wahrscheinlich eher Anforderungen verteilen als Last ausgleichen. Nicht alle Abfragen sind gleich, aber allgemeine Load Balancer behandeln normalerweise alle Anforderungen als gleich. • Die meisten Load Balancer wissen, dass sie eine HTTP-Anforderung überprüfen und eine Sitzung an einen Server »kleben« müssen, um den Sitzungszustand auf einem Webserver zu erhalten. MySQL-Verbindungen sind ebenfalls zustandsbehaftet, allerdings weiß der Load Balancer wahrscheinlich nicht, wie er alle Verbindungsanforderungen von einer einzigen HTTP-Sitzung an einen einzigen MySQL-Server »klebt«. Das führt zu einem gewissen Effizienzverlust (wenn die Anforderungen einer Sitzung alle zum selben MySQL-Server gehen, dann funktioniert der Cache des Servers effizienter). • Verbindungs-Pooling und persistente Verbindungen können der Fähigkeit eines Load Balancers in die Quere kommen, Verbindungsanforderungen zu verteilen. Nehmen Sie z.B. an, ein Verbindungs-Pool öffnet seine konfigurierte Anzahl von Verbindungen und der Load Balancer verteilt sie unter den existierenden vier MySQL-Servern. Stellen Sie sich nun vor, dass Sie zwei weitere MySQL-Server hinzufügen. Da der Verbindungs-Pool keine neuen Verbindungen anfordert, bleiben sie untätig. Die Verbindungen in dem Pool werden unter Umständen unfair zwischen den Servern verteilt, so dass einige überlastet sind und andere nicht ausgelastet werden. Sie können diese Probleme umgehen, indem Sie die Verbindungen in dem Pool auf verschiedenen Ebenen verfallen lassen. Allerdings ist das kompliziert und schwierig. Lösungen zum Verbindungs-Pooling funktionieren am besten, wenn sie ihren eigenen Lastausgleich durchführen. • Die meisten allgemeinen Load Balancer können Überprüfungen des Befindens und der Last nur für HTTP-Server durchführen. Ein einfacher Load Balancer kann verifizieren, dass der Server Verbindungen an einem TCP-Port akzeptiert; das ist aber auch nur das Mindeste. Ein besserer Load Balancer kann eine HTTP-Anforderung ausführen und den Antwortcode überprüfen, um festzustellen, ob der Webserver gut funktioniert. MySQL akzeptiert allerdings keine HTTP-Anforderungen an Port 3306, so dass Sie einen eigenen Befindlichkeitstest herstellen müssen. Sie können auf dem MySQL-Server HTTP-Server-Software installieren und den Load Balancer ein eigenes Skript ausführen lassen, das tatsächlich den Status des MySQL-Servers überprüft und einen entsprechenden Statuscode zurückliefert.11 Am wichtigsten ist die Überprüfung der Betriebssystemlast (im Allgemeinen, indem man sich /proc/loadavg anschaut), der Replikationsstatus und der Anzahl der MySQL-Verbindungen. 11 Falls Ihr Kodierungs-Kung-Fu dazu führt, dass Sie ein Programm schreiben, das an Port 80 lauscht, oder falls Sie xinetd so konfigurieren, dass es Ihr Programm startet, müssen Sie nicht einmal einen Webserver installieren.
Lastausgleich | 483
Algorithmen für den Lastausgleich Es gibt viele unterschiedliche Algorithmen, um festzustellen, welcher Server die nächste Verbindung empfangen soll. Jeder Hersteller benutzt eine andere Terminologie, und diese Liste hier soll Ihnen einen Eindruck davon vermitteln, was es gibt: Zufällig Der Load Balancer richtet jede Anforderung an einen Server, der zufällig aus dem Pool der verfügbaren Server ausgewählt wurde. Round-Robin Der Load Balancer sendet die Anforderungen in einer sich wiederholenden Folge an die Server: A, B, C, A, B, C usw. Die wenigsten Verbindungen Die nächste Verbindung geht an den Server mit den wenigsten aktiven Verbindungen. Schnellste Antwort Der Server, der die Anforderungen am schnellsten verarbeitet, empfängt die nächste Verbindung. Das kann gut funktionieren, wenn der Pool eine Mischung aus schnellen und langsamen Verbindungen enthält. Mit SQL ist das jedoch nicht so einfach, wenn die Komplexität der Abfragen stark schwankt. Selbst die gleiche Abfrage kann unter verschiedenen Umständen sehr unterschiedlich ausgeführt werden, etwa wenn sie aus dem Abfrage-Cache heraus beantwortet wird oder wenn die Caches des Servers bereits die benötigten Daten enthalten. Hashed Der Load Balancer bildet einen Hash aus der Quell-IP-Adresse der Verbindung, den er auf einen der Server im Pool abbildet. Jedes Mal wenn eine Verbindungsanforderung von der gleichen IP-Adresse kommt, schickt sie der Load Balancer an den gleichen Server. Die Bindungen ändern sich nur, wenn sich die Anzahl der Maschinen im Pool ändert. Gewichtet Der Load Balancer kann mehrere der anderen Algorithmen kombinieren und gewichten. Sie haben vielleicht z.B. Single- und Dual-CPU-Maschinen. Die DualCPU-Maschinen sind ungefähr doppelt so leistungsfähig, so dass Sie den Load Balancer anweisen können, im Durchschnitt doppelt so viele Anforderungen an sie zu senden. Der beste Algorithmus für MySQL hängt von Ihrer Belastung ab. Der Algorithmus der wenigsten Verbindungen könnte z.B. neue Server überfluten, wenn Sie sie zu dem Pool der verfügbaren Server hinzufügen – und das ausgerechnet dann, wenn ihre Caches noch gar nicht aufgewärmt sind. Die Autoren der ersten Ausgabe dieses Buches haben dieses Problem am eigenen Leib erfahren.
484 | Kapitel 9: Skalierung und Hochverfügbarkeit
Sie müssen durch eigene Versuche die beste Leistung für Ihre Arbeitsbelastung ermitteln. Beachten Sie sowohl das, was unter ungewöhnlichen Umständen geschieht, als auch das, was die tägliche Norm ausmacht. Unter besonderen Umständen – bei hoher Last, Schema-Änderungen oder einer ungewöhnlichen Anzahl ausfallender Server – können Sie es sich am wenigsten leisten, dass etwas schiefgeht. Wir haben nur Algorithmen beschrieben, die sofort tätig werden und Verbindungsanforderungen nicht in Warteschlangen legen. Manchmal sind Algorithmen effizienter, die auf Warteschlangen zurückgreifen. Beispielsweise könnte ein Algorithmus eine bestimmte Nebenläufigkeit auf einem Datenbankserver aufrechterhalten, indem er nicht mehr als N aktive Transaktionen gleichzeitig erlaubt. Wenn es zu viele aktive Transaktionen gibt, kann der Algorithmus eine neue Anforderung in eine Warteschlange legen und sie vom ersten Server bedienen lassen, der entsprechend den Kriterien »zur Verfügung steht«. Manche Verbindungs-Pools unterstützen warteschlangenfähige Algorithmen.
Server zum Pool hinzufügen und aus ihm entfernen Beim Hinzufügen eines neuen Servers zu einem Pool reicht es normalerweise nicht aus, ihn einfach anzuschließen und den Load Balancer von seiner Existenz in Kenntnis zu setzen. Sie glauben vielleicht, das wäre in Ordnung, solange er nicht mit Verbindungen geflutet wird, aber das stimmt nicht immer. Manchmal können Sie die Last auf einem Server langsam erhöhen. Allerdings sind manche Server, deren Caches noch kalt sind, unter Umständen so langsam, dass sie für eine Weile überhaupt keine Abfragen erhalten sollten. Wenn die Caches eines Servers kalt sind, kann es selbst bei einfachen Abfragen lange dauern, bis sie abgeschlossen werden. Falls es 30 Sekunden dauert, um die Daten zurückzuliefern, die der Benutzer braucht, um eine Seite zu sehen, dann ist der Server selbst für ein geringes Datenaufkommen unbenutzbar. Sie vermeiden dieses Problem, indem Sie den SELECT-Verkehr für eine gewisse Zeit von einem aktiven Server spiegeln, bevor Sie den Load Balancer über den neuen Server informieren. Dazu lesen Sie die Log-Dateien des aktiven Servers und spielen sie auf dem neu gestarteten Server wieder ab. Konfigurieren Sie die Server in dem Verbindungs-Pool so, dass es genügend ungenutzte Kapazität gibt, damit Sie die Server zur Wartung herausnehmen oder im Falle eines Ausfalls die Last bewältigen können. Sie brauchen mehr als nur »ausreichend« Kapazität auf den einzelnen Servern. Achten Sie darauf, dass die Grenzen Ihrer Konfiguration hoch genug liegen, damit diese noch funktioniert, wenn die Server sich nicht im Pool befinden. Falls Sie z.B. feststellen, dass jeder MySQL-Server typischerweise 100 Verbindungen hat, sollten Sie max_connections auf jedem Server im Pool auf 200 setzen. Selbst wenn dann die Hälfte der Server ausfällt, müsste der Pool in der Lage sein, als Ganzes mit der gleichen Anzahl an Verbindungen klarzukommen.
Lastausgleich | 485
Lastausgleich mit einem Master und mehreren Slaves Die am weitesten verbreitete Replikationstopologie ist diejenige mit einem Master und mehreren Slaves. Es kann schwierig sein, von dieser Architektur wegzukommen. Viele Anwendungen gehen davon aus, dass es für alle Schreiboperationen ein einziges Ziel gibt oder dass alle Daten immer auf einem einzigen Server zur Verfügung stehen. Obwohl es sich nicht um die am besten skalierbare Architektur handelt, erlaubt es der Lastausgleich, sie mit gutem Nutzen einzusetzen. In diesem Abschnitt untersuchen wir einige dieser Techniken: Funktionelle Partitionierung Sie können die Kapazität ziemlich strecken, indem Sie Slaves oder Gruppen aus Slaves für bestimmte Zwecke konfigurieren. Gebräuchliche Funktionen, die Sie möglicherweise trennen sollten, sind Reporting und Analysen, Data-Warehousing und Volltextsuchen. Mehr Ideen finden Sie in »Eigene Replikationslösungen« auf Seite 403. Filterung und Datenpartitionierung Sie können Daten zwischen ansonsten ähnlichen Slaves mit Replikationsfiltern partitionieren (siehe »Replikationsfilter« auf Seite 391). Diese Strategie kann gut funktionieren, solange Ihre Daten bereits in unterschiedliche Datenbanken oder Tabellen auf dem Master aufgeteilt sind. Leider ist keine Methode vorgesehen, um auf der Ebene der einzelnen Zeilen zu filtern. Sie könnten allerdings ein Filterschema auf Zeilenebene implementieren, indem Sie in einen Distribution-Master replizieren und Blackhole-Tabellen mit Triggern verwenden, um die Zeilen auf der Grundlage des Spaltenwertes in unterschiedliche Tabellen einfügen zu lassen. Es sind sogar noch exotischere Dinge möglich, wie etwa das Replizieren in Federated-Tabellen, die aber unter Umständen ziemlich nerven. Federated-Tabellen führen zu Abhängigkeiten zwischen den Servern, die man im Allgemeinen am besten vermeidet. Selbst wenn Sie die Daten nicht unter den Slaves aufteilen, können Sie die Effizienz des Cache erhöhen, indem Sie die Leseoperationen aufteilen, anstatt sie zufällig zu verteilen. Sie könnten z.B. alle Leseoperationen für Benutzer, deren Namen mit den Buchstaben A–M beginnen, an einen bestimmten Slave leiten, und alle Leseoperationen für Benutzer, deren Namen mit N–Z beginnen, an einen anderen Slave. Auf diese Weise nutzen Sie den Cache der jeweiligen Maschinen besser aus, weil wiederholte Leseanfragen die relevanten Daten mit einer größeren Wahrscheinlichkeit im Cache finden. Im besten Fall, nämlich wenn es keine Schreiboperationen gibt, erhalten Sie eine Gesamt-Cache-Größe, die der Größe der Caches der beiden Maschinen entspricht. Zum Vergleich: Falls Sie die Leseoperationen zufällig zwischen den Slaves verteilen, werden die Daten im Prinzip in den Caches der einzelnen Maschinen dupliziert, so dass der tatsächliche Cache nur etwa so groß ist wie der Cache eines Slaves – egal, wie viele Slaves Sie haben.
486 | Kapitel 9: Skalierung und Hochverfügbarkeit
Teile der Schreiboperationen auf einen Slave verschieben Der Master muss nicht unbedingt die ganze Arbeit allein verrichten, die mit dem Schreiben zu tun hat. Sie können dem Master und den Slaves einen wesentlichen Teil der redundanten Arbeit ersparen, indem Sie Schreibabfragen auseinandernehmen und Teile von ihnen auf den Slaves ausführen. Mehr dazu erfahren Sie in »Übermäßiger Rückstand bei der Replikation« auf Seite 434. Garantieren, dass ein Slave aufgeholt hat Falls Sie einen bestimmten Prozess auf dem Slave ausführen wollen und er wissen muss, dass seine Daten zu einem bestimmten Zeitpunkt tatsächlich aktuell sind – selbst wenn er eine Weile warten muss, bis das geschieht –, können Sie die Funktion MASTER_POS_WAIT( ) einsetzen, um den Master zu blockieren, bis der Slave zu dem gewünschten Punkt auf dem Master aufgeholt hat. Alternativ können Sie die Aktualität mit einem Replikations-Heartbeat überprüfen, obwohl dessen Genauigkeit nur im Sekundenbereich liegt. Mehr zu dieser Technik erfahren Sie in »Den Rückstand des Slaves messen« auf Seite 412. Schreibsynchronisation Mit MASTER_POS_WAIT( ) können Sie auch sicherstellen, dass Ihre Schreiboperationen tatsächlich einen oder mehrere Slaves erreichen. Falls Ihre Anwendung eine synchrone Replikation emulieren muss, um Datensicherheit zu garantieren, kann sie zwischen den einzelnen Slaves wechseln und jeweils MASTER_POS_WAIT( ) ausführen. Dadurch entsteht eine »Synchronisationsbarriere«, die unter Umständen lange besteht, wenn einer der Slaves bei der Replikation weit zurückliegt. Sie sollten dies daher nur verwenden, wenn es absolut notwendig ist. (Sie können auch einfach warten, bis einer der Slaves das Event erhalten hat, falls Sie lediglich dafür sorgen wollen, dass irgendein Slave das Event hat.)
Hochverfügbarkeit Die meisten betrachten ein System als verfügbar, wenn es auf Benutzer antwortet. Verfügbarkeit kann aber auch ein wenig komplexer sein. Eine Anwendung kann antworten, allerdings nur in einem schwächeren Grade, wenn Teile von ihr ausgefallen sind, sie aber genügend fehlertolerant ist, um trotzdem weiterzulaufen. Sie könnten eine Anwendung zur Wartung oder im Fehlerfall aber auch in einen schreibgeschützten Modus setzen. Ob Sie diese Zeit dann als »Betriebszeit« zählen, müssen Sie selbst entscheiden. Den meisten Benutzern von Foto-Sharing-Sites ist es z.B. egal, wenn sie für einen kurzen Moment keine Fotos hochladen können. Der Benutzer eines Geldautomaten dagegen möchte keine »Wegen Wartungsarbeiten keine Ausgaben möglich«-Meldung sehen. Bei der Website kann man sagen, dass sie in Betrieb ist, der Geldautomat dagegen ist definitiv nicht in Betrieb. Das Implementieren von Hochverfügbarkeit ist eigentlich sehr einfach: Sie bauen Redundanz ein und veranlassen Ihre Systeme, einen Ersatz an den Start zu bringen, wenn etwas ausfällt. Schwieriger ist es, dies schnell und zuverlässig hinzubekommen.
Hochverfügbarkeit | 487
Hochverfügbarkeit einplanen Anwendungen haben ganz unterschiedliche Anforderungen an die Verfügbarkeit. Bevor Sie sich auf ein bestimmtes Ziel bezüglich der Betriebszeit versteifen, müssen Sie sich erst einmal fragen, was Sie erreichen müssen. Jeder Anstieg der Verfügbarkeit kostet normalerweise viel mehr als der vorhergehende; das Verhältnis von Verfügbarkeit zu Aufwand und Kosten ist nichtlinear. Das wichtigste Prinzip der Hochverfügbarkeit besteht darin, einzelne Schwachpunkte in Ihrem System zu finden und zu eliminieren. Gehen Sie Ihre Anwendung durch, und versuchen Sie, solche Punkte zu identifizieren. Ist es eine Festplatte, ein Server, ein Switch oder Router oder die Stromzufuhr für ein Rack? Befinden sich alle Ihre Maschinen in einem Rechenzentrum, oder gibt es »redundante« Rechenzentren desselben Unternehmens? Jede Stelle in Ihrem System, die nicht redundant ist, stellt einen Schwachpunkt dar. Andere verbreitete Schwachpunkte sind das Vertrauen auf Dienste wie DNS, einen einzelnen Netzwerkanbieter (überprüfen Sie, ob Ihre redundanten Netzwerkverbindungen wirklich an unterschiedliche Internet-Backbones angeschlossen sind) und ein einziges Energieversorgungsnetz. Versuchen Sie, alle Komponenten zu verstehen, die die Verfügbarkeit beeinflussen. Versuchen Sie auch, einen ausgewogenen Blick auf die Risiken zu entwickeln und die größten Risiken zuerst zu bearbeiten. Manche Leute bemühen sich sehr, Software zu erstellen, die mit allen möglichen Hardware-Ausfällen zurechtkommen kann, dennoch können Bugs in dieser Art von Software mehr Ausfallzeiten verursachen, als die Software eigentlich erspart. Manche Leute bauen »unsinkbare« Systeme mit allen Arten der Redundanz, vergessen aber, dass im Rechenzentrum der Strom ausfallen oder jemand ein Kabel durchhacken könnte. Oder Sie vergessen völlig, dass es bösartige Angreifer oder Programmierfehler gibt, die Daten löschen oder beschädigen – auch ein sorgloses DROP TABLE kann für Ausfälle sorgen. Sie können vorrangige Risiken erkennen, indem Sie Ihr Gefährdungspotenzial berechnen. Dabei handelt es sich um die Wahrscheinlichkeit eines Ausfalls, multipliziert mit den Kosten des Ausfalls. Eine einfache Tabelle der Risiken – mit Spalten für die Wahrscheinlichkeit, die Kosten und den Gefährdungsgrad – kann Ihnen helfen, die Prioritäten für Ihre Anstrengungen zu setzen. Es ist nicht immer möglich, Schwachstellen zu eliminieren. Vielleicht lässt sich eine Komponente aufgrund irgendeiner – örtlichen, finanziellen oder zeitlichen – Einschränkung, die Sie nicht umgehen können, nicht redundant ausführen. Denken Sie für den Fall eines Ausfalls, einer Systemaufrüstung, der Änderung einer Anwendung oder einer regulären Wartung als Nächstes über den Wechsel auf ein Ersatzsystem nach. Alles, was Teile Ihrer Anwendung unerreichbar macht, erfordert vermutlich einen Rettungsplan. Sie müssen feststellen, wie schnell diese Rettung (Failover) erfolgen muss.
488 | Kapitel 9: Skalierung und Hochverfügbarkeit
Damit verwandt ist die Frage, wie schnell Sie die ausgefallene Komponente nach einem Failover ersetzen müssen. Bis Sie die Reservekapazität des Systems wiederhergestellt haben, ist die Redundanz herabgesetzt, und es gibt ein erhöhtes Risiko für weitere Ausfälle. Das Vorhandensein einer Reserve enthebt Sie deshalb nicht der Notwendigkeit, ausgefallene Komponenten zeitnah zu ersetzen. Wie schnell können Sie einen neuen Reserveserver zusammenbauen, sein Betriebssystem installieren und ihm eine frische Kopie Ihrer Daten übergeben? Haben Sie genügend Reservemaschinen? Wahrscheinlich brauchen Sie mehr als eine. Eine weitere Überlegung ist, ob Sie Daten verlieren, obwohl Ihre Anwendung gar nicht offline geht. Wenn ein Server einen wirklich katastrophalen Ausfall erlebt, verlieren Sie vermutlich wenigstens ein paar Daten, wie etwa die letzten Transaktionen, die in das (nun verloren gegangene) Binärlog geschrieben wurden und es nicht in das Relay-Log eines Slaves geschafft haben. Können Sie das tolerieren? Die meisten Anwendungen können es; die Alternativen sind normalerweise teuer, komplex oder bringen einen erhöhten Aufwand mit sich. Sie können z.B. Googles Patches für eine synchrone Replikation einsetzen (mehr dazu später) oder das Binärlog auf ein Gerät legen, das durch DRBD repliziert wird, so dass Sie es selbst dann nicht verlieren, wenn das System komplett ausfällt. Eine clevere Anwendungsarchitektur kann die Anforderungen an die Verfügbarkeit oft verringern, zumindest für einen Teil des Systems, und daher das Erreichen von Hochverfügbarkeit erleichtern. Indem Sie wichtige von weniger wichtigen Teilen Ihrer Anwendung trennen, sparen Sie viel Arbeit und Geld, weil es viel einfacher ist, Redundanz und Hochverfügbarkeit für ein kleineres System zu bieten. Im Allgemeinen ist es schwierig und teuer, eine Anwendung bis über einen bestimmten Punkt hinaus hochverfügbar zu gestalten und Datenverlust zu vermeiden. Wir empfehlen Ihnen deshalb, sich realistische Ziele zu setzen und es nicht zu übertreiben. Glücklicherweise ist der Aufwand für das Erreichen von zwei oder drei Neunen für die Betriebszeit – je nach Anwendung – nicht allzu hoch.
Redundanz hinzufügen Das Hinzufügen von Redundanz zu Ihrem System kann auf zwei Arten erfolgen: indem freie Kapazität hinzugefügt wird und indem Komponenten verdoppelt werden. Es ist recht einfach, freie Kapazität hinzuzufügen – nutzen Sie eine der Techniken, die wir in diesem Kapitel bereits erwähnt haben. Eine Möglichkeit, um die Verfügbarkeit zu erhöhen, besteht darin, einen Server-Cluster oder -Pool anzulegen und eine Lösung für den Lastausgleich einzubauen. Wenn einer der Server ausfällt, übernehmen die anderen Server seine Last. Es ist im Allgemeinen keine schlechte Idee, die Komponenten nicht voll auszunutzen, weil Sie dadurch mehr »Kopffreiheit« erhalten, um mit Performance-Problemen aufgrund von erhöhter Last oder Ausfällen von Komponenten klarzukommen. Für viele Aufgaben müssen Sie Komponenten verdoppeln und eine Reserve bereithalten, die einspringt, wenn die Hauptkomponente ausfällt. Eine duplizierte Komponente kann
Hochverfügbarkeit | 489
ganz einfach sein, wie eine freie Netzwerkkarte, Festplatte oder ein freier Router – je nachdem, was Ihrer Meinung nach am wahrscheinlichsten ausfällt. Das Duplizieren eines ganzen MySQL-Servers ist etwas schwieriger, weil ein Server ohne seine Daten ziemlich sinnlos ist. Sie müssen daher sicherstellen, dass Ihre Reserveserver Zugriff auf die Daten des Hauptservers haben. Im folgenden Abschnitt stellen wir Möglichkeiten vor, dies zu erreichen.
Architekturen mit gemeinsam genutztem Speicher Gemeinsam genutzter Speicher ist eine Methode, mit der sich Schwachpunkte entfernen lassen, und zwar normalerweise mit einem SAN (mehr dazu in »Storage Area Networks« auf Seite 354). Bei dieser Strategie mountet der aktive Server das Dateisystem und arbeitet ganz normal. Wenn der aktive Server ausfällt, kann der Reserveserver dasselbe Dateisystem mounten, alle notwendigen Operationen zur Wiederherstellung ausführen und MySQL mit den Dateien des ausgefallenen Servers starten. Dieser Vorgang unterscheidet sich logisch nicht von der Reparatur des ausgefallenen Servers, geht jedoch viel schneller vonstatten, weil der Reserveserver bereits läuft und bereit ist. Die größten Verzögerungen, mit denen Sie es zu tun bekommen könnten, sind Dateisystemtests und die InnoDBWiederherstellung. Mit gemeinsam genutztem Speicher eliminieren Sie einige Szenarien, bei denen es zu Datenverlusten kommen könnte. Dennoch handelt es sich um eine Schwachstelle. Wenn der Speicher ausfällt, ist das gesamte System betroffen. Und wenn bei diesem Ausfall die Datendateien beschädigt werden, kann auch der Reserveserver sie nicht wiederherstellen. Wir empfehlen Ihnen dringend, InnoDB oder eine andere transaktionsfähige StorageEngine zusammen mit dem gemeinsam genutzten Speicher zu verwenden. Ein Absturz führt mit hoher Sicherheit zu einer Beschädigung der MyISAM-Tabellen, deren Reparatur dann wiederum sehr lange dauern kann.
Architekturen mit replizierten Festplatten Eine replizierte Festplatte stellt eine weitere Möglichkeit dar, Ihre Daten für den Fall eines katastrophalen Ausfalls eines Master-Servers abzusichern. Die Festplattenreplikation, die am häufigsten für MySQL eingesetzt wird, ist DRBD (http://www.drbd.org) in Kombination mit den Werkzeugen des Linux-HA-Projekts (mehr dazu später). DRBD ist eine synchrone, blockorientierte Replikation, die als Linux-Kernel-Modul implementiert ist. Es kopiert jeden Block von einem primären Gerät über eine Netzwerkkarte auf das Blockgerät eines anderen Servers (das sekundäre Gerät) und schreibt ihn, bevor der Block auf dem primären Gerät bestätigt wird.12
12 Tatsächlich können Sie die Synchronisationsstufe für DRDB sogar einstellen. Sie können sie auf asynchron setzen, darauf, dass es wartet, bis das entfernte Gerät die Daten empfängt, oder darauf, dass blockiert wird, bis das entfernte Gerät die Daten auf die Festplatte geschrieben hat. Es wird außerdem sehr empfohlen, eine Netzwerkkarte für DRBD vorzusehen.
490 | Kapitel 9: Skalierung und Hochverfügbarkeit
DRBD läuft nur im Aktiv-Passiv-Modus. Das passive Gerät ist eine heiße Reserve, auf das Sie nicht zugreifen können – nicht einmal im schreibgeschützten Modus –, solange es nicht zum primären Gerät gemacht wurde. Da Schreiboperationen auf dem sekundären Gerät abgeschlossen sein müssen, bevor sie auf dem primären Gerät als komplett angesehen werden, muss das sekundäre Gerät mindestens so gut funktionieren wie das primäre, da es ansonsten die Schreibleistung auf dem primären Gerät einschränkt. Falls Sie DRBD einsetzen, um eine austauschbare Reserve für den Fall des Ausfalls des primären Geräts zu haben, sollte die Hardware des Reserveservers der des primären Servers entsprechen. Wenn der aktive Server ausfällt, können Sie das sekundäre Gerät zum primären Gerät befördern. Da DRBD die Festplatte auf der Blockebene repliziert, kann jedoch das Dateisystem inkonsistent werden. Deshalb ist es am besten, wenn man für eine schnelle Wiederherstellung ein Journaling-Dateisystem benutzt. Sobald die Wiederherstellung abgeschlossen wurde, muss MySQL wahrscheinlich auch noch seine eigene Wiederherstellung durchführen. Ist der erste Server wiederhergestellt, synchronisiert er sein Gerät mit dem primären Gerät und nimmt die sekundäre Rolle ein. Bezüglich der tatsächlichen Implementierung eines Failovers ist DRBD sehr ähnlich zu SAN: Sie haben eine heiße Reservemaschine, und Sie sorgen dafür, dass sie ihre Dienste mit den gleichen Daten anbietet wie die ausgefallene Maschine. Der größte Unterschied besteht darin, dass es sich um replizierten Speicher handelt, nicht um gemeinsam genutzten Speicher. Mit DRBD präsentieren Sie also eine replizierte Kopie der Daten, während ein SAN die Daten, die Sie anbieten, vom selben physischen Gerät liefert wie die ausgefallene Maschine. In beiden Fällen sind die Caches des MySQL-Servers leer, wenn Sie auf der Reservemaschine beginnen. Im Gegensatz dazu sind die Caches eines ReplikationsSlaves wahrscheinlich zumindest teilweise aufgewärmt. DRBD besitzt einige hübsche Eigenarten und Fähigkeiten, mit denen Sie Probleme vermeiden können, die ansonsten für Cluster-Software typisch sind. Ein Beispiel ist das Split-Brain-Syndrom, das auftritt, wenn zwei Knoten sich gleichzeitig zu primären Knoten befördern. Sie können DRBD so konfigurieren, dass es das Split-Brain-Syndrom verhindert. Allerdings ist DRBD nicht für jeden Fall die perfekte Lösung. Schauen wir uns seine Nachteile an: • Das DRBD-Failover arbeitet nicht im Mikrosekundenbereich. Im Allgemeinen benötigt es wenigstens fünf Sekunden, um das sekundäre Gerät zum primären zu befördern – und darin ist die notwendige Dateisystem- und MySQL-Wiederherstellung noch nicht eingeschlossen. • Es ist teuer, weil Sie es im Aktiv-Passiv-Modus ausführen müssen. Das replizierte Gerät des heißen Reserveservers kann nicht für andere Aufgaben eingesetzt werden, während es im passiven Modus ist. Ob das wirklich ein Nachteil ist, hängt von Ihrem Standpunkt ab. Falls Sie echte Hochverfügbarkeit haben wollen und im Falle eines Ausfalls einen minderwertigen Service nicht tolerieren können, können Sie nicht mehr als die Last einer Maschine auf eine der beiden Maschinen packen, da Sie ansonsten bei einem Ausfall die Last gar nicht verarbeiten könnten. Sie können den Reserveserver für etwas anderes einsetzen, etwa als Replikations-Slave, verschwenden damit aber Ressourcen.
Hochverfügbarkeit | 491
• DRBD ist praktisch für MyISAM-Tabellen nicht einsetzbar, weil diese zu lange für die Überprüfung und Reparatur brauchen. MyISAM ist für keine Installation eine gute Wahl, die Hochverfügbarkeit verlangt; nehmen Sie stattdessen InnoDB oder eine andere Storage-Engine, die sich gut wiederherstellen lässt. • DRBD ersetzt Backups nicht. Wenn Ihre Daten auf der Festplatte aufgrund von bösartigen Störungen, Fehlern, Bugs oder Hardware-Ausfällen beschädigt werden, nützt DRBD nichts: Die replizierten Daten werden eine perfekte Kopie der beschädigten Originale sein. Sie brauchen Backups (oder eine zeitverzögerte MySQL-Replikation), um sich vor diesen Problemen zu schützen. Wir verwenden DRBD bevorzugt, um nur das Gerät zu replizieren, das die Binärlogs enthält. Wenn der aktive Knoten ausfällt, können Sie einen Log-Server auf dem passiven Knoten starten und die wiederhergestellten Binärlogs nehmen, um alle Slaves des ausgefallenen Masters an die neueste Binärlog-Position zu bringen (siehe »Einen Log-Server erzeugen« auf Seite 407). Anschließend wählen Sie einen der Slaves aus, befördern ihn zum Master und ersetzen damit das ausgefallene System.
Synchrone MySQL-Replikation Bei der synchronen Replikation kann eine Transaktion auf dem Master erst dann abgeschlossen werden, wenn sie auf einem oder mehreren Slave-Servern bestätigt wurde. Es gibt verschiedene Stufen der synchronen Replikation, für die mehrere Namen üblich sind. MySQL bietet momentan noch keine synchrone Replikation an, es gibt allerdings Lösungen von Drittanbietern. Eine dieser Lösungen sind die internen Patches von Google. Google bietet eine umfangreiche Liste von Patches für MySQL und InnoDB, die viele neue Funktionen und Eigenschaften einführen. Darunter ist die semisynchrone Replikation, die den Replikations-Master veranlasst zu warten, bis wenigstens ein Slave das Event empfangen hat, bevor er eine Transaktion bestätigt. Google hat seine Patches für MySQL 4.0.26 und 5.0.37 veröffentlicht. Sie können die Patches und mehrere verwandte Werkzeuge von http://code.google.com/p/google-mysql-tools herunterladen. Eine weitere Möglichkeit ist die Hochverfügbarkeitstechnik von Solid Information Technology, die auf solidDB für MySQL portiert wurde. Diese Lösung weist verschiedene Vorteile gegenüber der MySQL-Replikation auf: • Der Slave kann nicht hinter den Master zurückfallen. • Solid verwendet mehrere Threads zum Schreiben auf dem Slave, wobei sich die Replikationsleistung in vielen Fällen verbessert. • Die »Sicherheits«-Stufe während der Replikation kann vom Benutzer eingestellt werden. Im 1-Safe-Modus kehren Transaktionen zurück, sobald sie auf dem Master bestätigt wurden. Im 2-Safe-Modus kehren Transaktionen erst dann wieder zurück, wenn sie auch noch auf dem Slave bestätigt wurden, wodurch man für den Fall eines Ausfalls zusätzliche Sicherheit erhält.
492 | Kapitel 9: Skalierung und Hochverfügbarkeit
Das funktioniert jedoch nur mit solidDB, nicht mit MyISAM, InnoDB oder einer anderen Storage-Engine. Vielleicht portiert Solid seine Hochverfügbarkeitstechnik für künftige Versionen. Zusätzlich zu diesen beiden Varianten auf dem MySQL-Server selbst können Sie eine Middleware-Lösung wie Continuent benutzen.
Failover und Failback Failover (Ausfallsicherung) ist ein Vorgang, bei dem ein ausgefallener Server entfernt und stattdessen ein anderer Server benutzt wird. Es ist einer der wichtigsten Bestandteile einer Hochverfügbarkeitsarchitektur. Lassen Sie uns zuerst einige Begriffe definieren. Wir benutzen üblicherweise »Failover«, manche Leute nehmen auch »Fallback« (Rückgriff [auf andere Ressourcen]) als Synonym. Manchmal sagen Leute auch »Switchover«, wenn sie einen geplanten Wechsel (engl. Switch) anstatt der Reaktion auf einen Ausfall meinen. Wir benutzen den Begriff »Failback«, um das Gegenteil von Failover zu kennzeichnen. Mit Failback-Fähigkeiten kann das Failover ein wechselseitiger Prozess sein: Wenn Server A ausfällt und Server B ihn ersetzt, können Sie Server A reparieren und wieder zu ihm zurückkehren (fail back). Failover gibt es in vielen Varianten. Wir haben bereits einige beschrieben, weil Lastausgleich und Failover in vielerlei Hinsicht ähnlich sind und zwischen ihnen gar nicht so scharf getrennt werden kann. Im Allgemeinen glauben wir, dass eine vollständige Failover-Lösung mindestens dazu in der Lage sein muss, einen Server zu überwachen und automatisch zu ersetzen. Für die Anwendung erfolgt das idealerweise transparent. Lastausgleich muss diese Fähigkeit nicht bieten. In der Unix-Welt wird Failover üblicherweise mit den Werkzeugen erreicht, die das High Availability Linux-Projekt (http://linux-ha.org) bereitstellt. Diese Werkzeuge laufen – ungeachtet des Namens – auf vielen Unix-artigen Betriebssystemen. Das HeartbeatProgramm erledigt die Überwachung, und verschiedene andere Werkzeuge bieten IPÜbernahme und Lastausgleich. Sie können sie mit DRBD und/oder LVS kombinieren. Der wichtigste Teil des Failovers ist das Failback. Wenn Sie nicht nach Belieben zwischen den Servern hin- und herwechseln können, ist das Failover eine Sackgasse und verschiebt nur die Stillstandszeit. Deswegen mögen wir symmetrische Replikationstopologien, wie eine Dual-Master-Konfiguration, und lehnen eine Ring-Replikation mit drei oder mehr Co-Mastern ab. Wenn die Konfiguration symmetrisch ist, sind Failover und Failback die gleichen Operationen in unterschiedliche Richtungen. (Wir wollen es nicht versäumen zu erwähnen, dass DRBD eingebaute Failback-Fähigkeiten besitzt.) In manchen Anwendungen ist es sehr wichtig, dass Failover und Failback so schnell und atomar wie möglich sind. Selbst wenn sie es nicht sind, ist es keine gute Idee, sich auf Dinge zu verlassen, die man nicht selbst kontrollieren kann, wie etwa DNS-Änderungen
Hochverfügbarkeit | 493
oder Konfigurationsdateien von Anwendungen. Einige der schlimmsten Probleme zeigen sich erst, wenn ein System größer wird und Probleme wie erzwungende Neustarts von Anwendungen und der Bedarf nach Atomarität ihre Häupter erheben. Jeder, der einmal versucht hat, atomar Code über viele Server hinweg zu aktualisieren, weiß, dass dies schwierig ist. Da Lastausgleich und Failover eng miteinander verwandt sind und die gleiche Hardware oder Software oft beiden Zwecken dient, schlagen wir vor, dass alle Lastausgleichstechniken, die Sie wählen, auch Failover-Fähigkeiten bieten. Das ist der wahre Grund, weshalb wir empfohlen haben, DNS- oder Codeänderungen für den Lastausgleich zu vermeiden. Wenn Sie diese Strategien für den Lastausgleich benutzen, machen Sie sich zusätzliche Arbeit: Sie müssen den betroffenen Code später umschreiben, wenn Sie Hochverfügbarkeit brauchen. In den folgenden Abschnitten werden einige verbreitete Failover-Techniken vorgestellt.
Einen Slave befördern oder die Rollen tauschen Die Beförderung eines Slaves zum Master oder das Vertauschen der aktiven und passiven Rollen in einer Master-Master-Replikationsanordnung ist ein wichtiger Teil vieler Failover-Lösungen für MySQL. Ausführliche Erklärungen dafür finden Sie in »Die Master wechseln« auf Seite 415.
Virtuelle IP-Adressen oder IP-Übernahme Sie können Hochverfügbarkeit erreichen, indem Sie eine logische IP-Adresse zu einer MySQL-Instanz zuweisen, die bestimmte Dienste ausführen soll. Wenn die MySQL-Instanz ausfällt, verschieben Sie die IP-Adresse zu einem anderen MySQL-Server. Im Prinzip haben wir über diesen Ansatz schon in »IP-Adressen verschieben« auf Seite 481 geschrieben, allerdings setzen wir ihn nun ein, um anstelle des Lastausgleichs Failover zu bieten. Der Vorteil dieses Ansatzes ist seine Transparenz für die Anwendung. Er bricht bestehende Verbindungen ab, verlangt von Ihnen jedoch nicht, dass Sie die Konfiguration Ihrer Anwendung ändern. Es ist auch möglich, die IP-Adresse atomar zu verschieben, so dass alle Anwendungen die Änderung zur gleichen Zeit sehen. Das kann besonders dann wichtig sein, wenn ein Server zwischen den Zuständen »verfügbar« und »nicht verfügbar« hin- und herflattert. Folgende Nachteile gibt es: • Sie müssen entweder alle IP-Adressen im gleichen Netzwerksegment definieren oder eine Bridge einrichten. • Das Ändern der IP-Adresse erfordert Root-Zugang zum System. • Manchmal müssen Sie die ARP-Caches (Address Resolution Protocol) aktualisieren. Manche Netzwerkgeräte legen die ARP-Einträge manchmal zu lange im Cache ab und können eine IP-Adresse deshalb nicht sofort auf eine andere MAC-Adresse verschieben. 494 | Kapitel 9: Skalierung und Hochverfügbarkeit
• Sie müssen sicherstellen, dass die Netzwerkhardware schnelle IP-Übernahmen unterstützt. Manchmal verlangt die Hardware das Klonen der MAC-Adresse, damit das richtig funktioniert. • Es ist möglich, dass ein Server seine IP-Adresse behält, obwohl er nicht voll funktionstüchtig ist, so dass Sie ihn physisch herunterfahren oder vom Netzwerk trennen müssen. Fließende IP-Adressen und IP-Übernahmen funktionieren gut für das Failover zwischen Maschinen, die zueinander lokal sind, d.h. sich im selben Teilnetz befinden.
Warten, dass Änderungen sich ausbreiten Wenn Sie auf einer Ebene Redundanz definieren, müssen Sie oft darauf warten, bis eine niedrigere Ebene eine Änderung tatsächlich weitergegeben hat. Wir haben weiter vorn in diesem Kapitel bereits ausgeführt, dass das Ändern der Server über das DNS eine schlechte Lösung ist, weil das DNS Änderungen so langsam weitergibt. Das Ändern von IP-Adressen bietet Ihnen mehr Kontrolle, allerdings hängen IP-Adressen in einem LAN ebenfalls davon ab, dass eine niedrigere Ebene – ARP – die Änderungen weitergibt.
Der MySQL Master-Master Replication Manager Das Programm MySQL Master-Master Replication Manager (http://code.google.com/p/ mysql-master-master) oder kurz mmm besteht aus einer Reihe von Skripten, die Überwachung, Failover und die Verwaltung der Master-Master-Replikationskonfigurationen erledigen. Ungeachtet seines Namens kann es den Vorgang des Failovers auch für andere Topologien automatisieren, darunter für einfache Master-Slave- sowie Master-MasterKonfigurationen mit einem oder vielen Slaves. Es nutzt die Abstraktion einer Rolle, also etwa eines Lesers oder Schreibers, sowie einen Mix aus permanenten und fließenden IPAdressen. Es bemerkt, wenn ein Server ausfällt, und weist IP-Adressen neu zu, um notfalls »die Rollen zu tauschen«. Darüber hinaus unterstützt es geplantes Failover zu Wartungszwecken oder für andere Aufgaben. Die normale Anordnung besteht aus einem Paar aus Co-Master-MySQL-Servern, auf denen jeweils ein mmmd_agent-Prozess läuft. Sie müssen beide mit einigen grundlegenden Informationen konfigurieren, wie IP-Adressen, Benutzernamen und Passwörtern. Jeder der mmmd_agent-Prozesse ist sich des jeweils anderen Prozesses bewusst. Es gibt außerdem einen separaten Überwachungsknoten. Dieser sollte nicht auf der gleichen Hardware laufen wie die beiden Co-Master. Er überwacht beide Knoten und erledigt das Failover – d.h. verschiebt die Schreiberrolle. Es gibt insgesamt drei virtuelle IPAdressen, die Sie für die Verbindung zu den MySQL-Servern benutzen können: zwei für die Leserrolle und eine für den Schreiber. Mit dem mmm_control-Programm haben Sie die Möglichkeit, die MySQL-Instanzen anzuschauen und zu kontrollieren und bei Bedarf die Schreiberrolle zu verschieben.
Hochverfügbarkeit | 495
Sie können mmm mit anderen Techniken kombinieren (wie etwa Googles semisynchronen Replikations-Patches, die wir weiter vorn in diesem Kapitel schon erwähnt haben), um die Zuverlässigkeit und Verfügbarkeit weiter zu erhöhen.
Lösungen mit Vermittlern Für Failover und Failback können Sie Proxies, Portweiterleitung, Network Address Translation (NAT) und Hardware-Load-Balancer einsetzen. Diese führen allerdings sich selbst als Schwachstellen ein, die Sie wiederum redundant ausführen müssen, um Probleme zu vermeiden. Eine schöne Sache, die Sie mit solchen Lösungen erreichen, ist, dass Sie ein entferntes Rechenzentrum so erscheinen lassen, als würde es sich im gleichen Netzwerk befinden wie Ihre Anwendung. Auf diese Weise können Sie Techniken wie fließende IP-Adressen einsetzen, um Ihre Anwendung mit einem völlig anderen Rechenzentrum kommunizieren zu lassen. Sie können die einzelnen Anwendungsserver in den jeweiligen Rechenzentren so konfigurieren, dass sie sich über ihren eigenen Vermittler verbinden, der jeweils den Verkehr an die Maschinen im aktiven Rechenzentrum weiterleitet. Abbildung 9-7 verdeutlicht diese Konfiguration. MySQL-Server (aktiv)
Replikation
MySQL-Server (passiv)
Webserver
Proxy
Rechenzentrum 1
Webserver
Proxy
Rechenzentrum 2
Abbildung 9-7: Mit MySQL Proxy MySQL-Verbindungen durch Rechenzentren leiten
Wenn die MySQL-Installation des aktiven Rechenzentrums komplett ausfällt, kann der Vermittler den Verkehr an den Server-Pool in dem anderen Rechenzentrum leiten. Die Anwendung muss den Unterschied überhaupt nicht mitbekommen. Der größte Nachteil dieser Konfiguration ist die hohe Latenz zwischen dem Apache-Server in dem einen Rechenzentrum und den MySQL-Servern in dem anderen Rechenzentrum. Um dieses Problem zu mildern, können Sie den Webserver im Redirect-Modus betreiben. Dadurch wird der Verkehr an das Rechenzentrum umgeleitet, das den Pool der aktiven MySQL-Server beherbergt. Sie erreichen das auch mit einem HTTP-Proxy.
496 | Kapitel 9: Skalierung und Hochverfügbarkeit
Abbildung 9-7 zeigt MySQL Proxy als Mittel für die Verbindung zu den MySQL-Servern. Sie können diesen Ansatz aber auch mit vielen Vermittlerarchitekturen kombinieren, wie etwa LVS und Hardware-Load-Balancern.
Failover in der Anwendung verarbeiten Manchmal ist es einfacher oder flexibler, wenn die Anwendung das Failover erledigt. Falls z.B. in der Anwendung ein Fehler auftritt, der normalerweise von einem externen Beobachter nicht bemerkt wird, wie etwa eine Fehlermeldung, die eine Beschädigung der Datenbank anzeigt, kann sie den Failover-Prozess selbst durchführen. Das Integrieren des Failover-Vorgangs mag zwar attraktiv erscheinen, funktioniert aber oft nicht besonders gut. Die meisten Anwendungen enthalten viele Komponenten, wie cron-Jobs, Konfigurationsdateien und Skripten in verschiedenen Programmiersprachen. Die Integration des Failovers in die Anwendung ist daher möglicherweise schwer in den Griff zu bekommen, vor allem, wenn die Anwendung wächst und komplizierter wird. Es ist jedoch keine schlechte Idee, wenn man eine Überwachung in die Anwendung einbaut und diese den Failover-Prozess initiieren lässt, falls das erforderlich sein sollte. Die Anwendung muss außerdem in der Lage sein, passende Rückmeldungen für den Benutzer zu bieten, indem die Funktionalität entsprechend herabgesetzt wird und passende Fehlermeldungen angezeigt werden.
Hochverfügbarkeit | 497
KAPITEL 10
Optimierung auf Anwendungsebene
Dieses Buch wäre ohne ein Kapitel über die Optimierung der Anwendungen, die Verbindungen zu MySQL haben, nicht komplett, weil häufig diese für Performance-Probleme verantwortlich sind, die MySQL zu verursachen scheint. Wir konzentrieren uns in diesem Buch hauptsächlich auf die Optimierung von MySQL, wollen aber das Gesamtbild nicht vernachlässigen. Es gibt keine Möglichkeit, MySQL so gut zu optimieren, dass ein schlechtes Anwendungsdesign damit ausgeglichen werden würde. Manchmal ist es sogar besser, Operationen ganz aus MySQL herauszunehmen und in der Anwendung oder mithilfe von anderen Werkzeugen ausführen zu lassen, die eine viel bessere Leistung bieten. Dieses Kapitel ist keine Referenz für den Aufbau von Hochleistungsanwendungen, allerdings hoffen wir, dass es Ihnen hilft, einige verbreitete Fehler zu vermeiden, die die Leistung von MySQL herabsetzen können. Wir konzentrieren uns auf Webanwendungen, weil MySQL oft für sie eingesetzt wird.
Überblick über die Anwendungsleistung Die Suche nach einer schnelleren Performance beginnt ganz einfach: Die Anwendung braucht zu lange, um auf Anforderungen zu antworten, und Sie müssen etwas dagegen tun. Aber wo genau liegt das Problem? Die üblichen Engpässe sind langsame Abfragen, Sperren, CPU-Sättigung, Netzwerklatenz und Datei-Ein-/Ausgabe. Alle diese Dinge können zu einem Problem werden, wenn die Anwendung falsch konfiguriert ist oder Ressourcen falsch einsetzt.
Suchen Sie die Quelle des Problems Die erste Aufgabe besteht darin, den Übeltäter zu finden. Das geht viel einfacher, wenn Sie in Ihre Anwendung irgendwelche Profiling-Fähigkeiten eingebaut haben. Wenn dies der Fall ist, Sie aber trotzdem nicht erkennen können, was für die langsame Performance verantwortlich ist, müssen Sie vielleicht weitere Profiling-Aufrufe hinzufügen. Suchen Sie nach Stellen, an denen eine Ressource entweder langsam ist oder viele Male angefordert wird.
498 |
Wenn Ihre Anwendung wartet, weil sie CPU-gebunden ist und Sie eine hohe Nebenläufigkeit haben, könnte die »verlorene Zeit«, die wir in »Eine Anwendung profilieren« auf Seite 59 erwähnt haben, das Problem darstellen. Aus diesem Grund hilft es manchmal, bei eingeschränkter Nebenläufigkeit zu profilieren. Die Netzwerklatenz kann eine Menge Zeit konsumieren, selbst in einem lokalen Netzwerk. Das Profiling auf Anwendungsebene umfasst bereits die Netzwerklatenz, so dass Sie die Wirkungen von Netzwerkrundreisen in Ihrem Profiling-System sehen sollten. Falls eine Seite z.B. 1.000 Abfragen ausführt, addiert sich eine halbe Millisekunde Netzwerklatenz auf eine Antwortzeit von einer halben Sekunde. Für eine High-PerformanceAnwendung ist das viel. Mit einem gründlichen Profiling auf Anwendungsebene dürfte es nicht schwer sein, die Quelle Ihres Problems auszumachen. Falls das Profiling nicht in Ihre Anwendung integriert ist, dann fügen Sie es nach Möglichkeit hinzu. Ist das nicht möglich, probieren Sie einige der Vorschläge aus »Wann können Sie keinen Profiling-Code hinzufügen?« auf Seite 82. Das geht vermutlich einfacher und schneller, als wenn Sie sich in irgendwelche Theorien darüber versteigen, was die langsame Ausführung verursacht.
Suchen Sie nach verbreiteten Problemen Uns begegnen in Anwendungen immer wieder die gleichen Probleme, oft weil schlecht gestaltete vorgefertigte Systeme oder beliebte Frameworks genommen wurden, die die Entwicklung vereinfachen. Es ist zwar manchmal einfacher und schneller, etwas zu nehmen, was man nicht selbst hergestellt hat, allerdings erhöht man dadurch das Risiko, falls man nicht so genau weiß, was im Inneren des Systems eigentlich passiert. Folgende Dinge müssen Sie überprüfen: • Was benutzt die CPU-, Festplatten-, Netzwerk- und Speicherressourcen auf den jeweiligen beteiligten Maschinen? Erscheinen Ihnen die Zahlen vernünftig? Falls nicht, dann überprüfen Sie erst einmal die Anwendungen, die die Ressourcen verschwenden. Manchmal bildet die Konfiguration den einfachsten Weg, um Probleme zu lösen. Falls z.B. Apache der Speicher ausgeht, weil er 1.000 WorkerProzesse erzeugt hat, die jeweils 50 MByte Speicher benötigen, dann können Sie die Anwendung so konfigurieren, dass sie weniger Apache-Worker benötigt. Oder Sie konfigurieren das System so, dass es weniger Speicher für die einzelnen Prozesse einsetzt. • Benutzt die Anwendung wirklich all die Daten, die sie bekommt? Es ist ein verbreiteter Fehler, 1.000 Zeilen zu holen, aber nur 10 anzuzeigen und den Rest wegzuwerfen. (Wenn dagegen die Anwendung die anderen 990 Zeilen für einen späteren Einsatz im Cache speichert, könnte es sich um eine beabsichtigte Optimierung handeln.) • Führt die Anwendung Verarbeitungen durch, die in der Datenbank erledigt werden sollten, oder umgekehrt? Zwei Beispiele sind das Holen aller Zeilen aus einer Tabelle, um sie zu zählen, und das Durchführen komplexer Stringmanipulationen in Überblick über die Anwendungsleistung | 499
der Datenbank. Datenbanken eignen sich gut für das Zählen von Zeilen, während Anwendungssprachen gut bei regulären Ausdrücken sind. Verwenden Sie das beste Werkzeug für die jeweilige Aufgabe. • Führt die Anwendung zu viele Abfragen durch? Oft sind ORM-(Object-Relational Mapping-)Abfrageschnittstellen schuld, die »die Programmierer davor bewahren, SQL schreiben zu müssen«. Der Datenbankserver soll Daten aus mehreren Tabellen vergleichen. Entfernen Sie die geschachtelten Schleifen im Code, und schreiben Sie stattdessen ein Join. • Führt die Anwendung zu wenige Abfragen durch? Klar, wir haben gerade gesagt, dass zu viele Abfragen problematisch sein können. Manchmal aber können »manuelle Joins« und ähnliche Praktiken eine gute Idee sein, weil sie ein feineres und effizienteres Caching erlauben, weniger Locking erfordern (speziell für MyISAM) und manchmal sogar eine schnellere Ausführung zulassen, wenn Sie einen Hash-Join im Anwendungscode emulieren. (Die MySQL-Join-Methode mit geschachtelten Schleifen ist nicht immer effizient.) • Verbindet sich die Anwendung unnötigerweise mit MySQL? Wenn Sie die Daten aus dem Cache bekommen können, sollten Sie keine Verbindung herstellen. • Verbindet sich die Anwendung zu oft mit derselben MySQL-Instanz, vielleicht weil unterschiedliche Teile der Anwendung ihre eigenen Verbindungen öffnen? Normalerweise ist es besser, immer die gleiche Verbindung zu benutzen. • Führt die Anwendung viele Abfragen durch, die eigentlich »Ausschuss« sind? Ein verbreitetes Beispiel ist die Auswahl der gewünschten Datenbank vor jeder Abfrage. Es ist wahrscheinlich keine schlechte Idee, sich immer mit einer bestimmten Datenbank zu verbinden und vollqualifizierte Namen für Tabellen zu verwenden. (Damit lassen sich auch Abfragen aus dem Log oder über SHOW PROCESSLIST leichter analysieren, weil Sie sie ausführen können, ohne die Datenbank wechseln zu müssen.) Das »Vorbereiten« der Verbindung ist ein weiteres verbreitetes Problem. Speziell der Java-Treiber erledigt während der Vorbereitung viele Dinge, von denen Sie die meisten deaktivieren können. Eine weitere häufig vorkommende Ausschussabfrage ist SET NAMES UTF8, die sowieso das Falsche macht (sie ändert nicht den Zeichensatz der Clientbibliothek, sondern beeinflusst nur den Server). Wenn Ihre Anwendung für den größten Teil der Arbeit einen bestimmten Zeichensatz benutzt, dann vermeiden Sie es einfach, ihn zu ändern, und stellen Sie ihn als Vorgabewert ein. • Benutzt die Anwendung einen Verbindungs-Pool? Das kann sowohl gut als auch schlecht sein. Es hilft, die Anzahl der Verbindungen zu beschränken, was gut ist, wenn die Verbindungen nicht für viele Abfragen verwendet werden (ein typisches Beispiel sind Ajax-Anwendungen). Es kann aber auch Nebenwirkungen haben, wie etwa Anwendungen, die die Transaktionen, temporären Tabellen, verbindungspezifischen Einstellungen und benutzerdefinierten Variablen anderer Anwendungen stören.
500 | Kapitel 10: Optimierung auf Anwendungsebene
• Benutzt die Anwendung persistente Verbindungen? Diese können zu viel zu vielen Verbindungen zu MySQL führen. Im Allgemeinen sind solche Verbindungen keine gute Idee, es sei denn, die Kosten für die Verbindung zu MySQL sind aufgrund eines langsamen Netzwerks sehr hoch, die Verbindung wird nur für eine oder zwei schnelle Abfragen verwendet oder Sie verbinden sich so oft, dass Ihnen auf dem Client die lokalen Portnummern ausgehen (siehe »Die Netzwerkkonfiguration« auf Seite 358). Wenn Sie MySQL korrekt konfigurieren, brauchen Sie wahrscheinlich keine persistenten Verbindungen. Benutzen Sie skip-name-resolve, um Reverse-DNS-Lookups zu vermeiden, und sorgen Sie dafür, dass thread_cache hoch genug gesetzt ist. • Hält die Anwendung Verbindungen selbst dann offen, wenn sie sie nicht benutzt? In diesem Fall – speziell, wenn sie sich mit vielen Servern verbindet – verbraucht sie wahrscheinlich Verbindungen, die andere Prozesse brauchen. Nehmen Sie z.B. an, dass Sie sich mit 10 MySQL-Servern verbinden. Es ist kein Problem, von einem Apache-Prozess 10 Verbindungen zu bekommen, allerdings tut nur eine von ihnen zu einem bestimmten Zeitpunkt etwas. Die anderen neun verbringen viel Zeit im Zustand Sleep. Wird einer der Server langsamer oder gibt es einen langen Netzwerkaufruf, haben die anderen Server darunter zu leiden, weil ihnen die Verbindungen ausgehen. Die Lösung besteht darin, zu kontrollieren, wie die Anwendung die Verbindungen benutzt. Sie können z.B. Operationen gleich stapelweise im Wechsel an die einzelnen MySQL-Instanzen richten und die jeweilige Verbindung schließen, bevor Sie die nächste abfragen. Wenn Sie zwei aufwendige Operationen durchführen, wie etwa Aufrufe an einen Webservice, können Sie sogar die MySQLVerbindung schließen, die zeitaufwendige Arbeit verrichten, die MySQL-Verbindung wieder öffnen und weiter mit der Datenbank arbeiten. Der Unterschied zwischen persistenten Verbindungen und Verbindungs-Pooling ist vielleicht verwirrend. Persistente Verbindungen verursachen möglicherweise die gleichen Nebenwirkungen wie das Verbindungs-Pooling, da eine wiederverwendete Verbindung in beiden Fällen zustandsbehaftet ist. Allerdings führen Verbindungs-Pools nicht zu so vielen Verbindungen zum Server, weil sie die Verbindungen in Warteschlangen einreihen und mehrere Prozesse sich Verbindungen teilen können. Persistente Verbindungen werden dagegen prozessweise erzeugt und können nicht von mehreren Prozessen gemeinsam genutzt werden. Verbindungs-Pools bieten darüber hinaus mehr Kontrolle über die Verbindungsregeln als gemeinsam genutzte Verbindungen. Sie können einen Pool so konfigurieren, dass er sich automatisch erweitert; die übliche Praxis ist es allerdings, Verbindungsanforderungen in eine Warteschlange einzureihen, wenn der Pool komplett ausgelastet ist. Die Verbindungsanforderungen warten daher auf dem Anwendungsserver, anstatt den MySQL-Server mit zu vielen Verbindungen zu überlasten. Es gibt viele Methoden, um Abfragen und Verbindungen zu beschleunigen, wobei die allgemeine Regel jedoch lautet, dass es besser ist, sie ganz und gar zu vermeiden, als zu versuchen, sie zu beschleunigen.
Überblick über die Anwendungsleistung | 501
Webserverprobleme Apache ist der beliebteste Server für Webanwendungen. Er eignet sich gut für verschiedene Zwecke, verbraucht allerdings bei falscher Verwendung viele Ressourcen. Am verbreitetsten ist das Problem, dass seine Prozesse zu lange am Leben erhalten werden und dass er für einen Mix aus Aufgaben benutzt wird, anstatt ihn separat für die einzelnen Arten von Arbeit zu optimieren. Apache wird normalerweise mit mod_php, mod_perl und mod_python in einer »Prefork«Konfiguration eingesetzt. Preforking sieht für jede Anforderung einen Prozess vor. Da die PHP-, Perl- und Python-Skripten sehr fordernd sein können, ist es nicht ungewöhnlich, dass ein einzelner Prozess 50 oder 100 MByte an Speicher nutzt. Wenn eine Anforderung abgeschlossen wird, gibt sie den größten Teil ihres Speichers an das Betriebssystem zurück – allerdings nicht alles. Apache hält den Prozess offen und setzt ihn wieder für künftige Anforderungen ein. Falls also die nächste Anforderung nur eine statische Datei haben will, z.B. eine CSS-Datei oder ein Bild, kreuzen Sie mit einem dicken, fetten Prozess auf, der dann nur eine einfache Anforderung erledigt. Aus diesem Grund ist es gefährlich, Apache als allgemeinen Webserver einzusetzen. Er ist allgemein, Sie erzielen aber eine deutlich bessere Leistung, wenn Sie ihn spezialisieren. Das andere große Problem besteht darin, dass Prozesse für eine lange Zeit ausgelastet sein können, wenn Sie Keep-Alive aktiviert haben. Und selbst wenn Sie das nicht haben, bleiben einige der Prozesse einfach zu lange am Leben und »verfüttern« Inhalt an einen Client, der die Daten nur langsam holt.1 Oft begehen die Leute den Fehler, die vorgegebenen Apache-Module aktiviert zu lassen. Sie können den Ressourceneinsatz von Apache zurechtstutzen, indem Sie Module entfernen, die Sie nicht brauchen. Das ist einfach: Schauen Sie sich die Apache-Konfigurationsdatei an, und kommentieren Sie unerwünschte Module aus. Starten Sie anschließend Apache neu. Sie können auch unbenutzte PHP-Module aus Ihrer php.ini-Datei entfernen. Im Prinzip ist es so, dass Sie quasi übergewichtige Apache-Prozesse erhalten, wenn Sie eine Allzweck-Apache-Konfiguration anlegen und diese dem Web direkt präsentieren. Diese Prozesse verschwenden die Ressourcen Ihres Webservers. Sie halten möglicherweise viele Verbindungen zu MySQL offen und verschwenden damit auch die Ressourcen von MySQL. Hier sind einige Methoden, mit denen Sie die Last auf Ihren Servern vermindern können:2
1 Eine solche Fütterung oder besser Überfütterung tritt auf, wenn ein Client eine HTTP-Anforderung auslöst, dann aber das Ergebnis gar nicht so schnell holt. Die HTTP-Verbindung und damit der Apache-Prozess bleiben geöffnet, bis der Client das gesamte Ergebnis geholt hat. 2 Ein gutes Buch über die Optimierung von Webanwendungen ist High Performance Websites von Steve Souders (O’Reilly). Obwohl es hauptsächlich darum geht, wie man Websites aus Sicht des Clients beschleunigt, eignen sich die Vorgehensweisen, die er empfiehlt, auch gut für Ihre Server.
502 | Kapitel 10: Optimierung auf Anwendungsebene
• Verwenden Sie Apache nicht, um statische Inhalte anzubieten, oder nehmen Sie wenigstens eine andere Apache-Instanz. Beliebte Alternativen sind lighttpd und nginx. • Benutzen Sie einen Caching-Proxy wie Squid oder Varnish, um zu verhindern, dass Anforderungen Ihre Webserver erreichen. Selbst wenn Sie nicht alle Seiten auf dieser Ebene im Cache ablegen können, so doch einen Großteil der Seite. Benutzen Sie Techniken wie Edge Side Includes (ESI; siehe http://www.esi.org), um den kleinen dynamischen Teil der Seite in den statischen Teil einzubetten, der im Cache liegt. • Richten Sie sowohl für dynamische als auch für statische Inhalte eine Verfallsstrategie ein. Sie können Cache-Proxies wie Squid benutzen, um Inhalt explizit ungültig zu machen. Wikipedia verwendet diese Technik, um Artikel aus den Caches zu entfernen, wenn sie sich geändert haben. • Manchmal müssen Sie vielleicht die Anwendung so ändern, dass Sie längere Verfallszeiten benutzen können. Falls Sie z.B. den Browser anweisen, CSS- und JavaScript-Dateien für immer im Cache zu speichern, und dann eine Änderung am HTML der Site veröffentlichen, werden die Seiten möglicherweise schlecht dargestellt. Sie können die Dateien explizit mit einem eindeutigen Dateinamen für die einzelnen Revisionen versionieren. Passen Sie z.B. Ihr Website-Publishing-Skript so an, dass die CSS-Dateien nach /css/123_frontpage.css kopiert werden, wobei 123 die Nummer der Subversion-Revision ist. Gleiches können Sie mit den Dateinamen der Bilder tun – benutzen Sie einen Dateinamen niemals doppelt, und Ihre Seiten werden niemals beschädigt, wenn Sie sie ändern, egal, wie lange der Browser sie im Cache speichert. • Lassen Sie nicht zu, dass Apache den Client überfüttert. Er wird dadurch nicht nur langsam, sondern erleichtert auch Denial-of-Service-Angriffe. Hardware-LoadBalancer führen typischerweise eine Pufferung durch, so dass Apache schnell fertig werden kann und der Load Balancer den Client aus dem Puffer versorgt. Sie können der Anwendung auch lighttpd, Squid oder Apache im Event-gesteuerten Modus voranstellen. • Aktivieren Sie die gzip-Komprimierung. Sie ist für moderne CPUs sehr billig und spart eine Menge Verkehr. Falls Sie CPU-Zyklen sparen wollen, können Sie den Cache einsetzen und die komprimierte Version der Seite mit einem leichtgewichtigen Server wie lighttpd anbieten. • Konfigurieren Sie Apache für langlebige Verbindungen nicht mit einem Keep-Alive, weil dadurch umfangreiche Apache-Prozesse sehr lange am Leben bleiben würden. Erlauben Sie es stattdessen einem serverseitigen Proxy, das Keep-Alive zu erledigen, und schirmen Sie Apache vor dem Client ab. Es ist in Ordnung, wenn Sie die Verbindungen zwischen dem Proxy und Apache mit einem Keep-Alive konfigurieren, weil der Proxy nur wenige Verbindungen benutzt, um die Daten von Apache zu holen. Abbildung 10-1 verdeutlicht den Unterschied.
Webserverprobleme | 503
Keep-Alive-Verbindungen
Apache-Worker Clients
Keep-Alive-Verbindungen
Keep-Alive-Verbindungen Apache-Worker Proxy
Clients
Abbildung 10-1: Ein Proxy kann Apache vor langlebigen Keep-Alive-Verbindungen abschirmen, wodurch weniger Apache-Worker entstehen.
Diese Taktiken sollten dazu führen, dass die Apache-Prozesse nicht mehr so lange leben, so dass Sie nicht mehr Prozesse erhalten, als Sie benötigen. Manche Operationen führen dennoch dazu, dass ein Apache-Prozess lange am Leben bleibt und viele Ressourcen verbraucht. Ein Beispiel ist eine Abfrage an eine externe Ressource, die eine hohe Latenz aufweist, wie etwa ein entfernter Webservice. Dieses Problem ist oft nicht lösbar.
Die optimale Nebenläufigkeit finden Jeder Webserver hat eine optimale Nebenläufigkeit – d.h. eine optimale Anzahl an nebenläufigen Verbindungen, die zu Abfragen führen, die so schnell wie möglich ausgeführt werden, ohne dass Ihre Systeme überlastet werden. Vermutlich sind einige Anläufe erforderlich, um diesen »magischen Wert« zu finden, es lohnt sich aber. Für eine viel besuchte Website ist es üblich, dass gleichzeitig Tausende von Verbindungen an den Webserver verarbeitet werden. Allerdings müssen nur einige dieser Verbindungen aktiv Anforderungen verarbeiten. Die anderen lesen nur Anforderungen, verarbeiten Datei-Uploads, liefern Inhalte aus oder erwarten einfach weitere Anforderungen vom Client. Wenn die Nebenläufigkeit zunimmt, gibt es irgendwann einen Punkt, an dem der Server seinen Spitzendurchsatz erreicht. Hinter diesem Punkt bleibt der Durchsatz gleich und beginnt oft abzufallen. Noch wichtiger: Die Antwortzeit (Latenz) beginnt zuzunehmen. Wieso geschieht das? Überlegen Sie einmal, was passiert, wenn Sie eine CPU haben und der Server gleichzeitig 100 Anforderungen empfängt. Es ist eine Sekunde der CPU-Zeit
504 | Kapitel 10: Optimierung auf Anwendungsebene
erforderlich, um eine Anforderung zu verarbeiten. Nehmen Sie an, Sie haben einen perfekten Betriebssystem-Scheduler ohne Overhead, und es fällt auch beim Kontextwechsel kein Overhead an. Dann brauchen die Anforderungen insgesamt 100 CPU-Sekunden, um vollständig verarbeitet zu werden. Welches ist die beste Methode, um die Anforderungen zu bedienen? Sie können sie nacheinander in eine Warteschlange einreihen, oder Sie führen sie parallel aus und wechseln zwischen ihnen hin und her, wobei Sie jeder Anforderung die gleiche Zeit zugestehen. In beiden Fällen beträgt der Durchsatz eine Anforderung pro Sekunde. Allerdings beträgt die durchschnittliche Latenz 50 Sekunden bei der Warteschlangenvariante (Nebenläufigkeit = 1) und 100 Sekunden, wenn sie parallel ablaufen (Nebenläufigkeit = 100). In der Praxis wäre die durchschnittliche Latenz für die parallele Ausführung sogar noch höher, weil auch die Wechsel Kosten verursachen. Bei einer CPU-gebundenen Last ist die optimale Nebenläufigkeit gleich der Anzahl der CPUs (oder CPU-Kerne). Allerdings sind die Prozesse gar nicht immer lauffähig, weil sie blockierende Aufrufe ausführen, wie etwa Ein-/Ausgaben, Datenbankabfragen und Netzwerkanforderungen. Die optimale Nebenläufigkeit ist daher normalerweise höher als die Anzahl der CPUs. Sie können die optimale Nebenläufigkeit abschätzen. Das erfordert allerdings ein exaktes Profiling. Meist ist es einfacher, mit verschiedenen Nebenläufigkeitswerten herumzuexperimentieren und festzustellen, wann sich der Spitzendurchsatz ergibt, ohne dass die Antwortzeit leidet.
Caching Caching ist für stark belastete Anwendungen ausgesprochen wichtig. Eine typische Webanwendung liefert eine Menge Inhalt, dessen Generierung viel teurer ist als seine Speicherung im Cache (einschließlich der Kosten für die Überprüfung und den Verfall des Cache). Das Caching verbessert daher die Leistung normalerweise um Größenordnungen. Der Trick besteht darin, die richtige Kombination aus Granularität und Verfallsregeln zu finden. Außerdem müssen Sie entscheiden, welcher Inhalt in den Cache gelangt und wo er gespeichert wird. Eine typische stark belastete Anwendung besitzt viele Caching-Ebenen. Caching wird nicht nur in Ihren Servern durchgeführt, sondern bei praktisch jedem Schritt, auch im Webbrowser des Benutzers (für diesen sind dann auch die Verfalls-Header gedacht). Im Allgemeinen gilt: Je näher der Cache sich am Client befindet, umso mehr Ressourcen spart er und umso effektiver ist er. Es ist besser, ein Bild aus dem Cache des Browsers auszuliefern als aus dem Speicher des Webservers, was wiederum besser ist, als es von der Festplatte des Servers zu lesen. Jede Art von Cache hat ihre einzigartigen Charakteristika, wie etwa Größe und Latenz. Einige von ihnen werden wir in den folgenden Abschnitten untersuchen.
Caching | 505
Sie können Caches grob in zwei Kategorien einordnen: passive Caches und aktive Caches. Passive Caches tun nichts weiter, als zu speichern und Daten zurückzuliefern. Wenn Sie etwas aus einem passiven Cache anfordern, erhalten Sie entweder das Ergebnis, oder der Cache teilt Ihnen mit, dass das angeforderte Element nicht existiert. Ein Beispiel für einen passiven Cache ist memcached. Im Gegensatz dazu tut der aktive Cache etwas, wenn es zu einem »Cache-Miss« kommt, ein angefordertes Element also nicht existiert. Normalerweise reicht er Ihre Anforderung an einen anderen Teil der Anwendung weiter, die dann das angeforderte Ergebnis generiert. Der aktive Cache speichert dann das Ergebnis und liefert es zurück. Der SquidCaching-Proxy-Server ist ein aktiver Cache. Wenn Sie Ihre Anwendung entwerfen, werden Sie normalerweise wollen, dass Ihre Caches aktiv sind (das wird auch als transparent bezeichnet), weil sie die ÜberprüfenGenerieren-Speichern-Logik vor der Anwendung verbergen. Sie können aktive Caches auf passiven Caches aufbauen.
Caching hilft nicht immer Vergewissern Sie sich, dass das Caching die Leistung auch tatsächlich verbessert. Es hilft nämlich nicht immer. In der Praxis geht es z.B. oft schneller, den Inhalt aus dem Speicher von lighttpd auszuliefern als von einem Caching-Proxy. Das gilt vor allem, wenn sich der Cache des Proxys auf der Festplatte befindet. Der Grund ist ganz einfach: Caching ist selbst mit Aufwand verbunden. Das Überprüfen des Cache verursacht einen gewissen Aufwand, genau wie das Anbieten der Daten aus dem Cache, falls es einen Treffer gab. Auch das Ungültigmachen des Cache und das Speichern der Daten in ihm ist aufwendig. Caching ist nur dann hilfreich, wenn diese Kosten niedriger sind als die Kosten für das Generieren und Anbieten der Daten ohne Cache. Wenn Sie die Kosten für all diese Operationen kennen, können Sie berechnen, wie viel der Cache hilft. Ohne Cache fallen im Prinzip nur Kosten für das Generieren der Daten für die einzelnen Anforderungen an. Mit Cache bestehen die Kosten aus den Kosten für das Überprüfen des Cache plus die Wahrscheinlichkeit für einen Cache-Miss multipliziert mit den Kosten für das Generieren der Daten plus die Wahrscheinlichkeit für einen Cache-Treffer multipliziert mit den Kosten für das Anbieten der Daten aus dem Cache. Wenn die Kosten mit Cache niedriger sind als ohne, dann ist es eine Verbesserung, allerdings ohne Garantie. Denken Sie außerdem daran, dass manche Caches billiger sind als andere, wie der oben erwähnte Fall der Daten aus dem lighttpd-Speicher zeigt.
Caching unterhalb der Anwendung Der MySQL-Server besitzt seine eigenen internen Caches. Außerdem können Sie Ihren eigenen Cache sowie Summary-Tabellen erzeugen. Sie können Ihre eigenen Cache-Tabellen entwerfen, um für Sie das meiste aus dem Filtern, Sortieren, aus Joins mit anderen
506 | Kapitel 10: Optimierung auf Anwendungsebene
Tabellen, dem Zählen oder anderen Aufgaben herauszuholen. Cache-Tabellen sind darüber hinaus beständiger als viele Caches auf Anwendungsebene, weil sie Serverneustarts überleben. Wir haben in den Kapiteln 3 und 4 über diese Cache-Strategien geschrieben, weshalb wir uns in diesem Kapitel auf das Caching auf der Anwendungsebene sowie darüber konzentrieren.
Caching auf Anwendungsebene Ein Cache auf Anwendungsebene speichert typischerweise die Daten im Speicher auf derselben Maschine oder über das Netzwerk im Speicher einer anderen Maschine. Das Caching auf Anwendungsebene kann deutlich effizienter sein als das Caching auf einer niedrigeren Ebene, weil die Anwendung teilweise berechnete Ergebnisse im Cache ablegen kann. Daher erspart der Cache zweierlei Arbeit: das Holen der Daten und das Durchführen von Berechnungen damit. Ein gutes Beispiel sind Blöcke mit HTML-Text. Die Anwendung kann HTML-Schnipsel generieren, wie etwa Top-Schlagzeilen, und sie im Cache speichern. Beim Betrachten nachfolgender Seiten wird dieser im Cache gespeicherte Text einfach auf der Seite eingesetzt. Je stärker Sie die Daten verarbeiten, bevor Sie sie im Cache speichern, umso mehr Arbeit sparen Sie bei einem Cache-Treffer. Nachteilig ist, dass die Cache-Trefferrate niedriger sein kann und der Cache möglicherweise mehr Speicher benutzt. Nehmen Sie an, Sie brauchen 50 unterschiedliche Versionen der Top-Schlagzeilen, damit der Benutzer den Inhalt je nach seinem Wohnort anders angezeigt bekommen kann. Sie brauchen genügend Speicher, um alle 50 zu speichern. Alle vorhandenen Versionen der Schlagzeilen werden nur von wenigen Anforderungen abgerufen, und das Entwerten des Cache wird verkompliziert. Es gibt viele Arten von Anwendungs-Caches, unter anderem: Lokale Caches Diese Caches sind normalerweise klein und leben nur für die Dauer der Anforderung im Speicher des Prozesses. Mit ihnen kann man eine wiederholte Anforderung einer Ressource vermeiden, wenn sie mehr als einmal benötigt wird. Diese Art von Cache ist nicht besonders originell: Meist ist es einfach nur eine Variable oder eine Hash-Tabelle im Anwendungscode. Nehmen Sie z.B. an, dass Sie den Namen eines Benutzers anzeigen müssen und die ID des Benutzers kennen. Sie können eine get_name_from_id( )-Funktion erzeugen und diese um Caching erweitern:
Caching | 507
Wenn Sie Perl benutzen, bietet das Memoize-Modul eine standardisierte Methode, um die Ergebnisse von Funktionsaufrufen im Cache abzulegen: use Memoize qw(memoize); memoize 'get_name_from_id'; sub get_name_from_id { my ( $user_id ) = @_; my $name = # holt Namen aus der Datenbank return $name; }
Es sind einfache Techniken, die Ihrer Anwendung aber eine Menge Arbeit ersparen können. Lokale Shared-Memory-Caches Es handelt sich um mittelgroße (einige GByte), schnelle Caches, die sich schwer über mehrere Maschinen hinweg synchronisieren lassen. Sie eignen sich gut für kleine, semistatische Daten, z.B. für Listen von Städten in einzelnen Bundesländern, die Partitionierungsfunktion (Zuordnungstabelle) für einen zerlegten Datenspeicher oder für Daten, die Sie mit TTL-Regeln (Time to Live) ungültig machen können. Der größte Vorteil gemeinsam genutzten Speichers besteht darin, dass der Zugriff auf ihn sehr schnell erfolgt – normalerweise viel schneller als auf jede andere Art von entferntem Cache. Verteilte Speicher-Caches Das bekannteste Beispiel für einen verteilten Speicher-Cache ist memcached. Verteilte Caches sind viel größer als lokale Shared-Memory-Caches und können leicht vergrößert werden. Es wird von allen im Cache befindlichen Daten immer nur eine Kopie erzeugt, Sie verschwenden also keinen Speicher und holen sich Konsistenzprobleme ins Haus, indem Sie dieselben Daten an vielen Stellen im Cache speichern. Verteilter Speicher eignet sich großartig zum Ablegen gemeinsam genutzter Objekte, wie Benutzerprofile, Kommentare und HTML-Schnipsel. Diese Caches haben allerdings eine viel höhere Latenz als lokale Shared-MemoryCaches, so dass Sie sie am effizientesten mit mehrfachen Get-Operationen benutzen (d.h., indem Sie bei einem Besuch gleich viele Objekte holen). Sie müssen außerdem planen, wie Sie weitere Knoten hinzufügen und was Sie tun, falls einer der Knoten ausfällt. In beiden Fällen muss die Anwendung entscheiden, wie sie die im Cache gespeicherten Objekte über die Knoten verteilt bzw. neu verteilt. Konsistentes Caching ist wichtig, um Leistungsprobleme zu vermeiden, wenn Sie einen Server zu Ihrem Cache-Cluster hinzufügen oder aus ihm entfernen. Es gibt für memcached unter http://www.audioscrobbler.net/development/ketama/ eine konsistente Caching-Bibliothek. Caches auf der Festplatte Festplatten sind langsam. Das Caching auf der Festplatte eignet sich daher am besten für persistente Objekte, für Objekte, die schwer in den Speicher passen, oder für statische Inhalte (z.B. vorab generierte eigene Bilder).
508 | Kapitel 10: Optimierung auf Anwendungsebene
Ein nützlicher Trick für Festplatten-Caches und Webserver ist der Einsatz von Fehler-404-Handlern zum Erfassen von Cache-Misses. Nehmen Sie an, Ihre Webanwendung zeigt im Header ein speziell generiertes Bild auf der Grundlage des Benutzernamens (»Willkommen daheim, Jochen!«). Sie können mit /images/welcomeback/jochen.jpg auf das Bild verweisen. Wenn das Bild nicht existiert, verursacht es einen Fehler 404 und löst den Fehler-Handler aus. Der Fehler-Handler kann das Bild generieren, es auf der Festplatte ablegen und entweder einen Redirect auslösen oder das Bild einfach wieder zurück an den Browser leiten. Folgende Anforderungen liefern einfach wieder das Bild aus der Datei aus. Dieser Trick eignet sich für viele Arten von Inhalt. Anstatt z.B. die neuesten Schlagzeilen als Block aus HTML im Cache zu speichern, können Sie sie in einer JavaScript-Datei ablegen und dann im Header der Webseite auf /latest_headlines.js verweisen. Die Cache-Entwertung ist einfach: Löschen Sie die Datei. Sie können eine TTL-Entwertung implementieren, indem Sie regelmäßig einen Job ausführen, der Dateien löscht, die vor mehr als N Minuten erzeugt wurden. Und falls Sie die Cache-Größe beschränken wollen, richten Sie eine LRU-Entwertungsregelung (Least Recently Used; am längsten nicht verwendet) ein, indem Sie Dateien in der Reihenfolge ihrer letzten Zugriffszeit löschen. Eine Entwertung anhand der letzten Zugriffszeit verlangt von Ihnen, dass Sie die Zugriffszeitoption in den Mount-Optionen Ihres Dateisystems aktivieren. (Das erreichen Sie, indem Sie die Mount-Option noatime weglassen.) In diesem Fall sollten Sie ein im Speicher befindliches Dateisystem verwenden, um zu viel Festplattenaktivität zu vermeiden. Mehr dazu erfahren Sie in »Ein Dateisystem wählen« auf Seite 361.
Strategien zur Cache-Kontrolle Caches werfen das gleiche Problem auf wie das Denormalisieren Ihres Datenbankentwurfs: Caches duplizieren Daten, was bedeutet, dass es mehrere Stellen gibt, an denen Daten aktualisiert werden müssen, und dass Sie herausbekommen müssen, wie Sie das Lesen schlechter Daten vermeiden. Dies sind einige der gebräuchlichsten Strategien zur Cache-Kontrolle: TTL (Time to Live; Lebensdauer) Das im Cache abgelegte Objekt wird mit einem Verfallszeitpunkt gespeichert. Sie können das Objekt dann entweder mit einem Aufräumprozess löschen, wenn der Zeitpunkt eintritt, oder es bestehen lassen, bis wieder einmal jemand darauf zugreift (zu diesem Zeitpunkt ersetzen Sie es dann durch eine frische Version). Diese Entwertungsstrategie eignet sich am besten für Daten, die sich selten ändern oder nicht frisch sein müssen.
Caching | 509
Explizite Entwertung Wenn veraltete Daten nicht akzeptabel sind, kann der Prozess, der die Quelldaten aktualisiert, die alte Version im Cache entwerten. Es gibt zwei Varianten dieser Strategie: schreiben-entwerten und schreiben-aktualisieren. Die Schreiben-EntwertenStrategie ist einfach: Sie kennzeichnen die Daten im Cache als abgelaufen (und entfernen sie optional aus dem Cache). Die Schreiben-Aktualisieren-Strategie erfordert etwas mehr Arbeit, weil Sie den alten Cache-Eintrag durch die aktualisierten Daten ersetzen müssen. Das hat jedoch auch seine Vorteile, vor allem, wenn es aufwendig ist, die Daten im Cache zu generieren (die der Schreiber-Prozess bereits haben könnte). Falls Sie die im Cache gespeicherten Daten aktualisieren, müssen künftige Anforderungen nicht darauf warten, bis die Anwendung sie generiert hat. Falls Sie im Hintergrund Entwertungen vornehmen, wie etwa TTL-basierte Entwertungen, können Sie neue Versionen der entwerteten Daten in einem Prozess generieren, der völlig von irgendeiner Benutzeranforderung losgelöst ist. Entwertung beim Lesen Anstatt veraltete Daten zu entwerten, wenn Sie die Quelldaten ändern, von denen sie stammen, können Sie Informationen speichern, mit denen Sie feststellen, ob die Daten verfallen sind, wenn Sie sie aus dem Cache lesen. Das hat einen deutlichen Vorteil gegenüber der expliziten Entwertung: Es hat feste Kosten, die Sie über die Zeit verteilen können. Nehmen Sie an, Sie entwerten ein Objekt, von dem eine Million im Cache gespeicherter Objekte abhängen. Wenn Sie das Entwerten beim Schreiben vornehmen, müssen Sie eine Million Dinge im Cache bei einem Treffer entwerten. Das kann selbst dann sehr lange dauern, wenn Sie auf effiziente Weise die Objekte suchen. Entwerten Sie sie beim Lesen, kann das Schreiben sofort abgeschlossen werden, die einzelnen Lesevorgänge für die eine Million Objekte werden dagegen leicht verzögert. Dadurch werden die Kosten für die Entwertung verteilt und Lastspitzen sowie lange Latenzen vermieden. Eine der einfachsten Methoden zum Entwerten beim Lesen bildet die Objektversionierung. Wenn Sie mit diesem Ansatz ein Objekt im Cache speichern, dann speichern Sie auch die aktuelle Versionsnummer oder den Zeitstempel der Daten, von denen es abhängt. Nehmen Sie z.B. an, dass Sie Statistiken über die Blog-Nachrichten eines Benutzers im Cache speichern, inklusive der Anzahl der Nachrichten, die der Benutzer verschickt hat. Wenn Sie das Objekt blog_stats im Cache speichern, dann speichern Sie damit auch die aktuelle Versionsnummer des Benutzers, weil die Statistiken von dem Benutzer abhängen. Immer wenn Sie Daten aktualisieren, die ebenfalls von dem Benutzer abhängen, ändern Sie auch die Versionsnummer des Benutzers. Nehmen Sie an, die Version des Benutzers ist zunächst 0, und Sie generieren und speichern die Statistiken. Wenn der Benutzer etwas im Blog veröffentlicht, erhöhen Sie die Version des Benutzers auf 1 (das würden Sie auch mit bei der Blog-Nachricht speichern, obwohl wir das für dieses Beispiel eigentlich nicht brauchen). Müssen Sie dann die Statistiken anzeigen, vergleichen Sie die Version des im Cache befindlichen blog_stats-Objekts mit der Version des im Cache befindli-
510 | Kapitel 10: Optimierung auf Anwendungsebene
chen Benutzers. Da die Version des Benutzers größer ist als die Version des Objekts, wissen Sie, dass die Statistiken veraltet sind und neu berechnet werden müssen. Das ist eine ziemlich plumpe Methode, um Inhalte zu entwerten, weil sie davon ausgeht, dass alle Daten, die von dem Benutzer abhängig sind, auch mit allen anderen Daten interagieren. Das stimmt jedoch nicht immer. Wenn ein Benutzer z.B. eine Blog-Nachricht bearbeitet, erhöhen Sie die Version des Benutzers. Das entwertet dann wiederum die gespeicherte Statistik, obwohl sich die Statistik (die Anzahl der Blog-Nachrichten) gar nicht geändert hat. Der Kompromiss heißt Einfachheit. Eine einfache Cache-Entwertungsstrategie ist nicht nur einfacher zu erstellen, sondern könnte auch effizienter sein. Die Objektversionierung ist ein vereinfachter Ansatz eines Tagged-Cache, der viel komplexere Abhängigkeiten verarbeiten kann. Ein Tagged-Cache kennt die verschiedenen Arten von Abhängigkeiten und verfolgt die Versionen jeweils getrennt. Um zum Buchclubbeispiel aus dem vorangegangenen Kapitel zurückzukehren: Sie könnten dafür sorgen, dass die im Cache gespeicherten Kommentare von der Version des Benutzers und der Version des Buches abhängen, indem Sie die Kommentare mit diesen Versionsnummern markieren: user_ver=1234 und book_ver=5678. Wenn sich eine der Versionen ändert, erneuern Sie die im Cache gespeicherten Kommentare.
Cache-Objekthierarchien Das hierarchische Speichern von Objekten in einem Cache kann beim Abfragen, Entwerten und bei der Benutzung von Speicher helfen. Anstatt einfach nur die Objekte im Cache abzulegen, speichern Sie die Objekt-IDs sowie die Gruppen der Objekt-IDs, die Sie normalerweise zusammen abfragen. Ein gutes Beispiel für diese Technik ist ein Suchergebnis auf einer E-Commerce-Website. Eine Suche könnte eine Liste passender Produkte zurückliefern, mit Namen, Beschreibungen, Miniaturbildern und Preisen. Die gesamte Liste im Cache zu speichern, wäre ineffizient: Andere Suchen würden wahrscheinlich auch einige dieser Produkte enthalten, was zu Dopplungen bei den Daten und verschwendetem Speicher führen würde. Diese Strategie würde außerdem das Auffinden und Entwerten von Suchergebnissen erschweren, wenn sich der Preis eines Produkts ändert, weil Sie in jeder Liste nachschauen müssten, ob sie das aktualisierte Produkt enthält. Anstelle der Liste könnten Sie minimale Informationen über die Suche im Cache speichern, wie etwa die Anzahl der zurückgelieferten Ergebnisse und eine Liste der ProduktIDs. Sie können dann jedes Produkt separat speichern. Das löst beide Probleme: Es werden keine Ergebnisse dupliziert, und es ist einfach, den Cache auf der Ebene einzelner Produkte zu entwerten. Nachteilig ist, dass Sie mehrere Objekte aus dem Cache abfragen müssen, anstatt das ganze Suchergebnis auf einmal zu holen. Das ist allerdings effizient, weil Sie die Liste der Produkt-IDs für das Ergebnis speichern. Ein Cache-Treffer liefert nun eine Liste der IDs zurück, die Sie dann für einen zweiten Aufruf an den Cache nutzen können. Der zweite
Caching | 511
Aufruf kann mehrere Produkte zurückliefern, wenn der Cache es Ihnen erlaubt, mehrere Ergebnisse mit einem einzigen Aufruf zu holen (memcached unterstützt dies durch den Aufruf mget( )). Wenn Sie nicht aufpassen, verursacht dieser Ansatz eigenartige Ergebnisse. Nehmen Sie an, Sie verwenden eine TTL-Strategie, um Suchergebnisse zu entwerten, und Sie entwerten einzelne Produkte explizit, wenn sie sich ändern. Stellen Sie sich nun vor, dass die Beschreibung eines Produkts sich derart ändert, dass sie nicht mehr die für eine Suche passenden Stichwörter enthält, dass die Suche aber noch nicht so alt ist, dass sie im Cache bereits verfallen ist. Ihre Benutzer sehen veraltete Suchergebnisse, weil die im Cache gespeicherte Suche auf das Produkt verweist, obwohl es nicht länger den Suchstichwörtern entspricht. Für die meisten Anwendungen ist das normalerweise kein Problem. Falls Ihre Anwendung das nicht toleriert, können Sie ein versionsbasiertes Caching einsetzen und die Produktversionen mit den Ergebnissen speichern, wenn Sie eine Suche durchführen. Finden Sie dann ein Suchergebnis im Cache, können Sie die Version des Produkts in den Suchergebnissen mit der aktuellen (im Cache befindlichen) Version vergleichen. Falls eines der Produkte veraltet ist, wiederholen Sie die Suche und setzen die Ergebnisse wieder in den Cache.
Inhalt vorab generieren Zusätzlich zum Speichern von Daten auf Anwendungsebene im Cache können Sie Seiten mithilfe von Hintergrundprozessen bereits vorher abrufen und die Ergebnisse als statische Seiten speichern. Bei dynamischen Seiten generieren Sie Teile der Seiten vorher und verwenden eine Technik wie Server-Side Includes, um die endgültigen Seiten aufzubauen. Damit reduzieren Sie die Größe und die Kosten für den vorab generierten Inhalt, weil Sie ansonsten aufgrund kleiner Variationen beim Zusammensetzen der einzelnen Teile zur fertigen Seite möglicherweise viele Daten duplizieren. Das Ablegen von vorab generiertem Inhalt im Cache kann eine Menge Platz beanspruchen. Es ist außerdem nicht immer möglich, alles vorab zu generieren. Wie bei jeder Form des Cachings sind die wichtigsten Teile, die vorab generiert werden müssen, diejenigen, die am häufigsten angefordert werden. Sie können dieses Vorab-Generieren daher mit den bereits erwähnten Fehler-404-Handlern erledigen. Um Festplatten-Ein-/Ausgaben zu vermeiden, ist es vielleicht ganz günstig, wenn vorab generierter Inhalt auf einem im Speicher befindlichen Dateisystem abgelegt wird.
MySQL erweitern Falls MySQL nicht das tun kann, was Sie brauchen, dann ist es möglich, seine Fähigkeiten zu erweitern. Wir wollen Ihnen hier nicht zeigen, wie das geht, sondern nur einige der Möglichkeiten erwähnen. Es gibt online gute Ressourcen sowie Bücher zu vielen der Themen, falls es Sie interessiert.
512 | Kapitel 10: Optimierung auf Anwendungsebene
Wenn wir sagen, dass »MySQL nicht das tun kann, was Sie brauchen«, dann meinen wir zwei Dinge: MySQL kann es überhaupt nicht, oder MySQL kann es zwar, aber auf eine so langsame oder seltsame Art, dass es uns nicht gut genug ist. Beides sind gute Gründe, um über das Erweitern von MySQL nachzudenken. Die gute Nachricht lautet, dass MySQL immer modularer und allgemeiner wird. So besitzt z.B. MySQL 5.1 viele nützliche Funktionen, die als Plugin realisiert sind. Sogar Storage-Engines können Plugins sein, so dass es nicht nötig ist, sie in den Server zu kompilieren. Storage-Engines eignen sich großartig, um MySQL für einen besonderen Zweck zu erweitern. Brian Aker hat ein Storage-Engine-Gerüst geschrieben sowie eine Reihe von Artikeln und Präsentationen verfasst, die darstellen, wie man seine eigene Storage-Engine schreiben kann. Diese bilden die Basis für einige der wichtigsten Storage-Engines von Drittanbietern. Viele Unternehmen schreiben inzwischen ihre eigenen internen StorageEngines, wie Sie feststellen werden, wenn Sie die MySQL-Internals-Mailinglisten verfolgen. So benutzt z.B. Friendster eine spezielle Storage-Engine für Operationen auf sozialen Beziehungen. Wir kennen noch ein weiteres Unternehmen, das eine eigene Engine für unscharfe Suchen erstellt hat. Es ist nicht so schwer, eine einfache eigene Storage-Engine zu schreiben. Sie können eine Storage-Engine auch als Schnittstelle zu einer anderen Software benutzen. Ein gutes Beispiel dafür ist die Storage-Engine Sphinx, die als Schnittstelle zur Volltextsuchmaschine Sphinx dient (siehe Anhang C). MySQL 5.1 erlaubt darüber hinaus Volltext-Suchparser-Plugins. Sie können auch benutzerdefinierte Funktionen (UDFs) schreiben (siehe Kapitel 5), die sich hervorragend für CPU-intensive Aufgaben eignen, die im Thread-Kontext des Servers laufen müssen und in SQL zu langsam oder zu schwerfällig sind. Sie können sie für die Administration, die Diensteintegration, das Lesen von Betriebssysteminformationen, das Aufrufen von Webservices, das Synchronisieren von Daten und vieles mehr verwenden. MySQL Proxy ist eine weitere großartige Möglichkeit, um Ihre eigene Funktionalität in das MySQL-Protokoll einzubringen. Paul McCullaghs skalierbares Blob-Streaming-Infrastruktur-Projekt (http://www.blobstreaming.org) eröffnet eine Reihe neuer Möglichkeiten zum Speichern großer Objekte in MySQL. Da MySQL frei ist, können Sie sogar den Server selbst hacken, falls er nicht das tut, was Sie von ihm erwarten. Wir kennen z.B. Unternehmen, die die Parser-Grammatik des Servers erweitert haben. Auf den Gebieten der Leistungsprofilierung, der Skalierbarkeit und neuer Funktionen sind in den letzten Jahren von Dritten interessante MySQL-Erweiterungen beigesteuert worden. Die MySQL-Entwickler sind sehr entgegenkommend und hilfsbereit, wenn jemand MySQL erweitern möchte. Man erreicht sie über die Mailingliste [email protected] (zum Abonnieren siehe http://lists.mysql.com), die MySQL-Foren oder den IRC-Kanal #mysql-dev IRC auf Freenode.
MySQL erweitern | 513
Alternativen zu MySQL MySQL ist nicht unbedingt die beste Lösung für jede Anforderung. Oft ist es viel besser, wenn man einen Teil der Arbeit völlig außerhalb von MySQL erledigt, selbst wenn MySQL theoretisch auch dazu in der Lage wäre. Eines der offensichtlichsten Beispiele ist das Speichern von Daten in einem traditionellen Dateisystem anstatt in Tabellen. Bilddateien sind der klassische Fall: Sie können sie in eine BLOB-Spalte setzen, aber das ist meist eine ganz schlechte Idee.3 Die übliche Vorgehensweise sieht so aus, dass man Bilder oder andere große Binärdateien im Dateisystem speichert und die Dateinamen in MySQL ablegt; die Anwendung kann dann die Dateien von außerhalb von MySQL holen. Bei einer Webanwendung erreichen Sie das, indem Sie den Dateinamen in das src-Attribut des -Elements setzen. Eine Volltextsuche ist ebenfalls etwas, was Sie am besten außerhalb von MySQL erledigen – MySQL führt diese Suchen nicht so gut aus wie Lucene oder Sphinx (siehe Anhang C). Auch die NDB-API kann für bestimmte Aufgaben nützlich sein. Obwohl sich z.B. die MySQL-Storage-Engine NDB Cluster (noch) gar nicht gut zum Ablegen all der Daten einer High-Performance-Webanwendung eignet, ist es möglich, die NDB-API direkt zum Speichern von Website-Sitzungsdaten oder Benutzerregistrierungsinformationen zu verwenden. Sie erfahren unter http://dev.mysql.com/doc/ndbapi/en/index.html mehr über die NDB-API. Es gibt auch ein NDB-Modul für Apache, mod_ndb, das Sie von http://code. google.com/p/mod-ndb/ herunterladen können. Schließlich ist für manche Operationen – wie etwa Graphenbeziehungen und das Durchlaufen von Bäumen – eine relationale Datenbank einfach nicht das richtige Paradigma. MySQL eignet sich nicht gut für die verteilte Datenverarbeitung, da ihm die Fähigkeiten zur parallelen Ausführung von Abfragen fehlen. Für diesen Zweck sollten Sie eher andere Werkzeuge einsetzen (vielleicht in Kombination mit MySQL).
3 Es bietet Vorteile, wenn man die MySQL-Replikation einsetzt, um schnell Bilder an viele Maschinen zu verteilen, und wir kennen Anwendungen, die diese Technik benutzen.
514 | Kapitel 10: Optimierung auf Anwendungsebene
KAPITEL 11
Backup und Wiederherstellung
Es passiert leicht, dass man sich darauf konzentriert, die »eigentliche Arbeit zu erledigen«, und darüber Backup und Wiederherstellung vernachlässigt. Was dringend ist, ist oft nicht wichtig, und was wichtig ist, ist oft nicht dringend. Backups sind für eine hohe Leistung genauso wichtig wie für die Wiederherstellung im Katastrophenfall. Sie müssen Backups von Anfang an einplanen, damit sie keine Ausfallzeiten verursachen oder die Performance herabsetzen. Falls Sie Backups nicht einplanen und frühzeitig einbauen, müssen Sie später eine Lösung anstückeln. Möglicherweise werden Sie dann feststellen, dass Ihre früheren Entscheidungen die beste Möglichkeit für den Umgang mit High-Performance-Backups ausschließen. So richten Sie z.B. vielleicht einen Server ein und merken dann, dass Sie eigentlich LVM haben wollen, damit Sie Schnappschüsse vom Dateisystem erstellen können. Aber nun ist es zu spät! Möglicherweise bemerken Sie auch nicht die wichtigen Einflüsse auf die Leistung, die entstehen, wenn Sie Ihre Systeme für Backups einrichten. Und wenn Sie die Wiederherstellung nicht einplanen und üben, werden Sie im Bedarfsfall nicht unbedingt glimpflich davonkommen. Backup-Systeme sind wie Überwachungs- und Alarmierungssysteme: Die meisten Systemadministratoren haben sie irgendwann neu erfunden. Das ist eine Schande, weil es gute, unterstützte, flexible Backup-Programme gibt – einige auch als Open Source und kostenlos. Wir ermutigen Sie, die Teile dieser Systeme zu verwenden, die für Sie sinnvoll sind. Wir werden in diesem Kapitel nicht alle Teile einer gut gestalteten Lösung für Backup und Wiederherstellung behandeln. Das Thema könnte von seinem Umfang her ein ganzes Buch füllen, und – Sie werden es nicht glauben – es gibt sogar mehrere Bücher dazu.1 Wir überfliegen einige Themen und konzentrieren uns auf Lösungen für High-Performance-MySQL. Im Gegensatz zur ersten Ausgabe dieses Buches nehmen wir an, dass viele Leser InnoDB zusätzlich zu oder anstelle von MyISAM benutzen. Einige BackupSzenarien werden dadurch verkompliziert. 1 Unserer Meinung nach ist W. Curtis Prestons Backup & Recovery (O’Reilly) eine gute Wahl.
515 |
Überblick Wir beginnen dieses Kapitel mit einer Betrachtung der Terminologie und einer Besprechung der verschiedenen Probleme, die Sie im Hinterkopf haben sollten, wenn Sie Ihre Backup- und Wiederherstellungslösungen, einschließlich der potenziellen Erfordernisse, planen. Wir präsentieren dann einen Überblick über die verschiedenen Techniken und Methoden zum Erstellen von Backups und erkunden Techniken zum Erneuern von Daten und zum Wiederherstellen nach Katastrophen. Schließlich stellen wir eine Auswahl der verfügbaren Backup-Programme vor und schließen dieses Kapitel mit einigen Beispielen für den Aufbau eigener Backup-Hilfsmittel.
Terminologie Bevor wir anfangen, wollen wir einige Schlüsselbegriffe klären. Erstens werden Sie oft von sogenannten heißen, warmen und kalten Backups hören. Im Allgemeinen sollen diese Begriffe die Auswirkungen eines Backups kennzeichnen: »heiße« Backups sollen z.B. keine Serverausfallzeit erfordern. Leider bedeuten diese Begriffe nicht für alle Leute dasselbe. Manche Werkzeuge haben das Wort »hot« (heiß) sogar in ihren Namen, machen aber definitiv nicht das, was wir unter heißen Backups verstehen. Wir versuchen, diese Begriffe zu vermeiden, und sagen Ihnen stattdessen, wie stark eine bestimmte Technik oder ein Werkzeug Ihren Server unterbricht. Zwei weitere verwirrende Wörter sind erneuern und wiederherstellen. Wir verwenden sie in diesem Kapitel auf spezielle Weise. Erneuern bedeutet, dass Daten aus einem Backup geholt und entweder in MySQL geladen werden oder dass die Dateien dort platziert werden, wo MySQL sie erwartet. Wiederherstellen bezeichnet im Allgemeinen den gesamten Vorgang der Rettung eines Systems oder eines Teils des Systems, nachdem etwas schiefgegangen ist. Dies schließt das Erneuern der Daten aus Backups ebenso ein wie alle Schritte, die erforderlich sind, um einen Server wieder voll funktionstüchtig zu machen, wie etwa das Neustarten von MySQL, das Ändern der Konfiguration, das Aufwärmen der Server-Caches usw. Für viele Leute bedeutet die Wiederherstellung einfach nur die Reparatur beschädigter Tabellen nach einem Absturz. Das ist nicht das Gleiche wie die Wiederherstellung eines ganzen Servers. Die Wiederherstellung einer Storage-Engine stimmt die Daten- und die Log-Dateien miteinander ab. Sie sorgt dafür, dass die Datendateien nur die Modifikationen enthalten, die von den bestätigten Transaktionen vorgenommen wurden, und sie spielt Transaktionen aus den Log-Dateien wieder ab, die noch nicht auf die Datendateien angewandt wurden. Wenn Sie eine transaktionsfähige Storage-Engine benutzen, kann das zum gesamten Wiederherstellungsprozess gehören oder sogar Bestandteil der Backup-Erstellung sein. Es ist jedoch nicht identisch mit der Wiederherstellung, die Sie z.B. möglicherweise nach einem versehentlichen DROP TABLE benötigen.
516 | Kapitel 11: Backup und Wiederherstellung
Es dreht sich alles um Wiederherstellung Falls alles gut geht, müssen Sie nie über die Wiederherstellung nachdenken. Wenn Sie es aber tun, dann hilft Ihnen auch das beste Backup-System der Welt nichts. Stattdessen brauchen Sie ein großartiges Wiederherstellungssystem. Das Problem besteht darin, dass es einfacher ist, Ihre Backup-Systeme zum reibungslosen Arbeiten zu bewegen, als einen guten Wiederherstellungsvorgang und die entsprechenden Werkzeuge zu bauen. Hier sind die Gründe: • Backups kommen zuerst. Sie können erst wiederherstellen, wenn Sie etwas mit einem Backup gesichert haben. Ihre Aufmerksamkeit wird sich deshalb beim Aufbau eines Systems natürlich zuerst auf die Backups konzentrieren. Es ist wichtig, dieser Tendenz entgegenzuwirken, indem Sie zuerst die Wiederherstellung planen. Um genau zu sein, sollten Sie Ihre Backup-Systeme erst dann aufbauen, wenn Sie die Anforderungen an die Wiederherstellung ermittelt haben. • Backups sind Routine. Sie werden sich deshalb vermutlich um das Automatisieren und Feineinstellen des Backup-Vorgangs kümmern, oft ohne groß darüber nachzudenken. Es scheint nicht viel Aufwand zu sein, sich fünf Minuten lang um den Backup-Vorgang zu kümmern. Widmen Sie jedoch der Wiederherstellung die gleiche Aufmerksamkeit, und das jeden Tag? Sie sollten Ihr Vorgehen bei der Wiederherstellung absichtlich so lange üben, bis es genauso geschmeidig und fehlerfrei verläuft wie Ihr Backup. • Backups werden normalerweise nicht unter extremem Druck erzeugt, die Wiederherstellung dagegen erfolgt meist in einer Krisensituation. Man kann gar nicht genug betonen, wie wichtig das ist. • Oft kommt einem die Sicherheit in die Quere. Falls Sie Ihre Backups extern durchführen, dann verschlüsseln Sie die Backup-Daten wahrscheinlich oder nehmen andere Maßnahmen vor, um sie zu schützen. Man versteift sich leicht darauf, wie schädlich es wäre, wenn Ihre Daten kompromittiert werden würden, und vergisst, wie viel Schaden angerichtet werden könnte, wenn niemand Ihr verschlüsseltes Volume entschlüsseln kann, um Ihre Daten wiederherzustellen – oder wenn Sie eine einzige Datei aus einer monolithischen verschlüsselten Datei extrahieren müssen. • Eine Person kann Backups planen, entwerfen und implementieren, vor allem mit den ausgezeichneten Werkzeugen, die es gibt. Diese Person ist im Katastrophenfall vielleicht gar nicht da. Sie müssen mehrere Personen anlernen und eine umfassende Betreuung einplanen, damit es nicht dazu kommt, dass Sie unqualifiziertes Personal bitten müssen, Ihre Daten wiederherzustellen. Hier ist ein Beispiel, das uns im richtigen Leben untergekommen ist: Ein Kunde berichtete, dass die Backups blitzschnell wurden, als die Option -d zu mysqldump hinzugefügt wurde. Jetzt wollte er wissen, wieso niemand bisher erwähnt hatte, wie sehr diese Option den Backup-Vorgang beschleunigen kann. Wenn dieser Kunde versucht hätte, die Backups wiederherzustellen, dann hätte er den Grund schmerzlich erfahren: Die Option -d
Überblick | 517
erzeugt keine Dumps der Daten! Der Kunde war auf Backups fixiert, nicht auf die Wiederherstellung, und war sich deshalb des Problems gar nicht bewusst. Wenn Sie anfangen, über die Wiederherstellung nachzudenken, dann sollten Sie zuallererst Ihre Anforderungen definieren. Folgende Dinge müssen Sie bedenken: • Wie viele Daten können Sie verlieren, ohne dass es ernste Folgen hat? Benötigen Sie eine punktgenaue Wiederherstellung, oder ist es akzeptabel, die Arbeit zu verlieren, die seit dem letzten regulären Backup angefallen ist? Gibt es gesetzliche Anforderungen? • Wie schnell muss die Wiederherstellung sein? Welche Art der Ausfallzeit ist akzeptabel? Welche Auswirkungen (z.B. teilweise Nichtverfügbarkeit) können Ihre Anwendung und die Benutzer verkraften, und wie bauen Sie die Fähigkeit ein, die Funktionstüchtigkeit weiter zu gewährleisten, wenn diese Szenarien eintreten? • Was müssen Sie wiederherstellen? Übliche Anforderungen sind die Wiederherstellung eines ganzen Servers, einer einzigen Datenbank, einer einzigen Tabelle oder nur spezieller Transaktionen oder Anweisungen. Schreiben Sie die Antworten auf diese Fragen auf, fügen Sie sie der Dokumentation Ihres Systems hinzu, und bedenken Sie sie, wenn Sie den Rest dieses Kapitels lesen. Wenn Sie zuerst diese Übung durchführen, erleichtern Sie es sich, sich auf die Wiederherstellung zu konzentrieren, während Sie Ihre Backups planen. Und das Ablegen bei Ihrer Dokumentation hilft Ihnen später, Ihre Schritte nachzuverfolgen.
Backup-Mythos #1: »Ich benutze Replikation als Backup.« Das ist ein Fehler, der uns oft begegnet. Ein Replikations-Slave ist kein Backup. Auch ein RAID-Array ist es nicht. Um festzustellen, wieso nicht, sollten Sie sich Folgendes fragen: Helfen sie Ihnen dabei, alle Daten zurückzubekommen, wenn Sie in Ihrer Produktionsdatenbank versehentlich DROP DATABASE ausführen? RAID und Replikation bestehen nicht einmal diesen einfachen Test. Es sind nicht nur keine Backups, sie sind nicht einmal ein Ersatz für Backups. Nichts anderes als ein Backup erfüllt die Kriterien für ein Backup.
Themen, die wir nicht behandeln Das Sichern von MySQL durch ein Backup ist in vielerlei Hinsicht einfach nur ein spezieller Fall des allgemeineren Problems von Backup und Wiederherstellung. Wir wollen uns auf High-Performance-MySQL konzentrieren, das ist aber relativ schwer, ohne Material aus vielen anderen Themen einzubeziehen, vor allem, weil wir so viele Leute gesehen haben, die mit den gleichen Problemen bezüglich Backup und Wiederherstellung zu kämpfen haben. Dies sind einige Punkte, die wir nicht behandeln werden: • Sicherheit (Wer kann auf das Backup zugreifen; wer hat die Berechtigung, Daten zu erneuern; müssen die Dateien verschlüsselt werden?)
518 | Kapitel 11: Backup und Wiederherstellung
• Wo müssen die Backups gespeichert werden; wie weit von der Quelle entfernt sollten sie gespeichert werden (auf einer anderen Festplatte, auf einem anderen Server, an einem anderen Standort), und wie verschiebt man die Daten von der Quelle zum Ziel? • Aufbewahrungsstrategien, Überprüfung, rechtliche Anforderungen und verwandte Themen • Speicherlösungen und -medien, Komprimierung und inkrementelle Backups • Speicherformate (wir sagen nur so viel: vermeiden Sie proprietäre Backup-Formate) • Überwachung und Auswertung Ihrer Backups • Backup-Fähigkeiten, die in Speicherschichten eingebaut sind, oder spezielle Geräte wie vorgefertigte Fileserver Das sind wichtige Themen. Lesen Sie ein Buch, falls Sie mit ihnen nicht vertraut sind.
Das Gesamtbild Bevor wir uns an die Einzelheiten der verfügbaren Optionen machen, können Sie hier lesen, was die meisten Administratoren unserer Meinung nach für eine Backup- und Wiederherstellungslösung brauchen. Betrachten Sie diese Empfehlungen als Ausgangspunkt oder als Richtung, in die Sie hinarbeiten können: • Rohe Backups sind praktisch eine Notwendigkeit für große Datenbanken, denn sie sind schnell, was sehr wichtig ist. Unser Favorit sind schnappschussbasierte Backups, aber InnoDB Hot Backup bildet eine gute Alternative, falls Sie nur InnoDBTabellen benutzen. • Sichern Sie Ihre Binärlogs für eine punktgenaue Wiederherstellung. • Bewahren Sie mehrere Backup-Generationen auf, und behalten Sie die Binärlogdateien so lange, dass Sie die Daten aus ihnen erneuern können. • Testen Sie Ihre Backups und den Wiederherstellungsprozess regelmäßig, indem Sie den gesamten Vorgang durchlaufen. • Erzeugen Sie regelmäßig logische Backups (aus Effizienzgründen wahrscheinlich aus den rohen Backups). Achten Sie darauf, genügend Binärlogs zu behalten, um die Daten aus Ihrem letzten logischen Backup wiederherstellen zu können. • Testen Sie nach Möglichkeit Ihre rohen Backups, um sicherzustellen, dass sie sich für die Wiederherstellung eignen. Testen Sie sie möglichst während des BackupVorgangs, bevor Sie sie an das Ziel kopieren. • Denken Sie gründlich über die Sicherheit nach. Was passiert, wenn jemand Ihren Server kompromittiert – bekommt er dann auch Zugriff auf den Backup-Server oder umgekehrt? • Überwachen Sie Ihre Backups und die Backup-Prozesse unabhängig von den Backup-Werkzeugen selbst. Sie müssen sich von externer Seite aus vergewissern, dass sie in Ordnung sind. Überblick | 519
• Seien Sie klug, wenn es darum geht, wie Sie die Dateien zwischen den Maschinen kopieren. Es gibt effizientere Methoden zum Kopieren von Dateien als mit scp oder rsync. Mehr dazu erfahren Sie in Anhang A.
Wozu Backups? Wenn Sie ein High-Performance-System aufbauen, das auf MySQL beruht, ist es wichtig, Backups durchzuführen. Hier sind einige Gründe: Wiederherstellung im Katastrophenfall Wiederherstellung ist das, was Sie tun, wenn die Hardware ausfällt, ein hässlicher Bug Ihre Daten beschädigt oder Ihr Server und seine Daten unerreichbar oder aus einem anderen Grund unbenutzbar werden (es gibt viele und verschiedenartige potenzielle Gründe – benutzen Sie Ihre Fantasie). Die Unannehmlichkeiten einiger Katastrophenfälle sind zwar relativ gering, in der Summe sind sie jedoch ganz beträchtlich. Sie müssen auf alles vorbereitet sein: von jemandem, der sich versehentlich mit dem falschen Server verbindet, über jemanden, der ALTER TABLE eintippt,2 bis hin zu einem Brand, der das ganze Gebäude vernichtet, einem bösartigen Angreifer oder einem MySQL-Bug. Leute ändern ihre Meinung Sie werden überrascht sein, wie oft wir wenigstens einige Daten bis zu einem bestimmten Punkt wiederherstellen mussten. Bei manchen Anwendungen kommt das viel öfter vor als eine echte Katastrophe (wenn z.B. ein wichtiger Kunde absichtlich irgendwelche Daten löscht und sie dann doch zurückhaben möchte). Überprüfung Manchmal müssen Sie wissen, wie Ihre Daten oder Ihr Schema an einem bestimmten Punkt in der Vergangenheit ausgesehen haben. Sie könnten z.B. in einen Rechtsstreit verwickelt sein oder haben einen Bug in Ihrer Anwendung entdeckt und müssen wissen, was der Code getan hat (manchmal reicht es einfach nicht, den Code in der Versionskontrolle zu haben). Tests Eine der einfachsten Methoden, um mit realistischen Daten zu testen, besteht darin, einen Testserver regelmäßig mit den neuesten Produktionsdaten auszustatten. Wenn Sie Backups machen, ist das einfach; nehmen Sie einfach das Backup. Überprüfen Sie Ihre Annahmen. Gehen Sie z.B. davon aus, dass Ihr Shared-Hosting-Provider ein Backup des MySQL-Servers durchführt, der Ihrem Account zur Verfügung gestellt wird? Shared-Hosting ist zwar für High Performance eigentlich nicht relevant, wir wollen aber darauf hinweisen, dass solche Annahmen täuschen können. (Nur falls Sie sich wundern: Viele Hosting-Provider erstellen für MySQL-Server überhaupt keine Back2 Baron erinnert sich noch daran, wie er als Entwickler bei einer E-Commerce-Site den Befehl in das falsche Fenster eingetippt hat. Es war der Fehler der Datenbankadministratoren; sie hätten den Entwicklern keine Berechtigungen auf den echten Servern geben dürfen. Wirklich!
520 | Kapitel 11: Backup und Wiederherstellung
ups, andere legen lediglich eine Dateikopie an, während der Server läuft, wodurch mit hoher Wahrscheinlichkeit ein beschädigtes Backup entsteht, das sinnlos ist.)
Überlegungen und Kompromisse Das Sichern von MySQL durch Backups ist schwerer, als es aussieht. Im einfachsten Fall ist ein Backup lediglich eine Kopie der Daten. Die Anforderungen Ihrer Anwendung, die Architektur der MySQL-Storage-Engine und die Systemkonfiguration können es jedoch schwierig machen, eine Kopie Ihrer Daten zu erzeugen.
Was können Sie sich leisten zu verlieren? Ihre Backup-Strategie wird dadurch bestimmt, welchen Datenverlust Sie notfalls verschmerzen könnten. Benötigen Sie die Fähigkeit zur punktgenauen Wiederherstellung, oder reicht es, das Backup der letzten Nacht wiederherzustellen und die Arbeit zu verlieren, die seitdem verrichtet wurde? Falls Sie eine punktgenaue Wiederherstellung brauchen, können Sie wahrscheinlich ein normales Backup herstellen und müssen dafür sorgen, dass das Binärlog aktiviert ist, damit Sie dieses Backup erneuern und die Daten bis zum gewünschten Zeitpunkt wiederherstellen können, indem Sie das Binärlog abspielen. Im Allgemeinen gilt: Je mehr Sie verlieren können, umso einfacher wird es, Backups durchzuführen. Bei sehr strengen Anforderungen dagegen ist es schwieriger, tatsächlich alles wiederherzustellen. Es gibt sogar unterschiedliche Arten der punktgenauen Wiederherstellung. Eine »weiche« punktgenaue Wiederherstellung bedeutet, dass Sie in der Lage sein wollen, Ihre Daten so wiederherzustellen, dass sie dem »nahekommen«, wie es vor dem Auftreten des Problems war. Eine »harte« Anforderung bedeutet, dass Sie den Verlust einer bestätigten Transaktion niemals tolerieren können, selbst wenn etwas Schreckliches passiert (etwa, wenn der Server Feuer fängt). Dies erfordert spezielle Techniken, wie etwa das Vorhalten Ihres Binärlogs auf einem separaten SAN-Volume oder den Einsatz der DRBD-Festplattenreplikation. Mehr über diese Ansätze erfahren Sie in Kapitel 9.
Online- oder Offline-Backups? Wenn Sie es sich erlauben können, dann bildet das Herunterfahren von MySQL für das Backup die einfachste, sicherste und insgesamt beste Methode, um eine konsistente Kopie der Daten mit einem minimalen Risiko für Beschädigung oder Inkonsistenzen zu bekommen. Falls Sie MySQL herunterfahren, können Sie die Daten kopieren, ohne Komplikationen durch solche Dinge wie schmutzige Puffer im InnoDB-Puffer-Pool oder anderen Caches zu riskieren. Sie müssen keine Angst haben, dass Ihre Daten modifiziert werden, während Sie versuchen, das Backup zu erstellen; und da der Server nicht von der Anwendung belastet wird, ist das Backup schneller fertig.
Überlegungen und Kompromisse | 521
Allerdings ist es teurer, als es scheinen mag, einen Server offline zu nehmen. Selbst wenn Sie die Ausfallzeit minimieren können, kann das Herunterfahren und Neustarten von MySQL bei erhöhter Last und großen Datenmengen relativ lange dauern: • Wenn Sie viele schmutzige Puffer im InnoDB-Puffer-Pool haben – d.h. viele Daten, die im Speicher modifiziert, aber noch nicht auf die Platte geschrieben wurden –, braucht InnoDB möglicherweise lange, um die modifizierten Daten auf die Festplatte zu übertragen. Sie können die Zeit zum Herunterfahren von InnoDB mit der Konfigurationsvariablen innodb_fast_shutdown beeinflussen. Diese Variable steuert, wie InnoDB den Puffer-Pool und die Eingabepuffer beim Herunterfahren behandelt,3 allerdings wird die Arbeit damit nur weitergeschoben und nicht eliminiert. Sie können deshalb auf diese Weise den Zyklus aus Herunterfahren und Neustarten nicht merklich abkürzen. Manchmal ist es möglich, indem man andere Aspekte von InnoDB konfiguriert, allerdings haben diese Änderungen breitere Auswirkungen auf die Leistung. Mehr dazu erfahren Sie in »Das Ein-/Ausgabeverhalten von MySQL anpassen« auf Seite 304. • Auch das Neustarten kann lange dauern. Das Öffnen aller Tabellen und das Aufwärmen der Caches geht nicht besonders schnell, wenn es viele Tabellen und Daten gibt. Falls Sie innodb_fast_shutdown=2 setzen, damit InnoDB schnell herunterfährt, muss InnoDB die Wiederherstellung durchlaufen, bevor es vollständig startet. Selbst nachdem Ihr Server vollständig gestartet zu sein scheint, kann es lange dauern, bis er aufgewärmt und voll einsatzbereit ist. Falls Sie Ihr System auf High Performance ausrichten, müssen Sie somit Ihre Backups so gestalten, dass der Produktionsserver dafür nicht vom Netz genommen werden muss. Je nach Ihren Anforderungen an die Konsistenz kann das Erstellen von Backups auf einem Server, der weiterhin online ist, dennoch bedeuten, dass der Service merklich unterbrochen wird. Beispielsweise beginnt eine der am häufigsten genannten Backup-Methoden mit FLUSH TABLES WITH READ LOCK. Damit wird MySQL angewiesen, alle Tabellen zu übertragen4 und zu sperren und auch den Abfrage-Cache zu leeren. Es kann eine Weile dauern, bis das beendet ist. (Wie lange genau das dauert, ist nicht vorhersehbar; es dauert länger, wenn der globale Lese-Lock darauf warten muss, dass eine lange laufende Anweisung abgeschlossen wird, oder wenn Sie viele Tabellen haben.) Bevor die Sperren nicht freigegeben werden, können Sie auf dem Server keine Daten ändern. FLUSH TABLES WITH READ LOCK ist nicht so teuer wie das Herunterfahren, weil die meisten Ihrer Caches sich noch im Speicher befinden und der Server noch »warm« ist. Dennoch ist es eine relativ starke Störung. Falls das ein Problem darstellt, müssen Sie eine Alternative finden. Wir erstellen z.B. ein Backup von einem Replikations-Slave, der zu einem Pool von Slaves gehört, die ziemlich
3 Der Eingabepuffer wird mit all den anderen Daten in den InnoDB-Tablespace-Dateien gespeichert; ein Hintergrund-Thread fügt die eingegebenen Datensätze schließlich in den passenden Tabellen zusammen. 4 Beim Entleeren der Tabellen werden die MyISAM-Daten, nicht die InnoDB-Daten, auf die Platte übertragen.
522 | Kapitel 11: Backup und Wiederherstellung
billig ins Spiel gebracht werden können. Wir kommen weiter hinten in diesem Kapitel zu diesem Thema zurück und stellen weitere Überlegungen für Online- und Offline-Backups an. Für den Moment wollen wir nur sagen: Online-Backups, die den Service nicht unterbrechen, sind in MySQL momentan schwer zu realisieren.
Logische oder rohe Backups? Es gibt zwei wesentliche Methoden, um die MySQL-Daten in einem Backup zu sichern: mit einem logischen Backup (auch als »Dump« bezeichnet) und durch das Kopieren der rohen Dateien. Ein logisches Backup enthält die Daten in einer Form, die MySQL entweder als SQL oder als separierten Text interpretieren kann.5 Die rohen Dateien dagegen sind die Dateien, wie sie auf der Festplatte existieren. Jede Methode des Kopierens der Daten hat ihre Vor- und Nachteile.
Logische Backups Logische Backups bringen folgende Vorteile mit sich: • Es sind normale Dateien, die Sie manipulieren und mit Editoren und Kommandozeilenwerkzeugen wie grep und sed untersuchen können. Das ist ganz hilfreich, wenn Sie die Daten erneuern wollen oder wenn Sie sie einfach untersuchen wollen, ohne sie zu erneuern. • Sie lassen sich einfacher erneuern. Sie können sie einfach in mysql leiten oder mysqlimport benutzen. • Sie können das Sichern und Erneuern über das Netzwerk vornehmen, d.h. auf einer anderen Maschine als dem MySQL-Host. • Sie können sehr flexibel sein, weil mysqldump – das Werkzeug, das von den meisten Leuten benutzt wird, um sie herzustellen6 – viele Optionen akzeptiert, wie etwa eine WHERE-Klausel, um einzuschränken, welche Zeilen in dem Backup gesichert werden sollen. • Sie sind unabhängig von der Storage-Engine. Da Sie sie erzeugen, indem Sie Daten aus dem MySQL-Server extrahieren, werden Unterschiede in der zugrunde liegenden Datenspeicherung außer Acht gelassen. Sie können daher mit relativ wenig Aufwand ein Backup der InnoDB-Tabellen erzeugen und daraus MyISAM-Tabellen wiederherstellen. Mit Kopien der Rohdateien ist das nicht möglich.
5 Logische Backups, die von mysqldump erzeugt werden, sind nicht immer Textdateien. SQL-Dumps können viele unterschiedliche Zeichensätze und sogar Binärdaten enthalten, die überhaupt keine gültigen Zeichendaten darstellen. Außerdem sind die Zeilen unter Umständen für viele Editoren zu lang. Dennoch enthalten viele solcher Dateien Daten, die ein Texteditor öffnen und lesen kann, vor allem, wenn Sie mysqldump mit der Option --hex-blob ausführen. 6 Es gibt andere Möglichkeiten, wie etwa Werkzeuge zum parallelen Speichern und Erneuern, allerdings ist dieses Programm am beliebtesten.
Überlegungen und Kompromisse | 523
• Wenn Sie für mysqldump die richtigen Optionen angeben, können Sie in vielen Fällen sogar die logischen Backups auf einen anderen Datenbankserver importieren, wie z.B. PostgreSQL. • Mit ihrer Hilfe können Sie Beschädigungen der Daten vermeiden. Wenn Ihre Festplattenlaufwerke ausfallen und Sie die rohen Dateien kopieren, erzeugen Sie ein beschädigtes Backup. Sie würden es erst bemerken, wenn Sie das Backup später tatsächlich überprüfen; es wäre unbenutzbar. Wenn die Daten, die MySQL im Speicher hat, nicht beschädigt sind, können Sie manchmal ein vertrauenswürdiges logisches Backup erhalten, obwohl eine gute Kopie der Rohdateien nicht möglich ist. Logische Backups haben aber auch Nachteile: • Der Server muss sie generieren, sie erfordern daher mehr CPU-Zyklen. • Logische Backups sind in manchen Fällen größer als die zugrunde liegenden Dateien.7 Die ASCII-Repräsentation der Daten ist nicht immer so effizient wie die Speicherform der Storage-Engine. So erfordert z.B. ein Integer 4 Byte für die Speicherung; wird es in ASCII geschrieben, verlangt es dagegen bis zu 12 Zeichen. Oft können Sie die Dateien effektiv komprimieren, allerdings werden dazu CPU-Ressourcen benötigt. • Der Mangel an Genauigkeit bei der Fließkommadarstellung verhindert möglicherweise eine exakte Erneuerung aus Dump-Dateien. (Die Google-Patches für den MySQL-Server enthalten einen Patch für mysqldump, der dieses Problem löst.) • Das Erneuern aus einem logischen Backup verlangt von MySQL, die Anweisungen zu laden und zu interpretieren sowie die Indizes neu aufzubauen. Das verursacht auf dem Server mehr Arbeit. Die größten Nachteile sind die Kosten für das Speichern der Daten aus MySQL sowie für das erneute Laden mittels SQL-Anweisungen.
Rohe Backups Rohe Backups haben folgende Vorteile: • Backups der rohen Dateien verlangen von Ihnen, die gewünschten Dateien zum Sichern irgendwohin zu kopieren. Die rohen Dateien müssen nicht extra generiert werden. • Das Erneuern roher Backups kann, je nach Storage-Engine, einfacher sein. Bei MyISAM müssen einfach nur die Dateien an ihre Zielorte kopiert werden. InnoDB dagegen verlangt, dass Sie den Server anhalten und möglicherweise auch noch andere Schritte ausführen. • Rohe Backups lassen sich meist relativ gut zwischen Plattformen, Betriebssystemen und MySQL-Versionen portieren. 7 Unserer Erfahrung nach sind logische Backups im Allgemeinen kleiner als rohe Backups, allerdings nicht immer.
524 | Kapitel 11: Backup und Wiederherstellung
• Es kann schneller sein, rohe Backups wiederherzustellen, da der MySQL-Server weder SQL ausführen noch Indizes bauen muss. Wenn Sie InnoDB-Tabellen haben, die nicht komplett in den Speicher des Servers passen, dann kann es viel schneller sein, rohe Dateien zu erneuern. Dies sind einige der Nachteile von rohen Backups: • Die rohen Dateien von InnoDB sind oft viel großer als die entsprechenden logischen Backups. Der InnoDB-Tablespace enthält typischerweise eine Menge unbenutzten Platzes. Ziemlich viel Platz wird auch für andere Zwecke als das Speichern von Tabellendaten benutzt (den Eingabepuffer, das Rollback-Segment usw.). • Rohe Backups sind zwischen Plattformen, Betriebssystemen und MySQL-Versionen nicht immer portierbar. Probleme könnten Sie z.B. bei der Schreibweise der Dateinamen (Groß-/Kleinschreibung) und bei den Fließkommaformaten bekommen. Möglicherweise sind Sie nicht in der Lage, auf ein System zu wechseln, das ein anderes Fließkommaformat benutzt (die Mehrzahl der Prozessoren verwendet jedoch das IEEE-Fließkommaformat). Rohe Backups sind im Allgemeinen einfacher und effizienter. Verlassen Sie sich darauf jedoch nicht für die Langzeitspeicherung oder für gesetzliche Erfordernisse; Sie müssen wenigstens regelmäßig logische Backups anlegen. Betrachten Sie ein Backup (speziell ein rohes Backup) erst dann als gut, wenn Sie es getestet haben. Für InnoDB bedeutet dies, dass Sie eine MySQL-Instanz starten, InnoDB laufen lassen und dann CHECK TABLES ausführen. Sie können das auch überspringen oder einfach innochecksum auf den Dateien ausführen – davon raten wir jedoch ab. Bei MyISAM sollten Sie CHECK TABLES aufrufen oder myisamchk einsetzen. Eine weitere kluge Methode ist es, die beiden Ansätze zu mischen: Zuerst erzeugen Sie rohe Kopien, dann starten Sie eine MySQL-Serverinstanz und legen mit ihrer Hilfe logische Backups aus den rohen Kopien an. Sie erhalten die Vorteile beider Ansätze, ohne dass Sie den Produktionsserver während des Dumps unnötig belasten. Besonders bequem ist es, wenn Sie die Möglichkeit haben, Schnappschüsse vom Dateisystem aufzunehmen – Sie nehmen einen Schnappschuss auf, kopieren den Schnappschuss auf einen anderen Server und geben ihn frei. Anschließend testen Sie die rohen Dateien und führen ein logisches Backup aus.
Was Sie mit einem Backup sichern Ihre Anforderungen an die Wiederherstellung geben vor, was Sie in einem Backup sichern müssen. Die einfachste Strategie besteht darin, einfach Ihre Daten und Tabellendefinitionen zu sichern, aber das ist wirklich nur der Minimalansatz. Im Allgemeinen brauchen Sie ein wenig mehr, um einen Server, der in der Produktion eingesetzt werden soll, wiederherstellen zu können. Hier sind einige Dinge, die Sie auch in Ihre MySQLBackups aufnehmen sollten:
Überlegungen und Kompromisse | 525
Nichtoffensichtliche Daten Vergessen Sie nicht die Daten, die leicht übersehen werden: zum Beispiel Ihre Binärlogs und die InnoDB-Transaktionslogs. Code Ein moderner MySQL-Server kann eine Menge Code speichern, wie etwa Trigger und gespeicherte Prozeduren. Wenn Sie die mysql-Datenbank sichern, dann sichern Sie einen Großteil dieses Codes. Allerdings wird es dann schwer, eine einzelne Datenbank in ihrer Gesamtheit wiederherzustellen, weil einige der »Daten« in dieser Datenbank, wie etwa die gespeicherten Prozeduren, eigentlich in der mysqlDatenbank gespeichert sind. Die Replikationskonfiguration Wenn Sie einen Server wiederherstellen, der an der Replikation beteiligt ist, müssen Sie in Ihre Backups auch alle Replikationsdateien aufnehmen, die Sie dafür brauchen – z.B. Binärlogs, Relay-Logs, Log-Indexdateien und die .info-Dateien. Zumindest müssen Sie die Ausgabe von SHOW MASTER STATUS und/oder SHOW SLAVE STATUS mit aufnehmen. Es hilft außerdem, FLUSH LOGS auszuführen, damit MySQL ein neues Binärlog starten kann. Es ist einfacher, eine punktgenaue Wiederherstellung vom Anfang einer Logdatei durchzuführen als aus der Mitte. Die Serverkonfiguration Wenn Sie die Wiederherstellung nach einer echten Katastrophe durchführen – sagen wir, dass Sie in einem neuen Rechenzentrum nach einem Erdbeben einen Server von Grund auf neu aufbauen –, werden Sie es begrüßen, die Konfigurationsdateien des Servers in dem Backup vorzufinden. Ausgewählte Betriebssystemdateien Wie bei der Serverkonfiguration ist es wichtig, externe Konfigurationen in einem Backup zu sichern, die für einen Produktionsserver gebraucht werden. Auf einem Unix-Server könnte das die cron-Jobs, Benutzer- und Gruppenkonfigurationen, administrative Skripten und sudo-Regeln einschließen. Diese Empfehlungen lassen sich in vielen Szenarien schnell mit »alles rein ins Backup« übersetzen. Falls Sie jedoch viele Daten haben, kann das sehr teuer werden, und Sie müssen genauer darüber nachdenken, wie Sie Ihre Backups erstellen. Vermutlich sollten Sie unterschiedliche Daten in unterschiedliche Backups sichern. Zum Beispiel können Sie Daten, Binärlogs und Betriebssystem- und Systemkonfigurationsdateien separat sichern.
Inkrementelle Backups Eine verbreitete Strategie für den Umgang mit zu vielen Daten ist die regelmäßige Durchführung inkrementeller Backups. Hier sind einige Vorschläge: • Sichern Sie Ihre Binärlogs im Backup. Das ist die einfachste, die am weitesten verbreitete und die insgesamt beste Methode, um inkrementelle Backups zu erzeugen. • Packen Sie keine Tabellen in das Backup, die sich nicht geändert haben. Manche Storage-Engines, wie etwa MyISAM, zeichnen den letzten Zeitpunkt auf, an dem die 526 | Kapitel 11: Backup und Wiederherstellung
einzelnen Tabellen jeweils geändert wurden. Sie können diese Zeiten ermitteln, indem Sie die Dateien auf der Festplatte untersuchen oder indem Sie SHOW TABLE STATUS ausführen. Wenn Sie InnoDB benutzen, kann ein Trigger Ihnen helfen, die letzten Änderungen zu verfolgen, indem Sie die Änderungszeiten in einer kleinen »Letzte Änderungszeit«-Tabelle aufzeichnen. Das müssen Sie nur für Tabellen tun, die sich nicht so oft ändern. Die Kosten sollten also minimal sein. Mit einem eigenen Backup-Skript können Sie leicht feststellen, welche Tabellen sich geändert haben. »Lookup«-Tabellen, die Daten wie Listen von Monatsnamen in verschiedenen Sprachen oder Abkürzungen für Staaten oder Regionen enthalten, sollten Sie in eine separate Datenbank legen, damit Sie sie nicht jedes Mal in das Backup schreiben müssen. • Sichern Sie keine Zeilen, die sich nicht geändert haben. Wenn eine Tabelle nur zum Einfügen (INSERT-only) gedacht ist, wie z.B. eine Tabelle, die Treffer auf einer Webseite verzeichnet, können Sie eine TIMESTAMP-Spalte hinzufügen und nur solche Zeilen sichern, die seit dem letzten Backup eingefügt wurden. Hier lässt sich auch gut mit der Merge-Storage-Engine dafür sorgen, dass ältere Daten in statische Tabellen kommen. • Sichern Sie überhaupt keine Daten in einem Backup. Manchmal ist das wirklich sinnvoll – z.B. falls Sie ein Data-Warehouse haben, das aus anderen Daten aufgebaut wird und technisch gesehen redundant ist. Sichern Sie hier einfach nur die Daten, die Sie zum Aufbau des Warehouse verwendet haben, und nicht das DataWarehouse selbst. Das ist wahrscheinlich keine schlechte Idee, selbst wenn es nur langsam »wiederherzustellen« ist, indem man das Warehouse aus den Originaldateien wieder aufbaut. Indem man Backups vermeidet, kann man über die Zeit gesehen mehr sparen als durch die potenziell schnellere Wiederherstellungszeit, die sich bei einem vollständigen Backup ergibt. Vielleicht entscheiden Sie sich auch dafür, temporäre Daten nicht mehr zu sichern, wie etwa Tabellen mit Website-Sitzungsdaten. • Sichern Sie nur die Änderungen am Binärlog im Backup. Mittels rdiff erhalten Sie die Binär-Deltas Ihrer Binärlogs und sichern nur die Änderungen seit dem letzten Backup (wobei Sie regelmäßig ein vollständiges Backup durchführen). Ein weiteres nützliches Werkzeug ist rdiff-backup, das die Funktionalität von rdiff und rsync zu einer kompletten Backup-Lösung kombiniert. Oder Sie beginnen mit FLUSH LOGS ein neues Binärlog nach jedem Backup, damit Sie überhaupt keine Binär-Deltas benötigen. • Sichern Sie nur die Änderungen an den Datendateien. Das entspricht dem Sichern der Differenzen in Ihren Binärlogs. Gebräuchliche Unix-Werkzeuge für diesen Zweck sind wieder rdiff und rdiff-backup. Diese Strategie eignet sich für außerordentlich große Datenbanken, die sich nicht sehr ändern. Nehmen Sie an, bei einem Terabyte an Daten ändern sich jeden Tag nur 50 GByte. Es ist möglicherweise keine schlechte Idee, die Binär-Differenz täglich zu sichern und nur hin und wieder ein vollständiges Backup anzulegen. Der Vorteil wäre, dass Sie die Binär-Differenzen in
Überlegungen und Kompromisse | 527
einer sequenziellen Lese-/Schreiboperation viel schneller auf das vollständige Backup anwenden können als ein Binärlog. Das Backup der Binär-Differenz selbst könnte allerdings langsamer sein als ein vollständiges Backup. Nachteilig an inkrementellen Backups ist die erhöhte Komplexität während der Wiederherstellung. Wenn Sie die Wiederherstellung in einer Stresssituation durchführen, werden Sie froh sein, wenn Sie nur ein Backup erneuern müssen, anstatt mehrere inkrementelle Backups nacheinander anzuwenden. Wir empfehlen Ihnen aus Gründen der Einfachheit, vollständige Backups zu generieren, falls das möglich ist. Unabhängig davon müssen Sie definitiv gelegentlich vollständige Backups durchführen – wir empfehlen einmal pro Woche. Sie können nicht erwarten, dass eine Wiederherstellung aus den inkrementellen Backups des ganzen letzten Jahres funktioniert. Selbst eine Woche stellt eine Menge Arbeit und ein großes Risiko dar.
Storage-Engines und Konsistenz MySQLs Wahl der Storage-Engines kann Backups deutlich verkomplizieren. Das Problem besteht darin, wie man mit einer bestimmten Storage-Engine ein konsistentes Backup bekommt. Sie müssen sich eigentlich über zwei Arten von Konsistenz Gedanken machen: über Datenkonsistenz und über Dateikonsistenz.
Datenkonsistenz Wenn Sie Backups durchführen, müssen Sie sicherstellen, dass Ihre Daten auf den Moment genau konsistent sind. In einer E-Commerce-Datenbank z.B. müssen Sie dafür sorgen, dass Ihre Rechnungen und Zahlungen untereinander konsistent sind. Das Wiederherstellen einer Zahlung ohne die dazugehörende Rechnung oder umgekehrt macht mit Sicherheit Ärger! Bei Online-Backups (d.h. von einem laufenden Server) müssen Sie darauf achten, dass Sie ein konsistentes Backup aller verwandten Tabellen bekommen. Sie können die Tabellen also nicht einfach nacheinander sperren und in einem Backup sichern – was wiederum bedeutet, dass Ihre Backups aufwendiger werden, als Ihnen vermutlich lieb ist. Wenn Sie keine transaktionsfähige Storage-Engine benutzen, haben Sie daher keine andere Wahl, als auf allen Tabellen, die Sie zusammen im Backup sichern wollen, LOCK TABLES auszuführen, und die Sperre erst dann wieder aufzuheben, wenn alle zusammengehörenden Tabellen gesichert wurden. InnoDBs MVCC-Fähigkeiten können dabei helfen. Sie können eine Transaktion beginnen, eine Gruppe verwandter Tabellen in einem Dump speichern und die Transaktion bestätigen. (Verwenden Sie nicht LOCK TABLES, wenn Sie eine Transaktion einsetzen, um ein konsistentes Backup zu erhalten, weil es Ihre Transaktion implizit bestätigt – Genaueres erfahren Sie im MySQL-Handbuch.) Solange Sie das Transaktionsisolationslevel
528 | Kapitel 11: Backup und Wiederherstellung
REPEATABLE READ verwenden, bekommen Sie auf diese Weise einen absolut konsistenten,
punktgenauen Schnappschuss der Daten, der die weitere Arbeit auf Ihrem Server nicht blockiert, während das Backup erstellt wird. Dieser Ansatz schützt Sie jedoch nicht vor einer schlecht entworfenen Anwendungslogik. Nehmen Sie an, Ihr E-Commerce-Geschäft fügt eine Zahlung hinzu, bestätigt die Transaktion und fügt dann die Rechnung in eine andere Transaktion ein. Ihr Backup-Vorgang könnte zwischen diesen beiden Operationen beginnen, wodurch zwar die Zahlung im Backup auftaucht, nicht jedoch die Rechnung. Aus diesem Grund müssen Sie beim Entwurf der Transaktionen darauf achten, dass verwandte Operationen zusammengefasst werden. Mit mysqldump erhalten Sie ebenfalls ein konsistentes logisches Backup der InnoDBTabellen. mysqldump unterstützt die Option --single-transaction, die genau das macht, was wir gerade beschrieben haben. Allerdings kann das eine sehr lange Transaktion verursachen, die bei manchen Lasten einen unakzeptabel großen Overhead mit sich bringt. Werkzeuge, die »Backup-Sets« unterstützen, wie ZRM (darauf kommen wir später) oder Maatkits mk-parallel-dump, helfen Ihnen dabei, verwandte Tabellen in Backups zu sichern.
Dateikonsistenz Es ist darüber hinaus wichtig, dass jede Datei in sich selbst konsistent ist – dass also das Backup nicht den Zustand auf halbem Wege durch eine große UPDATE-Anweisung wiedergibt – und dass alle Dateien, die Sie sichern, untereinander konsistent sind. Wenn Sie keine in sich konsistenten Dateien erhalten, dann erleben Sie eine böse Überraschung, sobald Sie versuchen, sie wiederherzustellen (sie sind dann wahrscheinlich beschädigt). Und wenn Sie zusammengehörende Dateien zu unterschiedlichen Zeitpunkten kopieren, werden sie untereinander nicht konsistent sein. Ein Beispiel sind die .MYD- und .MYIDateien von MyISAM. Bei einer nichttransaktionsfähigen Storage-Engine wie MyISAM besteht Ihre einzige Möglichkeit darin, die Tabellen zu sperren und zu übertragen. Das heißt, Sie müssen entweder eine Kombination aus LOCK TABLES und FLUSH TABLES benutzen, damit der Server die im Speicher befindlichen Änderungen auf die Festplatte überträgt, oder Sie müssen FLUSH TABLES WITH READ LOCK einsetzen. Wenn das Übertragen abgeschlossen ist, können Sie sicher eine rohe Kopie der MyISAM-Dateien vornehmen. Bei InnoDB ist es etwas schwieriger sicherzustellen, dass die Dateien auf der Festplatte konsistent sind. Selbst mit FLUSH TABLES WITH READ LOCK arbeitet InnoDB im Hintergrund weiter: Eingabepuffer-, Log- und Schreib-Threads fügen weiterhin Änderungen in seine Log- und Tablespace-Dateien ein. Diese Threads sind aufgrund ihres Entwurfs asynchron – aufgrund der Verrichtung dieser Arbeit in Hintergrund-Threads kann InnoDB seine hohe Nebenläufigkeit erreichen – und daher unabhängig von LOCK TABLES. Sie müssen deshalb nicht nur dafür sorgen, dass die einzelnen Dateien in sich konsistent sind,
Überlegungen und Kompromisse | 529
sondern dass Sie die Log- und Tablespace-Dateien im gleichen Augenblick kopieren. Wenn Sie ein Backup machen, während ein Thread eine Datei ändert, oder wenn Sie die Log-Dateien zu einem anderen Zeitpunkt sichern als die Tablespace-Dateien, kann es passieren, dass Sie nach der Wiederherstellung wieder ein beschädigtes System haben. Es gibt zwei Möglichkeiten, dieses Problem zu umgehen: • Sie warten, bis InnoDBs Threads zum Aufräumen und Zusammenführen des Eingabepuffers fertig sind. Sie können die Ausgabe von SHOW INNODB STATUS anschauen und die Dateien kopieren, wenn es keine schmutzigen Puffer oder ausstehenden Schreiboperationen mehr gibt. Dieser Ansatz ist aber unter Umständen sehr langwierig; er erfordert wegen der InnoDB-Hintergrund-Threads zu viele Vermutungen und ist möglicherweise nicht sicher. Wir raten deshalb davon ab. • Sie erzeugen mit einem System wie LVM einen konsistenten Schnappschuss der Daten- und Log-Dateien. Sie müssen die Daten- und Log-Dateien konsistent in Bezug aufeinander aufnehmen; es ist nicht gut, die Schnappschüsse getrennt vorzunehmen. Wir kommen später noch auf LVM-Schnappschüsse zurück. Sobald Sie die Dateien irgendwohin kopiert haben, können Sie die Sperren freigeben und den MySQL-Server wieder normal laufen lassen.
Replikation Eine gängige Weisheit besagt, dass die MySQL-Replikation sich fantastisch für Backups eignet. Es hat sicher seine Vorzüge, die Replikation als Teil einer umfassenden BackupStrategie einzusetzen, es ist jedoch nicht das A und O für Backups, wie oft behauptet wird. Der größte Vorteil der Datensicherung von einem Slave besteht darin, dass der Master nicht unterbrochen oder zusätzlich belastet wird. Das ist eine gute Begründung, um einen Slave-Server einzurichten, selbst wenn Sie ihn für den Lastausgleich oder zur Gewährleistung von Hochverfügbarkeit nicht brauchen. Wenn Sie auf das Geld achten müssen, dann können Sie den Backup-Slave auch für andere Aufgaben einsetzen, etwa für die Erstellung von Berichten – solange Sie nicht auf ihm schreiben und damit die Daten ändern, die Sie im Backup sichern wollen. Der Slave muss nicht speziell für Backups gedacht sein, Hauptsache, er ist in der Lage, dem Master zu folgen, um Ihr nächstes Backup zu erzeugen, falls seine anderen Rollen ihn bei der Replikation zurückfallen lassen. Wenn Sie ein Backup von einem Slave erstellen, dann sichern Sie alle Informationen über die Replikationsprozesse, wie etwa die Position des Slaves auf dem Master. Das ist nützlich, wenn Sie neue Slaves klonen wollen, erneut Binärlogs auf den Master zwecks punktgenauer Wiederherstellung anwenden wollen, den Slave zum Master befördern wollen und dergleichen mehr. Achten Sie darauf, dass keine temporären Tabellen geöffnet sind, wenn Sie Ihren Slave stoppen, weil diese verhindern könnten, dass Sie die Replikation neu starten. Mehr darüber erfahren Sie in »Fehlende temporäre Tabellen« auf Seite 429.
530 | Kapitel 11: Backup und Wiederherstellung
Manchmal ist es sinnvoll, die Replikation auf einem der Slaves absichtlich zu verzögern. Nehmen Sie an, Sie verzögern die Replikation um eine Stunde. Wenn auf dem Master eine unerwünschte Anweisung läuft, haben Sie eine Stunde Zeit, um es zu bemerken und den Slave zu stoppen, bevor er das Event aus seinem Relay-Log wiederholt. Sie können dann den Slave zum Master befördern und eine relativ kleine Anzahl von Log-Events wieder abspielen, wobei Sie die falschen Anweisungen überspringen. Das geht möglicherweise viel schneller als die Technik zur punktgenauen Wiederherstellung, die wir später besprechen wollen. Das mk-slave-delay-Skript von Maatkit kann Ihnen dabei helfen. Der Slave hat möglicherweise nicht die gleichen Daten wie der Master. Viele Leute gehen davon aus, dass Slaves exakte Kopien ihrer Master sind. Unserer Erfahrung nach sind allerdings Datenunterschiede auf Slaves relativ verbreitet. MySQL bietet keine Möglichkeit, dieses Problem zu erkennen. Werden die falschen oder beschädigte Daten auf dem Slave im Backup gesichert, dann nützt das Backup praktisch überhaupt nichts. In »Feststellen, ob Slaves konsistent mit dem Master sind« auf Seite 413 erfahren Sie, wie Sie sicherstellen können, dass die Daten auf dem Slave identisch mit denen auf dem Master sind. Kapitel 8 enthält außerdem Hinweise darauf, wie Sie verhindern, dass die Slaves anders werden als der Master. Das Vorhandensein einer replizierten Kopie Ihrer Daten schützt Sie vermutlich vor Problemen wie Festplattenausfällen auf dem Master, bietet allerdings keine Garantie. Eine Replikation ist kein Backup.
Binärlogs organisieren und sichern Die Binärlogs Ihres Servers gehören zu den wichtigsten Dingen, die Sie in einem Backup sichern können. Sie sind für eine punktgenaue Wiederherstellung unerlässlich. Da sie normalerweise kleiner sind als Ihre Daten, kann man sie leicht häufiger sichern. Falls Sie ein Backup Ihrer Daten von einem bestimmten Zeitpunkt sowie alle Binärlogs bis zu diesem Zeitpunkt haben, können Sie die Binärlogs wieder abspielen und die Änderungen wieder einbringen, die Sie seit dem letzten vollständigen Backup vorgenommen haben. MySQL benutzt das Binärlog auch für die Replikation. Das bedeutet, dass Ihre Backupund Wiederherstellungsstrategien oft mit Ihrer Replikationskonfiguration zusammenspielen. Binärlogs sind »besonders«. Wenn Sie Ihre Daten verlieren, wollen Sie sie nicht auch noch verlieren. Um die Wahrscheinlichkeit zu minimieren, dass dies geschieht, können Sie sie auf einem separaten Volume aufbewahren. Das ist auch dann gut, wenn Sie mit LVM einen Schnappschuss der Binärlogs erstellen wollen. Um zusätzliche Sicherheit zu gewinnen, können Sie sie auf einem SAN ablegen oder sie mit DRBD auf ein anderes Gerät replizieren. Mehr dazu erfahren Sie in Kapitel 9. Binärlogs sollten oft in Backups gesichert werden. Falls Sie es sich nicht leisten können, das Datenaufkommen von mehr als 30 Minuten zu verlieren, dann sichern Sie sie wenigstens alle 30 Minuten. Um die Sicherheit noch zu erhöhen, können Sie auch einen schreibBinärlogs organisieren und sichern | 531
geschützten Replikations-Slave mit --log_slave_updates benutzen. Die Log-Positionen entsprechen nicht denen des Masters, allerdings ist es normalerweise nicht schwer, für die Wiederherstellung die richtigen Positionen zu finden. Das Folgende ist unsere empfohlene Serverkonfiguration für das Binär-Logging: log_bin = mysql-bin sync_binlog = 1 innodb_support_xa = 1 # ab MySQL 5.0 innodb_safe_binlog # nur für MySQL 4.1, entspricht in etwa innodb_support_xa
Es gibt für das Binärlog verschiedene weitere Konfigurationsoptionen, wie etwa Optionen zum Beschränken der Größe der einzelnen Logs. Mehr dazu erfahren Sie im MySQL-Handbuch.
Das Binärlogformat Das Binärlog besteht aus einer Folge von Events. Jedes Event besitzt einen Header fester Größe, der eine Vielzahl an Informationen enthält, wie etwa den aktuellen Zeitstempel und die vorgegebene Datenbank. Mit mysqlbinlog können Sie den Inhalt des Binärlogs untersuchen und einige der Header-Informationen ausgeben. Hier ist ein Beispiel für die Ausgabe: 1 # at 277 2 #071030 10:47:21 server id 3 end_log_pos 369 error_code=0 3 SET TIMESTAMP=1193755641/*!*/; 4 insert into test(a) values(2)/*!*/;
Query
thread_id=13
exec_time=0
Zeile 1 enthält den Byte-Offset innerhalb der Log-Datei (in diesem Fall 277). Zeile 2 enthält folgende Elemente: • Datum und Uhrzeit des Events, die MySQL auch verwendet, um die SET TIMESTAMPAnweisung zu generieren. • Die Server-ID des Ursprungsservers, die notwendig ist, um Endlosschleifen bei der Replikation und weitere Probleme zu verhindern. • Die end_log_pos, also den Byte-Offset des nächsten Events. Bei einer Transaktion, die aus mehreren Anweisungen besteht, ist dieser Wert für die meisten Events nicht richtig. MySQL kopiert während solcher Transaktionen die Events in einen Puffer auf dem Master, kennt aber dabei nicht die Position des nächsten Log-Events. • Der Event-Typ. Unser Beispieltyp ist Query, es gibt aber viele unterschiedliche Typen. • Die Thread-ID des Threads, der das Event auf dem Ursprungsserver ausgeführt hat. Dieser Wert ist wichtig für die Überprüfung sowie für das Ausführen der CONNECTION_ID( )-Funktion.
532 | Kapitel 11: Backup und Wiederherstellung
• Die exec_time, deren wahre Bedeutung selbst einigen der MySQL-Entwickler unklar ist, die wir danach gefragt haben. Im Allgemeinen wird hier aufgezeichnet, wie lange die Anweisung für die Ausführung brauchte, manchmal allerdings stehen hier seltsame Werte. Zum Beispiel werden die Werte im Relay-Log auf einem Slave sehr groß sein, dessen Ein-/Ausgabe-Thread sehr weit hinter dem Master herhinkt, selbst wenn die Anweisungen auf dem Master schnell ausgeführt wurden. Greifen Sie einfach nicht auf diesen Wert zurück. • Jeden Fehlercode, den das Event auf dem Ursprungsserver verursacht hat. Ruft das Event beim Abspielen auf einem Slave einen anderen Fehler hervor, dann schlägt die Replikation zur Sicherheit fehl. Alle weiteren Zeilen enthalten das SQL, das zum Abspielen des Events benötigt wurde. Auch benutzerdefinierte Variablen und andere spezielle Einstellungen, wie der Zeitstempel, der wirksam war, als die Anweisung ausgeführt wurde, erscheinen hier. Falls Sie das zeilenbasierte Logging einsetzen, das in MySQL 5.1 verfügbar ist, ist das Event kein SQL. Stattdessen handelt es sich um ein nicht vom Menschen lesbares »Image« der Modifikationen, die die Anweisung an der Tabelle vorgenommen hat.
Alte Binärlogs sicher aufräumen Sie müssen eine Strategie zum Ungültigmachen von Logs wählen, um zu verhindern, dass MySQL Ihre Festplatte mit Binärlogs füllt. Wie groß Ihre Logs werden, hängt von Ihrer Belastung sowie dem Format der Logs ab (das zeilenbasierte Logging, das es in MySQL 5.1 gibt, verursacht normalerweise größere Log-Einträge). Wir empfehlen Ihnen, nach Möglichkeit die Logs so lange aufzuheben, wie sie Ihnen nützen. Wenn Sie sie aufheben, können Sie Replikations-Slaves einrichten, die Belastung Ihres Servers analysieren, Dinge überprüfen und aus Ihrem letzten vollständigen Backup eine punktgenaue Wiederherstellung durchführen. Berücksichtigen Sie all diese Anforderungen, wenn Sie darüber entscheiden, wie lange Sie Ihre Logs aufheben wollen. Üblicherweise wird MySQL mit der Variablen expire_logs_days angewiesen, die Logs nach einer gewissen Zeit aufzuräumen. Diese Variable gibt es erst seit MySQL 4.1; vor dieser Version mussten Sie die Binärlogs manuell aufräumen. Daher werden Sie vermutlich gelegentlich den Rat finden, die alten Binärlogs mit einem cron-Eintrag zu entfernen: 0 0 * * * /usr/bin/find /var/log/mysql -mtime +N -name "mysql-bin.[0-9]*" | xargs rm
Dies war zwar in den Versionen vor MySQL 4.1 die einzige Möglichkeit, die Logs aufzuräumen, dennoch sollten Sie sie ab MySQL 4.1 nicht mehr benutzen! Wenn Sie die Logs mit rm löschen, stimmt die Statusdatei mysql-bin.index nicht mehr mit den Dateien auf der Festplatte überein, und manche Anweisungen, wie SHOW MASTER LOGS, beginnen, stillschweigend zu versagen. Das Problem wird auch nicht durch das manuelle Ändern der Datei mysql-bin.index behoben. Benutzen Sie stattdessen einen cron-Befehl: 0 0 * * * /usr/bin/mysql -e "PURGE MASTER LOGS BEFORE CURRENT_DATE - INTERVAL N DAY"
Binärlogs organisieren und sichern | 533
Die Einstellung expire_logs_days wird beim Serverstart wirksam bzw. wenn MySQL das Binärlog wechselt. Falls also Ihr Binärlog niemals voll wird und wechselt, räumt der Server die älteren Einträge niemals auf. Er entscheidet über das Aufräumen nämlich nach einem Blick auf die Änderungszeiten und nicht auf den Inhalt.
Daten in einem Backup sichern Wie bei vielen anderen Themen gibt es bessere und schlechtere Methoden, um das eigentliche Backup durchzuführen – und die offensichtlichen Methoden sind manchmal gar nicht so gut. Der Trick besteht darin, die Netzwerk-, Festplatten- und CPU-Kapazitäten zu maximieren, um die Backups so schnell wie möglich zu erzeugen. Das ist ein Balanceakt, und Sie müssen herumexperimentieren, um den optimalen Punkt zu finden. Es ist schwer, einen speziellen Rat zu erteilen, weshalb wir Ihnen lieber einige allgemeinere Techniken zeigen.
Ein logisches Backup anlegen Zuerst müssen Sie in Bezug auf logische Backups verstehen, dass sie nicht alle gleich erzeugt werden. Es gibt eigentlich zwei Arten logischer Backups: SQL-Dumps und separierte Dateien.
SQL-Dumps Die meisten Leute sind vermutlich mit SQL-Dumps vertraut, da diese standardmäßig von mysqldump erzeugt werden. So produziert z.B. ein Dump einer kleinen Tabelle mit den vorgegebenen Optionen die folgende (gekürzte) Ausgabe: $ mysqldump test t1 -- [Versions- und Host-Kommentare] /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -- [Weitere versionsspezifische Kommentare, um Optionen zum Wiederherstellen zu sichern] --- Tabellenstruktur fuer Tabelle `t1` -DROP TABLE IF EXISTS `t1`; CREATE TABLE `t1` ( `a` int(11) NOT NULL, PRIMARY KEY (`a`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; --- Sichern der Daten fuer Tabelle `t1` in einem Dump --
534 | Kapitel 11: Backup und Wiederherstellung
LOCK TABLES `t1` WRITE; /*!40000 ALTER TABLE `t1` DISABLE KEYS */; INSERT INTO `t1` VALUES (1); /*!40000 ALTER TABLE `t1` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -- [Weitere Wiederherstellung von Optionen]
Die Dump-Datei enthält sowohl die Tabellenstruktur als auch die Daten, ausgeschrieben als gültige SQL-Befehle. Die Datei beginnt mit Kommentaren, die die verschiedenen MySQL-Optionen setzen. Diese sollen entweder das Erneuern effizienter machen oder die Kompatibilität und die Korrektheit erhöhen. Anschließend sehen Sie die Struktur der Tabelle und dann ihre Daten. Schließlich setzt das Skript die Optionen wieder zurück, die es am Anfang des Dumps geändert hat. Die Ausgabe des Dumps lässt sich für eine Wiederherstellungsoperation ausführen. Das ist zwar sehr bequem, allerdings eignen sich die Standardoptionen von mysqldump nicht besonders gut für das Erzeugen eines riesigen Backups (wir schauen uns die Optionen von mysqldump später genauer an). mysqldump ist nicht das einzige Werkzeug, das logische SQL-Backups erstellt. Sie können sie z.B. auch mit phpMyAdmin erzeugen. Wir wollen hier eigentlich nicht so sehr auf Probleme mit den einzelnen Werkzeugen hinweisen, sondern auf die Nachteile monolithischer logischer SQL-Backups. Die wichtigsten Problemfelder dabei sind: Schema und Daten werden zusammen gespeichert Es ist zwar bequem, wenn Sie die Wiederherstellung aus einer einzigen Datei vornehmen wollen, allerdings wird es kompliziert, wenn Sie nur eine Tabelle oder nur die Daten erneuern müssen. Sie können dieses Problem lindern, indem Sie zwei Dumps erzeugen – einen für die Daten, einen für das Schema. Allerdings haben Sie dann immer noch das nächste Problem. Riesige SQL-Anweisungen Es stellt für den Server viel Arbeit dar, alle SQL-Anweisungen zu parsen und auszuführen. Das ist eine relativ langsame Methode, um die Daten zu laden. Eine einzige riesige Datei Die meisten Texteditoren können große Dateien oder Dateien mit sehr langen Zeilen nicht bearbeiten. Manchmal helfen zwar Kommandozeilen-Stream-Editoren wie sed oder grep, die gewünschten Daten zu erhalten, allerdings ist es besser, die Dateien klein zu halten. Logische Backups sind teuer Es gibt effizientere Methoden, die Daten aus MySQL herauszubekommen, als sie als Ergebnismenge über das Client-/Serverprotokoll zu schicken. Diese Einschränkungen bedeuten, dass SQL-Dumps bei wachsender Tabellengröße schnell unbenutzbar werden. Es gibt allerdings noch eine andere Möglichkeit: Exportieren Sie die Daten in separierte Dateien.
Daten in einem Backup sichern | 535
Backups in separierte Dateien Mit dem Befehl SELECT INTO OUTFILE SQL erzeugen Sie ein logisches Backup Ihrer Daten in einem separierten Dateiformat. (Mit der mysqldump-Option --tab, die den SQL-Befehl für Sie ausführt, können Sie einen Dump in separierte Dateien erzeugen.) Separierte Dateien enthalten die Rohdaten, dargestellt in ASCII, ohne SQL, Kommentare und Spaltennamen. Hier ist ein Beispiel, das den Dump im CSV-Format (Comma-Separated Values; durch Kommas abgetrennte Werte) anlegt, das eine gute Lingua franca für Tabellendaten bildet: mysql> -> -> ->
SELECT * INTO OUTFILE '/tmp/t1.txt' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' FROM test.t1;
Die resultierende Datei ist kompakter und lässt sich leichter mit Kommandozeilenwerkzeugen manipulieren als eine SQL-Dump-Datei. Der größte Vorteil dieses Ansatzes besteht in der Geschwindigkeit für Backup und Wiederherstellung. Sie können die Daten mit LOAD DATA INFILE wieder zurück in die Tabelle laden; die Optionen sind dabei gleich denen beim Anlegen des Dumps: mysql> -> -> ->
LOAD DATA INFILE '/tmp/t1.txt' INTO TABLE test.t1 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';
Dies ist ein informeller Test, den wir durchgeführt haben, um die Unterschiede in Backup- und Wiederherstellungsgeschwindigkeit zwischen SQL-Dateien und separierten Dateien zu zeigen. Wir haben für diesen Test einige Produktionsdaten angepasst. Die Tabelle, aus der wir den Dump erzeugen, sieht so aus: CREATE TABLE load_test ( col1 date NOT NULL, col2 int NOT NULL, col3 smallint unsigned NOT NULL, col4 mediumint NOT NULL, col5 mediumint NOT NULL, col6 mediumint NOT NULL, col7 decimal(3,1) default NULL, col8 varchar(10) NOT NULL default '', col9 int NOT NULL, PRIMARY KEY (col1,col2) ) ENGINE=InnoDB;
Die Tabelle enthält 15 Millionen Zeilen und belegt etwa 700 MByte auf der Festplatte. Tabelle 11-1 vergleicht die Leistung der beiden Backup- und Wiederherstellungsmethoden. Sie erkennen, dass es bei der Wiederherstellung einen großen Geschwindigkeitsunterschied gibt.
536 | Kapitel 11: Backup und Wiederherstellung
Tabelle 11-1: Backup- und Wiederherstellungszeiten für SQL- und separierte Dumps Methode
Dump-Größe
Dump-Zeit
Wiederherstellungszeit
SQL-Dump
727 MByte
102 s
600 s
Separierter Dump
669 MByte
86 s
301 s
Die SELECT INTO OUTFILE-Methode unterliegt allerdings einigen Beschränkungen: • Sie können das Sichern nur in eine Datei auf der Maschine vornehmen, auf der der MySQL-Server läuft. (Sie können sich ein eigenes SELECT INTO OUTFILE zurechtzimmern, indem Sie ein Programm schreiben, das ein SELECT-Ergebnis liest und auf die Festplatte schreibt. Wir haben diesen Ansatz schon gesehen.) • MySQL muss die Berechtigungen haben, in das Verzeichnis zu schreiben, in das die Datei geschrieben wurde, weil der MySQL-Server ja die Datei schreibt – und nicht der Benutzer die Datei ausführt. • Aus Sicherheitsgründen können Sie eine bestehende Datei nicht überschreiben – ungeachtet der Berechtigungen der Datei. • Sie können den Dump nicht direkt in eine komprimierte Datei schreiben.
Paralleles Dumpen und Wiederherstellen Auf einem System mit mehreren CPUs ist es oft schneller, wenn man Backup und Wiederherstellung parallel ausführt. Mit »parallel« meinen wir das Speichern oder Wiederherstellen mehrerer Tabellen auf einmal, nicht mehrere Programme, die an derselben Tabelle arbeiten. Es funktioniert im Allgemeinen nicht besonders gut, zwei Programme gleichzeitig Daten in eine einzige Tabelle schreiben zu lassen. Sie brauchen keine ausgefallenen Werkzeuge, um das Sichern und Wiederherstellen parallel durchzuführen; Sie können es von Hand erledigen, indem Sie mehrere Instanzen eines Backup-Werkzeugs ausführen. Es gibt jedoch Werkzeuge und Skripten, die speziell für diesen Zweck gedacht sind, wie etwa mk-parallel-dump von Maatkit und mysqlpdump (http://www.fr3nd.net/projects/mysqlpdump/). Momentan sind diese Werkzeuge noch relativ neu. Benchmarks haben jedoch gezeigt, dass mk-parallel-dump für Backups mehrere Male schneller sein kann mysqldump. In MySQL 5.1 unterstützt mysqlimport den Import mehrerer Threads auf einmal. Sie können die 5.1-Version von mysqlimport auch auf früheren Versionen von MySQL benutzen. Das parallele Sichern und Wiederherstellen dauert allerdings länger, wenn Sie mit einem zu hohen Grad an Parallelität arbeiten. Darüber hinaus verursacht es unter Umständen eine stärkere Datenfragmentierung, was wiederum Auswirkungen auf die Leistung des Systems hat.
Daten in einem Backup sichern | 537
Dateisystemschnappschüsse Schnappschüsse des Dateisystems eignen sich großartig für Online-Backups. Schnappschussfähige Dateisysteme können ein konsistentes Abbild ihres Inhalts zu einem ganz bestimmten Augenblick erzeugen, das Sie dann für das Backup benutzen können. Zu den schnappschussfähigen Dateisystemen und Einrichtungen gehören das Dateisystem von FreeBSD, das ZFS-Dateisystem, der Logical Volume Manager (LVM) von GNU/Linux sowie viele SAN-Systeme und Dateispeicherlösungen, wie die NetApp-Speichereinrichtung. Verwechseln Sie einen Schnappschuss nicht mit einem Backup. Einen Schnappschuss aufzunehmen bedeutet einfach, dass Sie die Zeit verkürzen, während der die Sperren aufrechterhalten werden müssen; nach dem Lösen der Sperren müssen Sie die Dateien in das Backup kopieren. Sie können optional sogar Schnappschüsse in InnoDB aufnehmen, ohne Sperren zu beanspruchen. Wir zeigen Ihnen zwei Möglichkeiten, wie Sie auf einem System mit ausschließlich InnoDB LVM einsetzen, um Backups zu erstellen, wobei Sie nur minimale oder keine Sperren setzen müssen. Lenz Grimmers mylvmbackup ist ein fertiges Perl-Skript zum Erzeugen von MySQL-Backups mit LVM. Mehr dazu erfahren Sie in »Backup-Werkzeuge« auf Seite 558.
Wie LVM-Schnappschüsse funktionieren LVM verwendet eine Kopieren-beim-Schreiben-Technik (Copy-on-write), um einen Schnappschuss anzulegen – d.h. eine logische Kopie eines ganzen Volumes in einem ganz bestimmten Augenblick. Das ist ein bisschen wie MVCC in einer Datenbank, wobei allerdings nur eine alte Version der Daten aufgehoben wird. Beachten Sie, dass wir nicht von einer physischen Kopie gesprochen haben. Eine logische Kopie scheint all die Daten zu enthalten wie das Volume, von dem Sie den Schnappschuss aufgenommen haben, allerdings enthält sie zunächst überhaupt keine Daten. Anstatt die Daten in den Schnappschuss zu kopieren, merkt sich LVM einfach den Zeitpunkt, zu dem Sie den Schnappschuss erzeugt haben, und liest die Daten erst dann von dem Original-Volume, wenn Sie sie von dem Schnappschuss anfordern. Auf diese Weise ist die erste Kopie im Prinzip eine nicht verzögerte Operation, und zwar unabhängig davon, wie groß ein Volume ist, von dem Sie den Schnappschuss erzeugen. Wenn etwas die Daten auf dem Original-Volume ändert, dann kopiert LVM die betroffenen Blöcke in einen Bereich, der für den Schnappschuss reserviert ist, bevor es Änderungen in sie schreibt. LVM behält nicht mehrere »alte Versionen« der Daten, so dass weitere Schreiboperationen in Blöcke, die auf dem Original-Volume geändert wurden, keine weitere Arbeit für den Schnappschuss erfordern. Mit anderen Worten: Nur die erste Schreiboperation auf jeden Block verursacht ein Kopieren-beim-Schreiben in den reservierten Bereich.
538 | Kapitel 11: Backup und Wiederherstellung
Wenn Sie dann diese Blöcke in dem Schnappschuss anfordern, liest LVM die Daten aus den kopierten Blöcken anstatt vom Original-Volume. Auf diese Weise können Sie weiterhin die gleichen Daten in dem Schnappschuss sehen, ohne dass Sie irgendetwas auf dem Original-Volume blockieren. Abbildung 11-1 verdeutlicht das.
OriginalVolume
Reservierter Bereich
Applikation ändert das Original-Volume
A B C D
A B C D
Reservierter Bereich
B
Die gleiche logische Kopie des Original-Volume Schnappschuss
Schnappschuss
Abbildung 11-1: Hier sehen Sie, wie die Kopieren-beim-Schreiben-Technik die Größe reduziert, die für den Schnappschuss eines Volumes erforderlich ist.
Der Schnappschuss erzeugt ein neues logisches Gerät im Verzeichnis /dev, das Sie genau wie jedes andere Gerät mounten können. Mit dieser Technik können Sie theoretisch ein enorm großes Volume in einem Schnappschuss aufnehmen und nur sehr wenig physischen Platz belegen. Sie müssen allerdings genügend Platz aufsparen, um alle Blöcke aufzunehmen, die möglicherweise auf dem Original-Volume aktualisiert werden, während Sie den Schnappschuss offen halten. Falls Sie nicht genug Platz für das Kopieren-beim-Schreiben reservieren, geht dem Schnappschuss der Platz aus, und das Gerät wird unerreichbar. Das ist so, als würden Sie ein externes Laufwerk abstöpseln: Jeder Backup-Job, der von dem Gerät liest, schlägt mit einem Ein-/Ausgabe-Fehler fehl.
Voraussetzungen und Konfiguration Es ist fast schon trivial, einen Schnappschuss zu erzeugen, allerdings müssen Sie Ihr System auf jeden Fall so konfigurieren, dass Sie eine konsistente Kopie aller Dateien erhalten, die Sie in einem bestimmten Augenblick im Backup sichern wollen. Stellen Sie zunächst einmal sicher, dass Ihr System diese Bedingungen erfüllt: • Alle InnoDB-Dateien (InnoDB-Tablespace-Dateien und InnoDB-Transaktions-Logs) müssen sich auf einem einzigen logischen Volume (Partition) befinden. Sie brauchen absolut punktgenaue Konsistenz, und LVM kann keine Schnappschüsse von mehr als einem Volume zur gleichen Zeit aufnehmen. (Dies ist eine Einschränkung von LVM; andere Systeme haben dieses Problem nicht.) Daten in einem Backup sichern | 539
• Falls Sie auch die Tabellendefinitionen sichern müssen, muss sich das MySQLDatenverzeichnis auf demselben logischen Volume befinden. Wenn Sie die Tabellendefinitionen mit einer anderen Methode sichern, wie etwa einem auf das Schema beschränkten Backup in Ihrem Versionskontrollsystem, dann müssen Sie sich darum vermutlich nicht kümmern. • Sie müssen in der Volume-Gruppe genug freien Platz haben, um den Schnappschuss zu erzeugen. Wie viel das ist, hängt von Ihrer Belastung ab. Wenn Sie Ihr System einrichten, dann belegen Sie nicht den gesamten Platz, damit später noch etwas für die Schnappschüsse bleibt. LVM bietet das Konzept einer Volume-Gruppe, die ein oder mehrere logische Volumes enthält. Sie können Volume-Gruppen auf Ihrem System folgendermaßen anschauen: # vgs VG vg
#PV #LV #SN Attr VSize VFree 1 4 0 wz--n- 534.18G 249.18G
Diese Ausgabe zeigt eine Volume-Gruppe, deren vier logische Volumes über ein physisches Volume mit ungefähr 250 GByte freiem Platz verteilt sind. Der Befehl vgdisplay liefert Ihnen bei Bedarf mehr Details. Schauen Sie sich nun die logischen Volumes auf dem System an: # lvs LV home mysql tmp var
VG vg vg vg vg
Attr LSize Origin Snap% -wi-ao 40.00G -wi-ao 225.00G -wi-ao 10.00G -wi-ao 10.00G
Move Log Copy%
Die Ausgabe zeigt, dass das mysql-Volume 225 GByte Platz umfasst. Der Gerätename lautet /dev/vg/mysql. Das ist nur ein Name, auch wenn es wie ein Dateisystempfad aussieht. Um die Verwirrung noch zu steigern, gibt es einen symbolischen Link von der Datei des gleichen Namens zu dem echten Geräteknoten unter /dev/mapper/vg-mysql, den Sie mit den Befehlen ls und mount sehen können: # ls -l /dev/vg/mysql lrwxrwxrwx 1 root root 20 Sep 19 13:08 /dev/vg/mysql -> /dev/mapper/vg-mysql # mount | grep mysql /dev/mapper/vg-mysql on /var/lib/mysql type reiserfs (rw,noatime,notail)
Gewappnet mit diesen Informationen, können Sie einen Dateisystemschnappschuss herstellen.
Einen LVM-Schnappschuss erzeugen, mounten und entfernen Ein einziger Befehl reicht, um den Schnappschuss zu erzeugen. Sie müssen nur entscheiden, wohin er gelangen und wie viel Platz für das Kopieren-beim-Schreiben reserviert werden soll. Zögern Sie nicht, mehr Platz zu benutzen, als Sie Ihrer Meinung nach brauchen. LVM benutzt den Platz, den Sie angeben, nicht sofort, sondern reserviert ihn nur für eine spätere Benutzung. Es schadet also nichts, viel Platz zu reservieren, es sei denn, Sie müssen ihn gleichzeitig für andere Schnappschüsse übrig lassen. 540 | Kapitel 11: Backup und Wiederherstellung
Wir wollen nun zur Übung einen Schnappschuss erzeugen. Für das Kopieren-beimSchreiben geben wir ihm 16 GByte Platz, und wir nennen ihn backup_mysql: # lvcreate --size 16G --snapshot --name backup_mysql /dev/vg/mysql Logical volume "backup_mysql" created
Wir haben das Volume absichtlich backup_mysql genannt und nicht mysql_backup, damit die Tab-Vervollständigung unmissverständlich verläuft. Auf diese Weise vermeiden Sie es, versehentlich die Volume-Gruppe mysql zu löschen, nur weil Sie bei der Tab-Vervollständigung nicht aufgepasst haben. Solche kleinen Details können Ihnen wirklich helfen, eine Katastrophe zu vermeiden. Wenigstens einer der Autoren dieses Buches ist in Bezug auf diese Problematik ein gebranntes Kind.
Jetzt wollen wir uns den Status des neu erzeugten Volumes anschauen: # lvs LV backup_mysql home mysql tmp var
VG vg vg vg vg vg
Attr LSize Origin Snap% Move Log Copy% swi-a- 16.00G mysql 0.01 -wi-ao 40.00G owi-ao 225.00G -wi-ao 10.00G -wi-ao 10.00G
Beachten Sie, dass die Attribute des Schnappschusses sich von denen des Originalgerätes unterscheiden und dass die Ausgabe einige Zusatzinformationen zeigt: den Ursprung des Schnappschusses und wie viel der reservierten 16 GByte momentan für das Kopierenbeim-Schreiben benutzt werden. Es ist keine schlechte Idee, dies beim Erstellen des Backups zu überwachen, damit Sie merken, ob das Gerät voll wird und kurz davor ist, auszufallen. Sie können den Status Ihres Geräts mit einem Überwachungssystem wie Nagios kontrollieren: # watch 'lvs | grep backup'
Wie Sie bereits bei der Ausgabe von mount gesehen haben, enthält das mysql-Volume ein ReiserFS-Dateisystem. Das bedeutet, dass dies auch bei dem Schnappschuss-Volume so ist und Sie es wie jedes andere Dateisystem mounten und benutzen können: # mkdir /tmp/backup # mount /dev/mapper/vg-backup_mysql /tmp/backup # ls -l /tmp/backup/mysql total 5336 -rw-r----- 1 mysql mysql 0 Nov 17 2006 columns_priv.MYD -rw-r----- 1 mysql mysql 1024 Mar 24 2007 columns_priv.MYI -rw-r----- 1 mysql mysql 8820 Mar 24 2007 columns_priv.frm -rw-r----- 1 mysql mysql 10512 Jul 12 10:26 db.MYD -rw-r----- 1 mysql mysql 4096 Jul 12 10:29 db.MYI -rw-r----- 1 mysql mysql 9494 Mar 24 2007 db.frm ... gekürzt ...
Das ist nur zur Übung. Wir unmounten den Schnappschuss deshalb jetzt und löschen ihn mit dem Befehl lvremove:
Daten in einem Backup sichern | 541
# umount /tmp/backup # rmdir /tmp/backup # lvremove --force /dev/vg/backup_mysql Logical volume "backup_mysql" successfully removed
LVM-Schnappschüsse für Online-Backups Nachdem Sie gesehen haben, wie man Schnappschüsse erzeugt, mountet und entfernt, können Sie sie zum Erstellen von Backups einsetzen. Zuerst wollen wir uns anschauen, wie man eine InnoDB-Datenbank in einem Backup sichert, ohne den MySQL-Server zu stoppen. Stellen Sie eine Verbindung zum MySQL-Server her, und übertragen Sie die Dateien mit einem globalen Lese-Lock auf die Festplatte. Holen Sie anschließend die Binärlog-Koordinaten: mysql> FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS;
Zeichnen Sie die Ausgabe von SHOW MASTER STATUS auf, und sorgen Sie dafür, dass die Verbindung zu MySQL offen bleibt, damit die Sperre nicht freigegeben wird. Sie können dann den LVM-Schnappschuss aufnehmen und sofort den Lese-Lock freigeben – entweder mit UNLOCK TABLES oder indem Sie die Verbindung schließen. Mounten Sie schließlich den Schnappschuss, und kopieren Sie die Dateien an den Ort des Backups. Wenn Sie diesen Vorgang mit einem Skript automatisieren, können Sie die Zeit für die Sperre auf wenige Sekunden drücken. Das größte Problem bei diesem Ansatz besteht darin, dass es eine Weile dauern könnte, bis Sie den Lese-Lock haben, vor allem wenn es lange laufende Abfragen gibt. Alle Abfragen werden blockiert, während die Verbindung auf den globalen Lese-Lock wartet. Es lässt sich unmöglich vorhersagen, wie lange das dauern wird.
Lock-freie InnoDB-Backups mit LVM-Schnappschüssen Lock-freie Backups sind nur ein bisschen anders. Der Unterschied besteht darin, dass sie kein FLUSH TABLES WITH READ LOCK durchführen. Es gibt also keine Garantie dafür, dass Ihre MyISAM-Dateien auf der Festplatte konsistent sind. Falls Sie jedoch nur InnoDB benutzen, ist das wahrscheinlich kein Problem. Sie haben immer noch einige MyISAM-Tabellen in der mysql-Systemdatenbank, aber bei einer typischen Benutzung werden sie sich wahrscheinlich nicht ausgerechnet in dem Moment ändern, in dem Sie den Schnappschuss aufnehmen. Falls Sie glauben, dass sich die mysql-Systemtabellen ändern könnten, dann sperren und übertragen Sie sie. Auf diesen Tabellen haben Sie wahrscheinlich keine lange laufenden Abfragen, so dass das normalerweise sehr schnell gehen sollte: mysql> LOCK TABLES mysql.user READ, mysql.db READ, ...; mysql> FLUSH TABLES mysql.user, mysql.db, ...;
Sie erhalten keinen globalen Lese-Lock, werden daher also nichts Sinnvolles aus SHOW MASTER STATUS herauslesen können. Wenn Sie allerdings MySQL auf dem Schnappschuss starten (um die Integrität Ihres Backups zu überprüfen), werden Sie in der Log-Datei so etwas sehen:
542 | Kapitel 11: Backup und Wiederherstellung
Dateisystemschnappschüsse und InnoDB Die Hintergrund-Threads von InnoDB arbeiten selbst dann weiter, wenn Sie alle Tabellen gesperrt haben, so dass es wahrscheinlich noch in seine Dateien schreibt, wenn Sie den Schnappschuss aufnehmen. Und da InnoDB seine Shutdown-Sequenz noch nicht ausgeführt aus, werden die InnoDB-Dateien des Schnappschusses aussehen, als wäre der Server völlig unerwartet stromlos geworden. Das ist kein Problem, weil InnoDB ein ACID-System ist. In jedem Augenblick (wie etwa dem Augenblick, in dem Sie den Schnappschuss aufnehmen) befindet sich jede bestätigte Transaktion entweder in den InnoDB-Datendateien oder in den Log-Dateien. Wenn Sie MySQL nach dem Erneuern des Schnappschusses starten, führt InnoDB seinen Wiederherstellungsprozess aus, so als wäre beim Server der Strom ausgefallen. Es sucht im Transaktions-Log nach allen bestätigten Transaktionen, die noch nicht auf die Datendateien angewandt wurden, und wendet sie an, so dass Sie keine Transaktionen verlieren. Aus diesem Grund ist es zwingend notwendig, dass Sie den Schnappschuss von den InnoDBDaten- und Log-Dateien zusammen erzeugen. Deshalb müssen Sie auch Ihre Backups testen, wenn Sie sie erzeugen. Starten Sie eine Instanz von MySQL, verweisen Sie sie an das neue Backup, lassen Sie die InnoDB-Wiederherstellung laufen, und überprüfen Sie alle Tabellen. Auf diese Weise vermeiden Sie es, beschädigte Daten zu sichern, ohne es zu merken (die Dateien könnten aus verschiedenen Gründen beschädigt sein). Ein weiterer Vorteil dieses Vorgehens besteht darin, dass das Wiederherstellen aus dem Backup in Zukunft schneller vonstatten gehen wird, weil Sie den Wiederherstellungsprozess bereits einmal ausgeführt haben. Sie können diesen Prozess optional auch auf dem Schnappschuss ausführen, bevor Sie ihn tatsächlich in das Backup kopieren. Das ist aber unter Umständen etwas aufwendiger. Denken Sie einfach daran, es einzuplanen. (Mehr dazu später.)
InnoDB: Doing recovery: scanned up to log sequence number 0 40817239 InnoDB: Starting an apply batch of log records to the database... InnoDB: Progress in percents: 3 4 5 6 ...[omitted]... 97 98 99 InnoDB: Apply batch completed InnoDB: Last MySQL binlog file position 0 3304937, file name /var/log/mysql/mysqlbin.000001 070928 14:08:42 InnoDB: Started; log sequence number 0 40817239
InnoDB vermerkt die MySQL-Binärlog-Position entsprechend der Stelle, an der es wiederhergestellt wurde. Diese Binärlog-Position können Sie dann für eine punktgenaue Wiederherstellung benutzen. Dieser Ansatz für Lock-freie Backups mit Schnappschüssen hat in Versionen ab MySQL 5.0 einen Haken. Diese MySQL-Versionen verwenden XA, um Transaktionen zwischen InnoDB und dem Binärlog zu koordinieren. Wenn Sie das Backup auf einem Server mit einer anderen server_id als dem Ausgangsserver wiederherstellen, könnte der Server vorbereitete Transaktionen von einem Server vorfinden, dessen ID nicht seiner eigenen entspricht. Der Server wird in diesem Fall verwirrt, und es ist möglich, dass Transaktionen im
Daten in einem Backup sichern | 543
Status PREPARED vor der Wiederherstellung hängenbleiben. Das kommt selten vor, kann aber passieren. Deshalb sollten Sie Ihr Backup immer auf Richtigkeit überprüfen, bevor Sie es zu einem Erfolg erklären. Es ist möglicherweise nicht wiederherstellbar! Falls Sie einen Schnappschuss von einem Slave aufnehmen, gibt die InnoDB-Wiederherstellung einige Zeilen aus, die so aussehen: InnoDB: In a MySQL replication slave the last master binlog file InnoDB: position 0 115, file name mysql-bin.001717
In manchen MySQL-Versionen zeigt Ihnen diese Ausgabe die Binärlog-Koordinaten des Masters (anstatt der Binärlog-Koordinaten des Slaves) an der Stelle, an der InnoDB die Wiederherstellung ausgeführt hat, was sich als nützlich erweisen könnte, wenn man Backups von Slaves erzeugen oder Slaves aus anderen Slaves klonen möchte. Seit MySQL 5.0 sind diese Werte jedoch nicht vertrauenswürdig.
Für LVM-Backups planen Backups aus LVM-Schnappschüssen sind nicht kostenlos. Je mehr Ihr Server auf das Original-Volume schreibt, umso mehr Overhead verursachen sie. Wenn der Server viele getrennte Blöcke in zufälliger Reihenfolge modifiziert, muss der Schreib-/Lesekopf auf dem Kopieren-beim-Schreiben-Block hin- und hersuchen und die alte Version der Daten dort schreiben. Das Lesen vom Schnappschuss ist ebenfalls aufwendig, weil LVM tatsächlich die meisten der Daten vom Original-Volume liest. Es liest vom Kopieren-beimSchreiben-Platz nur, wenn das nötig ist. Ein logisch sequenzieller Schreibvorgang vom Schnappschuss veranlasst den Schreib-/Lesekopf, hin- und herzufahren. Sie müssen das einplanen. Das bedeutet, dass sowohl das Original-Volume als auch der Schnappschuss sowohl für Lese- als auch für Schreiboperationen schlechter als normal funktionieren – wahrscheinlich sogar viel schlechter, falls Sie viel Kopieren-beim-Schreiben-Platz benutzen. Das kann nicht nur Ihren MySQL-Server ausbremsen, sondern auch den Vorgang, die Dateien für das Backup zu kopieren. Die andere wichtige Sache, die Sie einplanen müssen, ist das Reservieren von ausreichend Platz für den Schnappschuss. Wir benutzen folgenden Ansatz: • Denken Sie daran, dass LVM jeden modifizierten Block nur einmal in den Schnappschuss kopieren muss. Wenn MySQL einen Block in das Original-Volume schreibt, kopiert es den Block in den Schnappschuss und setzt dann einen Hinweis auf den kopierten Block in seine Exception-Tabelle. Künftige Schreiboperationen in diesen Block verursachen keine weiteren Kopien in den Schnappschuss. • Wenn Sie nur InnoDB benutzen, dann bedenken Sie, wie InnoDB Daten schreibt. Da es alle Daten zweimal schreibt, geht wenigstens die Hälfte von InnoDBs SchreibEin-/Ausgaben in den Doublewrite-Puffer, in Log-Dateien und in andere relativ kleine Bereiche auf der Festplatte. Diese benutzen die gleichen Festplattenblöcke immer und immer wieder, so dass sie zuerst Auswirkungen auf den Schnappschuss haben, aber danach keine Schreiboperationen in den Schnappschuss mehr verursachen. 544 | Kapitel 11: Backup und Wiederherstellung
• Schätzen Sie als Nächstes ab, wie viele Ihrer Ein-/Ausgaben Schreibvorgänge in Blöcke sein werden, die noch nicht in den Schnappschuss kopiert wurden, im Gegensatz zu immer wieder erfolgenden Änderungen der gleichen Daten. Seien Sie großzügig bei Ihrer Schätzung. Sie gibt an, wie viele zusätzliche Ein-/Ausgaben der Schnappschuss Ihrer Meinung nach verursachen wird. (Rechnen Sie etwas für den LVM-Prozess selbst ein.) • Sammeln Sie mit vmstat oder iostat Statistiken darüber, wie viele Blöcke Ihr Server pro Sekunde schreibt. Mehr über diese Werkzeuge erfahren Sie in Kapitel 7. • Messen (oder schätzen) Sie, wie lange es dauern wird, um Ihr Backup an eine andere Stelle zu kopieren. Mit anderen Worten: Wie lange müssen Sie den LVM-Schnappschuss offen halten? Nehmen wir an, Ihre Schätzung hat ergeben, dass die Hälfte Ihrer Schreiboperationen Schreibvorgänge auf den Kopieren-beim-Schreiben-Platz verursacht und dass Ihr Server 10 MByte pro Sekunde schreibt. Wenn es eine Stunde (3.600 Sekunden) dauert, den Schnappschuss auf einen anderen Server zu kopieren, brauchen Sie 1/2 × 10 MByte × 3.600 oder 18 GByte Platz für den Schnappschuss. Gehen wir auf Nummer sicher und runden wir den Platzbedarf noch etwas auf. Manchmal kann man leicht berechnen, wie viele Daten sich ändern werden, während Sie den Schnappschuss offen halten. Kommen wir noch einmal zu einem Beispiel, das wir bereits an anderen Stellen bemüht haben. Die BoardReader-Forensuchmaschine enthält ungefähr 1 TByte an InnoDB-Tabellen pro Speicherknoten. Wir wissen jedoch, dass das Laden neuer Daten die größten Kosten verursacht. Pro Tag werden ungefähr 10 GByte an neuen Daten hinzugefügt, so dass 50 GByte mehr als ausreichend für den Schnappschuss sein sollten. Diese Schätzung trifft jedoch nicht immer zu. Einmal hatten wir ein lange laufendes ALTER TABLE, das alle Shards nacheinander geändert hat, wodurch mehr als 50 GByte Daten modifiziert wurden; während diese Operation lief, konnten wir kein Backup machen.
Andere Anwendungen und Alternativen Schnappschüsse eignen sich nicht nur für Backups. Sie stellen z.B. eine sinnvolle Möglichkeit dar, einen »Checkpoint« direkt vor einer potenziell gefährlichen Aktion zu setzen. Manche Systeme, wie etwa ZFS, erlauben es Ihnen, den Schnappschuss zum Originaldateisystem zu befördern. Auf diese Weise können Sie leicht zu dem Punkt zurückkehren, an dem Sie den Schnappschuss aufgenommen haben. Dateisystemschnappschüsse sind jedoch nicht die einzige Methode, um Sofortkopien Ihrer Daten zu erhalten. Eine andere Möglichkeit ist ein RAID-Split: Falls Sie z.B. einen Software-RAID-Mirror mit drei Festplatten haben, können Sie eine Festplatte aus dem Mirror entfernen und separat mounten. Es gibt nicht den Kopieren-beim-SchreibenNachteil, und es ist einfach, diese Art von »Schnappschuss« bei Bedarf zur Master-Kopie zu befördern.
Daten in einem Backup sichern | 545
Wiederherstellung aus einem Backup Die Wiederherstellung ist das, was wirklich zählt. In diesem Abschnitt konzentrieren wir uns auf die MySQL-spezifischen Aspekte der Wiederherstellung. Wir gehen davon aus, dass Sie wissen, wie Sie die anderen Teile Ihrer Umgebung handhaben müssen. Führen Sie regelmäßig Übungen durch, damit Sie wissen, wie Sie bei einem echten Notfall Ihre Daten sichern und wiederherstellen. Testen Sie vor allem Ihre Backups. Wie Sie Ihre Daten wiederherstellen, hängt davon ab, wie Sie sie im Backup gesichert haben. Sie müssen möglicherweise einen oder alle folgenden Schritt ausführen: • Stoppen Sie den MySQL-Server. • Notieren Sie sich die Konfiguration des Servers sowie die Dateiberechtigungen. • Verschieben Sie die Daten vom Backup in das MySQL-Datenverzeichnis. • Nehmen Sie Änderungen an der Konfiguration vor. • Ändern Sie die Dateiberechtigungen. • Starten Sie den Server mit eingeschränktem Zugriff neu, und warten Sie, bis er vollständig gestartet ist. • Laden Sie die Dateien des logischen Backups. • Untersuchen Sie die Binärlogs, und spielen Sie sie wieder ein. • Überprüfen Sie, was Sie wiederhergestellt haben. • Starten Sie den Server mit vollständigem Zugriff neu. In den folgenden Abschnitten demonstrieren wir Ihnen, wie Sie die einzelnen Schritte bei Bedarf ausführen. In den jeweiligen Abschnitten geben wir darüber hinaus Hinweise zu bestimmten Backup-Methoden oder Werkzeugen. Wenn es eine Gelegenheit gibt, bei der Sie die aktuellen Versionen Ihrer Dateien brauchen, dann ersetzen Sie sie nicht durch die Dateien aus dem Backup. Falls z.B. Ihr Backup die Binärlogs enthält und Sie die Binärlogs für eine punktgenaue Wiederherstellung wieder abspielen müssen, dann überschreiben Sie die aktuellen Binärlogs nicht mit den älteren Kopien aus dem Backup. Benennen Sie sie notfalls um, oder verschieben Sie sie an eine andere Stelle.
Den Zugriff auf MySQL beschränken Während der Wiederherstellung ist es oft wichtig, den Zugriff auf MySQL für alles zu sperren bis auf den Wiederherstellungsprozess. Bei komplexen Systemen ist das schwer durchzusetzen. Wir starten MySQL gern mit den Optionen --skip-networking und --socket=/tmp/mysql_recover.sock, um sicherzustellen, dass es für vorhandene Anwendungen erst dann verfügbar wird, wenn wir es geprüft und wieder online gebracht haben. Das ist vor allem für logische Backups von Bedeutung, die stückweise geladen werden.
546 | Kapitel 11: Backup und Wiederherstellung
Rohe Dateien erneuern Das Erneuern von rohen Dateien ist meist relativ unkompliziert – man könnte auch sagen, es gibt nicht viele Optionen. Das kann gut oder schlecht sein, je nach Ihren Anforderungen an die Wiederherstellung. Das übliche Vorgehen besteht darin, einfach die Dateien an die passende Stelle zu kopieren. Ob Sie MySQL herunterfahren müssen, hängt von der Storage-Engine ab. Die Dateien von MyISAM sind im Allgemeinen unabhängig voneinander, und es funktioniert gut, wenn man einfach die .frm-, .MYI- und .MYD-Dateien der einzelnen Tabellen kopiert – selbst wenn der Server läuft. Der Server sucht die Tabelle, sobald irgendjemand sie abfragt oder den Server anderweitig veranlasst, danach zu schauen (z.B., indem SHOW TABLES ausgeführt wird). Falls die Tabelle offen ist, wenn Sie diese Dateien kopieren, gibt es wahrscheinlich Ärger, Sie sollten also vor dem Kopieren die Tabelle verwerfen oder umbenennen oder sie mit LOCK TABLES und FLUSH TABLES schließen. InnoDB ist ein anderer Fall. Wenn Sie eine traditionelle InnoDB-Anordnung wiederherstellen, bei der alle Tabellen in einem einzigen Tablespace gespeichert sind, müssen Sie MySQL herunterfahren, die Dateien an ihre Stelle kopieren oder verschieben und MySQL dann neu starten. Außerdem müssen Sie dafür sorgen, dass die TransaktionsLog-Dateien von InnoDB seinen Tablespace-Dateien entsprechen. Wenn die Dateien nicht passen – falls Sie z.B. die Tablespace-Dateien ersetzen, aber nicht die TransaktionsLog-Dateien –, lehnt InnoDB es möglicherweise ab zu starten. Dies ist einer der Gründe, weshalb es so wichtig ist, das Transaktions-Log zusammen mit den Datendateien in einem Backup zu sichern. Falls Sie die neuere InnoDB-File-per-Table-Funktion (innodb_file_per_table) benutzen, speichert InnoDB die Daten und Indizes für die einzelnen Dateien in einer .ibd-Datei, die sozusagen eine Kombination aus MyISAMs .MYI- und .MYD-Dateien darstellt. Sie können die einzelnen Tabellen sichern und wiederherstellen, indem Sie diese Dateien kopieren, und das sogar, während der Server läuft, allerdings ist es nicht ganz so einfach wie bei MyISAM. Die einzelnen Dateien sind nicht unabhängig von InnoDB als Ganzem. Jede .ibd-Datei besitzt interne Informationen, die InnoDB mitteilen, wie die Datei sich zum (gemeinsam genutzten) Haupt-Tablespace verhält. Wenn Sie eine solche Datei wiederherstellen, müssen Sie InnoDB anweisen, die Datei zu »importieren«. Es gibt für diesen Vorgang viele Beschränkungen, die Sie im MySQL-Handbuchabschnitt über die Verwendung tabellenweiser Tablespaces nachlesen können. Die größte Einschränkung ist, dass Sie eine Tabelle nur auf dem Server wiederherstellen können, von dem Sie sie gesichert haben. Es ist nicht unmöglich, in dieser Konfiguration Tabellen zu sichern und wiederherzustellen, es ist aber komplizierter, als Sie vielleicht glauben. Diese ganze Komplexität bedeutet, dass das Erneuern von Rohdateien sehr nervtötend sein und leicht schiefgehen kann. Eine gute Faustregel lautet: Je schwieriger und komplexer Ihre Wiederherstellungsprozedur wird, umso mehr müssen Sie sich selbst mit logischen Backups schützen. Es ist immer eine gute Idee, ein logisches Backup zu haben, falls einmal etwas schiefgeht und Sie MySQL nicht überzeugen können, Ihre rohen Backups zu benutzen.
Wiederherstellung aus einem Backup | 547
MySQL nach dem Erneuern roher Dateien starten Sie müssen ein paar Dinge erledigen, bevor Sie den MySQL-Server starten, den Sie wiederherstellen. Das Erste und Wichtigste, das am ehesten vergessen wird, ist das Überprüfen der Konfiguration Ihres Servers. Stellen Sie sicher, dass die erneuerten Dateien den richtigen Besitzer und die richtigen Berechtigungen haben, bevor Sie versuchen, den MySQL-Server zu starten. Diese Attribute müssen genau stimmen, da sonst MySQL vielleicht gar nicht startet. Die Attribute unterscheiden sich von System zu System. Halten Sie sich deshalb an Ihre Notizen, damit Sie die richtigen Werte einstellen. Typischerweise sollen die Dateien und Verzeichnisse dem Benutzer und der Gruppe mysql gehören, und sie sollen von diesem Benutzer und dieser Gruppe les- und schreibbar sein – und von sonst niemandem. Wir empfehlen beim Serverstart außerdem einen Blick in das MySQL-Fehler-Log. Auf einem Unix-artigen System schauen Sie sich die Datei folgendermaßen an: $ tail -f /var/log/mysql/mysql.err
Der exakte Standort des Fehler-Logs variiert von System zu System. Sobald Sie die Datei überwachen, können Sie den MySQL-Server starten und nach Fehlern Ausschau halten. Wenn alles gutgeht, haben Sie nach dem Start von MySQL einen hübsch wiederhergestellten Server. Das Beobachten des Fehler-Logs ist bei neueren MySQL-Versionen sogar noch wichtiger. Ältere Versionen starten gar nicht erst, wenn InnoDB einen Fehler hat; bei neueren Versionen startet der Server dagegen tatsächlich und deaktiviert einfach InnoDB. Selbst wenn der Server ohne Probleme zu starten scheint, sollten Sie in jeder Datenbank SHOW TABLE STATUS ausführen und dann noch einmal das Fehler-Log überprüfen.
Logische Backups erneuern Falls Sie anstelle von rohen Dateien logische Backups erneuern, müssen Sie den MySQLServer selbst einsetzen, um die Daten wieder zurück in die Tabellen zu laden. Sie können es nicht dem Betriebssystem überlassen, einfach die Dateien wieder an ihre Stellen zu kopieren. Bevor Sie jedoch diese Dump-Datei laden, sollten Sie sich einen Augenblick Zeit nehmen, um zu überlegen, wie groß sie ist, wie lange es dauern wird, sie zu laden, und was Sie vor dem Laden noch erledigen müssen: etwa Ihre Benutzer benachrichtigen oder Teile Ihrer Anwendung deaktivieren. Vermutlich ist es keine schlechte Idee, das Binär-Logging auszuschalten, es sei denn, Sie müssen die Wiederherstellung auf einen Slave replizieren: Für den Server ist es schwer genug, eine riesige Dump-Datei zu laden, und das Schreiben in das Binärlog bringt einen weiteren (vermutlich unnötigen) Overhead mit sich. Auch für manche Storage-Engines hat das Laden riesiger Dateien Folgen. Es ist z.B. keine gute Idee, 100 GByte an Daten in einer einzigen Transaktion in InnoDB zu laden, weil dadurch ein riesiges Rollback-Segment entsteht. Sie sollten das Laden in handlichen Stücken vornehmen und die Transaktion nach jedem Stück bestätigen. 548 | Kapitel 11: Backup und Wiederherstellung
Es gibt zwei Arten von Instandsetzung, die den beiden Arten von logischen Backups entsprechen, die man durchführen kann.
Laden von SQL-Dateien Falls Sie einen SQL-Dump haben, enthält die Datei ausführbares SQL. Sie müssen es einfach nur ausführen. Nehmen Sie an, dass Sie die Sakila-Beispieldatenbank und das Schema in eine einzige Datei gesichert haben. Dann ist dies ein typischer Befehl, mit dem Sie sie wiederherstellen können: $ mysql < sakila-backup.sql
Sie können die Datei auch aus dem mysql-Kommandozeilenclient heraus mit dem SOURCEBefehl laden. Das ist zwar eigentlich nur eine andere Methode dafür, macht aber einiges einfacher. Falls Sie z.B. ein administrativer Benutzer in MySQL sind, können Sie das Binärlogging der Anweisungen ausschalten, die Sie aus Ihrer Clientverbindung heraus ausführen, und dann die Datei laden, ohne dass Sie den MySQL-Server neu starten müssen: mysql> SET SQL_LOG_BIN = 0; mysql> SOURCE sakila-backup.sql; mysql> SET SQL_LOG_BIN = 1;
Falls Sie SOURCE verwenden, dann denken Sie daran, dass ein Fehler einen Stapel aus Anweisungen nicht abbricht, wie das normalerweise der Fall wäre, wenn Sie die Datei nach mysql umleiten. Haben Sie das Backup komprimiert, dann dekomprimieren und laden Sie es nicht einzeln, sondern führen Sie diese beiden Schritte in einer einzigen Operation durch. Das geht viel schneller: $ gunzip -c sakila-backup.sql.gz | mysql
Falls Sie eine komprimierte Datei mit dem SOURCE-Befehl laden wollen, dann lesen Sie die Diskussion über benannte Pipes im nächsten Abschnitt. Was ist, wenn Sie nur eine einzige Tabelle (z.B. die actor-Tabelle) instandsetzen wollen? Falls Ihre Daten keine Zeilenumbrüche enthalten, dann ist es nicht schwer, die Daten zu erneuern, solange das Schema bereits vorhanden ist: $ grep 'INSERT INTO `actor`' sakila-backup.sql | mysql sakila
Oder verwenden Sie, falls die Datei komprimiert ist: $ gunzip -c sakila-backup.sql.gz | grep 'INSERT INTO `actor`'| mysql sakila
Falls Sie nicht nur die Daten erneuern, sondern auch die Tabelle erzeugen müssen und die gesamte Datenbank in einer einzigen Datei haben, müssen Sie die Datei bearbeiten. Aus diesem Grund speichern viele Leute jede Tabelle in ihre eigene Datei. Die meisten Editoren können mit riesigen Dateien nicht umgehen, vor allem dann nicht, wenn sie komprimiert sind. Abgesehen davon wollen Sie die Datei selbst eigentlich nicht bearbeiten – sondern nur die relevanten Zeilen extrahieren –, so dass Sie wahrscheinlich auf der
Wiederherstellung aus einem Backup | 549
Kommandozeile tätig werden müssen. Mit grep kann man leicht nur die INSERT-Anweisungen einer bestimmten Tabelle herausziehen, wie wir in dem vorherigen Befehl gezeigt haben. Schwieriger wird es dann mit der CREATE TABLE-Anweisung. Hier ist ein sed-Skript, das den benötigten Absatz herauszieht: $ sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `actor`/!d;q' sakila-backup.sql
Wir geben zu, dass das ziemlich kryptisch ist. Falls Sie so vorgehen müssen, um Ihre Daten wieder instandzusetzen, ist Ihr Backup ganz schön schlecht aufgebaut. Mit ein wenig Planung ist es möglich, eine Situation zu vermeiden, in der Sie in Panik versuchen herauszubekommen, wie sed funktioniert. Sichern Sie einfach jede Tabelle in ihrer eigenen Datei oder noch besser: Sichern Sie Daten und Schema getrennt.
Laden von separierten Dateien Falls Sie die Daten über SELECT INTO OUTFILE im Dump gespeichert haben, müssen Sie LOAD DATA INFILE mit den gleichen Parametern benutzen, um sie wiederherzustellen. Sie können auch mysqlimport verwenden, einen Wrapper um LOAD DATA INFILE. Die Namenskonventionen bestimmen, wohin die Daten einer Datei geladen werden. Wir hoffen, dass Sie auch das Schema in den Dump gelegt haben und nicht nur die Daten. Es handelt sich dann um einen SQL-Dump, den Sie mit den im vorherigen Abschnitt beschriebenen Techniken laden können. Es gibt eine großartige Optimierung für LOAD DATA INFILE. Es muss direkt aus einer Datei lesen, so dass Sie vielleicht glauben, dass Sie die Datei vor dem Laden dekomprimieren müssen, was sehr langsam vonstatten geht und eine intensive Festplattennutzung verursacht. Es gibt jedoch eine Möglichkeit, das zu umgehen, wenigstens auf Systemen, die »benannte Pipe«-Dateien mit FIFO unterstützen, wie etwa GNU/Linux. Erzeugen Sie zuerst eine benannte Pipe, und leiten Sie den dekomprimierten Datenstrom in sie hinein: $ $ $ c
mkfifo /tmp/backup/default/sakila/payment.fifo chmod 666 /tmp/backup/default/sakila/payment.fifo gunzip /tmp/backup/default/sakila/payment.txt.gz > /tmp/backup/default/sakila/payment.fifo
Beachten Sie, dass wir ein Größer-als-Zeichen (>) verwenden, um die dekomprimierte Ausgabe in die payment.fifo-Datei zu leiten – und kein Pipe-Symbol, das anonyme Pipes zwischen Programmen erzeugt. Die Datei payment.fifo ist eine benannte Pipe, es gibt also keinen Bedarf für eine anonyme Pipe. Die Pipe wartet, bis ein Programm sie öffnet, um von der anderen Seite zu lesen. Das ist der nette Teil: Der MySQL-Server kann die dekomprimierten Daten genau wie jede andere Datei aus der Pipe lesen. Vergessen Sie nicht, eventuell das Binärlogging zu deaktivieren: mysql> SET SQL_LOG_BIN = 0; -- Optional -> LOAD DATA INFILE '/tmp/backup/default/sakila/payment.fifo' -> INTO TABLE sakila.payment; Query OK, 16049 rows affected (2.29 sec) Records: 16049 Deleted: 0 Skipped: 0 Warnings: 0
550 | Kapitel 11: Backup und Wiederherstellung
Sobald MySQL mit dem Laden der Daten fertig ist, beendet sich gunzip, und Sie können die benannte Pipe löschen. Mithilfe dieser Technik können Sie auch komprimierte Dateien mit dem SOURCE-Befehl innerhalb des MySQL-Kommandozeilenclients laden.
Warum sollte man Backups testen? Einer der Autoren hat neulich eine Spalte von DATETIME in TIMESTAMP geändert, um Platz zu sparen und die Verarbeitung zu beschleunigen, wie in Kapitel 3 empfohlen. Die resultierende Tabellendefinition sah dann so aus: CREATE TABLE tbl ( col1 timestamp NOT NULL, col2 timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, ... weitere Spalten ... );
Diese Tabellendefinition verursacht einen Syntaxfehler in MySQL 5.0.40, der Serverversion, von der sie erzeugt wurde. Sie können sie als Dump speichern, sie lässt sich aber nicht wieder laden. Eigenartige, unvorhergesehene Fehler wie dieser gehören zu den Gründen, weshalb es wichtig ist, Ihre Backups zu testen. Sie wissen nie, was Sie daran hindert, Ihre Daten wiederherzustellen!
Punktgenaue Wiederherstellung Die gebräuchlichste Methode, um eine punktgenaue Wiederherstellung mit MySQL durchzuführen, besteht darin, das letzte vollständige Backup wiederherzustellen und dann das Binärlog von diesem Zeitpunkt an erneut einzuspielen (was auch als »Roll-Forward Recovery« bezeichnet wird). Solange Sie das Binärlog haben, können Sie zu jedem gewünschten Punkt wiederherstellen. Sie können sogar ohne größere Probleme eine einzelne Datenbank wiederherstellen. Ein verbreitetes Szenario ist das Widerrufen der Auswirkungen einer schädlichen Anweisung wie DROP TABLE. Schauen wir uns ein vereinfachtes Beispiel dafür an, bei dem nur MyISAM-Tabellen zum Einsatz kommen. Nehmen Sie an, dass der Backup-Job um Mitternacht das Äquivalent der folgenden Anweisungen ausgeführt hat, wodurch die Datenbank irgendwohin auf dem gleichen Server kopiert wurde: mysql> -> mysql> -> mysql>
FLUSH TABLES WITH READ LOCK; server1# cp -a /var/lib/mysql/sakila /backup/sakila; FLUSH LOGS; server1# mysql -e "SHOW MASTER STATUS" --vertical > /backup/master.info; UNLOCK TABLES;
Später am Tag hat jemand die folgende Anweisung ausgeführt: mysql> USE sakila; mysql> DROP TABLE sakila.payment;
Wiederherstellung aus einem Backup | 551
Zur Verdeutlichung wollen wir annehmen, dass wir diese Datenbank isoliert wiederherstellen können (d.h., keine Tabellen aus dieser Datenbank sind an datenbankübergreifenden Abfragen beteiligt). Wir wollen außerdem davon ausgehen, dass wir die Anweisung, die das Ärgernis verursacht, erst nach einiger Zeit bemerken. Unser Ziel besteht nun darin, alles, was an der Datenbank geschehen ist, wiederherzustellen, mit Ausnahme dieser Anweisung. Das bedeutet, dass wir auch alle Modifikationen, die an anderen Tabellen vorgenommen wurden, schützen müssen, und zwar einschließlich solcher Änderungen, die erst nach dem Ausführen dieser Anweisung geschehen sind. Das ist alles nicht allzu schwer. Zuerst stoppen wir MySQL, um weitere Modifikationen zu verhindern und nur die sakila-Datenbank aus dem Backup wiederherzustellen: server1# /etc/init.d/mysql stop server1# mv /var/lib/mysql/sakila /var/lib/mysql/sakila.tmp server1# cp -a /backup/sakila /var/lib/mysql
Wir deaktivieren normale Verbindungen, indem wir Folgendes zur my.cnf-Datei des Servers hinzufügen: skip-networking socket=/tmp/mysql_recover.sock
Jetzt können wir den Server sicher wieder starten: server1# /etc/init.d/mysql start
Die nächste Aufgabe besteht darin, herauszufinden, welche Anweisungen im Binärlog wir wieder einspielen und welche wir überspringen wollen. Wie sich zeigt, hat der Server seit dem Backup um Mitternacht erst ein Binärlog erzeugt. Wir können diese Datei mit grep untersuchen und die schlechte Anweisung ermitteln: server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 | grep -B 3 -i 'drop table sakila.payment' # at 352 #070919 16:11:23 server id 1 end_log_pos 429 Query thread_id=16 exec_time=0 error_code=0 SET TIMESTAMP=1190232683/*!*/; DROP TABLE sakila.payment/*!*/;
Die Anweisung, die wir überspringen wollen, steht an Position 352 in der Log-Datei, die nächste Anweisung befindet sich an Position 429. Wir können mit den folgenden Befehlen das Log bis Position 352 und dann wieder von 429 erneut einspielen: server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 --stop-position=352 | mysql -uroot -p server1# mysqlbinlog --database=sakila /var/log/mysql/mysql-bin.000215 --start-position=429 | mysql -uroot -p
Jetzt müssen wir nur noch diese Daten überprüfen, den Server stoppen, die Änderungen an my.cnf rückgängig machen und den Server neu starten.
552 | Kapitel 11: Backup und Wiederherstellung
Höherentwickelte Techniken zur Wiederherstellung Replikation und punktgenaue Wiederherstellung benutzen den gleichen Mechanismus: das Binärlog des Servers. Die Replikation kann also auch auf nicht ganz so offensichtliche Weise bei der Wiederherstellung helfen. Wir zeigen Ihnen in diesem Abschnitt einige der Möglichkeiten. Es ist keine vollständige Liste, dürfte Ihnen aber einige Anregungen liefern, wie Sie den Wiederherstellungsvorgang für Ihre Bedürfnisse gestalten. Denken Sie daran, alles als Skript umzusetzen und auszuprobieren, was Sie vermutlich während einer Wiederherstellung brauchen werden.
Verzögerte Replikation für eine schnelle Wiederherstellung Wie wir bereits in diesem Kapitel erwähnt haben, kann ein verzögerter ReplikationsSlave die Wiederherstellung schneller und einfacher machen, wenn Sie den Zwischenfall bemerken, bevor der Slave die bösartige Anweisung ausgeführt hat. Das Vorgehen ist ein wenig anders als im vorangegangenen Abschnitt beschrieben, der Grundgedanke ist jedoch gleich. Sie stoppen den Slave und benutzen dann START SLAVE UNTIL, um die Events bis direkt vor der Anweisung, die Sie überspringen wollen, wieder einzuspielen. Anschließend führen Sie SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1 aus, um die schlechte Anweisung zu überspringen. Bei einem Wert größer als 1 überspringen Sie mehrere Events (mit CHANGE MASTER TO können Sie auch die Position des Slaves im Log anheben). Sie müssen dann nur noch START SLAVE ausführen und den Slave laufen lassen, bis er mit dem Ausführen seiner Relay-Logs fertig ist. Ihr Slave hat dann die ganze nervtötende Arbeit der punktgenauen Wiederherstellung für Sie erledigt. Jetzt können Sie den Slave zum Master befördern und haben die Wiederherstellung mit nur einer kurzen Unterbrechung hinter sich gebracht. Auch wenn Sie keinen verzögerten Replikations-Slave haben, um die Wiederherstellung zu beschleunigen, können Slaves sich nützlich machen, weil sie die Binärlogs des Masters auf eine andere Maschine holen. Wenn die Festplatte Ihres Masters ausfällt, sind die Relay-Logs eines Slaves möglicherweise die einzige Stelle, an der Sie eine einigermaßen aktuelle Kopie der Binärlogs des Masters haben. (Noch sicherer ist es, wenn Sie die Binärlogs auf einem SAN aufbewahren oder sie mit DRBD replizieren, wie wir in Kapitel 9 besprochen haben.)
Wiederherstellung mit einem Log-Server Es gibt noch eine weitere Möglichkeit, die Replikation zur Wiederherstellung zu verwenden: Richten Sie einen Log-Server ein. (Wie Sie das genau tun, erfahren Sie in »Einen Log-Server erzeugen« auf Seite 407.) Ein Log-Server ist flexibler und einfacher für die Wiederherstellung einzusetzen als mysqlbinlog, und zwar nicht nur wegen der Option START SLAVE UNTIL, sondern auch wegen der Replikationsregeln, die Sie anwenden können (wie etwa replicate-do-table). Mit einem Log-Server können Sie eine viel komplexere Filterung durchführen, als sonst möglich wäre.
Wiederherstellung aus einem Backup | 553
Zum Beispiel erlaubt es Ihnen ein Log-Server ganz einfach, eine einzige Tabelle wiederherzustellen. Mit mysqlbinlog und den Kommandozeilenwerkzeugen ist es viel schwieriger, dies korrekt zu erledigen – um ehrlich zu sein, ist es so schwierig, dass wir davon abraten. Nehmen wir an, unser unaufmerksamer Entwickler hat die gleiche Tabelle verworfen wie vorhin und wir wollen sie wiederherstellen, ohne den gesamten Server zum Backup der letzten Nacht zurückzuführen. Mit einem Log-Server gehen Sie folgendermaßen vor: 1. Den Server, den Sie wiederherstellen wollen, nennen wir server1. 2. Stellen Sie das Backup der letzten Nacht auf einem anderen Server her, den wir hier server2 nennen. Führen Sie die Wiederherstellung auf diesem Server aus, um das
Risiko zu vermeiden, dass die Dinge aufgrund eines Fehlers bei der Wiederherstellung noch schlimmer werden. 3. Richten Sie einen Log-Server ein, der die Binärlogs von server1 anbietet. Folgen Sie
dazu den Hinweisen aus »Einen Log-Server erzeugen« auf Seite 407. (Sie sollten aus Gründen der Vorsicht die Logs auf einen anderen Server kopieren und den Log-Server dort einrichten.) 4. Erweitern Sie die Konfigurationsdatei von server2 um Folgendes: replicate-do-table=sakila.payment
5. Starten Sie server2 neu, machen Sie ihn dann mit CHANGE MASTER TO zum Slave des
Log-Servers. Konfigurieren Sie ihn so, dass er von den Binärlogkoordinaten des Backups der letzten Nacht liest. Führen Sie noch nicht START SLAVE aus. 6. Untersuchen Sie die Ausgabe von SHOW SLAVE STATUS auf server2, und stellen Sie
sicher, dass alles korrekt ist. Messen Sie lieber zweimal nach, bevor Sie einmal etwas abschneiden! 7. Suchen Sie die Binärlog-Position der falschen Anweisung, und führen Sie START SLAVE UNTIL aus, um Events bis direkt vor dieser Position auf server2 einzuspielen.
8. Stoppen Sie den Slave-Prozess auf server2 mit STOP SLAVE. Dort sollte nun die
Tabelle in dem Zustand vorhanden sein, den sie hatte, bevor sie verworfen wurde. 9. Kopieren Sie die Tabelle von server2 auf server1.
Dieser Vorgang ist nur möglich, wenn die Tabelle nicht das Ziel von MehrtabellenUPDATE-, DELETE- oder INSERT-Anweisungen ist. Solche Anweisungen würden an einem
anderen Datenbankzustand ausgeführt als beim Aufzeichnen der Events im Binärlog, so dass die Tabelle schließlich andere Daten enthalten würde, als sie eigentlich sollte.
InnoDB-Wiederherstellung InnoDB überprüft bei jedem Start seine Daten- und Log-Dateien, um festzustellen, ob es seinen Wiederherstellungsprozess durchführen muss. Allerdings ist die InnoDB-Wiederherstellung etwas anderes als das, was wir im Kontext dieses Kapitels besprochen haben. Es geht nicht um das Wiederherstellen von Daten aus einem Backup, sondern um das
554 | Kapitel 11: Backup und Wiederherstellung
Anwenden von Transaktionen aus den Logs auf die Datendateien und das Zurücknehmen unbestätigter Modifikationen an den Datendateien. Wie die InnoDB-Wiederherstellung genau funktioniert, ist zu kompliziert, um es hier zu beschreiben. Wir konzentrieren uns stattdessen darauf, wie die Wiederherstellung eigentlich ausgeführt wird, wenn InnoDB ein ernstes Problem hat. Meist ist InnoDB ziemlich gut beim Beheben von Problemen. Solange es keinen Bug in MySQL gibt oder Ihre Hardware fehlerhaft ist, sollten Sie keine ungewöhnlichen Aktionen vornehmen müssen, selbst wenn bei Ihrem Server der Strom ausfällt. InnoDB führt einfach beim Start seine normale Wiederherstellung durch, und alles wird gut. In der Log-Datei finden Sie solche Meldungen: InnoDB: Doing recovery: scanned up to log sequence number 0 40817239 InnoDB: Starting an apply batch of log records to the database...
InnoDB schreibt Fortschrittsmeldungen in Prozentwerten in die Log-Datei. Manche Leute berichten, dass sie diese erst sehen, wenn der vollständige Vorgang abgeschlossen ist. Haben Sie Geduld; es gibt keine Möglichkeit, den Vorgang zu beschleunigen. Abschießen und Neustarten lässt ihn noch länger dauern. Wenn es ein ernstes Problem mit Ihrer Hardware gibt, wie etwa eine Beschädigung von Speicher oder Festplatte, oder wenn Ihnen ein Bug in MySQL oder InnoDB in die Quere kommt, müssen Sie wahrscheinlich eingreifen und entweder die Wiederherstellung erzwingen oder verhindern, dass die normale Wiederherstellung durchgeführt wird.
Gründe für InnoDB-Schäden InnoDB ist im Allgemeinen recht robust. Es ist extra auf Zuverlässigkeit ausgelegt und enthält viele Plausibilitätstests, um beschädigte Daten zu vermeiden, zu finden und zu reparieren – viel mehr als manch andere Storage-Engine. Es kann sich jedoch nicht vor allen Dingen schützen. InnoDB verlässt sich zum Mindesten darauf, dass ungepufferte Ein-/Ausgabe-Aufrufe und fsync( )-Aufrufe erst dann zurückkehren, wenn die Daten sicher auf das physische Medium geschrieben wurden. Falls Ihre Hardware sie nicht liefert, kann InnoDB Ihre Daten nicht schützen, und ein Absturz kann eine Beschädigung verursachen. Viele Probleme mit InnoDB-Beschädigungen hängen mit der Hardware zusammen (z.B. beschädigte Seitenschreiboperationen, die durch Stromausfälle oder schlechten Speicher hervorgerufen werden). Unserer Erfahrung nach bildet jedoch fehlkonfigurierte Hardware eine viel größere Problemquelle. Zu den häufiger auftretenden Fehlkonfigurationen gehört das Aktivieren des Writeback-Cache auf einer RAID-Karte, die keine Battery Backup Unit besitzt, oder das Aktivieren des Writeback-Cache auf den Festplatten selbst. Diese Fehler führen dazu, dass der Controller oder das Laufwerk falsche Aussagen treffen und behaupten, dass fsync( ) abgeschlossen wäre, obwohl sich die Daten tatsächlich nur im Writeback-Cache befinden und nicht auf der Festplatte. Mit anderen Worten: Die Hardware bietet nicht die Garantien, die InnoDB benötigt, um Ihre Daten sicher zu halten.
Wiederherstellung aus einem Backup | 555
Manchmal sind Maschinen standardmäßig auf diese Weise konfiguriert, weil dadurch die Leistung besser wird – was für manche Zwecke in Ordnung sein mag, allerdings nicht für einen transaktionsfähigen Datenbankserver. Sie müssen immer die Maschine überprüfen, falls Sie sie nicht selbst eingerichtet haben. Zu einer Beschädigung kann es auch kommen, wenn Sie InnoDB auf einem NAS (Network-Attached Storage) betreiben, weil das Beenden eines fsync( ) auf einem solchen Gerät bedeutet, dass das Gerät die Daten empfangen hat. Die Daten sind sicher, falls InnoDB abstürzt, nicht unbedingt jedoch, falls das NAS-Gerät abstürzt. Manchmal ist die Beschädigung schlimmer als bei anderen Gelegenheiten. Schwere Beschädigungen können InnoDB oder MySQL zum Absturz bringen, weniger schwere Beschädigungen dagegen bedeuten möglicherweise nur, dass einige Transaktionen verlorengegangen sind, weil die Log-Dateien nicht mit der Festplatte synchronisiert waren.
Wie man beschädigte InnoDB-Daten wiederherstellt Es gibt drei wesentliche Arten von InnoDB-Beschädigungen, die jeweils andere Anstrengungen erfordern, um die Daten wiederherzustellen: Beschädigung des sekundären Index Oft kann man einen beschädigten sekundären Index mit OPTIMIZE TABLE reparieren; alternativ können Sie SELECT INTO OUTFILE benutzen, die Tabelle verwerfen und neu erzeugen und anschließend LOAD DATA INFILE einsetzen. Diese Vorgänge beheben die Beschädigung, indem sie eine neue Tabelle erzeugen und daraus den betroffenen Index neu aufbauen. Beschädigung des Cluster-Index Möglicherweise brauchen Sie die innodb_force_recovery-Einstellungen, um die Tabelle in einem Dump zu speichern (mehr dazu später). Manchmal bringt der Dump-Prozess InnoDB zum Absturz; falls dies geschieht, müssen Sie Zeilen bereichsweise in einem Dump speichern, um die beschädigten Seiten zu überspringen, die den Absturz hervorrufen. Ein beschädigter Cluster-Index ist schlimmer als ein beschädigter sekundärer Index, weil er die Datenzeilen selbst beeinträchtigt. In vielen Fällen ist es jedoch möglich, nur die betroffenen Tabellen zu reparieren. Beschädigte Systemstrukturen Zu den Systemstrukturen gehören das InnoDB-Transaktions-Log, der Undo-LogBereich des Tablespace und das Data-Dictionary. Diese Art der Beschädigung verlangt wahrscheinlich einen vollständigen Dump sowie eine Wiederherstellung, weil viele der internen Funktionen von InnoDB betroffen sein können. Normalerweise reparieren Sie einen beschädigten sekundären Index, ohne irgendwelche Daten zu verlieren. Die anderen beiden Szenarien dagegen sind oft mit einem gewissen Datenverlust verbunden. Falls Sie ein Backup haben, sollten Sie lieber die Wiederherstellung aus diesem Backup durchführen, als zu versuchen, die Daten aus den beschädigten Dateien zu extrahieren.
556 | Kapitel 11: Backup und Wiederherstellung
Falls Sie es auf sich nehmen müssen, die Daten aus den beschädigten Dateien zu extrahieren, dann probieren Sie zuerst, InnoDB zu starten, und speichern Sie die Daten dann mit SELECT INTO OUTFILE in einem Dump. Ist Ihr Server bereits abgestürzt und können Sie nicht einmal InnoDB starten, ohne es zu Fall zu bringen, dann konfigurieren Sie es so, dass es die normale Wiederherstellung vermeidet und keine Hintergrundprozesse ausführt. Auf diese Weise können Sie möglicherweise den Server starten und ein logisches Backup durchführen, ohne die Integrität überhaupt bzw. allzu streng zu überprüfen. Der Parameter innodb_force_recovery kontrolliert, welche Arten von Operationen InnoDB beim Start und während des normalen Betriebs ausführt. Der normale Wert ist 0, Sie können ihn aber bis auf 6 erhöhen. Im MySQL-Handbuch sind die exakten Verhaltensweisen der einzelnen Optionen dokumentiert; wir wollen diese Informationen hier nicht noch einmal widerkäuen, sondern lediglich anmerken, dass Sie den Wert ohne große Gefahr bis auf 4 erhöhen können. Mit dieser Einstellung verlieren Sie vielleicht einige Daten auf Seiten, die beschädigt sind. Wenn Sie höher gehen, dann extrahieren Sie unter Umständen schlechte Daten von beschädigten Seiten oder erhöhen das Risiko eines Absturzes während SELECT INTO OUTFILE. Mit anderen Worten: Bis Stufe 4 sind Ihre Daten nicht gefährdet, allerdings verpassen Sie vielleicht die Gelegenheit, Probleme zu beheben. Die Stufen 5 und 6 sind aggressiver beim Beheben von Problemen, richten aber vielleicht auch Schäden an. Wenn Sie innodb_force_recovery auf einen Wert größer als 0 setzen, ist InnoDB im Prinzip schreibgeschützt, dennoch können Sie Tabellen anlegen und verwerfen. Dies verhindert weitere Schäden und lässt InnoDB einige seiner normalen Überprüfungen lockerer angehen, damit es nicht absichtlich abstürzt, wenn es schlechte Daten findet. Im Normalbetrieb ist das eine Absicherung, allerdings wollen Sie das bei der Wiederherstellung nicht. Falls Sie die InnoDB-Wiederherstellung erzwingen müssen, dann ist es besser, MySQL normale Verbindungsanforderungen erst dann wieder zu erlauben, wenn Sie fertig sind. Sind die Daten von InnoDB derart beschädigt, dass Sie MySQL überhaupt nicht starten können, dann extrahieren Sie die Daten mit dem InnoDB Recovery Toolkit direkt von den Tablespace-Seiten. Diese Werkzeuge wurden von einigen der Autoren dieses Buches entwickelt und stehen unter http://code.google.com/p/innodb-tools/ frei zur Verfügung. Normalerweise weisen wir nicht auf spezielle Bugs in MySQL hin, allerdings gibt es einen besonders ernsten Bug in vielen Versionen von MySQL, der das Durchführen der Wiederherstellung verhindert, wenn innodb_force_ recovery definiert ist. Sie können den Status des Bugs unter http://bugs.mysql. com/28604 verfolgen. Falls Sie den Fehler »Incorrect key file« erhalten, während Sie versuchen, einen Dump einer beschädigten InnoDB-Tabelle anzulegen, dann lesen Sie diesen Bug-Report, um festzustellen, ob dies das Problem ist. Ist es der Fall, könnten Sie den Dump der Daten vielleicht mit MySQL 5.0.22 erzeugen. Hoffentlich müssen Sie sich jedoch nie darum kümmern.
Wiederherstellung aus einem Backup | 557
Backup- und Wiederherstellungsgeschwindigkeit Die Geschwindigkeit ist gleich nach der Korrektheit die wichtigste Überlegung bei Backup und Wiederherstellung von High-Performance-Systemen. Hier sind einige Dinge, die Sie bedenken müssen: Lock-Zeit Wie lange müssen Sie Sperren halten, wie das globale FLUSH TABLES WITH READ LOCK, während Sie Daten im Backup sichern? Backup-Zeit Wie lange dauert es, das Backup ans Ziel zu kopieren? Backup-Last Wie stark belastet es die Leistung des Servers, das Backup ans Ziel zu kopieren? Wiederherstellungszeit Wie lange dauert es, Ihr Backup-Image von seinem Speicherplatz auf den MySQLServer zu kopieren, die Binärlogs wieder abzuspielen usw.? Der größte Kompromiss betrifft die Backup-Zeit im Verhältnis zur Backup-Last. Oft können Sie das eine auf Kosten des anderen verbessern. Zum Beispiel können Sie die Priorität des Backups auf Kosten eines größeren Leistungsabfalls auf dem Server erhöhen. Es ist auch möglich, die Backups so zu gestalten, dass sie Lastmuster ausnutzen. Falls z.B. Ihr Server nachts während 8 Stunden nur zu 50 % ausgelastet ist, können Sie versuchen, Ihre Backups so zu entwerfen, dass sie den Server mit weniger als 50 % belasten und trotzdem innerhalb von 8 Stunden abgeschlossen sind. Das erreichen Sie auf verschiedene Weisen: Sie können z.B. ionice und nice benutzen, um den Kopier- oder Komprimierungsoperationen den Vorrang zu geben, unterschiedliche Komprimierungsstufen verwenden oder die Daten auf dem Backup-Server komprimieren und nicht auf dem MySQL-Server. Oder Sie umgehen mit O_DIRECT oder madvice den Cache des Betriebssystems für die Kopieroperationen, damit diese nicht die Caches des Servers verschmutzen. Im Allgemeinen ist es ein ganzes Stück schneller und erfordert weniger Arbeit, rohe Kopien anstelle von logischen Backups anzulegen. Allerdings sind logische Backups eine wichtige Ergänzung, weil rohe Dateien nicht so portabel und nicht unendlich wiederherstellbar sind und Beschädigungen enthalten können, die schwer zu erkennen sind. Falls Sie regelmäßig logische Backups von einer rohen Dateikopie machen, erhalten Sie bei nur geringem Zusatzaufwand das Beste aus beiden Welten.
Backup-Werkzeuge Alles, was über das einfache Stoppen des Servers, das Wiederherstellen der Daten und das Neustarten des Servers hinausgeht, kann ziemlich aufwendig werden. Es ist wichtig, diese Aktionen zu testen und als Skript festzuhalten. In den folgenden Abschnitten werden einige Werkzeuge vorgestellt, die sich für Backup und Wiederherstellung eignen, und zwar sowohl für den Einsatz in Skripten als auch für einmalige Aktionen. 558 | Kapitel 11: Backup und Wiederherstellung
In der ersten Ausgabe dieses Buches hieß es, »Falls Sie eine komplexe Konfiguration oder ungewöhnliche Anforderungen haben, dann ist es relativ wahrscheinlich, dass keines dieser Werkzeuge allein die Arbeit für Sie tun kann. Stattdessen müssen Sie sich eine eigene Lösung erarbeiten.« Die Zeiten haben sich geändert, so dass wir heute davon abraten, sich eigene Backup-Werkzeuge zusammenzubasteln, wenn es sich vermeiden lässt. Mit großer Wahrscheinlichkeit kann irgendein vorhandenes Werkzeug Ihren Ansprüchen genügen, und falls nicht, können Sie immer noch eines so anpassen, dass es tut, was Sie wollen. Dennoch werden einige der komplizierteren Backup-Szenarien es notwendig machen, dass Sie eigene Skripten entwerfen, weshalb wir am Ende dieses Kapitels dazu einige grundlegende Hinweise geben.
mysqldump Das beliebteste Programm zum Erzeugen logischer Backups von Daten und Schema ist mysqldump. mysqldump wird mit dem Server geliefert, Sie müssen es also nicht einmal installieren. Es ist ein vielseitiges Werkzeug, das sich für alle möglichen Aufgaben eignet, wie etwa für das Kopieren einer Tabelle von einem Server auf einen anderen: $ mysqldump --host=server1 test t1 | mysql --host=server2 test
Wir haben bereits mehrfach gezeigt, wie man logische Backups mit mysqldump erzeugt. Standardmäßig gibt es ein Skript aus, das alle Befehle enthält, die man braucht, um eine Tabelle zu erzeugen und mit Daten zu füllen; es gibt auch Optionen zum Ausgeben von Sichten, gespeicherten Routinen und Triggern. Hier sind weitere Beispiele für den typischen Einsatz: • Erzeugt ein logisches Backup aller Dinge auf einem Server in eine einzige Datei: $ mysqldump --all-databases > dump.sql
• Erzeugt ein logisches Backup nur von der Sakila-Beispieldatenbank: $ mysqldump --databases sakila > dump.sql
• Erzeugt ein logisches Backup nur von der Tabelle sakila.actor: $ mysqldump sakila actor > dump.sql
Mit der Option --result-file geben Sie eine Ausgabedatei an, womit Sie die Newline-Konvertierung unter Windows vermeiden können: $ mysqldump sakila actor --result-file=dump.sql
Die vorgegebenen Optionen für mysqldump sind für ernsthafte Backup-Aufgaben nicht ganz so gut geeignet. Wahrscheinlich werden Sie einige Optionen explizit angeben, um die Ausgabe zu ändern. Hier sind Optionen, die wir häufig einsetzen, um mysqldump effizienter zu machen und die Nutzung seiner Ausgabe zu vereinfachen:
Backup-Werkzeuge | 559
--opt Aktiviert eine Gruppe von Optionen, die das Puffern deaktiviert (durch das Puffern könnte Ihrem Server der Speicher ausgehen), mehr Daten in weniger SQL-Anweisungen in den Dump schreibt, damit sie effizienter geladen werden, und weitere sinnvolle Dinge tut. Näheres erfahren Sie in der Hilfe Ihrer Version. Falls Sie die Gruppe der Optionen deaktivieren, speichert mysqldump jede Tabelle, die Sie als Dump ablegen, in seinem Speicher, bevor es sie auf die Festplatte schreibt, was bei großen Tabellen nicht besonders praktisch ist. --allow-keywords, --quote-names Ermöglicht es, solche Tabellen als Dump zu speichern und wiederherzustellen, die reservierte Wörter als Namen benutzen. --complete-insert Ermöglicht es, Daten zwischen Tabellen zu verschieben, die keine identischen Spalten besitzen. --tz-utc Ermöglicht es, Daten zwischen Servern in unterschiedlichen Zeitzonen zu verschieben. --lock-all-tables Setzt FLUSH TABLES WITH READ LOCK ein, um ein global konsistentes Backup zu erhalten. --tab Speichert Dateien mit SELECT INTO OUTFILE, was sowohl beim Speichern als Dump als auch beim Wiederherstellen sehr schnell geht. --skip-extended-insert Sorgt dafür, dass jede Datenzeile ihre eigene INSERT-Anweisung besitzt. Das kann Ihnen helfen, nötigenfalls selektiv bestimmte Zeilen wieder instand zu setzen. Dadurch entstehen aber größere Dateien, deren Import nach MySQL teurer ist; Sie sollten diese Option nur dann aktivieren, wenn Sie sie benötigen. Falls Sie die Optionen --databases oder --all-databases für mysqldump benutzen, dann sind die Daten des resultierenden Dumps innerhalb der einzelnen Datenbanken konsistent, weil mysqldump alle Tabellen einer Datenbank auf einmal sperrt und speichert. Tabellen aus anderen Datenbanken dagegen sind möglicherweise nicht konsistent miteinander. Dieses Problem lösen Sie mit der Option --lock-all-tables.
mysqlhotcopy mysqlhotcopy ist ein Perl-Skript, das in den Standard-MySQL-Server-Downloads enthalten ist. Es wurde für MyISAM-Tabellen geschaffen und führt unserer Meinung nach keine »heißen« Backups durch, weil es alle Tabellen sperrt, bevor es sie kopiert. Es war zwar eine der beliebtesten Möglichkeiten für Backups auf einem Live-Server, ist aber heutzutage nicht mehr ganz so populär. Viele High-Performance-Installationen gehen weg von MyISAM, und selbst wenn Sie nur MyISAM verwenden, sind Dateisystemschnappschüsse oft weniger störend, weil sie die Daten für eine kürzere Zeit sperren.
560 | Kapitel 11: Backup und Wiederherstellung
Als Beispiel haben wir eine Kopie der Sakila-Beispieldatenbank ausschließlich mit MyISAM-Tabellen erzeugt. Um diese Datenbank in einem Backup in /tmp zu sichern, führten wir folgenden Befehl aus: $ mysqlhotcopy sakila_myisam /tmp
Dies erzeugte das Unterverzeichnis sakila_myisam in /tmp, in dem sich alle Tabellen der Datenbank befinden: $ ls -l /tmp/sakila_myisam/ total 3632 -rw-rw---- 1 mysql mysql 8694 -rw-rw---- 1 mysql mysql 5016 -rw-rw---- 1 mysql mysql 7168 ... weggelassen ... -rw-rw---- 1 mysql mysql 8708 -rw-rw---- 1 mysql mysql 18 -rw-rw---- 1 mysql mysql 4096
2007-09-28 09:57 actor.frm 2007-09-28 09:57 actor.MYD 2007-09-28 09:57 actor.MYI 2007-09-28 09:57 store.frm 2007-09-28 09:57 store.MYD 2007-09-28 09:57 store.MYI
Es kopierte die Daten-, Index- und Tabellendefinitionsdateien für die einzelnen Tabellen in der Datenbank. Um Platz zu sparen, können Sie die Option --noindices benutzen. Dadurch werden nur die ersten 2.048 Bytes jeder .MYI-Datei gesichert, die ausreichen, damit MySQL die Indizes später rekonstruieren kann. Wenn Sie diese Option einsetzen, müssen Sie die Indizes neu aufbauen, nachdem die Dateien wiederhergestellt wurden. Sie können entweder myisamchk mit der Option --recover verwenden oder den Befehl REPAIR TABLE SQL einsetzen. mysqlhotcopy ist ziemlich kompliziert und nicht besonders flexibel. Viele Leute bauen sich deshalb ihre eigenen Skripten, die im Prinzip auf etwas andere Art die gleiche Arbeit erledigen. mysqlhotcopy kopiert die .ibd-Datendateien, wenn InnoDB mit innodb_ file_per_table konfiguriert ist, aber das ist sinnlos. Lassen Sie sich davon nicht fälschlicherweise in Sicherheit wiegen; dies ist keine sichere Methode für das Backup Ihrer InnoDB-Daten.
InnoDB Hot Backup Das InnoDB Hot Backup, ibbackup, ist ein kommerzielles Werkzeug, das von den Herstellern von InnoDB (Innobase) vertrieben wird. Wenn Sie es benutzen, ist es nicht erforderlich, MySQL zu stoppen, Sperren zu setzen oder die normale Datenbankaktivität zu unterbrechen (obwohl dies eine gewisse Extralast auf Ihrem Server verursacht). Außerdem kann es Backups komprimieren. Sie konfigurieren ibbackup, indem Sie ihm eine Konfigurationsdatei anbieten, die der my.cnf-Datei Ihres Produktionsservers entspricht, allerdings ein anderes Datenverzeichnis angibt. Das Programm liest beide Konfigurationsdateien und kopiert die InnoDBDateien vom Produktionsserver an die Stellen die in der zweiten Konfigurationsdatei genannt ist: $ ibbackup /etc/my.cnf /etc/ibbackup.cnf
Backup-Werkzeuge | 561
Um das Backup wiederherzustellen, fahren Sie MySQL herunter und führen den folgenden Befehl aus: $ ibbackup --restore /etc/ibbackup.cnf
Es gibt allerdings ein kleines Problem: ibbackup kopiert nur die InnoDB-Dateien, nicht die Tabellendefinition oder andere notwendige Dateien. Innobase bietet darüber hinaus ein innobackup-Helper-Skript, das die Dateikopien, Tabellen-Locks und ibbackup in einen einzigen Befehl einpackt, der die Tabellendefinitionen und MyISAM-Dateien sowie die InnoDB-Dateien in einem Backup sichern kann. Im Gegensatz zu ibbackup selbst unterbricht dies die normale Verarbeitung, weil es FLUSH TABLES WITH READ LOCK benutzt. Es ist freie Software. Unserer Meinung nach sind die Schnappschussfähigkeiten von LVM bequemer und sinnvoller für InnoDB-Backups als ibbackup. Einer der größten Vorteile von LVM besteht darin, dass Sie keine zweite Kopie Ihrer Daten im Dateisystem machen müssen – Sie erzeugen einen Schnappschuss, führen auf Wunsch die InnoDB-Wiederherstellung damit aus und schicken sie direkt an Ihr Backup-Ziel. LVM und ibbackup haben im Allgemeinen eine vergleichbare Leistung, je nachdem, wie Sie das Backup konfiguriert haben und ob Sie eine schreibintensive Belastung haben. In diesem Fall gibt es viel Kopieren-beim-Schreiben-Overhead für LVM. Andererseits skaliert ibbackup möglicherweise nicht linear mit der Datengröße. Es arbeitet, indem es Datendateien seitenweise kopiert und dann die Log-Datei auf die kopierten Datendateien anwendet, um sie zu einem bestimmten Zeitpunkt »wiederherzustellen«, der mit dem Ende des Backup-Vorgangs zusammenhängt.
mk-parallel-dump Dieses Werkzeug ist Teil des Maatkit (http://maatkit.sourceforge.net). Es führt mehrere Backup-Operationen gleichzeitig durch. Standardmäßig agiert mk-parallel-dump als Multithread-fähiger Wrapper für mysqldump, es kann aber auch Tab-separierte Exporte mit SELECT INTO OUTFILE durchführen. Üblicherweise gibt es einen Thread pro CPU. Je mehr CPUs Sie haben, umso schneller wird es also im Allgemeinen. Es kann die einzelnen Tabellen in Stücken mit einer gewünschten Größe sichern, was die Wiederherstellung für InnoDB-Tabellen beschleunigt. Der Vorteil besteht darin, dass Sie damit beim Wiederherstellen eine riesige Transaktion vermeiden. Eine große Transaktion könnte den Tablespace von InnoDB sehr stark anwachsen lassen und die Rollback-Zeit im Fehlerfall verlängern. Das Werkzeug besitzt außerdem einige hübsche Funktionen, wie etwa die Fähigkeit, inkrementelle Backups durchzuführen und Tabellen zu logischen Backup-Sets zu gruppieren. Benchmarks haben gezeigt, dass parallel ausgeführte logische Backups einen deutlichen Geschwindigkeitszuwachs bringen.
562 | Kapitel 11: Backup und Wiederherstellung
Maatkit enthält darüber hinaus mk-parallel-restore, ein Begleitprogramm für Multithread-fähige Importe. Beide Werkzeuge greifen gern auf Unix-Konstrukte wie Pipes und FIFO-Geräte zurück, um die Auswirkungen des Komprimierens und Dekomprimierens von Dateien zu verringern.
mylvmbackup Bei Lenz Grimmers mylvmbackup (http://lenz.homelinux.org/mylvmbackup/) handelt es sich um ein Perl-Skript, mit dem man MySQL-Backups über LVM-Schnappschüsse automatisieren kann. Es holt sich einen globalen Lese-Lock, erzeugt einen Schnappschuss und gibt den Lock dann wieder frei. Anschließend komprimiert es die Daten mit tar und löscht den Schnappschuss. Die tar-Datei wird entsprechend dem Zeitstempel benannt, zu dem das Backup erzeugt wurde.
Zmanda Recovery Manager Zmanda Recovery Manager for MySQL oder ZRM (http://www.zmanda.com) ist das umfassendste der hier vorgestellten Backup- und Wiederherstellungswerkzeuge. Es gibt dieses Werkzeug sowohl in freien (GPL) als auch in kommerziellen Versionen. Die Enterprise-Edition bringt eine Management-Konsole mit einer webbasierten grafischen Oberfläche für Konfiguration, Backup, Verifizierung, Wiederherstellung, Reporting und Scheduling mit. Es kann auch MySQL Cluster sichern und besitzt all die üblichen Vorteile (wie etwa Support). Die Open-Source-Variante ist in keiner Weise eingeschränkt, ihr fehlen aber einige der Extrafeinheiten, wie etwa die webbasierte Konsole. Wenn Sie mit der Kommandozeile vertraut sind, ist sie absolut ausreichend. Zum Beispiel können Sie am Befehlsprompt genauso Backups eintakten und überprüfen. ZRM ist eigentlich mehr ein »Backup-Koordinator« als ein einzelnes Werkzeug. Es packt seine eigene Funktionalität um Standardwerkzeuge und -techniken, wie mysqldump und LVM-Schnappschüsse, und speichert die Daten in Standardformaten, so dass es nicht notwendig ist, proprietäre Software zu erwerben, um ein Backup wiederherzustellen. Eine seiner hübschen Funktionen ist sein vereinheitlichter Wiederherstellungsmechanismus, der immer gleich funktioniert, egal, wie das Backup erzeugt wurde. Abbildung 11-2 zeigt die Kalenderübersicht der Enterprise-Version für MySQL-Backups und den Binärlog-Analyzer, die Zmanda als »Database Events Viewer« bezeichnet. Im Prinzip handelt es sich um ein Suchwerkzeug für Ihre Binärlogs; Sie verwenden eine normale Suchsyntax zum Auffinden von Events, was es erleichtert, die Wiederherstellung zu einem bestimmten Log-Event oder Zeitpunkt durchzuführen.
Backup-Werkzeuge | 563
Abbildung 11-2: ZRMs Backup-Kalender und Suchoberfläche für das Binärlog
ZRM installieren und testen Auf der Zmanda-Website wird behauptet, dass es etwa 15 Minuten dauert, das System zu installieren, ein Backup durchzuführen und zu verifizieren, einen Tagesplan einzurichten und zu verifizieren und eine Wiederherstellung durchzuführen. Zum Test installierten wir ZRM von Grund auf neu auf einem Laptop mit Ubuntu. Das herunterzuladende ZRM-Paket selbst ist winzig, und wir installierten es mit sudo dpkg -i mysqlzrm_1.2.1_all.deb. Es mussten verschiedene Vorbedingungen erfüllt werden, die sich aber leicht mit sudo apt-get -f install installieren ließen. Das dauerte weniger als eine Minute. Wir folgten den Anweisungen auf der Website, um das Backup-Set zu konfigurieren, bei dem es sich in unserem Fall um ein logisches Backup der Sakila-Beispieldatenbank handelte. Dies dauerte etwa drei Minuten. Dann tippten wir den folgenden Befehl ein, um das Backup zu starten: # mysql-zrm-scheduler --now --backup-set dailyrun
Das Backup nahm einen Augenblick in Anspruch, die resultierende Datei wurde in /var/lib/mysql-zrm/dailyrun gespeichert. Wir führten es dann noch einmal aus und verursachten absichtlich einige Fehler für ZRM, indem wir z.B. einen seiner Kindprozesse beendeten und die falschen Login-Parameter vorgaben. Es korrigierte die entdeckten
564 | Kapitel 11: Backup und Wiederherstellung
Fehler und vermerkte sie in den Backup-E-Mails, die es verschickte. Die Einzelheiten wurden im erwarteten System-Log protokolliert. Schließlich verwarfen wir die sakila-Datenbank und stellten sie mit den folgenden Befehlen aus dem letzten erfolgreichen Backup wieder her: # mysql-zrm-reporter --show restore-info --where backup-set=dailyrun # mysql-zrm-restore --backup-set dailyrun --source-directory /var/lib/mysql-zrm/dailyrun/20070930134242/
ZRM ist ein im Allgemeinen gut aufgebautes System mit einer guten Fehlererkennung, das einen Großteil der nervtötenden Arbeit beim Erzeugen von Backups und bei der Wiederherstellung automatisiert. Und wie sein Name schon andeutet, ist es ganz und gar für die Wiederherstellung gedacht.
R1Soft R1Soft (http://www.r1soft.com) bietet ein Produkt namens Continuous Data Protection, das mit Dateisystemschnappschüssen vergleichbar ist. Wenn es einen Schnappschuss auf einen anderen Server kopiert, dann kopiert es allerdings nur die Blöcke, die sich geändert haben. Sie können mit ihm zu mehreren vergangenen Versionen Ihrer Daten zurückkehren. Es ist eine kommerzielle Software.
MySQL-Online-Backup Das MySQL-Online-Backup ist kein Werkzeug, sondern eine Funktion, die für MySQL 5.2 entwickelt wurde (momentan im Alpha-Stadium) und wahrscheinlich in MySQL 6.0 veröffentlicht wird. Die Schnittstelle ist eine neue BACKUP DATABASE-SQL-Anweisung, die mit hoher Geschwindigkeit einen konsistenten Schnappschuss von jeder Tabelle in eine Datei schreibt. Sie benutzt entweder einen Standardtreiber, der jede Storage-Engine als Backup sichern kann, oder einen Treiber, der für eine bestimmte Storage-Engine implementiert ist, um das Backup effizienter durchzuführen. Der Standardtreiber blockiert andere SQL-Anweisungen, native Treiber dagegen können das Backup ohne Blockade durchführen. Auch das Wiederherstellen der Funktionalität ist enthalten. Momentan hat das Projekt eine erste Implementierung im 5.2-Quellbaum und eine ansehnliche Liste fertiger Funktionen. Viele Funktionen müssen allerdings erst noch implementiert werden, wie etwa ein nativer Treiber für MyISAM und konsistente Backups über Storage-Engines hinweg. Das Online-Backup wird bereits von den Anwendern dringend erwartet und wird wahrscheinlich, wenn es fertig ist, zu den wichtigsten Methoden gehören, um MySQL zu sichern.
Backup-Werkzeuge | 565
Vergleich der Backup-Werkzeuge Tabelle 11-2 bietet eine kurze Zusammenfassung einiger der Backup-Methoden, die wir in diesem Kapitel vorgestellt haben. Tabelle 11-2: Eigenschaften von Backup-Werkzeugen mylvmbackup
mysqldump
mk-parallel-dump
mysqlhotcopy
ibbackup
Blockiert die Verarbeitung?
Optional
Ja
Ja
Ja
Nein
Logisch oder roh
Roh
Logisch
Logisch
Roh
Roh
Engines
Alle
Alle
Alle
MyISAM/Archive
InnoDB
Geschwindigkeit
Sehr gut
Langsam
Gut
Sehr gut
Sehr gut
Entfernte Backups
Nein
Ja
Ja
Nein
Nein
Verfügbarkeit
Frei
Frei
Frei
Frei
Kommerziell
Lizenz
GPL
GPL
GPL
GPL
Proprietär
Backups mit Skripten Wir haben Ihnen empfohlen, das Rad nicht neu zu erfinden, wenn es bereits ein System gibt, das die Arbeit für Sie erledigen könnte. Vielleicht müssen Sie jedoch trotzdem ein eigenes Skript entwerfen oder ein vorhandenes Skript anpassen. Hier sind einige der Backup-Konfigurationen, die wir im richtigen Leben schon gesehen haben: • das Sichern vieler Server auf eine bestimmte Anzahl von Backup-Servern, die große, billige Festplatten ohne RAID haben. Das Backup-Skript reserviert für jedes Backup unterschiedliche Volumes, je nachdem, auf welchen Servern genug Platz ist. Dies stellt außerdem sicher, dass unterschiedliche Backup-Generationen auf unterschiedliche Server gelangen, damit es kein großes Problem ist, wenn ein Server verloren geht. • das Aufteilen eines Backup-Archivs in Stücke, das Verschlüsseln dieser Stücke und das Speichern außerhalb des Rechenzentrums bei Amazons S3-Dienst oder einem anderen großen Speicher-Service • das Integrieren der Wiederherstellung mit Replikation, so dass Sie einen Slave aus dem Backup klonen können Anstatt Ihnen ein Beispielprogramm zu zeigen, das mit Sicherheit eine Menge Kram enthalten würde, der hier nicht wichtig ist, aber Platz auf der Seite beansprucht, führen wir hier die Zutaten für ein typisches Backup-Skript auf und zeigen Ihnen die Codeschnippsel für ein Perl-Skript. Sie können diese als Bausteine betrachten, die Sie zusammensetzen, um ein eigenes Skript zu erzeugen. Wir zeigen sie in ungefähr der Reihenfolge, in der Sie sie benutzen müssen:
566 | Kapitel 11: Backup und Wiederherstellung
Plausibilitätstest Erleichtert Ihnen und Ihren Kollegen das Leben – schalten Sie eine strenge Fehlerüberprüfung und die Benutzung englischer Variablennamen ein: use strict; use warnings FATAL => 'all'; use English qw(-no_match_vars);
Wenn Sie das Skript in der Bash erstellen, können Sie ebenfalls eine strengere Variablenüberprüfung einschalten. Folgende Befehle erzeugen einen Fehler, wenn es eine undefinierte Variable in einer Ersetzung gibt oder wenn ein Programm sich mit einem Fehler beendet: set -u; set -e;
Kommandozeilenargumente Jedes Skript muss Kommandozeilenargumente akzeptieren. Falls Sie merken, dass Sie Konfigurationen wie Benutzernamen und Passwörter fest kodieren, dann sollten Sie wirklich auf einer höheren Ebene arbeiten. use Getopt::Long; Getopt::Long::Configure('no_ignore_case', 'bundling'); GetOptions( .... );
Mit MySQL verbinden Die Standard-Perl-DBI-Bibliothek ist nahezu allgegenwärtig und bietet eine gute Leistung und Flexibilität. Wie Sie sie benutzen, erfahren Sie in der Perldoc (online verfügbar unter http://search.cpan.org). use DBI; $dbh = DBI->connect( 'DBI:mysql:;host=localhost', 'user', 'pass', {RaiseError => 1 });
Für die Erstellung von Kommandozeilenskripten lesen Sie den --help-Text für das normale mysql-Programm. Es besitzt viele Optionen, die das Skripten erleichtern. Folgendermaßen iterieren Sie z.B. über eine Liste von Datenbanken in der Bash: for DB in `mysql --skip-column-names --silent --execute 'SHOW DATABASES'` do echo $DB done
MySQL stoppen und starten Am besten stoppen und starten Sie MySQL mit der bevorzugten Methode Ihres Betriebssystems, etwa mit dem /etc/init.d/mysql-Initialisierungsskript oder der Dienstkontrolle (unter Windows). Das ist jedoch nicht die einzige Methode. Sie können die Datenbank von Perl aus über eine vorhandene Datenbankverbindung herunterfahren: $dbh->func("shutdown", 'admin');
Verlassen Sie sich nicht darauf, dass MySQL tatsächlich heruntergefahren wurde, wenn dieser Befehl fertig ist – möglicherweise ist es noch dabei. Sie können MySQL von der Kommandozeile aus auch stoppen: $ mysqladmin shutdown
Backups mit Skripten | 567
Listen der Datenbanken und Tabellen holen Jedes Backup-Skript fragt MySQL nach einer Liste der Datenbanken und Tabellen. Hüten Sie sich vor Einträgen, die eigentlich keine Datenbanken sind, wie etwa dem lost+found-Verzeichnis in einigen Journaling-Dateisystemen und dem INFORMATION_ SCHEMA. Sorgen Sie dafür, dass Ihr Skript mit Sichten umgehen kann, und bedenken Sie, dass SHOW TABLE STATUS sehr lange dauern kann, wenn Sie viele Daten in InnoDB haben: mysql> SHOW DATABASES; mysql> SHOW /*!50002 FULL*/ TABLES FROM ; mysql> SHOW TABLE STATUS FROM ;
Tabellen sperren, übertragen und entsperren Sie werden nicht umhin kommen, eine oder mehrere Tabellen zu sperren und/oder auf die Festplatte zu übertragen. Entweder sperren Sie die gewünschten Tabellen, indem Sie sie alle benennen, oder Sie sperren sie einfach global: mysql> mysql> mysql> mysql> mysql>
LOCK TABLES READ [, ...]; FLUSH TABLES; FLUSH TABLES [, ...]; FLUSH TABLES WITH READ LOCK; UNLOCK TABLES;
Achten Sie auf Race-Conditions, wenn Sie Listen von Tabellen holen und sperren. Es könnten neue Tabellen erzeugt oder vorhandene Tabellen verworfen oder umbenannt werden. Wenn Sie sie jedoch nacheinander sperren und im Backup sichern, erhalten Sie keine konsistenten Backups. Binärlogs übertragen Es ist praktisch, wenn man den Server bittet, ein neues Binärlog zu beginnen (nachdem Sie die Tabellen gesperrt, aber bevor Sie ein Backup begonnen haben): mysql> FLUSH LOGS;
Es erleichtert die Wiederherstellung sowie inkrementelle Backups, wenn Sie nicht daran denken müssen, in der Mitte einer Log-Datei zu beginnen. Das hat einige Nebenwirkungen in Bezug auf das Übertragen und erneute Öffnen von Logs und das potenzielle Zerstören alter Log-Einträge. Achten Sie also darauf, keine Daten wegzuwerfen, die Sie noch brauchen könnten. Binärlog-Positionen holen Ihr Skript sollte sowohl den Master- als auch den Slave-Status holen und aufzeichnen – selbst wenn der Server nur ein Master oder nur ein Slave ist: mysql> SHOW MASTER STATUS; mysql> SHOW SLAVE STATUS;
Führen Sie beide Anweisungen aus, und ignorieren Sie alle Fehler, die Sie erhalten, damit Ihr Skript alle möglichen Informationen erhält. Daten als Dump speichern Ihre zwei besten Möglichkeiten sind mysqldump oder SELECT INTO OUTFILE. Daten kopieren Verwenden Sie eine der Methoden, die wir in diesem Kapitel gezeigt haben.
568 | Kapitel 11: Backup und Wiederherstellung
Dies sind die Bausteine eines Backup-Skripts. Schwierig ist das Skripten der Wiederherstellung. Falls Sie eine Inspiration brauchen, wie Sie das gut hinbekommen, dann schauen Sie sich den Quellcode für ZRM an. Seine Skripten sind relativ schlau; sie heben z.B. bei jedem Backup die Metadaten auf, um die Wiederherstellung zu erleichtern.
Backups mit Skripten | 569
KAPITEL 12
Sicherheit
Für den Schutz der Integrität und Vertraulichkeit Ihrer Daten ist es unerlässlich, MySQL abzusichern. Genau wie bei Unix- oder Windows-Accounts müssen Sie auch für MySQLAccounts sicherstellen, dass gute Passwörter verwendet werden und nur solche Berechtigungen zum Einsatz kommen, die wirklich nötig sind. Da MySQL oft in einem Netzwerk benutzt wird, müssen Sie auch die Sicherheit des Hosts in Betracht ziehen, auf dem MySQL läuft. Sie müssen überlegen, wer Zugriff darauf hat und was jemand erfahren könnte, der den Verkehr in Ihrem Netzwerk ausspioniert. MySQL besitzt ein besonderes Sicherheits- und Berechtigungssystem, mit dem Sie viele spezielle Aufgaben ausführen können. Es baut auf einer Reihe einfacher Regeln auf, allerdings gibt es viele komplizierte Ausnahmen und Sonderfälle, so dass es möglicherweise schwer zu verstehen ist. In diesem Kapitel schauen wir uns an, wie die Berechtigungen von MySQL funktionieren und wie Sie den Zugriff auf Ihre Daten steuern können. Das MySQL-Handbuch enthält eine gründliche Dokumentation der Berechtigungen, so dass wir hier nur die verwirrenden Konzepte erklären und Ihnen zeigen, wie Sie einige allgemeine Aufgaben ausführen können, die man sich ansonsten nur schwer erarbeiten kann. Wir betrachten außerdem einige der grundlegenden Sicherheitsmaßnahmen für Betriebssystem und Netzwerk, die Sie ergreifen können, um die bösen Jungs aus Ihren Datenbanken herauszuhalten. Schließlich diskutieren wir die Verschlüsselung sowie den Betrieb von MySQL in einer stark eingeschränkten Umgebung.
Terminologie Zunächst wollen wir einige Begriffe definieren, die vielleicht verwirrend sind. Mit ihnen wollen wir bestimmte Dinge in diesem Kapitel bezeichnen: Authentifizierung Wer sind Sie? MySQL authentifiziert Sie mit einem Benutzernamen, einem Passwort und dem Host, von dem aus Sie die Verbindung herstellen. Zu wissen, wer Sie sind, ist eine Voraussetzung für das Ermitteln Ihrer Rechte.
570 |
Autorisierung Was dürfen Sie? Das Herunterfahren des Servers verlangt z.B., dass Sie die Berechtigung SHUTDOWN haben. Bei MySQL gilt die Autorisierung für globale Berechtigungen, die nicht mit bestimmten Schemaobjekten (wie etwa Tabellen oder Datenbanken) verknüpft sind. Zugriffskontrolle Welche Daten dürfen Sie sehen und/oder ändern? Wenn Sie versuchen, Daten zu lesen oder zu modifizieren, prüft MySQL, ob Ihnen das Recht gewährt wird, die Spalten, die Sie auswählen, zu sehen oder zu ändern. Im Gegensatz zu den globalen Berechtigungen gilt die Zugriffskontrolle für bestimmte Daten, wie eine spezielle Datenbank, Tabelle oder Spalte. Berechtigungen und Erlaubnisse Diese Begriffe meinen in etwa das Gleiche – eine Berechtigung oder Erlaubnis zeigt an, wie MySQL eine Autorisierung oder ein Zugriffsrecht darstellt.
Account-Grundlagen MySQL-Accounts sind anders als die Accounts in den meisten Systemen, da MySQL den Ursprung eines Login-Versuchs als Teil der Authentifizierung betrachtet. Im Gegensatz dazu wird ein Unix-Login-Versuch nur mit einem Benutzernamen und einem Passwort authentifiziert. Mit anderen Worten: Der Primärschlüssel eines Unix-Accounts ist der Benutzername, während es in MySQL der Benutzername und der Ort ist (normalerweise ein Hostname, eine IP-Adresse oder ein Wildcard). Wie wir sehen, wird ein ansonsten einfaches System durch das Hinzufügen des Ortes deutlich komplexer. Der Benutzer joe, der sich von joe.example.com aus anmeldet, ist vielleicht nicht derselbe wie der joe, der sich von sally.example.com aus anmeldet. Aus Sicht von MySQL kann es sich um völlig verschiedene Benutzer mit unterschiedlichen Passwörtern und Berechtigungen handeln. Andererseits könnte es auch derselbe Benutzer sein. Das hängt davon ab, wie Sie die Accounts konfiguriert haben.
Berechtigungen MySQL verwendet Ihre Account-Informationen (Benutzernamen, Passwort und Ort), um Sie zu authentifizieren. Sobald es das getan hat, muss es entscheiden, was Sie tun dürfen. Dazu schaut es sich Ihre Berechtigungen an, die normalerweise nach den SQL-Abfragen benannt werden, die diese Sie ausführen lassen. Zum Beispiel brauchen Sie die Berechtigung SELECT für eine Tabelle, um Daten aus ihr abzufragen. Es gibt zwei Arten von Berechtigungen: solche, die mit Objekten (wie etwa Tabellen, Datenbanken und Sichten) verknüpft sind, und solche, die dies nicht sind. Objektspezifische Berechtigungen gewähren Ihnen den Zugriff auf bestimmte Objekte. So steuern sie z.B., ob Sie Daten aus einer Tabelle abfragen, eine Tabelle verändern, eine Sicht in einer Datenbank erzeugen oder einen Trigger anlegen können. Seit MySQL 5.0 gibt es auf-
Account-Grundlagen | 571
grund der Einführung von Sichten, gespeicherten Prozeduren und anderer neuer Funktionen viele zusätzliche objektspezifische Berechtigungen. Globale Berechtigungen erlauben es Ihnen andererseits, Funktionen auszuführen wie das Herunterfahren des Servers, das Ausführen von FLUSH-Befehlen, das Ausführen verschiedener SHOW-Befehle und das Betrachten der Abfragen anderer Benutzer. Im Allgemeinen erlauben Ihnen die globalen Berechtigungen, Dinge auf dem Server zu tun, und die objektbasierten Berechtigungen, Dinge mit dem Inhalt des Servers zu tun, (auch wenn diese Unterscheidung nicht immer scharf definiert ist). Jede globale Berechtigung hat weitreichende Auswirkungen auf die Sicherheit, so dass Sie sehr vorsichtig sein müssen, wenn Sie eine dieser Berechtigungen gewähren! MySQL-Berechtigungen sind vom Booleschen Typus: Eine Berechtigung wird entweder gewährt oder nicht. Im Gegensatz zu anderen Datenbanksystemen kennt MySQL die Idee der explizit verbotenen Berechtigungen nicht. Das Zurücknehmen einer Berechtigung verbietet dem Benutzer nicht, eine Aktion auszuführen; es entfernt lediglich die Berechtigung, die Aktion auszuführen, falls sie existiert. Die MySQL-Berechtigungen sind außerdem hierarchisch, mit einem oder zwei Haken. Wir erklären das gleich.
Die Grant-Tabellen MySQL speichert die Benutzer und ihre Berechtigungen in einer Reihe von Grant-Tabellen. Bei diesen Tabellen handelt es sich um normale MyISAM-Tabellen1, die sich in der mysql-Datenbank befinden. Es ist sinnvoll, die Sicherheitsinformationen in Grant-Tabellen zu speichern. Es bedeutet aber auch, dass bei einer fehlerhaften Konfiguration des Servers jeder Benutzer Änderungen an den Sicherheitsmaßnahmen vornehmen kann, indem er die Daten in diesen Tabellen ändert! Die MySQL-Grant-Tabellen sind das Herzstück seines Sicherheitssystems. MySQL bietet mit den Befehlen GRANT, REVOKE und DROP USER (die wir später erörtern) eine nahezu vollständige Kontrolle über die Sicherheit. Allerdings war das Manipulieren der Grant-Tabellen bisher das einzige Mittel, bestimmte Aufgaben auszuführen. In alten MySQLVersionen bestand die einzige Möglichkeit, einen Benutzer vollständig zu entfernen, darin, diesen Benutzer mit DELETE aus der user-Tabelle zu löschen und ihm dann mit FLUSH PRIVILEGES die Berechtigungen zu entziehen. Wir empfehlen nicht, die Grant-Tabellen direkt zu manipulieren, Sie sollten jedoch verstehen, wie sie funktionieren, damit Sie unerwünschtes Verhalten erkennen und verhindern können. Untersuchen Sie einfach einmal die Strukturen der Grant-Tabelle mit DESCRIBE oder SHOW CREATE TABLE, vor allem, nachdem Sie Berechtigungen mit GRANT und REVOKE geändert haben. Dabei werden Sie mehr lernen, als wenn Sie nur darüber lesen.
1 Und sie müssen normale MyISAM-Tabellen bleiben. Ändern Sie Ihre Storage-Engine nicht auf etwas anderes.
572 | Kapitel 12: Sicherheit
Hier sind die Grant-Tabellen in der Reihenfolge, in der MySQL sie abfragt, wenn es überprüft, ob ein Benutzer autorisiert ist, eine bestimmte Operation durchzuführen: user
Jede Zeile enthält einen Benutzer-Account (Benutzernamen, Hostnamen und verschlüsseltes Passwort) und die globalen Berechtigungen des Benutzers. MySQL 5.0 fügt optional benutzerweise Einschränkungen hinzu, wie etwa die Anzahl der Verbindungen, die der Benutzer haben darf. db
Jede Zeile enthält Berechtigungen auf Datenbankebene für einen bestimmten Benutzer. host
Jede Zeile enthält Berechtigungen bezüglich einer Datenbank für einen Benutzer, der sich von einem bestimmten Host aus verbindet. Die Einträge werden mit Einträgen in db »zusammengeführt«, wenn der Zugriff auf Datenbankebene geprüft wird. Obwohl wir sie als Grant-Tabelle aufführen, wird die host-Tabelle nie durch die Befehle GRANT und REVOKE modifiziert. Sie können Einträge nur manuell hinzufügen und entfernen. Um später Wartungsprobleme und verwirrendes Verhalten zu vermeiden, empfehlen wir, diese Tabelle nicht zu benutzen. tables_priv
Jede Zeile enthält Berechtigungen auf Tabellenebene für einen bestimmten Benutzer und eine bestimmte Tabelle. Sie enthält außerdem Darstellungsrechte. columns_priv
Jede Zeile enthält Berechtigungen auf Spaltenebene für einen bestimmten Benutzer und eine bestimmte Spalte. procs_priv (neu in MySQL 5.0)
Jede Zeile enthält Berechtigungen für einen bestimmten Benutzer und eine gespeicherte Routine (gespeicherte Prozedur oder Funktion).
Wie MySQL die Berechtigungen überprüft MySQL prüft die Berechtigungen aus den Grant-Tabellen in der Reihenfolge, in der wir sie im vorhergehenden Abschnitt aufgeführt haben. Der Server stoppt das Überprüfen, wenn er einen Treffer findet, der die gewünschte Berechtigung gewährt. Das heißt, falls er einen passenden Eintrag in der db-Tabelle findet, der den gewünschten Zugriff gewährt, schaut er gar nicht in die tables_priv-Tabelle. Abbildung 12-1 verdeutlicht dieses Vorgehen. MySQL stellt fest, welche Berechtigungen gelten, indem es das Äquivalent einer SELECTAnweisung an den im Cache gespeicherten Grant-Tabellen ausführt. Die WHERE-Klausel dieser virtuellen Anweisung enthält die Spalten des Primärschlüssels der einzelnen Tabellen. Einige der Spalten erlauben Mustervergleiche, und viele von ihnen haben »magische« Bedeutungen, wenn sie besondere Werte enthalten, etwa wenn sie leer sind. Näheres erfahren Sie im MySQL-Handbuch. Account-Grundlagen | 573
OK
Benutzer
Abfrage
Nein host
db
OK
Nein tables_priv
OK
Nein columns_priv
OK
Nein Zugriff verweigert
Abfrage ausführen
Abbildung 12-1: Wie MySQL die Berechtigungen überprüft
Sie könnten sich die Zeit nehmen, sich mit den Grant-Tabellen und ihrer Funktionsweise vertraut zu machen, weil dieses Wissen vielleicht einmal ganz praktisch ist. Wir würden Ihnen diesen Rat jedoch nur geben, wenn es absolut notwendig wäre. Lesen Sie stattdessen den nächsten Abschnitt. Es lohnt sich nur dann, tief in das Thema der Grant-Tabellen einzusteigen, wenn Sie in eine Situation kommen, in der Sie mit den GRANT- und REVOKE-Befehlen nichts ausrichten können (oder die zu komplex für diese Befehle ist).
Grants hinzufügen, entfernen und betrachten Die empfohlene Methode zum Hinzufügen von Benutzer-Accounts sowie zum Hinzufügen und Entfernen von Berechtigungen in MySQL verwendet die Befehle GRANT und REVOKE, die im MySQL-Handbuch gut dokumentiert sind. Sie bieten eine einfache Syntax zum Ausführen der meisten Änderungen, ohne dass man die zugrunde liegenden GrantTabellen und ihre verschiedenen Filterregeln verstehen muss. Sie können einen neuen Benutzer-Account oder eine Berechtigung mit GRANT anlegen. REVOKE dagegen kann nur Berechtigungen entfernen, keine Accounts; dazu müssen Sie dann DROP USER verwenden. Mit SHOW GRANTS können Sie die Grants eines Benutzers sehen. Sie erhalten als Ergebnis die Syntax, die erforderlich ist, um den gleichen Account mit seinen aktuellen Berechtigungen anzulegen. Folgendes wird z.B. bei einer Standardinstallation auf einem DebianSystem angezeigt, wenn man sich als root anmeldet: mysql> SHOW GRANTS; +---------------------------------------------------------------------+ | Grants for root@localhost | +---------------------------------------------------------------------+ | GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION | +---------------------------------------------------------------------+
574 | Kapitel 12: Sicherheit
Diese Anweisung zeigt die Grants für den Benutzer, die MySQL standardmäßig ausführt. Man kann damit also leicht feststellen, als wer man angemeldet ist und welche Berechtigungen man gerade hat. Der hier gezeigte Benutzer besitzt alle Berechtigungen, es gibt aber kein Passwort. Das bedeutet, dass sich der Benutzer anmelden kann, ohne ein Passwort anzugeben.2 Das ist sehr unsicher! Sie sollten dies als eines der ersten Dinge überprüfen, wenn Sie eine frische MySQL-Installation aufsetzen. Falls Sie die Grants für einen anderen Benutzer sehen wollen, müssen Sie den Benutzernamen und das Passwort für diesen speziellen Benutzer angeben. So zeigt z.B. dasselbe Debian-System folgende Einträge in der user-Tabelle: +------------------+-----------+ | user | host | +------------------+-----------+ | repl | % | | root | 127.0.0.1 | | root | kanga | | debian-sys-maint | localhost | | root | localhost | +------------------+-----------+
Beachten Sie, dass es drei root-Accounts gibt! Falls Sie die Grants für einen ganz bestimmten Account sehen wollen, müssen Sie sowohl den Benutzer- als auch den Hostnamen angeben. Der vorgegebene Hostname ist %; wenn Sie ihn weglassen, wird daher ein Fehler ausgegeben: mysql> SHOW GRANTS FOR root; ERROR 1141 (42000): There is no such grant defined for user 'root' on host '%'
Falls Sie ein GRANT für einen Benutzer auslösen, ohne einen Hostnamen anzugeben, gewähren Sie im Prinzip eine Berechtigung für Benutzer@'%' (d.h. auf jedem Host). Es gibt nichts, was Sie daran hindert, die Grant-Tabellen direkt mit normalen INSERT-, UPDATE- und DELETE-Abfragen zu manipulieren; wenn Sie sich jedoch an die GRANT- und REVOKE-Befehle halten, werden Sie vor den Änderungen in diesen Tabellen abgeschirmt. Das direkte Ändern der Tabellen kann darüber hinaus leicht zu Fehlern führen. Zum Beispiel hält MySQL Sie nicht davon ab, Daten in die Tabellen zu setzen, die es gar nicht zu interpretieren weiß. Die GRANT- und REVOKE-Befehle bilden die empfohlene Methode zum Verwalten der Rechte und werden es wahrscheinlich auch bleiben. Falls Sie beschließen, die Grant-Tabellen von Hand zu ändern, anstatt die Befehle GRANT und REVOKE zu benutzen, dann müssen Sie dies MySQL mitteilen, indem Sie einen FLUSH PRIVILEGES-Befehl ausführen, der die Accounts und Berechtigungen in den Tabellen wieder holt und in die Caches packt. Alle Änderungen, die Sie an den Grant-Tabellen mit einem INSERT- oder anderen generischen Befehl vornehmen, werden erst dann bemerkt, wenn Sie den Server neu starten oder FLUSH PRIVILEGES aufrufen.
2 Der einzige mildernde Faktor ist die Tatsache, dass dieser Benutzer sich nicht von einem anderen Host aus anmelden kann, aber das ist keine besonders gute Sicherheitsmaßnahme.
Account-Grundlagen | 575
MySQL-Berechtigungen einrichten Wir wollen uns als Beispiel anschauen, wie man die passenden Benutzer-Accounts und Berechtigungen für eine fiktive Organisation namens widgets.example.com einrichtet. Wir nehmen an, dass Sie sich an einer neu installierten MySQL-Instanz anmelden und alle vorgegebenen Accounts mit DROP USER gelöscht haben. Überprüfen Sie die mysql.userTabelle, um sicherzugehen, dass Sie alle erwischt haben. MySQL bietet keine Unterstützung für Rollen oder Gruppen, die Ihnen vielleicht von anderen Datenbankservern vertraut sind. MySQL unterstützt nur Benutzer. Im Prinzip werden Kombinationen aus allen drei Befehlen benutzt: GRANT [Berechtigungen] ON [Objekten] TO [Benutzer]; GRANT [Berechtigungen] ON [Objekten] TO [Benutzer] IDENTIFIED BY [Passwort]; REVOKE [Berechtigungen] ON [Objekten] FROM [Benutzer];
Passwortsicherheit Zu Demonstrationszwecken nutzen wir absichtlich das süße »p4ssword«, das allerdings für echte Installationen nicht geeignet ist. Nur weil MySQL-Passwörter nicht im Klartext gespeichert werden, muss man in Bezug auf die Komplexität der Passwörter nicht sorglos werden. Jeder, der eine Verbindung zu Ihrem MySQL-Server herstellen kann, kann einen Brute-Force-Angriff gegen ihn durchführen, um zu versuchen, die Passwörter aufzudecken. In MySQL gibt es nicht so viele raffinierte Methoden, dies zu erkennen und zu verhindern, wie bei anderen Arten von Passwörtern, wie etwa bei Unix-Passwörtern. MySQL bietet darüber hinaus einem Administrator keine Möglichkeit, gute Passwortstandards zu erzwingen. Sie können MySQL nicht mit libcrack verknüpfen und verlangen, dass Passwörter diesen Kriterien entsprechen, egal, wie cool dieser Gedanke wäre. Es gibt viele gute Werkzeuge und Websites, die Ihnen und Ihren Benutzern beim Generieren starker Passwörter helfen – wir empfehlen Ihnen, darauf zurückzugreifen.
Hier ist eine Übersicht über die Account-Arten, die Sie vermutlich anlegen müssen, und die Berechtigungen, die Sie ihnen jeweils zuweisen sollten: Systemadministrator-Account In den meisten großen Organisationen haben Sie zwei wichtige Administratorrollen. Die Systemadministratoren verwalten den »physischen« Server einschließlich des Betriebssystems, der Unix-Login-Accounts usw., und die Datenbankadministratoren konzentrieren sich auf den Datenbankserver. Wie Sie die AdministratorenAccounts belegen, ist Ihnen überlassen – Sie könnten es sich leicht machen und jeden, der administrative Aufgaben ausführen muss, bitten, sich als Superuser an MySQL anzumelden. Oder Sie legen für jede Person, die administrativen Zugriff benötigt, einen separaten Account an. Wir machen es uns zunächst einfach und erzeugen einen superprivilegierten Benutzer namens root (nach dem traditionellen Unix-Superuser): 576 | Kapitel 12: Sicherheit
mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' -> IDENTIFIED BY 'p4ssword' WITH GRANT OPTION;
Datenbankadministrator-Accounts Wenn mehr als ein Datenbankadministrator Zugriff auf MySQL hat, dann ist es manchmal keine schlechte Idee, jedem einen eigenen Account zuzuweisen, als die Administratoren sich den root-Account teilen zu lassen. Diese Anordnung lässt sich besser absichern und überwachen: mysql> GRANT ALL PRIVILEGES ON *.* TO 'john'@'localhost' -> IDENTIFIED BY 'p4ssword' WITH GRANT OPTION;
Angestellten-Accounts Der durchschnittliche widgets.example.com-Angestellte ist beim Kundendienst dafür verantwortlich, Bestellungen aufzunehmen, die per Telefon aufgegeben werden, vorhandene Bestellungen zu aktualisieren usw. Wir wollen einmal annehmen, dass Tera, eine Kundendienstmitarbeiterin, sich bei einer Anwendung anmeldet, die ihren Benutzernamen und ihr Passwort zum Arbeiten an den MySQL-Server übergibt. Der Befehl zum Anlegen von Teras Account könnte so aussehen: mysql> GRANT INSERT,UPDATE PRIVILEGES ON widgets.orders -> TO 'tera'@'%.widgets.example.com' -> IDENTIFIED BY 'p4ssword';
Tera muss der Anwendung ihren Benutzernamen und ihr Passwort übergeben. Die Anwendung erlaubt es ihr dann, neue Bestellungen hinzuzufügen oder vorhandene Bestellungen zu aktualisieren. Sie kann jedoch nicht zurückgehen und Einträge löschen usw. In dieser Konfiguration hat jeder Angestellte von widgets.example. com, der eine Bestellung in das System eingeben muss, seinen eigenen individuellen Datenbankzugriff. Anstelle eines gemeinsam genutzten »Anwendungs-Accounts« werden alle Transaktionen eines Angestellten unter seinem eigenen Benutzernamen aufgezeichnet, und jeder Angestellte besitzt seine eigenen Berechtigungen, die er braucht, um Bestellungen einzugeben oder zu bearbeiten. Simulierte Gruppen MySQL bietet keine Funktionalität für Benutzergruppen oder Rollen, wie sie auf anderen Datenbankservern genannt werden. Manchmal ist es sinnvoll, einen Account anzulegen, der nach einem bestimmten Angestellten oder einer Anwendungsrolle benannt ist, wie etwa kundendienst oder analyst, was wir aber in diesem Beispiel unterlassen. Logging, nur Schreibzugriff Es ist üblich, MySQL als Backend für das Logging verschiedener Datenarten einzusetzen. Ob Sie nun Apache dazu bringen, jede Anforderung in MySQL aufzuzeichnen, oder ob Sie vermerken, wann es bei Ihnen an der Tür klingelt, das Logging ist eine ausschließlich schreibende Anwendung, die wahrscheinlich auch nur in eine einzige Datenbank oder Tabelle schreiben muss. Sie können mit einem solchen Befehl einen Logging-Account erzeugen: mysql> GRANT INSERT ON logs.* TO 'logger'@'%.widgets.example.com' -> IDENTIFIED BY 'p4ssword';
Account-Grundlagen | 577
Dieser Befehl fügt der user-Tabelle eine Zeile hinzu; da wir aber keine globalen (*.*) Berechtigungen angegeben haben, enthalten alle Berechtigungsspalten in der resultierenden Zeile in user den Wert N. Der einzige Zweck der Zeile besteht darin, dem Benutzer eine Verbindung von jedem Host zu erlauben und sein Passwort abzufragen. Da wir eine Berechtigung festgelegt haben, die für eine spezielle Datenbank gilt, wurden die interessanten Teile der db-Tabelle hinzugefügt, wo die Spalten der resultierenden Zeile alle N enthalten, ausgenommen die Spalte Insert_priv, die Y enthält. Backups Ein Backup-Benutzer, der Backups über mysqldump durchführt, braucht typischerweise nur die Berechtigungen SELECT und LOCK TABLES. Wenn der Benutzer mit der Option --tab für mysqldump oder über SELECT INTO OUTFILE Tab-separierte Dumps durchführt, müssen Sie diesem Benutzer auch die Berechtigung FILE gewähren. Hier ist ein Beispiel-Backup-Benutzer, der nur vom lokalen Host aus eine Verbindung herstellen darf: mysql> GRANT SELECT, LOCK TABLES, FILE ON *.* TO 'backup'@'localhost' -> IDENTIFIED BY 'p4ssword';
Um Konsistenz zu garantieren, benutzen viele Backup-Operationen FLUSH TABLES WITH READ LOCK, was auch noch die Berechtigung RELOAD verlangt. Diese Berechtigung erlaubt auch verschiedene andere gebräuchliche Operationen wie FLUSH LOGS. Operationen und Überwachung Oft wird es vorkommen, dass Sie jemandem oder etwas (z.B. einem Benutzer oder irgendeiner Überwachungssoftware in einem Network Operations Center oder NOC) zum Zwecke der Wartung oder der Fehlerbehebung den Zugriff auf Ihren MySQL-Server gewähren. Dieser Benutzer-Account muss in der Lage sein, eine Verbindung herzustellen, die Befehle KILL und SHOW aufzurufen und den Server herunterzufahren. Da diese Fähigkeit sehr mächtig ist, muss sie auf einen einzigen Host beschränkt werden. Das bedeutet: Selbst wenn ein unautorisierter Benutzer das Passwort kompromittiert, muss er im NOC sein, um irgendetwas tun zu können. Sie erreichen das mit folgender Anweisung: mysql> GRANT PROCESS, SHUTDOWN on *.* -> TO 'noc'@'monitorserver.noc.widgets.example.com' -> IDENTIFIED BY 'p4ssword';
Vielleicht müssen Sie auch noch die Berechtigung SUPER gewähren, die es dem Benutzer erlaubt, SHOW INNODB STATUS auszuführen.
Besondere Änderungen in MySQL 4.1 MySQL 4.1 führte ein neues, viel sichereres Passwort-Hashing-Schema ein. Sie können in neueren Versionen jedoch immer noch das alte Passwort-Hashing-Schema verwenden (selbst in MySQL 5.0 und neueren Versionen). Wir raten davon allerdings ab, weil das alte Passwort-Hashing leicht zu knacken ist. Falls Sie sich Sorgen um die Sicherheit machen, dann benutzen Sie MySQL 4.1 oder neuer, und bleiben Sie beim neuen Schema.
578 | Kapitel 12: Sicherheit
Manche GNU/Linux-Distributionen konfigurieren MySQL standardmäßig so, dass es das alte Passwort-Hashing verwendet, um kompatibel mit älteren Clientprogrammen zu bleiben. Überprüfen Sie Ihre Standardkonfiguration für die Option old_passwords. Falls Sie wollen, dass der MySQL-Server alle Verbindungsversuche mit einem alten, unsicheren Passwort abweist, dann können Sie die Option secure_auth in der Konfigurationsdatei des Servers setzen. Es gibt eine ähnliche Option für Clientprogramme, die verhindert, dass diese versuchen, Passwörter im alten Format an den Server zu senden, obwohl der Server sie darum bittet. Auch hier ist es günstig, diese Option zu aktivieren, da das alte Format leicht ausgespäht und geknackt werden kann. Passwörter im neuen Stil beginnen mit einem Sternchen, so dass sie leicht durch bloßes Hinschauen zu erkennen sind. Meist werden Benutzer-Accounts, die von älteren MySQL-Versionen aufgerüstet wurden, korrekt authentifiziert. Wenn allerdings ein VorMySQL-4.1-Clientprogramm versucht, eine Verbindung zu einem neueren MySQL-Server mit einem Benutzer-Account aufzunehmen, dessen Passwort im neuen Format gespeichert ist, dann kann es diese Verbindung nicht herstellen. Um dieses Problem zu beheben, können Sie entweder das Passwort des Accounts manuell mit OLD_PASSWORD( ) wieder auf das alte Hashing zurücksetzen oder die MySQL-Clientbibliothek des Clientprogramms aufrüsten.
Besondere Änderungen in MySQL 5.0 Seit MySQL 5.0 gibt es einige neue Arten von Berechtigungen sowie leichte Änderungen bei vorhandenen Verhaltensweisen. In diesem Abschnitt erhalten Sie einen Überblick über die Änderungen. Bevor Sie auf eine neue MySQL-Version umsteigen, sollten Sie die Release-Hinweise lesen, um zu erfahren, was neu ist und was sich geändert hat.
Gespeicherte Routinen Wie wir in Kapitel 5 beschrieben haben, gibt es seit MySQL 5.0 Unterstützung für gespeicherte Routinen. Diese können in zwei Sicherheitskontexten ausgeführt werden: als Definierer (d.h. als der Benutzer, der die Routine definiert hat) oder als Aufrufer (d.h. als der Benutzer, der die Routine aufgerufen hat). Gespeicherte Routinen werden gebräuchlicherweise als Proxies verwendet, um bestimmte Rechte Tabellen zu gewähren, bei denen den Benutzern die Rechte nicht direkt zugewiesen wurden. Das übliche Vorgehen besteht darin, dass man einen privilegierten Benutzer anlegt, dann die Routinen mit diesem Benutzer als Definierer erzeugt und ihnen die Eigenschaft SQL SECURITY DEFINER gibt. Tabelle 12-1 verdeutlicht, wie eine gespeicherte Prozedur es Benutzern erlaubt, Anweisungen mit den Berechtigungen eines anderen Benutzers auszuführen.
Account-Grundlagen | 579
Tabelle 12-1: Der Sicherheitskontext für Anweisungen innerhalb einer gespeicherten Prozedur Benutzer, der die Prozedur aufruft
Sicherheitskontext für Anweisungen Mit SQL SECURITY INVOKER
Mit SQL SECURITY DEFINER und DEFINER=LegalStaff
LegalStaff
LegalStaff
LegalStaff
HumanResources
HumanResources
LegalStaff
CustomerService
CustomerService
LegalStaff
Dieser Ansatz erlaubt es Ihnen, den Zugriff auf Tabellen auf der Grundlage des Benutzers zu gewähren oder zu verweigern und gleichzeitig die Berechtigung zu gewähren, bestimmte Aktionen in den Tabellen auszuführen – wobei die Aktionen in der gespeicherten Prozedur eingekapselt sind –, wenn Sie nicht wollen, dass der Benutzer direkt auf die Tabellen zugreift. Nehmen Sie z.B. an, dass private rechtliche Daten in einer Gruppe von Tabellen (wie etwa der Status eines Vertrages mit einem Außenstehenden) nur für die Rechtsabteilung zu sehen sind, allerdings müssen die Kundendienstmitarbeiter in der Lage sein, eine bestimmte Spalte in diesen Tabellen zu aktualisieren. Sie können die Berechtigung SELECT für diese Tabellen für jeden bis auf die Leute aus der Rechtsabteilung verweigern und dann eine gespeicherte Prozedur schreiben, die es jedem erlaubt, die gewünschte Spalte zu aktualisieren, und SQL SECURITY DEFINER einsetzen, um sie mit den Berechtigungen von LegalStaff auszuführen. Dies entspricht in etwa den SUID-Berechtigungen in Unix-artigen Betriebssystemen. Der Namensraum der gespeicherten Routinen geht vom Schema (der Datenbank) aus, Sie können also db1.func_1( ) und db2.func_1( ) haben, ohne dass es zu Namenskonflikten kommt. MySQL prüft die Berechtigungen aller Anweisungen innerhalb der gespeicherten Routine. Die Berechtigung zum Ausführen der Routine bietet keine Blankoautorisierung für die Anweisungen in ihr. Die in ihr enthaltenen Anweisungen werden anhand der Berechtigungen des Definierers oder des Aufrufers geprüft, je nachdem, ob Sie die Routine mit SQL SECURITY DEFINER oder mit SQL SECURITY INVOKER erzeugt haben.
Trigger Seit MySQL 5.0 gibt es außerdem Unterstützung für Trigger, die besondere Berechtigungen für die Ausführung verlangen, wenn sie nicht mit der Eigenschaft SQL SECURITY DEFINER definiert wurden. Dass kann verwirrende Effekte haben, wie etwa die folgende Fehlermeldung, wenn Sie versuchen, UPDATE-, INSERT- oder DELETE-Abfragen in einer Tabelle auszuführen: mysql> INSERT INTO ...; ERROR 1142 (42000): Access denied; you need the SUPER privilege for this operation
Wenn der Trigger nicht mit der Eigenschaft SQL SECURITY DEFINER erzeugt wurde, muss der Benutzer, der in die Tabelle einfügt, die Berechtigung SUPER haben, um den Trigger auszuführen. Deshalb scheint die Fehlermeldung zu besagen, dass die Berechtigung SUPER
580 | Kapitel 12: Sicherheit
erforderlich ist, um in die Tabelle einzufügen. (MySQL 5.1 enthält eine Berechtigung TRIGGER, wodurch diese Fehlermeldung etwas weniger verwirrend sein sollte.) MySQL prüft die Berechtigungen für die Anweisungen innerhalb eines Triggers genau wie bei einer gespeicherten Routine.
Sichten Sie können Sichten genau wie gespeicherte Prozeduren und Trigger mit den Berechtigungen des Definierers oder des Aufrufers ausführen. Definiererberechtigungen erlauben es Ihnen, dem Benutzer den Zugriff auf eine Sicht, nicht jedoch auf die zugrunde liegenden Tabellen zu geben. Damit können Sie Sicherheit auf Zeilenebene implementieren, aber auch den Zugriff auf Spalten beschränken. Wir glauben, dass dies eine bessere Lösung ist, als spaltenbezogene Berechtigungen mit GRANT anzugeben, da es viel einfacher zu verwalten ist. Wenn Sie die Sichten in eine separate Datenbank legen, können Sie Benutzern ganz leicht Berechtigungen auf Datenbankebene gewähren, als die Berechtigungen in den einzelnen Tabellen oder Sichten zu pflegen. Abbildung 12-2 zeigt den Unterschied zwischen dem Gewähren des Zugriffs auf bestimmte Spalten mit GRANT und dem Extrahieren der relevanten Spalten in eine Sicht.
Account-Grundlagen | 581
Berechtigungen in den INFORMATION_SCHEMA-Tabellen Die offiziellen SQL-Standards definieren ein Set aus Sichten, gemeinhin bekannt als INFORMATION_SCHEMA-Tabellen, die Ihnen Informationen über die Datenbanken, Tabellen
und anderen Teile Ihres Datenbankservers liefern. MySQL versucht, diesen Standards so eng wie möglich zu folgen. Daher verwaltet der Server die Berechtigungen für diese Tabellen automatisch, und es ist am besten, Berechtigungen nicht explizit zu definieren. Falls ein Benutzer ohne passende Berechtigungen versucht, auf Zeilen oder Werte in diesen Tabellen zuzugreifen, zeigt MySQL die Zeilen nicht und liefert für die Werte NULL. Beispielsweise ist ein Benutzer nur dann in der Lage, die Tabellen in der INFORMATION_ SCHEMA.TABLES-Sicht zu sehen, wenn er irgendwelche Berechtigungen für diese Tabellen hat. Das ist analog zum Verhalten des MySQL-Befehls SHOW TABLES: MySQL zeigt Tabellen nicht an, für die der Benutzer keine Berechtigungen hat.
Berechtigungen und Leistung Berechtigungen scheinen mit der Leistung nicht viel zu tun zu haben. Dennoch können sie unter bestimmten Umständen Leistungsprobleme hervorrufen. Hier sind einige Dinge, an die Sie denken müssen: Zu viele Berechtigungen Falls Sie in Ihren Grant-Tabellen sehr viele Einträge haben, kann das einen bedeutenden Overhead mit sich bringen. Jede Berechtigung vergrößert die Arbeit, die der Server zu erledigen hat, wenn er prüft, ob ein Benutzer eine Anweisung ausführen kann. Außerdem belegen die Berechtigungen Speicher. Die Berechtigungen sind zu fein aufgelöst Jede Stufe der Berechtigungshierarchie in MySQL (Benutzer, Datenbank, Host, Tabelle und Spalte) verteuert die Überprüfungen der Berechtigungen. Das Prüfen der globalen Berechtigungen geht relativ schnell. Falls Sie dagegen nur eine Spaltenberechtigung definieren, muss der Server potenziell jede Abfrage auf globale, Datenbank-, Tabellen- und Spaltenberechtigungen prüfen. (In »Wie MySQL die Berechtigungen überprüft« auf Seite 573 haben wir ausgeführt, dass der Server auf der höchsten Stufe beginnt und so lange weitermacht, bis er einen Treffer findet, der die notwendige Berechtigung gewährt.) Spaltenberechtigungen und der Abfrage-Cache Momentan können Abfragen, die auf eine Tabelle mit Spaltenberechtigungen zugreifen, nicht aus dem Abfrage-Cache bedient werden. Wir empfehlen Ihnen, Sichten anstelle von Spaltenberechtigungen zu verwenden, wie im vorhergehenden Abschnitt besprochen, um dieses und andere Probleme mit Spaltenberechtigungen zu vermeiden. Standardmäßig führt MySQL sowohl einen Forward- als auch einen Reverse-DNSLookup durch, wenn es Benutzer authentifiziert. Dies wird deaktiviert, wenn Sie skip_name_resolve zu Ihrer my.cnf-Datei hinzufügen. Das kann für Sicherheit und Leis-
582 | Kapitel 12: Sicherheit
tung gut sein, weil es Verbindungen beschleunigt, die Rückgriffe auf die DNS-Server reduziert und die Anfälligkeit für Denial-of-Service-Attacken verringert. Die Nebenwirkung dieser Änderung besteht darin, dass Sie keine Benutzer definieren können, die Hostnamen in der Host-Spalte besitzen. Diese Benutzerdefinitionen funktionieren dann einfach nicht mehr. Stattdessen müssen Sie IP-Adressen verwenden (Wildcards, wie 192 %, sind aber weiterhin möglich). Sie können dennoch den besonderen Wert localhost einsetzen, wenn skip_name_resolve aktiviert ist.
Verbreitete Probleme und Lösungen Da das MySQL-Handbuch die Berechtigungen gründlich behandelt, haben wir entschieden, diesen Abschnitt des Buches auf eine Erläuterung der verbreiteten Anforderungen, Warnungen und unerwarteten Verhaltensweisen zu beschränken, damit Sie ihn als Kurzreferenz oder für die Fehlersuche benutzen können. In den folgenden Abschnitten finden Sie häufig gestellte Fragen, gängige Aufgaben und verzwickte Situationen, die uns begegnet sind.
Fehler beim Verbinden Mailinglisten, Foren und IRC-Kanäle sind voll mit Leuten, die Probleme haben, Verbindungen zu MySQL-Servern herzustellen. Für diese Probleme gibt es Dutzende von Ursachen: von fehlgeschlagenen TCP-Verbindungen, weil skip_networking in my.cnf definiert ist, über bind_address, das auf eine falsche IP-Adresse gesetzt ist, bis hin zu Fehlern mit der GRANT-Anweisung und Firewalls. Wir können hier nicht alle Gründe aufzählen, aber im MySQL-Handbuch ist diesem Thema ein ganzer Abschnitt gewidmet.
Verbindung über localhost oder über 127.0.0.1? Der Hostname localhost ist normalerweise ein Alias für die IP-Adresse 127.0.0.1, allerdings zeigt MySQL ein etwas anderes Standardverhalten. Wenn Sie den Hostnamen localhost als Verbindungsparameter angeben, dann versucht es standardmäßig, sich über ein Unix-Socket3 anstatt über TCP/IP zu verbinden, wie Sie vielleicht erwarten würden. Der folgende Befehl stellt die Verbindung über ein Unix-Socket her: $ mysql --host=localhost
Das ist eine etwas unglückliche Designentscheidung, weil es sich nicht erwartungskonform verhält; allerdings ist es nun zu spät, das zu ändern, weil damit die Kompatibilität mit älteren Anwendungen und Clientbibliotheken nicht mehr gewährleistet wäre. Falls Sie über TCP/IP eine Verbindung auf die Maschine herstellen wollen, auf der Sie arbeiten, haben Sie zwei Möglichkeiten: Angabe einer IP-Adresse anstelle des Hostnamens oder explizite Angabe des Protokolls. Diese beiden Befehle verbinden Sie jeweils über TCP/IP: 3 localhost ist unter Windows nicht speziell, aber . ist es; es bedeutet eine Verbindung über eine benannte Pipe.
Account-Grundlagen | 583
$ mysql --host=127.0.0.1 $ mysql --host=localhost --protocol=tcp
Falls Sie entsprechend versuchen, eine Verbindung zum weitergeleiteten TCP-Port auf localhost herzustellen, wenn Sie einen SSH-Tunnel einrichten, wird auch das nicht funktionieren. Sie müssen TCP verwenden, um sich mit einem Port zu verbinden, und müssen deshalb stattdessen die IP-Adresse 127.0.0.1 benutzen. Wir gehen später auf SSHTunnel ein. Dieser Hostname ist noch auf andere Weise besonders: MySQL versucht nicht, localhost mit einem %-Wildcard zu vergleichen. Mit anderen Worten: Es ist nicht redundant, Berechtigungen für Benutzer@'%' und Benutzer@localhost anzugeben.
Temporäre Tabellen sicher einsetzen MySQL besitzt, abgesehen von der Berechtigung CREATE TEMPORARY TABLE, keine besonderen Berechtigungen für temporäre Tabellen. Sobald eine temporäre Tabelle erzeugt wurde, gelten die normalen tabellenbezogenen Berechtigungen des Benutzers. Das bedeutet, dass ein Benutzer eine temporäre Tabelle anlegen kann, aber dann nicht das Recht besitzt, weitere Spalten hinzuzufügen, die Tabelle zu ändern und Indizes anzulegen (oder etwa mit SELECT auf sie zuzugreifen). Gewährt man jedoch diese Rechte, könnte der Benutzer echte Tabellen beschädigen, was nicht gewünscht ist. Die Lösung besteht darin, diese Berechtigungen zu verbieten, ausgenommen in einer speziellen Datenbank, die für temporäre Tabellen reserviert ist: mysql> CREATE DATABASE temp; mysql> GRANT SELECT, INSERT, UPDATE, DELETE, DROP, ALTER, INDEX, -> CREATE TEMPORARY TABLES ON temp.* TO analyst@'%';
Passwortlosen Zugriff verbieten MySQL erlaubt den passwortlosen Zugriff. Bei einem Account ohne Passwort enthält die Zeile in der user-Tabelle einen leeren String in der password-Spalte. Sie können einen solchen Account mit einer GRANT-Anweisung erzeugen, die keine IDENTIFIED BY-Klausel besitzt. Es ist nicht möglich, den passwortlosen Zugriff in MySQL völlig zu verbieten, aber falls Sie die Kontrolle über die Maschinen haben, von denen aus sich die Benutzer verbinden, können Sie einen Eintrag in den Abschnitt [client] von my.cnf setzen: password
Dieser Eintrag sorgt dafür, dass Programme, die diese Datei standardmäßig lesen (was alle Programme einschließt, die MySQL mitbringt, solange sie nicht anders angewiesen werden), den Benutzer nach einem Passwort fragen. Seit MySQL 5.0 können Sie den SQL-Modus des Servers auf NO_AUTO_CREATE_USER setzen, um zu verhindern, dass GRANT Benutzer ohne Passwort anlegt. Allerdings könnte ein entschlossener Benutzer dies umgehen.
584 | Kapitel 12: Sicherheit
Denken Sie daran, dass ein Benutzer, dessen Passwort in der mysql.user-Tabelle ein leerer String ist, ein Benutzer ohne Passwort ist, und nicht ein Benutzer mit einem leeren Passwort.
Anonyme Benutzer deaktivieren MySQL erlaubt auch anonyme Benutzer: Jeder Eintrag in der Grant-Tabelle, dessen UserSpalte den leeren String enthält, definiert Berechtigungen für anonyme Benutzer. Seien Sie vorsichtig mit solchen Einträgen, da SHOW GRANTS die resultierenden Berechtigungen nicht anzeigt. Wir glauben, dass es am besten ist, diese Einträge zu entfernen. Dazu können Sie das in MySQL enthaltene Programm mysql_secure_installation einsetzen.
Denken Sie daran, Hostnamen separat in Anführungszeichen zu setzen Man vergisst leicht, Benutzernamen und Hostnamen getrennt in Anführungszeichen zu setzen. Der folgende Befehl macht etwas anderes, als man vermuten würde: mysql> GRANT USAGE ON *.* TO 'fred@%';
Es sieht so aus, als würde er einen Account für einen Benutzer namens fred anlegen, der sich von irgendwoher verbinden könnte. Tatsächlich wird jedoch ein Benutzer namens fred@% erzeugt. Die korrekte Syntax sieht so aus (beachten Sie, dass Benutzer und Host getrennt in Anführungszeichen gesetzt werden): mysql> GRANT USAGE ON *.* TO 'fred'@'%';
Verwenden Sie Benutzernamen nicht noch einmal MySQL betrachtet Benutzer mit denselben Benutzernamen, aber unterschiedlichen Hosts als völlig verschiedene Benutzer. Es mag hilfreich sein, dass Sie einem Benutzer völlig unterschiedliche Berechtigungen gewähren können, je nachdem, von wo der Verbindungsversuch stammte, aber unserer Erfahrung nach ist das nur selten eine gute Idee. Das Potenzial für Verwirrung oder Ärger ist viel größer als der Nutzen. Es ist viel einfacher, Benutzernamen so zu behandeln, als ob sie einmalig sein sollten, und mithilfe der Host-Spalte zu beschränken, woher die Benutzer sich verbinden dürfen, anstatt was sie tun können, sobald sie die Verbindung hergestellt haben. Sie könnten z.B. beschließen, dass Sie Verbindungen nur von der lokalen Maschine, nur aus dem lokalen Netzwerk oder nur aus einem bestimmten Teilnetz zulassen wollen. Das ist eine vernünftige Sicherheitsmaßnahme, obwohl eine Firewall eine viel bessere Methode darstellt, um Verbindungsversuche zu beschränken (mehr dazu später). Nur weil MySQL eine Menge Flexibilität zulässt, muss es Ihnen noch nicht das Leben erleichtern. Unserer Meinung nach ist es besser, alles einfach zu halten.
Das Gewähren von SELECT erlaubt SHOW CREATE TABLE Das Gewähren der SELECT-Berechtigung erlaubt es einem Benutzer, SHOW CREATE TABLE auszuführen, was den SQL-Befehl ausgibt, mit dem man eine Tabelle neu erschafft. Norma-
Account-Grundlagen | 585
lerweise ist das schön, aber manchmal treten dabei sensible Details zutage. Den offensichtlichsten Fall gibt es für Federated-Tabellen in MySQL 5.0: Der Benutzer kann den Benutzernamen und das Passwort sehen, mit denen sich die Engine an dem entfernten Server anmeldet. (In MySQL 5.1 gibt es einen getrennten Mechanismus zum Verwalten entfernter Verbindungen für Federated-Tabellen.)
Gewähren Sie keine Berechtigungen in der mysql-Datenbank Falls Sie in der mysql-Datenbank Berechtigungen gewähren, könnte ein Benutzer in die Lage versetzt werden, seine eigenen Berechtigungen zu ändern, die Berechtigungen der anderen Benutzer anzuschauen (wodurch Angriffen mithilfe erratener Passwörter Tür und Tor geöffnet werden) oder sogar die Tabellen umzubenennen oder zu ändern, die MySQL für die Ausführung benötigt. Es besteht überhaupt keine Notwendigkeit, normalen Benutzern irgendeinen Zugriff auf diese Tabellen zu geben – nicht einmal reinen Lesezugriff. Das bedeutet, dass Folgendes eine schlechte Idee ist, weil es global Berechtigungen gewährt: mysql> GRANT ... ON *.* ...;
Wenn ein Benutzer die Berechtigung hat, die Tabellen in der mysql-Datenbank zu modifizieren, dann sollte dieser Benutzer auch die GRANT-Option haben. Ansonsten ist es möglich, dass der Benutzer Berechtigungen verwirft, indem er Zeilen löscht, aber nicht in der Lage ist, sie wieder hinzuzufügen. Einer der Autoren dieses Buches hat einmal auf diese Weise versehentlich jeden Benutzer im System gelöscht. Er musste den MySQL-Server herunterfahren und die Benutzer wiederherstellen, indem er den Server mit der Option --skip_grant_tables startete.
Gewähren Sie nicht freimütig die Berechtigung SUPER Die Berechtigung SUPER erlaubt es einem Benutzer, Superuser-Operationen durchzuführen (wie etwa das Ändern der Daten auf einem Server, der aufgrund seiner Konfiguration schreibgeschützt ist), wie Sie wahrscheinlich vermuten. Er bietet aber noch eine zusätzliche Verhaltensweise: MySQL reserviert eine Verbindung für einen Benutzer mit der Berechtigung SUPER, selbst wenn es seine max_connections-Grenze erreicht hat. Dadurch können Sie selbst dann eine Verbindung zum Server herstellen und ihn administrieren, wenn er keine normalen Clientverbindungen mehr akzeptiert. Vermeiden Sie es nach Möglichkeit, zu vielen Benutzern die Berechtigung SUPER zu gewähren. Das kann allerdings schwierig sein, da sie für verschiedene gebräuchliche Aufgaben benötigt wird (wie etwa für das Aufräumen der Master-Logs).
Gewähren von Berechtigungen in Datenbanken mit Wildcards MySQLs Datenbank-Mustererkennung erlaubt es Ihnen nicht, »alle Datenbanken mit Ausnahme dieser« anzugeben. Das bedeutet, dass es mühsam ist, Berechtigungen für die mysql-Datenbank wegzulassen. Eine gute Namenskonvention kann hier Abhilfe schaffen:
586 | Kapitel 12: Sicherheit
Benennen Sie einfach alle Datenbanken mit einem gemeinsamen Präfix, und gewähren Sie die Berechtigungen für den mit Wildcards angegebenen Datenbanknamen. Zum Beispiel: mysql> GRANT ... ON `analysis%`.* TO 'analyst' ...;
Leider besitzt MySQL keine echten Schemata, die bei diesem Problem helfen könnten. Namenskonventionen können das allerdings ein wenig lindern. Beachten Sie, dass Sie den Datenbanknamen als Identifikator in der GRANT-Anweisung in Backticks setzen müssen. Diese Technik eignet sich auch gut zum Einrichten einer gemeinsam genutzten HostingUmgebung. Die meisten dieser Umgebungen beschränken die Benutzer auf Datenbanken, deren Namen mit ihrem eigenen Benutzernamen und einem Unterstrich beginnen. Der Unterstrich ist ein Wildcard-Muster, das Sie in der GRANT-Anweisung schützen müssen. Sie könnten z.B. einen solchen Befehl verwenden, um einen neuen Hosting-Account für einen Benutzer namens sunny einzurichten: mysql> GRANT ... ON `sunny\_%`.* TO 'sunny' ...;
In einer gemeinsam genutzten Hosting-Umgebung sollten Sie wahrscheinlich die Berechtigung SHOW DATABASES nicht gewähren. Damit stellen Sie sicher, dass die Benutzer keine Datenbanken sehen, auf die sie keinen Zugriff haben – je weniger sie wissen, umso besser.
Bestimmte Berechtigungen zurücknehmen Wenn Sie Berechtigungen global gewähren, können Sie sie nicht nichtglobal zurücknehmen: mysql> GRANT SELECT ON *.* TO 'user'; mysql> SHOW GRANTS FOR user; +-----------------------------------+ | Grants for user@% | +-----------------------------------+ | GRANT SELECT ON *.* TO 'user'@'%' | +-----------------------------------+ mysql> REVOKE SELECT ON sakila.film FROM user; ERROR 1147 (42000): There is no such grant defined for user 'user' on host '%' on table ' film'
Diese Berechtigung wurde global gewährt und kann auch nur global zurückgenommen werden; wenn Sie versuchen, sie auf einer bestimmten Tabelle zurückzunehmen, beschwert sich MySQL, dass es keine tabellenbezogene Berechtigung gibt, die dem angegebenen Kriterium entspricht.
Benutzer können sich sogar nach einem REVOKE verbinden Nehmen Sie an, Sie nehmen für einen Benutzer jede Berechtigung zurück: mysql> REVOKE ALL PRIVILEGES ON...;
Account-Grundlagen | 587
Der Benutzer wird weiterhin in der Lage sein, sich zu verbinden, weil REVOKE die Benutzer-Accounts nicht löscht; es entfernt nur die Berechtigungen. Um den Account vollständig zu entfernen, müssen Sie DROP USER benutzen (oder in alten MySQL-Versionen die Zeile mit DELETE aus der mysql.user-Tabelle löschen). Falls Sie lediglich alle Berechtigungen zurücknehmen, zeigt SHOW GRANTS, dass der Benutzer weiterhin die Berechtigung USAGE besitzt. Sie können diese Berechtigung nicht zurücknehmen, da sie das Synonym für »keine Berechtigungen« ist. Es bedeutet einfach, dass sich der Benutzer mit MySQL verbinden kann.
Wann können Sie keine Berechtigung gewähren oder zurücknehmen? Zusätzlich zur Option GRANT müssen Sie die Berechtigung besitzen, die Sie zu gewähren oder zurückzunehmen versuchen. Diese Vorsichtsmaßnahme verhindert, dass die Benutzer einander Berechtigungen zuschanzen. Falls Sie versuchen, ALL PRIVILEGES zurückzunehmen, müssen Sie die Berechtigung CREATE USER haben.
Unsichtbare Berechtigungen SHOW GRANTS zeigt eigentlich nicht alle Berechtigungen für einen Benutzer: Es zeigt ledig-
lich die Berechtigungen, die diesem Benutzer explizit zugewiesen wurden. Ein Benutzer könnte auch noch andere Berechtigungen haben, vielleicht aufgrund von Berechtigungen, die anonymen Benutzern gewährt wurden. So gewähren z.B. Standard-MySQLInstallationen jedem Benutzer Berechtigungen in der test-Datenbank sowie in Datenbanken, deren Namen mit test_ beginnen! Wir wollen uns ein Beispiel anschauen. Zuerst melden wir uns als root an und erzeugen einen Benutzer ohne Berechtigungen: mysql> GRANT USAGE ON *.* TO 'restricted'@'%' IDENTIFIED BY 'p4ssword'; mysql> SHOW GRANTS FOR restricted; +------------------------------------------------------+ | Grants for restricted@% | +------------------------------------------------------+ | GRANT USAGE ON *.* TO 'restricted'@’%' IDENTIFIED BY | | PASSWORD '*544F2E9C6390E7D5A5E0A508679188BBF7467B57' | +------------------------------------------------------+
Das sieht gut aus; der Benutzer scheint in der Lage zu sein, sich anzumelden und sonst nichts. Das ist jedoch nicht alles. Melden Sie sich einmal als dieser Benutzer an, und führen Sie SHOW DATABASES aus: $ mysql -u restricted -pp4ssword mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | test | +--------------------+
588 | Kapitel 12: Sicherheit
Dieser Server enthält außerdem eine Kopie der Sakila-Beispieldatenbank, die hier nicht aufgeführt wird, weil der Benutzer nicht die SHOW DATABASES-Berechtigung besitzt. Allerdings wird die test-Datenbank aufgeführt. Wie Sie sehen können, besitzt der neue Benutzer Berechtigungen für diese Datenbank und alle Tabellen darin: mysql> USE test; mysql> SHOW TABLES; +----------------+ | Tables_in_test | +----------------+ | heartbeat | +----------------+ mysql> SELECT * FROM heartbeat; +----+---------------------+ | id | ts | +----+---------------------+ | 1 | 2007-10-28 21:31:08 | +----+---------------------+
Der Benutzer-Account kann nicht nur aus diesen Tabellen lesen, er hat sogar die meisten anderen Berechtigungen. Er kann sogar eine neue Datenbank anlegen: mysql> CREATE DATABASE test_muah_ha_ha; Query OK, 1 row affected (0.01 sec)
Der Übeltäter sind zwei Zeilen in der mysql.db-Tabelle: mysql> SELECT * FROM mysql.db\G *************************** 1. row *************************** Host: % Db: test User: Select_priv: Y ... weggelassen ... *************************** 2. row *************************** Host: % Db: test\_% User: Select_priv: Y ... weggelassen ...
Beachten Sie, dass die User-Spalten leer sind, was bedeutet, dass anonyme Benutzer – also im Prinzip alle Benutzer – diese Berechtigungen haben, auch wenn sie nicht in der SHOW GRANTS4-Ausgabe auftauchen. Die Moral von der Geschicht’ ist, dass SHOW GRANTS Ihnen nicht immer alles zeigt. Manchmal müssen Sie trotzdem wissen, wie Sie die GrantTabellen zu lesen und zu interpretieren haben.
4 Dieses Beispiel verdeutlicht einen der »magischen« Werte in den Rechtetabellen von MySQL. Der leere String in der User-Spalte kann einen anonymen Benutzer kennzeichnen. Auf diese Weise authentifiziert MySQL Sie, wenn Sie sich mit einem nichtexistenten Benutzernamen anmelden. Oder es weist darauf hin, dass die Berechtigung für jeden gilt.
Account-Grundlagen | 589
Dies ist jedoch nicht die einzige Gelegenheit, bei der sich die Berechtigungen eines Benutzers seltsam verhalten können. Da bei der Filterung von Hostnamen und Datenbank der speziellste Treffer bevorzugt wird, werden Grants für einen weniger spezifischen Treffer verborgen, obwohl sie möglicherweise freizügiger sind, d.h. mehr erlauben. Schauen Sie sich das folgende Szenario an: Anstatt eine Namenskonvention zu übernehmen, wie wir vorgeschlagen haben, und das Prinzip der geringsten Berechtigungen einzusetzen, geht ein fauler Datenbankadministrator den entgegengesetzten Weg. Er gewährt einem Benutzer alle Berechtigungen und setzt unerwünschte Berechtigungen dann außer Kraft, indem er die Berechtigungen in der mysql-Datenbank mit einer Zeile ausblendet, deren Berechtigungsspalten alle auf N gesetzt sind: mysql> mysql> mysql> mysql>
GRANT USAGE ON *.* TO 'gotcha'@'%' IDENTIFIED BY 'p4ssword'; GRANT ALL PRIVILEGES ON `%`.* TO 'gotcha'@'%'; INSERT INTO mysql.db(Host, DB, User) VALUES('%', 'mysql', 'gotcha'); FLUSH PRIVILEGES;
Da das mysql-Muster spezieller ist als das %-Muster, kommt der faule Datenbankadministrator zu dem Schluss, dass dem Benutzer solche Berechtigungen wie SELECT in der mysqlDatenbank verwehrt sein sollten. Und tatsächlich, das ist der Fall: mysql> SELECT * FROM mysql.user; ERROR 1142 (42000): SELECT command denied to user 'gotcha'@'localhost' for table 'user'
Das Problem besteht darin, dass dieses Rechteschema jedem, der künftig die Berechtigungen für diesen Benutzer ändert, eine Falle stellt. Man kann sehr leicht fälschlicherweise die mysql-spezifischen Berechtigungen entfernen, wodurch die höheren Rechte in dieser Datenbank demaskiert werden. Mit anderen Worten: Das Entfernen von Berechtigungen kann dazu führen, dass tatsächlich mehr Rechte gewährt werden! Dieses Schema ist nicht annähernd so schlau, wie es scheinen mag, im Gegenteil, es ist sogar gefährlich. Es wird auch nicht in SHOW GRANTS gezeigt, kann also leicht übersehen werden. Ähnliche Spielchen mit den gleichen Folgen sind auch mit der Hostnamenfilterung möglich. Falls Sie es z.B. dem Benutzer gotcha erlauben wollen, sich von allen Hostnamen aus bis auf einen bestimmten anzumelden, können Sie kein »Negierter Hostname«-Muster einsetzen, da es so etwas in MySQL nicht gibt. Die einzige Möglichkeit besteht darin, einen Benutzer mit dem gleichen Benutzernamen zu erzeugen, aber den blockierten Hostnamen und ein unechtes Passwort anzugeben: mysql> GRANT USAGE ON *.* TO 'gotcha'@'denied.com' IDENTIFIED BY 'b0gus';
Wenn gotcha nun versucht, sich von diesem Hostnamen aus anzumelden, versucht MySQL die Authentifizierung anhand der Zeile [email protected] in der user-Tabelle und verweigert das Login, weil das Passwort nicht passt. Diese »Lösung« kann sich jedoch als sehr gefährlich herausstellen, falls jemand diesen Eintrag in der Tabelle für einen Fehler hält und entfernt oder Hostnamen-Lookups aus Leistungsgründen deaktiviert oder falls der Benutzer das Reverse-DNS kompromittiert. In jedem dieser Fälle kann sich der Benutzer als gotcha@'%' anmelden.
590 | Kapitel 12: Sicherheit
Wir raten Ihnen, verborgene Berechtigungen und »schlaue Tricks«, wie die gerade gezeigten Schemata, zu vermeiden. Machen Sie sich stattdessen einen vernünftigen Ansatz zu eigen, versuchen Sie nicht, mit Berechtigungen irgendetwas Ausgefallenes anzustellen, wenn es nicht nötig ist, und folgen Sie den optimalen Verfahren, wie dem Prinzip der geringsten Rechte.
Veraltete Berechtigungen MySQL räumt alte Berechtigungen nicht auf, wenn Sie Objekte entfernen. Nehmen wir einmal an, Sie haben Folgendes getan: mysql> GRANT ALL PRIVILEGES ON my_db.* TO analyst;
Später haben Sie diesen Befehl ausgeführt: $ mysqladmin drop my_db
Es wäre schön gewesen, wenn MySQL das GRANT zerstört hätte, aber die Berechtigungen verbleiben in Wirklichkeit in der db-Tabelle. Falls Sie später eine weitere Datenbank mit demselben Namen erzeugen, bestehen die Berechtigungen immer noch. Das könnte Probleme verursachen, da Sie sich vielleicht nicht einmal mehr daran erinnern, dass der analyst-Account jemals irgendwelche Berechtigungen hatte. Seit MySQL 5.0 unterstützen die INFORMATION_SCHEMA-Tabellen Sie dabei, veraltete Berechtigungen zu finden. Zum Beispiel können Sie mit einer Ausschluss-Join-Abfrage Berechtigungen suchen, die auf nichtexistierende Datenbanken verweisen: mysql> SELECT d.Host, d.Db, d.User -> FROM mysql.db AS d -> LEFT OUTER JOIN INFORMATION_SCHEMA.SCHEMATA AS s -> ON s.SCHEMA_NAME LIKE d.Db -> WHERE s.SCHEMA_NAME IS NULL; +------+---------+------+ | Host | Db | User | +------+---------+------+ | % | test\_% | | +------+---------+------+
Ähnliche Abfragen können Sie für jede der anderen Tabellen in INFORMATION_SCHEMA schreiben. In früheren MySQL-Versionen müssen Sie manuell nach veralteten Berechtigungen suchen oder ein Skript schreiben, das das für Sie erledigt. MySQL erlaubt es Ihnen, datenbankbezogene Berechtigungen für Datenbanken zu erzeugen, die gar nicht existieren, nicht jedoch, tabellenbezogene Berechtigungen für nichtexistierende Tabellen anzulegen. Falls Sie das tun müssen, müssen Sie direkt in mysql. tables_priv Zeilen einfügen.
Account-Grundlagen | 591
Betriebssystemsicherheit Auch die tollsten und sichersten Grant-Tabellen nützen Ihnen gar nichts, wenn ein Angreifer root-Zugang zu Ihrem Server bekommen kann. Bei unbegrenztem Zugriff könnte der Benutzer einfach alle Ihre Datendateien auf eine andere Maschine kopieren, auf der MySQL läuft.5 Dadurch würde der Angreifer im Prinzip eine identische Kopie Ihrer Datenbank erhalten. Datenklau ist allerdings nicht die einzige Bedrohung, vor der Sie sich schützen müssen. Ein kreativer Angreifer könnte es für viel lustiger halten, im Laufe von Wochen oder gar Monaten subtile Änderungen an Ihren Daten vorzunehmen. Je nachdem, wie lange Sie Backups aufheben und wie viel Zeit vergeht, bis Sie den Schaden bemerken, könnte ein solcher Angriff verheerende Folgen haben.
Richtlinien Die hier vorgestellten Richtlinien bilden keinen allumfassenden Führer zu mehr Systemsicherheit. Falls es Ihnen mit der Sicherheit ernst ist – und das sollte es –, lesen Sie ein gutes Buch zum Thema. Nichtsdestoweniger sind hier einige Anregungen, wie Sie auf Ihren Datenbankservern für Sicherheit sorgen können: Führen Sie MySQL nicht auf einem privilegierten Account aus Der root-Benutzer unter Unix und der Systembenutzer (Administrator) unter Windows besitzen die absolute Kontrolle über das System. Wenn jemand eine Sicherheitslücke in MySQL findet und Sie es als privilegierter Benutzer ausführen, kann der Angreifer umfassenden Zugriff auf Ihren Server erlangen. Die Installationsanweisungen sind diesbezüglich ziemlich deutlich, wir wollen sie aber noch einmal wiederholen: Legen Sie einen separaten Account an, üblicherweise mysql, um MySQL auszuführen. Halten Sie Ihr Betriebssystem auf dem neuesten Stand Alle Betriebssystemhersteller (Microsoft, Sun, Red Hat, Novell usw.) geben bekannt, wenn sicherheitsrelevante Updates zur Verfügung stehen. Abonnieren Sie die entsprechende Mailingliste Ihres Herstellers. Achten Sie besonders auf die Sicherheitsliste für MySQL selbst sowie auf alles, was direkt mit der Datenbank zusammenarbeitet, wie etwa PHP oder Perl. Beschränken Sie Logins auf dem Datenbank-Host Benötigt jeder Entwickler, der eine MySQL-basierte Anwendung herstellt, einen Account auf dem Server? Sicherlich nicht; nur System- und Datenbankadministratoren brauchen Accounts auf der Maschine. Für die Entwickler reicht es, wenn sie über TCP/IP Abfragen auf der Datenbank ausführen können.
5 Denken Sie daran: MyISAM-Datendateien sind über Betriebssysteme und CPU-Architekturen hinweg portabel (vorausgesetzt, das Fließkommaformat der CPU ist ebenfalls gleich).
592 | Kapitel 12: Sicherheit
Trennen Sie die Produktion von allem anderen Trennen Sie Ihre Produktionsumgebung von Ihren Entwicklungs- und Testumgebungen. Am besten verwenden Sie physisch völlig unterschiedliche Server. Die Sicherheits- und Zugriffsanforderungen für einen Produktionsserver unterscheiden sich vollkommen von denen eines Entwicklungsservers, weshalb es sinnvoll ist, sie physisch zu trennen. Dadurch vermeiden Sie auch Fehler und vereinfachen die Verwaltung und Pflege. Dazu ist es nötig, dass Sie gleich von Anfang an passende Vorgehensweisen und Werkzeuge einsetzen, etwa zum Übertragen der Daten zwischen den Servern. Überprüfen Sie Ihren Server Viele größere Einrichtungen haben interne Prüfer, die die Sicherheit eines Servers abschätzen können und Vorschläge für deren Verbesserung vorlegen. Falls Sie nicht das Glück haben, solche Prüfer zur Verfügung zu haben, können Sie für das Durchführen der Überprüfung einen Sicherheitsexperten anheuern. Setzen Sie die stärksten verfügbaren Mittel ein Mithilfe von Techniken wie chroot, Jails, Zonen oder virtuellen Servern können Sie MySQL noch weiter isolieren. Eine weitere wichtige Sicherheitsmaßnahme ist das Aufbewahren Ihrer Backups auf einem anderen Server. Falls jemand in Ihren Server einbricht, müssen Sie das Betriebssystem von einer fehlerlosen Quelle aus neu installieren. Anschließend sehen Sie sich der Aufgabe gegenüber, alle Daten wiederherzustellen. Wenn Sie Zeit haben, werden Sie den kompromittierten Server mit einem guten Backup vergleichen wollen, damit Sie feststellen können, wie der Angreifer sich Zutritt verschafft hat.
Netzwerksicherheit Es ist immer am besten, Ihre Server zu isolieren und sie unerreichbar zu machen. Aber vielleicht brauchen Sie ja einen MySQL-Server, auf den Clients zugreifen können, die sich nicht auf demselben Host befinden. Wir schauen uns verschiedene Techniken an, mit denen Sie den Gefährdungsgrad eines solchen Servers verringern. Selbst wenn Sie Ihren Server nur in einem internen Netzwerk in Ihrer Einrichtung verwenden, sollten Sie Schritte unternehmen, um die Daten vor allzu neugierigen Augen zu schützen. Schließlich stammen einige der ernstesten Bedrohungen für die Sicherheit aus dem Inneren eines Unternehmens. Denken Sie daran, dass diese Informationen nur den Ausgangspunkt für den gesamten Prozess der umfassenden Absicherung Ihrer MySQL-Server bilden. Falls es Ihnen mit der Netzwerksicherheit ernst ist, dann tun Sie sich den Gefallen, und besorgen Sie sich ein Buch zum Thema (wenn Sie mit diesem hier fertig sind!). Genau wie bei der Betriebssystemsicherheit kann es hilfreich sein, Ihr Netzwerk von externer Seite überprüfen zu lassen, um Schwachstellen zu entdecken, bevor es irgendein Angreifer tut.
Netzwerksicherheit | 593
Auf den Localhost beschränkte Verbindungen Falls Sie MySQL in einer Anwendung einsetzen, die sich auf demselben Host befindet (wie es bei kleinen und mittelgroßen Websites üblich ist), besteht eine hohe Wahrscheinlichkeit, dass Sie den Zugriff auf MySQL über das Netzwerk gar nicht erlauben müssen. Wenn Sie externe Verbindungen nicht akzeptieren müssen, verringern Sie die Anzahl der Wege, über die ein Angreifer Zugriff auf Ihren MySQL-Server bekommen kann. Durch das Deaktivieren des Netzwerkzugriffs beschränken Sie Ihre Möglichkeiten, von außen administrative Änderungen vorzunehmen (Benutzer hinzuzufügen, Logs zu rotieren usw. ), so dass Sie sich entweder über SSH am MySQL-Server anmelden oder eine webbasierte Anwendung installieren müssen, die Ihnen diese Änderungen erlaubt. Die Notwendigkeit für entfernte Logins kann auf einigen Windows-Systemen schwierig sein, es gibt dafür jedoch Alternativen. Eine Lösung für dieses Problem könnte darin bestehen, phpMyAdmin zu installieren. Aber Achtung, auch dies ist nicht frei von Sicherheitsschwachstellen! Die Option skip_networking weist MySQL an, nicht an den TCP-Sockets zu lauschen, erlaubt aber weiterhin Verbindungen über ein Unix-Socket. Es ist leicht, MySQL ohne Netzwerkunterstützung zu starten. Setzen Sie einfach die folgende Option in den [mysqld]-Abschnitt Ihrer my.cnf-Datei: [mysqld] skip_networking
Die Option skip_networking hat einige unangenehme Nebenwirkungen: Sie verhindert den Einsatz von Werkzeugen wie stunnel für sichere Fernanbindungen und Replikation und verhindert die Verbindung von Java-Anwendungen (Connector/J verbindet sich nur über TCP/IP). Alternativ können Sie MySQL so konfigurieren: [mysqld] bind_address=127.0.0.1
Dies aktiviert TCP-Verbindungen allerdings nur von der lokalen Maschine, es ist also sowohl sicher als auch bequem. Einige beliebte GNU/Linux-Distributionen sind standardmäßig auf diese Konfiguration umgestiegen. Ein MySQL-Slave-Server, der mit skip_networking konfiguriert ist, stellt eine interessante Konfiguration dar. Da er seine Verbindungen zum Master initiiert, bekommt der Slave weiterhin alle Daten-Updates; weil aber keine TCP-Verbindungen erlaubt sind, erhalten Sie eine sicherere »Backup-Replik«, die von außen nicht verfälscht werden kann. Für eine Failover-Konfiguration ist ein solcher Slave jedoch ungeeignet, da kein anderer Client sich mit ihm verbinden könnte.
Firewall-Betrieb Es ist wie bei jedem anderen netzwerkbasierten Dienst wichtig, dass Sie Verbindungen nur von autorisierten Hosts erlauben. Mit dem MySQL-Befehl GRANT können Sie die Hosts einschränken, von denen aus sich ein Benutzer anmelden darf. Allerdings ist es
594 | Kapitel 12: Sicherheit
keine schlechte Idee, wenn man mehr als eine Schutzschicht hat. Wenn es mehrere Möglichkeiten gibt, Verbindungen zu filtern, dann bedeutet dies, dass ein einzelner Fehler, wie etwa ein Tippfehler in einem GRANT-Befehl nicht gleich Verbindungen von unautorisierten Hosts erlaubt. Die Verwendung einer Firewall zum Filtern von Verbindungen auf der Netzwerkebene bietet Ihnen zusätzliche Sicherheit.6 In vielen Einrichtungen wird die Netzwerksicherheit von anderen Leuten administriert als von denen, die die Anwendungen entwickeln. Dadurch reduziert sich das Risiko weiter, dass die Änderung einer einzigen Person einen Server bloßstellen kann. Der sicherste Ansatz beim Schutz einer Maschine durch eine Firewall besteht darin, alle Verbindungen standardmäßig zu verbieten. Anschließend fügen Sie Regeln hinzu, um auf die paar Dienste zuzugreifen, auf die andere Hosts möglicherweise zugreifen müssen. Bei einem System, das lediglich einen MySQL-Server bereitstellen soll, sollten Sie nur Verbindungen an den TCP-Port 3306 (MySQLs Standardport) und vielleicht an einen RemoteLogin-Dienst wie SSH (meist an TCP-Port 22) erlauben.
Keine Standardroute Stellen Sie auf Ihren mit einer Firewall versehenen MySQL-Servern besser keine Standardroute ein. Selbst wenn die Firewall-Konfiguration kompromittiert wird und jemand versucht, Ihren MySQL-Server von außen zu kontaktieren, gehen die Pakete in diesem Fall nicht wieder an diese Person zurück. Sie verlassen nämlich niemals Ihr lokales Netzwerk. Nehmen wir an, Ihr MySQL-Server hat die Adresse 192.168.0.10 und das lokale Netzwerk besitzt die Netzmaske 255.255.255.0. In dieser Konfiguration wird jedes Paket von 192.168.0.0/24 als »lokal« betrachtet, weil es direkt über die angeschlossene Netzwerkschnittstelle (wahrscheinlich eth0 oder das Äquivalent des Host-Betriebssystems) erreicht werden kann. Verkehr von einer anderen Adresse muss an ein Gateway geleitet werden, um sein endgültiges Ziel zu erreichen. Und da es keine Standardroute gibt, gibt es für diese Pakete keine Möglichkeit, ihr Gateway zu finden und an ihr Ziel zu gelangen. Falls Sie es einigen ausgewählten externen Hosts erlauben müssen, Ihren hinter der Firewall liegenden Server zu erreichen, dann legen Sie für diese Hosts statische Routen fest. Damit stellen Sie sicher, dass der Server so wenigen externen Hosts wie möglich antwortet. Allerdings ist der Ansatz mit der nichtkonfigurierten Standardroute nicht narrensicher und schützt Sie eher vor Fehlern bei der Firewall-Konfiguration als vor einer völligen Kompromittierung. Jedoch macht Kleinvieh bekanntlich auch Mist.
6 Für unsere Zwecke ist eine Firewall einfach ein Gerät, das der Netzwerkverkehr zum Filtern und möglicherweise für das Routing durchläuft. Es ist egal, ob es eine »echte« Firewall, ein Router oder ein alter 486er ist.
Netzwerksicherheit | 595
MySQL in einer DMZ Für viele Installationen reicht eine einfache Firewall für MySQL-Server nicht aus. Falls einer Ihrer Web- oder Anwendungsserver kompromittiert wird, könnte ein Angreifer diesen Server benutzen, um einen MySQL-Server direkt anzugreifen. Sobald der Angreifer Zugriff auf einen einzigen Computer innerhalb der Firewall hat, sind alle anderen Server in diesem Netzwerk mit meist relativ wenigen Einschränkungen erreichbar.7 Durch das Verschieben der MySQL-Server in ein eigenes Netzwerksegment, das von außen nicht erreichbar ist, kann sich die Sicherheit verbessern. Stellen Sie sich z.B. ein LAN vor, das den Webserver oder andere Anwendungsserver und eine Firewall enthält. Hinter der Firewall, in einem anderen physischen Netzwerksegment und einem anderen logischen Subnetz, befinden sich ein oder mehrere MySQL-Server. Die Anwendungsserver haben eingeschränkten Zugriff auf die MySQL-Server: Ihr gesamter Verkehr muss erst die Firewall passieren, die Sie sehr restriktiv konfigurieren können. Wenn sich jemand Zugriff auf den Anwendungsserver verschafft, die Firewall aber Verkehr nur an Port 3306 auf den MySQL-Servern erlaubt, kann dieser Eindringling keinen Angriff auf andere Dienste starten, die vielleicht auf dem MySQL-Server laufen, wie etwa SSH. Sie könnten die Anwendungsserver sogar in die DMZ (Demilitarized Zone; Entmilitarisierte Zone) oder in ihre eigene, getrennte DMZ setzen. Geht das zu weit? Vielleicht. Wie immer in Sicherheitsfragen müssen Sie Sicherheitsmaßnahmen und Bequemlichkeit abwägen; Sie sollten sich dabei jedoch der jeweiligen Risiken bewusst sein.
Verbindungsverschlüsselung und Tunnel Immer wenn Sie mit einem MySQL-Server über ein Netzwerk kommunizieren müssen, das öffentlich ist (wie das Internet) oder anderweitig das Ausschnüffeln des Verkehrs erlaubt (wie in vielen drahtlosen Netzwerken), sollten Sie über irgendeine Art von Verschlüsselung nachdenken. Auf diese Weise erschweren Sie es anderen, die Verbindung abzuhören und die Daten auszuspähen oder zu fälschen. Als zusätzliches Plus ergeben viele Verschlüsselungsalgorithmen einen komprimierten Datenstrom. Ihre Daten sind also nicht nur sicherer, sondern Sie nutzen die verfügbare Netzwerkbandbreite auch noch besser. Unsere Diskussion konzentriert sich zwar auf einen Client, der auf einen MySQL-Server zugreift, dieser Client könnte aber auch ein anderer MySQL-Server sein. Das ist üblich, wenn Sie die in MySQL integrierte Replikation nutzen: Alle Slave-Server melden sich mit dem gleichen Protokoll beim Master an wie normale MySQL-Clients.
7 Das stimmt nicht ganz. Viele moderne Netzwerk-Switches erlauben es Ihnen, mehrere virtuelle LANs (VLANs) in einem einzigen physischen Netzwerk zu konfigurieren. Maschinen, die sich nicht im selben VLAN befinden, dürfen nicht miteinander kommunizieren.
596 | Kapitel 12: Sicherheit
Virtuelle private Netzwerke Ein Unternehmen mit zwei oder mehr Büros an verschiedenen Orten könnte mithilfe verschiedener Techniken ein virtuelles privates Netzwerk (VPN) zwischen ihnen einrichten. Eine gebräuchliche Lösung sieht vor, dass die externen Router in den einzelnen Büros den gesamten Verkehr verschlüsseln, der für ein anderes Büro gedacht ist. In einer solchen Situation muss man sich kaum Sorgen machen. Der ganze Verkehr ist schon verschlüsselt, wenn er über das Netzwerk – ob öffentlich oder privat – verschickt wird, das die Büros verbindet. Schließt das VPN die Notwendigkeit einer MySQL-spezifischen Lösung aus? Nicht unbedingt. Falls das VPN aus irgendeinem Grund deaktiviert werden muss, wäre es schön, wenn der Netzwerkverkehr von MySQL geheim bliebe. Das Problem sollte man dadurch lösen können, dass man MySQL so konfiguriert, dass es nur Verbindungen von den VPN-IP-Adressen zulässt: Wenn das VPN deaktiviert wird, ist der MySQL-Server nicht erreichbar.
SSL in MySQL Seit Version 4.1 bietet MySQL native Unterstützung für SSL (Secure Sockets Layer) – die gleiche Technik, die auch Ihre Kreditkarten sichert, wenn Sie Bücher bei Amazon kaufen oder online einen Flug buchen. MySQL benutzt speziell die frei verfügbare yaSSL-Bibliothek (oder OpenSSL in älteren Ausgaben). In manchen Binärversionen von MySQL ist SSL nicht standardmäßig aktiviert. Um Ihren Server zu überprüfen, schauen Sie sich einfach die Variable have_openssl an: mysql> SHOW VARIABLES LIKE 'have_openssl'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | have_openssl | NO | +---------------+-------+
Wenn sie NO sagt, müssen Sie Ihren eigenen MySQL-Server kompilieren oder sich eine andere Version besorgen. Sagt sie YES, eröffnen sich dem Datenbankadministrator ganz neue Ebenen der Datenbankzugriffssicherheit. Wie Sie diese benutzen, hängt von den Sicherheitsanforderungen der jeweiligen Anwendung ab.f Im einfachsten Fall wollen Sie nur verschlüsselte Sitzungen erlauben, wobei Sie sich darauf verlassen, dass das SSL-Protokoll das Passwort des Benutzers schützt. Sie können verlangen, dass ein Benutzer sich über SSL anmeldet, indem Sie dem GRANT-Befehl optionale Argumente übergeben: mysql> GRANT ... IDENTIFIED BY 'p4ssword' REQUIRE SSL;
Dieses GRANT erlegt jedoch dem SSL-Zertifikat, das der verbindende Client benutzt, keine Einschränkungen auf. Solange der Client und der MySQL-Server eine SSL-Sitzung aushandeln können, prüft MySQL die Validität des Clientzertifikats nicht.
Netzwerksicherheit | 597
Mit der Option REQUIRE x509 fordern Sie eine minimale Prüfung des Clientzertifikats: mysql> GRANT ... IDENTIFIED BY 'p4ssword' REQUIRE x509;
Dies verlangt, dass das Clientzertifikat wenigstens anhand der CA-Zertifikate verifizierbar ist, die der MySQL-Server erkennt. Ein nächster Schritt würde darin bestehen, nur einem bestimmten Clientzertifikat den Zugriff auf die Datenbank zu erlauben. Dies erreichen Sie mit der REQUIRE SUBJECT-Syntax: mysql> GRANT ... IDENTIFIED BY 'p4ssword' -> REQUIRE SUBJECT "/C=US/ST=New York/L=Albany/O=Widgets Inc./CN=client-ray.example.com/[email protected]";
Vielleicht ist es Ihnen egal, welche Clientlizenz verwendet wird, solange sie mit dem CAZertifikat ausgegeben wurde. In diesem Fall könnten Sie die REQUIRE ISSUER-Syntax verwenden: mysql> GRANT ... IDENTIFIED BY 'p4ssword' -> REQUIRE ISSUER "/C=US/ST=New+20York/L=Albany/O=Widgets Inc./CN=cacert.example.com/[email protected]";
Für die ultimative Authentifizierung kombinieren Sie diese beiden Klauseln und verlangen, dass sowohl der Aussteller als auch das Subject vordefinierte Werte haben. Sie können z.B. verlangen, dass Raymond das spezielle Zertifikat verwendet, das mit dem CAZertifikat Ihrer Organisation erteilt wurde: mysql> GRANT ... IDENTIFIED BY 'p4ssword' -> REQUIRE SUBJECT "/C=US/ST=New York/L=Albany/O=Widgets Inc./CN=client-ray.example.com/[email protected]" -> AND ISSUER "/C=US/ST=New+20York/L=Albany/O=Widgets Inc./CN=cacert.example.com/ [email protected]";
Eine weitere kleine SSL-bezogene Option ist CIPHER, die es dem Administrator erlaubt, nur »vertrauenswürdige« (starke) Verschlüsselungschiffres zuzulassen. SSL ist chiffreunabhängig, die potenziell starke SSL-Verschlüsselung kann unterlaufen werden, wenn eine schwache Chiffre zum Schutz der zu übertragenden Daten eingesetzt wird. Sie können die Wahl der Protokolle auf eine Gruppe beschränken, die Sie als sicher betrachten, indem Sie einen solchen Befehl ausführen: mysql> GRANT ... IDENTIFIED BY 'p4ssword' -> REQUIRE CIPHER "EDH-RSA-DES-CBC3-SHA";
Das Verwalten einzelner Clientzertifikate mag wie ausgezeichnete Sicherheit aussehen, kann aber aus administrativer Sicht ein Albtraum sein. Wenn Sie ein Clientzertifikat erzeugen, müssen Sie ihm ein Verfallsdatum zuweisen – vorzugsweise eines, das nicht zu weit in der Zukunft liegt. Sein Leben soll lang genug sein, um nicht ständig wieder ein neues Zertifikat herstellen zu müssen, aber kurz genug, damit eine bösartige Person, die das Zertifikat in die Hände bekommt, nicht allzu lange Zugriff auf Ihre Daten hat. In einer kleinen Umgebung mit nur wenigen Angestellten mag es einfach sein, die jeweiligen Eigentümer eines Zertifikats im Auge zu behalten. Wenn Ihre Einrichtung jedoch auf
598 | Kapitel 12: Sicherheit
Hunderte oder Tausende von Angestellten anwächst, dann kann es recht mühselig werden, den Überblick darüber zu behalten, welche Zertifikate wann verfallen, und sicherzustellen, dass Clientzertifikate erst dann verfallen, wenn sie ersetzt worden sind. Manche Organisationen lösen dieses Problem mit einer Kombination aus REQUIRE ISSUER und einer Reihe monatlicher Clientzertifikate, die sie über einen vertrauenswürdigen Distributionsweg verteilen, wie etwa das Unternehmensintranet. Clients können sich mit Zertifikaten, die für einen oder zwei Monate gelten, mit dem MySQL-Server verbinden. Falls ein Angestellter den Zugriff zum Unternehmensintranet verliert oder ein Partner keinen Zugriff mehr auf den monatlichen Schlüssel bekommt, dann verfällt die Fähigkeit zum Anmelden zu einem vordefinierten Zeitpunkt, auch wenn der Administrator nicht extra angewiesen wird, die Zugriffsmöglichkeit dieses Benutzers zu entfernen.
SSH-Tunnel Falls Sie eine ältere Version von MySQL benutzen oder sich einfach nicht den Stress machen wollen, SSL-Unterstützung einzurichten, dann sollten Sie stattdessen über SSH nachdenken. Unter Linux oder Unix verwenden Sie es wahrscheinlich sogar bereits, um sich an entfernten Maschinen anzumelden.8 Viele Leute wissen jedoch nicht, dass man SSH benutzen kann, um einen verschlüsselten Tunnel zwischen zwei Hosts aufzubauen. Der Betrieb eines SSH-Tunnels wird am besten mit einem Beispiel verdeutlicht. Nehmen wir an, Sie wollen eine verschlüsselte Verbindung von einer GNU/Linux-Workstation zum MySQL-Server herstellen, der auf db.example.com läuft. Auf der Workstation führen Sie folgenden Befehl aus:9 $ ssh -N -f -L 4406:db.example.com:3306
Dadurch wird ein Tunnel zwischen dem TCP-Port 4406 auf der Workstation und dem Port 3306 auf db.example.com aufgebaut. Sie können sich jetzt von der Workstation aus durch den Tunnel direkt an MySQL anmelden: $ mysql -h 127.0.0.1 -P 4406
SSH ist sehr leistungsfähig und kann viel mehr, als dieses einfache Beispiel zeigt. Stunnel ist ein weiteres Werkzeug zum Anlegen sicherer Tunnel, allerdings ohne Login/ShellKomponente. Manchmal stellt es auch einen guten Ersatz für ein VPN dar.
TCP-Wrapper Sie können MySQL auf Unix-Systemen mit Unterstützung für TCP-Wrapper kompilieren. Wenn eine ausgewachsene Firewall keine Option darstellt, bieten TCP-Wrapper
8 Es gibt eine Variante von OpenSSH für Windows-Clients, und auch Putty ist beliebt (http://www.chiark. greenend.org.uk/~sgtatham/putty/). Sie finden eine komplette Anleitung zum Einrichten von SSH-Tunneln für die Verbindung mit MySQL-Maschinen unter http://www.vbmysql.com/articles/security/sshtunnel.html. 9 Vorausgesetzt, SSH Version 2 ist installiert. SSH Version 1 hat keine Option -N. Näheres erfahren Sie in der SSH-Dokumentation.
Netzwerksicherheit | 599
wenigstens einen gewissen grundlegenden Schutz: Sie können kontrollieren, über welche Hosts MySQL kommuniziert oder nicht kommuniziert, ohne dass Sie Ihre Grant-Tabellen ändern müssen. Manche Betriebssysteme, wie Debian GNU/Linux, kompilieren MySQL standardmäßig auf diese Weise. Um TCP-Wrapper zu benutzen, müssen Sie MySQL aus den Quellen zusammenbauen und die Option --with-libwrap an configure übergeben, damit es weiß, wo es die passenden Header-Dateien auf Ihrem Betriebssystem findet: $ ./configure --with-libwrap=/usr/local/tcp_wrappers
Angenommen, Sie haben einen Eintrag in Ihrer /etc/hosts.deny-Datei, der alle Verbindungen standardmäßig verbietet: # deny all connections ALL: ALL
Dann können Sie explizit MySQL zu Ihrer /etc/hosts.allow-Datei hinzufügen: # allow mysql connections from hosts on the local network mysqld: 192.168.1.0/255.255.0.0 : allow
Der einzige andere Haken ist, dass Sie für MySQL einen passenden Eintrag in /etc/services brauchen. Falls Sie noch keinen haben, fügen Sie eine solche Zeile hinzu: mysql
3306/tcp
# MySQL Server
Sollten Sie MySQL auf einem nichtstandardisierten Port ausführen, dann verwenden Sie dessen Nummer anstelle von 3306. TCP-Wrapper bringen einen gewissen Overhead mit sich, wie etwa Reverse-DNSLookups. Dies erzeugt eine Abhängigkeit vom DNS-Subsystem, die Sie sicher nicht wollen.
Automatische Host-Blockade MySQL hilft Ihnen, netzwerkbasierte Angriffe zu vermeiden: Wenn es zu viele schlechte Verbindungen von einem bestimmten Host bemerkt, dann beginnt es, Verbindungen von diesem Host zu blockieren. Die Servervariable max_connection_errors gibt an, wie viele schlechte Verbindungen MySQL erlaubt, bevor es mit der Blockade beginnt. Eine »schlechte Verbindung« ist jede Verbindung, die nicht abgeschlossen wird (d.h., die in einer gültigen MySQL-Sitzung resultiert). Oft sind falsche Passwörter die Übeltäter, aber auch Netzwerkprobleme können zu schlechten Verbindungen führen. Wenn MySQL einen Host blockiert, schreibt es eine solche Nachricht in das Log: Host 'host.badguy.com' blocked because of many connection errors. Unblock with 'mysqladmin flush-hosts'
Wie diese Nachricht andeutet, können Sie die Blockade des Hosts mit dem Befehl mysqladmin flush-hosts aufheben, vorzugsweise nachdem Sie festgestellt haben, wieso dieser Host Probleme mit der Verbindung hatte, und Sie das entsprechende Problem behoben haben. Der Befehl mysqladmin flush-hosts führt einfach einen FLUSH HOSTS-SQL-Befehl 600 | Kapitel 12: Sicherheit
aus, der die Host-Cache-Tabellen von MySQL leert. Damit werden die Blockaden aller blockierten Hosts aufgehoben; es gibt keine Möglichkeit, dies für einen einzelnen Host zu tun. Falls Sie merken, dass dies Problem aus welchem Grund auch immer häufiger auftritt, können Sie die Variable max_connection_errors in der my.cnf-Datei auf einen relativ hohen Wert setzen, um zu verhindern, dass Hosts blockiert werden: max_connection_errors=999999999
Es ist nicht möglich, max_connection_errors auf 0 zu setzen und die Prüfung ganz abzuschalten. Außerdem wollen Sie das bestimmt nicht. Es ist besser, das zugrunde liegende Problem zu finden und zu lösen.
Datenverschlüsselung In Anwendungen, die sensible Daten speichern, etwa Bankeinträge, werden Sie die Daten sicher in einem verschlüsselten Format speichern wollen. Dies erschwert es unautorisierten Personen, die Daten zu benutzen, auch wenn sie physischen Zugriff auf Ihren Server haben. Eine vollständige Diskussion der jeweiligen Vorzüge von Verschlüsselungsalgorithmen und -techniken würde den Rahmen dieses Buches sprengen; wir wollen uns aber dennoch einige der relevanten Themen kurz anschauen.
Passwort-Hashing In weniger sensiblen Anwendungen müssen Sie nur eine kleine Menge an Informationen schützen, wie z.B. eine Passwortdatenbank für eine andere Anwendung. Passwörter sollten wirklich nicht im Klartext gespeichert werden, sie werden daher in Anwendungen üblicherweise verschlüsselt. Anstatt jedoch Verschlüsselung zu verwenden, ist es möglicherweise klug, dem Beispiel der meisten Unix-Systeme und sogar von MySQL selbst zu folgen: Setzen Sie auf den Passwörtern einen Hashing-Algorithmus ein, und speichern Sie die Ergebnisse in Ihrer Tabelle. Im Gegensatz zur traditionellen Verschlüsselung, die umgekehrt werden kann, ist eine gute Hash-Funktion eine Einbahnstraße, die man nicht rückgängig machen kann. Die einzige Möglichkeit, das Passwort zu ermitteln, das einen bestimmten Hash-Wert generiert hat, besteht darin, einen überaus aufwendigen Brute-Force-Angriff auszuführen (bei dem alle möglichen Kombinationen aus Eingaben ausprobiert werden). MySQL bietet drei Benutzerfunktionen zum Hashing von Passwörtern: ENCRYPT( ), SHA1( ) und MD5( ).10 Am besten erkennen Sie die Ergebnisse der einzelnen Funktionen, wenn Sie sie auf dem gleichen Quelltext ausprobieren. Schauen wir uns das mit dem String p4ssword an: 10 MySQLs ENCRYPT( ) ruft einfach die crypt( )-Funktion der C-Bibliothek auf. Bei manchen Unix-Varianten ist crypt( ) eine MD5-Implementierung, wodurch es sich nicht von MD5( ) unterscheidet. Bei anderen ist es der traditionelle DES-Verschlüsselungsalgorithmus.
Datenverschlüsselung | 601
mysql> SELECT MD5('p4ssword'), ENCRYPT('p4ssword'), SHA1('p4ssword')\G *************************** 1. row *************************** MD5('p4ssword'): 93863810133ebebe6e4c6bbc2a6ce1e7 ENCRYPT('p4ssword'): dDCjeBzIycENk SHA1('p4ssword'): fbb73ec5afd91d5b503ca11756e33d21a9045d9d
Jede Funktion liefert einen alphanumerischen String fester Länge zurück, den Sie in einer CHAR-Spalte speichern können. Um die Möglichkeit zu berücksichtigen, dass sowohl Groß- als auch Kleinbuchstaben im Ergebnis von ENCRYPT( ) zu finden sind, ist es am besten, die Spalte als CHAR BINARY zu deklarieren. Benutzen Sie in Anwendungen niemals die interne PASSWORD( )-Funktion von MySQL. Das Ergebnis ist nicht in allen MySQL-Versionen gleich.
Das Speichern der ge-hash-ten Daten ist einfach: mysql> INSERT INTO user_table (user, pass) VALUES ('user', MD5('p4ssword') );
Um das Passwort von user zu verifizieren, führen Sie eine SELECT-Abfrage durch, mit der Sie feststellen können, ob der angegebene Benutzername und das Passwort richtig sind. So könnte das z.B. in Perl aussehen: my $sth = $dbh->prepare('SELECT * FROM user_table ' . 'WHERE user = ? AND pass = MD5(?)'); $sth->execute($username, $password);
Passwort-Hashing bildet eine einfach zu benutzende und relativ sichere Methode, um Passwörter in einer Datenbank zu speichern, ohne dass sie leicht wiederhergestellt werden können. Für einen etwas besseren Ansatz, der Wörterbuchangriffe erschwert, können Sie den Benutzernamen und das Passwort für den Hash kombinieren, damit dieser von mehr Variablen abhängig ist: my $sth = $dbh->prepare('SELECT * FROM user_table ' . 'WHERE user = ? AND pass = SHA1(CONCAT(?, ?))'); $sth->execute($username, $username, $password);
Das einzige Problem ist das mögliche Sicherheitsrisiko, das verursacht wird, weil das Passwort im Klartext an MySQL gesandt wird; es könnte im Klartext in die Logs auf den Festplatten gelangen, und es ist in der Prozessliste sichtbar. Sie könnten das Passwort als Benutzervariable speichern, um das Risiko ein wenig zu verringern, oder das Hashing in die Anwendung verschieben, um es ganz zu vermeiden. In den meisten Programmiersprachen gibt es Verschlüsselungsfunktionen oder -bibliotheken. Wir schauen uns die Verschlüsselung auf Anwendungsebene gleich an.
Verschlüsselte Dateisysteme Da die verschiedenen MySQL-Storage-Engines ihre Daten als normale Dateien auf dem von Ihnen benutzten Dateisystem speichern, ist es möglich, ein verschlüsseltes Dateisystem zu benutzen. Die meisten beliebten Betriebssysteme bieten wenigstens ein verschlüsseltes Dateisystem (entweder kostenlos oder gegen Bezahlung). 602 | Kapitel 12: Sicherheit
Der größte Vorteil dieses Ansatzes besteht darin, dass Sie für MySQL nichts Besonderes unternehmen müssen, um seine Vorteile nutzen zu können. Da die ganze Ver- und Entschlüsselung außerhalb von MySQL stattfindet, führt es einfach nur Lese- und Schreiboperationen durch, ohne zu wissen, was intern vor sich geht. Sie müssen nur dafür sorgen, dass Ihre Daten und Logs im richtigen Dateisystem gespeichert werden. Aus Sicht Ihrer Anwendung ist an dieser Anordnung nichts Besonderes. Einige Nachteile bringt die Benutzung eines verschlüsselten Dateisystems für MySQL mit sich. Da Sie alle Daten, Indizes und Logs verschlüsseln, ist erstens ein beträchtlicher CPU-Overhead erforderlich, um das ganze Ver- und Entschlüsseln der Daten zu erledigen. Falls Sie darüber nachdenken, ein verschlüsseltes Dateisystem einzusetzen, dann denken Sie daran, gute Benchmarks durchzuführen, damit Sie verstehen, wie es sich unter starker Last verhält. Achten Sie außerdem darauf, dass Sie die Daten nicht entschlüsseln, wenn Sie Backups von ihnen anlegen. Diese Regel zu befolgen, sollte nicht schwer sein; man kann sie aber leicht vergessen. Eine letzte Überlegung ist, dass ein verschlüsseltes Dateisystem keinen Schutz vor Leuten bietet, die Zugriff auf den Server erlangen, der die Daten enthält. Da der Server, der das Dateisystem mountet, es transparent entschlüsselt, kann jeder mit Zugriff auf den Server die Daten lesen – und entschlüsselte Kopien davon anlegen.
Verschlüsselung auf Anwendungsebene Ein gebräuchlicherer Ansatz sieht vor, die Verschlüsselung in die Anwendung (oder in Middleware) einzubauen. Wenn die Anwendung sensible Daten speichern muss, verschlüsselt sie zuerst die Daten und speichert das Ergebnis dann in MySQL. Wenn sie umgekehrt verschlüsselte Daten von MySQL holt, muss sie sie entschlüsseln. Dieser Ansatz bietet eine große Flexibilität. Er bindet Sie nicht an ein bestimmtes Dateisystem, Betriebssystem oder an eine bestimmte Datenbank (falls Sie Ihren Code generisch geschrieben haben) und gibt dem Anwendungsentwickler die Freiheit, den Verschlüsselungsalgorithmus zu wählen, der ihm für die zu speichernden Daten am passendsten erscheint (Ausgleichsgeschwindigkeit und -stärke). Da die Daten verschlüsselt sind, sind Backups sehr einfach. Wohin auch immer Sie die Daten kopieren, sie sind verschlüsselt. Das bedeutet aber auch, dass der Zugriff auf die Daten über Software erfolgen muss, die versteht, wie man sie entschlüsselt. Sie können nicht einfach das mysql-Kommandozeilenwerkzeug öffnen und anfangen, Abfragen auszuführen. Verschlüsselung auf Anwendungsebene ist oft eine gute Lösung, hat aber auch einige Nachteile. Es ist z.B. für MySQL viel schwieriger, verschlüsselte Daten effektiv zu indizieren, und auch die Optimierung der Leistung von MySQL ist beim Arbeiten mit verschlüsselten Daten nicht ganz einfach.
Datenverschlüsselung | 603
Entwurfsprobleme Die Freiheit und Flexibilität, die wir erwähnt haben, haben interessante Implikationen auf den Datenbankentwurf. Ein Problem besteht darin, dass Sie dafür sorgen müssen, dass die verwendeten Spaltentypen sich für die Art der Verschlüsselung eignen, die Sie einsetzen. Manche Algorithmen erzeugen Datenblöcke mit festen Mindestgrößen. Das bedeutet, dass Sie möglicherweise eine Spalte brauchen, die 256 Bytes aufnehmen kann, nur um ein Stück Daten zu speichern, das vor der Verschlüsselung deutlich kleiner war. Viele beliebte Verschlüsselungsbibliotheken erzeugen außerdem Binärdaten, so dass Sie Spalten anlegen müssen, die Binärdaten speichern können. Alternativ können Sie die Binärdaten in eine Hex- oder Base-64-Darstellung umwandeln. Das erfordert aber mehr Platz und Zeit. Es ist sowieso nicht leicht zu entscheiden, welche Daten verschlüsselt werden müssen und welche nicht. Sie müssen Sicherheit gegen die Schwierigkeit abwägen, die Informationen in Ihren Tabellen abzufragen. Sie könnten z.B. eine konto-Tabelle haben, die Bankkonten repräsentiert und folgende Spalten enthält: • • • • • •
id typ status saldo ueberziehungsschutz datum_der_einrichtung
Welche Spalten werden sinnvollerweise verschlüsselt? Wenn Sie den Saldo verschlüsseln, was vernünftig zu sein scheint, ist es schwierig, allgemeine Fragen nach dem Kontostand zu beantworten. Vielleicht wollen Sie z.B. die folgende Abfrage schreiben, um die minimalen, maximalen und durchschnittlichen Saldi für die einzelnen Kontentypen zu ermitteln: mysql> SELECT MIN(saldo), MAX(saldo), AVG(saldo) -> FROM konto GROUP BY typ;
Die Ergebnisse werden allerdings sinnlos sein. MySQL weiß nicht, was die verschlüsselten saldo-Spalten bedeuten, und versucht daher nur, diese Funktionen auf den verschlüsselten Daten auszuführen. Die Lösung für Ihre Anwendung besteht darin, alle Zeilen aus der konto-Tabelle zu lesen und die Berechnungen für den Report auszuführen. Das scheint nicht besonders schwer zu sein, ist aber ärgerlich. Sie implementieren damit nicht nur eine Funktionalität, die MySQL bereits bietet, sondern bremsen den Vorgang auch noch merklich aus. Das läuft insgesamt auf eine Diskrepanz zwischen der Sicherheit und den Vorteilen relationaler Datenbanken hinaus. Jede Spalte, die verschlüsselte Daten enthält, ist im Prinzip für die in MySQL integrierten Funktionen nutzlos, weil diese mit unverschlüsselten Daten arbeiten müssen. In einer unverschlüsselten Anordnung können Sie z.B. leicht alle Konten ermitteln, deren Saldo größer als 100.000 ist: mysql> SELECT * FROM konto WHERE saldo > 100000;
604 | Kapitel 12: Sicherheit
Wenn es einen Index in der saldo-Spalte gibt und dieser nicht verschlüsselt ist, kann MySQL die gewünschten Zeilen mithilfe des Index suchen. Sind die Daten dagegen verschlüsselt, müssen Sie alle Zeilen in Ihre Anwendung holen, sie entschlüsseln und anschließend filtern.
Ver- und Entschlüsseln innerhalb von MySQL Nichtsdestotrotz können Sie verschlüsselte Werte in MySQL speichern und sie mit seinen eigenen Funktionen ver- und entschlüsseln. Die besten Funktionen für diese Zwecke sind AES_ENCRYPT( ) und AES_DECRYPT( ). Sie konvertieren Strings in verschlüsselte Binärstrings und wieder zurück. Sie sind symmetrisch: Sie benutzen für Ver- und Entschlüsselung denselben Schlüssel. Zum Beispiel: mysql> SET @key := 's3cret'; mysql> SET @encrypted := AES_ENCRYPT('sensitive data', @key); mysql> SELECT AES_DECRYPT(@encrypted, @key); +-------------------------------+ | AES_DECRYPT(@encrypted, @key) | +-------------------------------+ | sensitive data | +-------------------------------+
Wir haben den verschlüsselten Wert hier nicht gezeigt, weil er aus binären Einsen und Nullen besteht und ziemlich wirr aussehen würde. Dieser Ansatz löst aber nicht alle genannten Probleme. Zum einen vermeidet er die Indizierungsprobleme nicht; zum anderen sind die Daten, die Sie verschlüsseln wollen, in der SQL-Abfrage immer noch im Klartext und werden auch so im Log des Servers aufgezeichnet (falls das Logging aktiviert ist). Wir haben Ihnen jedoch schon einen Schritt gezeigt, mit dem Sie das Risiko verringern können, dass andere Benutzer Ihre geheimen Daten sehen: Speichern Sie den Verschlüsselungsschlüssel in einer Benutzervariablen. Es gibt auch noch sicherere Methoden, um den Wert der Variablen tatsächlich zu setzen. Sie können z.B. die Variable in einer gespeicherten Prozedur platzieren und die gespeicherte Prozedur aufrufen, um ihren Wert zu setzen, und dann den Zugriff auf die gespeicherte Prozedur beschränken. Dadurch wird es anderen Benutzern erschwert, den Wert des Schlüssels festzustellen.
Quellcode-Modifikation Falls Sie einen flexibleren Ansatz suchen als verschlüsselte Dateisysteme oder anwendungsbasierte Verschlüsselung, können Sie sich immer auch eine eigene Lösung ausdenken. Der Quellcode für MySQL steht frei unter der GNU General Public License zur Verfügung. Diese Art von Arbeit erfordert es, dass Sie C++ beherrschen oder zumindest jemanden anheuern, der dies tut. Darüber hinaus könnten Sie eine eigene Storage-Engine mit eingebauter Unterstützung für die Verschlüsselung herstellen oder eine bestehende StorageEngine mit Verschlüsselung erweitern.
Datenverschlüsselung | 605
MySQL in einer chroot-Umgebung Das Ausführen eines Servers in einer mit chroot veränderten Umgebung verbessert die Gesamtsicherheit eines Unix-Systems ganz beträchtlich. Dazu wird eine isolierte Umgebung eingerichtet, in der Dateien außerhalb eines bestimmten Verzeichnisses nicht mehr erreichbar sind. Selbst wenn anschließend eine Sicherheitslücke im Server gefunden und von Angreifern ausgenutzt wird, wird der potenzielle Schaden auf die Dateien in diesem Verzeichnis beschränkt, d.h. auf die Dateien für die entsprechende Anwendung. Falls Sie Ihre MySQL-Anwendung in einer chroot-Umgebung ausführen wollen, müssen Sie zuerst entweder MySQL aus den Quellen kompilieren oder das Binärpaket von MySQL AB auspacken und installieren. Viele Administratoren tun dies ganz selbstverständlich, für eine chroot-Umgebung ist es aber eine absolute Notwendigkeit: Viele vorgepackte MySQL-Installationen legen einige Dateien in /usr/bin, einige in /var/lib/mysql usw. ab, in einer chroot-Installtion müssen sich jedoch alle Dateien unterhalb derselben Verzeichnisstruktur befinden. Üblicherweise erzeugen wir ein /chroot-Verzeichnis, in dem sich alle unter chroot laufenden Anwendungen befinden sollen. Konfigurieren Sie dazu Ihre MySQL-Installation folgendermaßen: $ ./configure --prefix=/chroot/mysql
Kompilieren Sie MySQL anschließend ganz normal, und lassen Sie die Installationsprozedur die MySQL-Dateien im /chroot/mysql-Baum installieren. Die nächste Aktion scheint Zauberei zu sein, um alles etwas schöner zu machen. chroot steht eigentlich für change root (root ändern). Wenn Sie $ chroot /chroot/mysql
eingeben, ist das /-Verzeichnung jetzt tatsächlich /chroot/mysql. Da sowohl der unter chroot laufende Server als auch die nicht unter chroot laufenden Clients die Dateien benutzen, ist es wichtig, das Dateisystem so einzurichten, dass sowohl der Server als auch die Clients die benötigten Dateien finden können. Eine leichte Lösung für dieses Problem sieht so aus: $ $ $ $
cd /chroot/mysql mkdir chroot cd chroot ln -s /chroot/mysql mysql
Dadurch wird ein symbolischer Verzeichnispfad, /chroot/mysql/chroot/mysql, angelegt, der tatsächlich auf /chroot/mysql verweist. Obwohl die Anwendung unter chroot läuft und versucht, /chroot/mysql zu erreichen, gelangt sie nun in das richtige Verzeichnis. Auch die Clientanwendung kann, obwohl sie außerhalb der chroot-Umgebung läuft, die benötigten Dateien finden.
606 | Kapitel 12: Sicherheit
Im letzten Schritt werden die richtigen Befehle an mysqld_safe gesandt, damit der MySQL-Server starten und mit chroot in das passende Verzeichnis wechseln kann. Geben Sie dazu einen solchen Befehl ein: $ mysqld_safe --chroot=/chroot/mysql --user=1001
Sie werden bemerken, dass wir die Unix-UID des MySQL-Benutzers (1001) anstelle von -user=mysql benutzt haben. Das liegt daran, dass der MySQL-Server in der chroot-Umgebung nicht mehr in der Lage sein wird, Ihr Authentifizierungsprogramm abzufragen, um Benutzername-nach-UID-Suchen durchzuführen.11 Sie müssen einige Dinge beachten, wenn Sie einen unter chroot stehenden MySQL-Server benutzen. LOAD DATA INFILE und andere Befehle, die direkt auf Dateinamen zugreifen, verhalten sich möglicherweise völlig anders als erwartet, weil der Server / nicht mehr als Wurzel des Dateisystems betrachtet. Falls Sie ihn also anweisen, die Daten aus /tmp/ filename zu laden, müssen Sie dafür sorgen, dass die Datei tatsächlich /chroot/mysql/tmp/ filename ist, da MySQL sie ansonsten nicht finden kann. Eine chroot-Umgebung ist nur eine Möglichkeit, um MySQL teilweise zu isolieren. Es gibt noch weitere, wie FreeBSD-Jails, Solaris-Zonen und Virtualisierung.
11 Unserer Erfahrung nach müsste das genauso einfach gehen wie das Kopieren von libnss* in Ihr MySQL-Bibliotheksverzeichnis in der chroot-Umgebung. Aus praktischer Sicht ist es wahrscheinlich am besten, sich keine Sorgen über solche Dinge zu machen und die UID direkt in das Startskript einzugeben.
MySQL in einer chroot-Umgebung | 607
KAPITEL 13
Der MySQL-Serverstatus
Man kann viele Fragen über einen MySQL-Server beantworten, indem man seinen Status untersucht. MySQL legt Informationen über die Serverinterna auf zwei wesentliche Weisen offen: die neueste ist die Standard-INFORMATION_SCHEMA-Datenbank und die traditionellere sind eine Reihe von SHOW-Befehlen (die MySQL weiterhin unterstützt, obwohl die INFORMATION_SCHEMA-Datenbank den bevorzugten Mechanismus für neue Funktionen bildet). Manche Informationen, die Sie über SHOW-Befehle erhalten können, sind in den INFORMATION_SCHEMA-Tabellen noch nicht zu finden. Die Herausforderungen für Sie bestehen darin, dass Sie feststellen müssen, was für Ihr Problem relevant ist, wie Sie die benötigten Informationen bekommen und wie Sie sie interpretieren müssen. MySQL bietet Ihnen zwar viele Informationen darüber an, was innerhalb des Servers vor sich geht, es ist jedoch nicht immer leicht, diese Informationen zu benutzen. Um sie zu verstehen, brauchen Sie Geduld, Erfahrung und sofortigen Zugriff auf das MySQL-Handbuch. Es gibt verschiedene Werkzeuge, die Ihnen dabei helfen, den Serverstatus in verschiedenen Kontexten zu verstehen, wie etwa für Überwachung und Profiling. Wir werden einige dieser Werkzeuge im nächsten Kapitel anschauen. Dennoch sollten Sie die Werte auch auf einer höheren Ebene verstehen – zumindest, aus welchen Kategorien die Werte stammen – und wissen, wie Sie sie vom Server bekommen können. In diesem Kapitel werden viele der Statusbefehle und deren Ausgaben erläutert. Wenn wir ein Thema an anderer Stelle bereits behandelt haben, dann verweisen wir Sie entsprechend darauf.
Systemvariablen MySQL stellt viele Systemvariablen über den SQL-Befehl SHOW VARIABLES dar, als Variablen, die Sie in Ausdrücken benutzen können, oder mit mysqladmin variables auf der Kommandozeile. Seit MySQL 5.1 können Sie sie auch über die Tabellen in der INFORMATION_SCHEMA-Datenbank erreichen.
608 |
Diese Variablen repräsentieren eine Vielzahl von Konfigurationsinformationen, wie etwa die voreingestellte Storage-Engine des Servers (storage_engine), die verfügbaren Zeitzonen, die Sortierreihenfolge der Verbindung und die Startparameter. Wir haben in Kapitel 6 erläutert, wie Sie die Systemvariablen einrichten und benutzen.
SHOW STATUS Der Befehl SHOW STATUS zeigt Serverstatusvariablen in einer zweispaltigen Name/WertTabelle. Im Gegensatz zu den Servervariablen, die wir im vorangegangenen Abschnitt erwähnt haben, sind sie schreibgeschützt. Sie können die Variablen betrachten, indem Sie entweder SHOW STATUS als SQL-Befehl oder mysqladmin extended-status als Shell-Befehl ausführen. Wenn Sie den SQL-Befehl benutzen, können Sie die Ergebnisse mit LIKE und WHERE begrenzen; LIKE führt auf dem Variablennamen einen normalen Mustervergleich durch. Die Befehle liefern eine Tabelle mit Ergebnissen zurück, Sie können diese aber nicht sortieren, mit anderen Tabellen zusammenführen oder andere Standardaktionen ausführen, die mit MySQL-Tabellen möglich sind. Wir beziehen uns mit dem Begriff »Statusvariable« auf einen Wert aus SHOW STATUS und mit dem Begriff »Systemvariable« auf eine Serverkonfigurationsvariable.
Das Verhalten von SHOW STATUS hat sich in MySQL 5.0 stark geändert, allerdings werden Sie das wahrscheinlich erst merken, wenn Sie genau hinschauen. Anstatt nur einen Satz an globalen Variablen zu pflegen, verwaltet MySQL jetzt einige Variablen global und einige verbindungsbezogen. SHOW STATUS enthält daher eine Mischung aus globalen und Sitzungsvariablen. Viele von ihnen haben einen doppelten Geltungsbereich: Es gibt sowohl eine globale als auch eine Sitzungsvariable, und sie tragen den gleichen Namen. SHOW STATUS zeigt jetzt standardmäßig Sitzungsvariablen. Falls Sie also daran gewöhnt waren, SHOW STATUS auszuführen und globale Variablen zu sehen, dann müssen Sie umdenken und nun stattdessen SHOW GLOBAL STATUS ausführen.1 Seit MySQL 5.1 können Sie die Werte direkt aus den Tabellen INFORMATION_SCHEMA. GLOBAL_STATUS und INFORMATION_SCHEMA.SESSION_STATUS auswählen.
Es gibt Hunderte von Statusvariablen in einem MySQL 5.0-Server, in den neueren Versionen sogar noch mehr. Die meisten sind entweder Zähler oder enthalten den aktuellen Wert irgendeines Statusmaßes. Zähler werden jedes Mal erhöht, wenn MySQL etwas tut, also z.B. einen vollständigen Tabellenscan initiiert (Select_scan). Maße, wie die Anzahl der offenen Verbindungen zu einem Server (Threads_connected), können sich erhöhen oder verringern. Manchmal gibt es mehrere Variablen, die sich auf dasselbe zu beziehen scheinen, wie etwa Connections (die Anzahl der Verbindungsversuche zum Server) und
1 Achtung: Wenn Sie eine alte Version von mysqladmin auf einem neuen Server benutzen, benutzt es nicht SHOW GLOBAL STATUS, zeigt also nicht die »richtigen« Informationen.
SHOW STATUS | 609
Threads_connected; in diesem Fall sind die Variablen verwandt. Ähnliche Namen impli-
zieren jedoch nicht immer eine Beziehung. Zähler werden als vorzeichenlose Integer gespeichert. Sie benutzen 4 Bytes in 32-Bit-Versionen und 8 Bytes in 64-Bit-Versionen und springen zurück auf 0, nachdem sie ihren Maximalwert erreicht haben. Wenn Sie die Variablen taktweise überwachen, müssen Sie möglicherweise auf den Übertrag achten und ihn korrigieren; falls Ihr Server schon sehr lange läuft, sehen Sie unter Umständen niedrigere Werte, als Sie erwarten würden, weil die Werte der Variablen ganz einfach wieder auf null gesprungen sind. (Bei 64-Bit-Versionen ist das nur selten ein Problem.) Am besten schaut man sich viele dieser Variablen an, indem man feststellt, wie stark sich ihre Werte im Verlauf einiger Minuten ändern. Dazu können Sie mysqladmin extendedstatus -r -i 5 oder innotop verwenden. Im Folgenden bieten wir einen Überblick – keine erschöpfende Liste – der verschiedenen Kategorien von Variablen, die Sie in SHOW STATUS finden werden. Vollständige Informationen über eine bestimmte Variable finden Sie im MySQL-Handbuch, das sie dankenswerterweise dokumentiert, unter http://dev.mysql.com/doc/en/mysqld-option-tables.html. Wenn wir über eine Gruppe verwandter Variablen reden, deren Namen mit einem gemeinsamen Präfix beginnen, bezeichnen wir die Gruppe gemeinsam als »die _ *-Variablen«.
Thread- und Verbindungsstatistiken Diese Variablen verfolgen Verbindungsversuche, abgebrochene Verbindungen, den Netzwerkverkehr und Thread-Statistiken: • Connections, Max_used_connections, Threads_connected • Aborted_clients, Aborted_connects • Bytes_received, Bytes_sent • Slow_launch_threads, Threads_cached, Threads_created, Threads_running Falls Aborted_connects nicht null ist, könnte das bedeuten, dass Sie Netzwerkprobleme haben oder dass jemand versucht, sich anzumelden, und es nicht schafft (vielleicht weil er das falsche Passwort oder eine ungültige Datenbank angegeben hat). Wird dieser Wert zu hoch, dann hat das möglicherweise ernsthafte Nebenwirkungen: Es kann MySQL veranlassen, einen Host zu blockieren. Mehr dazu erfahren Sie in Kapitel 12. Aborted_clients trägt einen ähnlichen Namen, hat aber eine völlig andere Bedeutung. Wenn dieser Wert sich erhöht, dann bedeutet das normalerweise, dass es einen Anwendungsfehler gegeben hat, also dass z.B. der Programmierer vergessen hat, die MySQLVerbindungen richtig zu schließen, bevor er das Programm beendet hat. Das ist meist kein Anzeichen für ein größeres Problem.
610 |
Kapitel 13: Der MySQL-Serverstatus
Ein sinnvolles Maß ist die Anzahl der Threads, die pro Sekunde erzeugt werden (Threads_created/Uptime). Wenn dieser Wert nicht nahe bei null liegt, kann das bedeuten, dass Ihr Thread-Cache zu klein ist und neue Verbindungen nicht in der Lage sind, freie Threads aus dem Thread-Cache zu finden. Am besten ist es, sich die Werte all dieser Variablen und Maße über die Dauer der letzten Minuten anzuschauen und nicht während der gesamten Laufzeit des Servers.
Der Status des Binär-Loggings Die Statusvariablen Binlog_cache_use und Binlog_cache_disk_use zeigen, wie viele Transaktionen im Binärlog-Cache gespeichert wurden und wie viele Transaktionen zu groß für den Binärlog-Cache waren, so dass sie ihre Anweisungen in einer temporären Datei speichern mussten. Wir haben in Kapitel 6 erläutert, wie man die Größe des Binärlogs anpasst.
Befehlszähler Die Com_*-Variablen zählen, wie oft die einzelnen Arten von SQL- oder C-API-Befehlen aufgerufen wurden. So zählt z.B. Com_select die Anzahl der SELECT-Anweisungen, und Com_change_db zählt, wie oft die vorgegebene Datenbank einer Verbindung geändert wurde, und zwar entweder mit der USE-Anweisung oder über einen C-API-Aufruf. Die Variable Questions ermittelt die Gesamtanzahl der Abfragen und Befehle, die der Server empfangen hat. Das ist jedoch nicht einfach gleich der Summe aller Com_*-Variablen, da es auch noch Treffer im Abfrage-Cache, geschlossene und abgebrochene Verbindungen und andere mögliche Faktoren gibt. Die Statusvariable Com_admin_commands kann sehr groß werden. Sie zählt nicht nur die administrativen Befehle, sondern auch ping-Anforderungen an die MySQL-Instanz. Diese Anforderungen werden über die C-API ausgelöst und kommen typischerweise vom Clientcode, wie etwa der folgende Perl-Code: my $dbh = DBI->connect(...); while ( $dbh && $dbh->ping ) { # Macht irgendetwas }
Diese ping-Anforderungen sind »Müll«-Abfragen. Sie belasten den Server wahrscheinlich nicht sehr stark, sind aber trotzdem eine Verschwendung. Wir haben ORM-Systeme gesehen, die den Server vor jeder Abfrage »anpingen«, was sinnlos ist. Uns sind auch schon Datenbankabstraktionsbibliotheken begegnet, die die Standarddatenbank vor jeder Abfrage geändert haben, was sich in einer sehr großen Anzahl von Com_change_dbBefehlen äußert. Es ist am besten, beide Praktiken zu unterbinden.
SHOW STATUS | 611
Temporäre Dateien und Tabellen Folgendermaßen können Sie die Variablen anschauen, die zählen, wie oft MySQL temporäre Tabellen und Dateien erzeugt hat: mysql> SHOW GLOBAL STATUS LIKE 'Created_tmp%';
Handler-Operationen Die Handler-API ist die Schnittstelle zwischen MySQL und seinen Storage-Engines. Die Handler_*-Variablen zählen die Handler-Operationen, etwa wie oft MySQL eine StorageEngine bittet, die nächste Zeile aus einem Index zu lesen. Die Untersuchung der Handler_*-Variablen Ihres Servers bietet Ihnen einen Einblick, welche Arten von Arbeit Ihr Server am häufigsten ausführt. Handler_*-Variablen eignen sich auch für ProfilingAbfragen. Sie können sich diese Variablen so anschauen: mysql> SHOW GLOBAL STATUS LIKE 'Handler_%';
MyISAM-Schlüsselpuffer Die Key_*-Variablen enthalten Maße und Zähler für den MyISAM-Schlüsselpuffer. Sie können diese Variablen so anschauen: mysql> SHOW GLOBAL STATUS LIKE 'Key_%';
In Kapitel 6 wird ausführlich erklärt, wie Sie die Schlüssel-Caches analysieren und einstellen.
Dateideskriptoren Falls Sie hauptsächlich die MyISAM-Storage-Engine benutzen, ist es wichtig, die Dateideskriptorstatistiken zu beobachten, da diese Ihnen mitteilen, wie oft MySQL die .frm-, .MYI- und .MYD-Dateien der einzelnen Tabellen öffnet. InnoDB bewahrt alle Daten in seinen Tablespace-Dateien auf. Falls Sie also vor allem InnoDB benutzen, sind diese Variablen nicht ganz so wichtig. Sie können die Open_*-Variablen so anschauen: mysql> SHOW GLOBAL STATUS LIKE 'Open_%';
In Kapitel 6 wird ausführlich erklärt, wie Sie die Einstellungen ändern, die diese Variablen beeinflussen.
Abfrage-Cache Sie können den Abfrage-Cache untersuchen, indem Sie sich die Qcache_*-Statusvariablen anschauen. Alle Variablen in dieser Gruppe sind wichtig, wenn Sie sich aus Leistungsgründen auf den Abfrage-Cache verlassen. Geben Sie zur Untersuchung Folgendes ein: mysql> SHOW GLOBAL STATUS LIKE 'Qcache_%';
Es gibt in Kapitel 5 eine ausführliche Erläuterung, wie Sie den Abfrage-Cache einstellen.
612 |
Kapitel 13: Der MySQL-Serverstatus
SELECT-Typen Die Select_*-Variablen sind Zähler für bestimmte Arten von SELECT-Abfragen. Mit ihrer Hilfe können Sie den Anteil an SELECT-Abfragen sehen, die verschiedene Abfragepläne benutzen. Leider gibt es solche Statusvariablen nicht für andere Arten von Abfragen wie UPDATE und REPLACE; allerdings können Sie sich die Handler_*-Statusvariablen anschauen, um einen Einblick in die Leistung der Nicht-SELECT-Abfragen zu erhalten. Um alle Select_*-Variablen zu sehen, schreiben Sie: mysql> SHOW GLOBAL STATUS LIKE 'Select_%';
Unserer Ansicht nach kann man die Select_*-Statusvariablen in der Reihenfolge aufsteigender Kosten folgendermaßen anordnen: Select_range
Die Anzahl der Joins, die einen Indexbereich in der ersten Tabelle gescannt haben. Select_scan
Die Anzahl der Joins, die die gesamte erste Tabelle gescannt haben. Es ist nichts falsch daran, falls jede Zeile in der ersten Tabelle an dem Join teilnehmen sollte; schlecht ist es nur, wenn Sie nicht alle Zeilen haben wollen und es keinen Index gibt, um die gewünschten Zeilen zu finden. Select_full_range_join
Die Anzahl der Joins, die einen Wert aus Tabelle n verwendet haben, um Zeilen aus einem Bereich des Referenzindex in Tabelle n + 1 zu beziehen. Je nach der Abfrage kann das mehr oder weniger kostenaufwendig sein als Select_scan. Select_range_check
Die Anzahl der Joins, die die Indizes in Tabelle n + 1 für jede Zeile in Tabelle n neu auswerten, um festzustellen, welcher am wenigsten teuer ist. Das bedeutet im Allgemeinen, dass keine Indizes aus Tabelle n + 1 für den Join geeignet sind. Dieser Abfrageplan hat einen sehr großen Overhead. Select_full_join
Ein Quer-Join oder ein Join ohne Kriterien zum Erfassen von Zeilen in den Tabellen. Die Anzahl der untersuchten Zeilen ist das Produkt aus der Anzahl der Zeilen in den einzigen Tabellen. Normalerweise ist das sehr schlecht. Die letzten beiden Variablen sollten sich auf einem gut eingestellten Server nicht sehr schnell erhöhen. Manchmal entdeckt man eine schlecht optimierte Arbeitslast, indem man das Verhältnis dieser beiden Zähler mit der Gesamtanzahl der SELECT-Abfragen vergleicht, die Ihr Server verarbeitet (Com_select). Falls eine der beiden mehr als nur ein paar Prozent des Gesamtwertes beträgt, müssen Sie wahrscheinlich Ihre Abfragen und/oder Ihr Schema überarbeiten. Eine verwandte Statusvariable ist Slow_queries. Mit den Patches, die wir für das SlowQuery-Log entwickelt haben, können Sie feststellen, ob eine Abfrage einen vollständigen Join erfordert hat, ob sie aus dem Abfrage-Cache ausgeliefert wurde usw. Mehr dazu erfahren Sie in »Feinere Kontrolle über das Logging« auf Seite 70.
SHOW STATUS | 613
Sortieren Wir haben viele der Sortieroptimierungen in MySQL in den Kapiteln 3 und 4 behandelt, so dass Sie eine gute Vorstellung davon haben sollten, wie das Sortieren funktioniert. Wenn MySQL keinen Index benutzen kann, um die Zeilen vorsortiert zu beziehen, muss es ein Filesort durchführen und inkrementiert die Sort_*-Statusvariablen. Abgesehen von Sort_merge_passes können Sie diese Variablen nur beeinflussen, indem Sie Indizes hinzufügen, die MySQL für die Sortierung benutzen kann. Sort_merge_passes hängt von der Servervariablen sort_buffer_size ab (die nicht mit der Servervariablen myisam_sort_ buffer_size verwechselt werden sollte). MySQL benutzt den Sortierpuffer, um eine Gruppe von Zeilen für die Sortierung aufzunehmen. Wenn es mit dem Sortieren fertig ist, fügt es die sortierten Zeilen in das Ergebnis ein, erhöht Sort_merge_passes und füllt den Puffer mit der nächsten Gruppe zu sortierender Zeilen. Ist der Sortierpuffer zu klein, muss es das sehr oft tun, und der Wert der Statusvariablen wird sehr groß. Sie können sich alle Sort_*-Variablen so anschauen: mysql> SHOW GLOBAL STATUS LIKE 'Sort_%';
MySQL erhöht die Variablen Sort_scan und Sort_range, wenn es sortierte Zeilen aus den Ergebnissen eines Filesorts liest und zurück an den Client liefert. Der Unterschied besteht lediglich darin, dass die erste erhöht wird, wenn der Abfrageplan Select_scan veranlasst, sich zu erhöhen (siehe vorhergehender Abschnitt), und dass die zweite erhöht wird, wenn Select_range sich erhöht. Es gibt zwischen den beiden keine Implementierung oder einen Kostenunterschied; sie zeigen lediglich die Art des Abfrageplans an, der die Sortierung verursacht hat.
Tabellen-Locking Die Variablen Table_locks_immediate und Table_locks_waited teilen Ihnen mit, wie viele Sperren sofort gewährt wurden und auf wie viele gewartet werden musste. Falls Sie in SHOW FULL PROCESSLIST viele Threads im Zustand Locked sehen, dann überprüfen Sie diese Variablen. Denken Sie allerdings daran, dass sie nur serverbezogene Locking-Statistiken zeigen, keine Storage-Engine-Locking-Statistiken. In Anhang D erfahren Sie mehr über die Fehlerbehebung bei Sperren.
Secure Sockets Layer (SSL) Die Ssl_*-Variablen zeigen, wie der Server für SSL konfiguriert ist, falls dies anwendbar ist. Sie schauen sich die SSL-Variablen so an: mysql> SHOW GLOBAL STATUS LIKE 'Ssl_%';
614 |
Kapitel 13: Der MySQL-Serverstatus
InnoDB-spezifisches Die Innodb_*-Variablen zeigen einige der Daten, die in SHOW INNODB STATUS enthalten sind, das wir später in diesem Kapitel noch besprechen. Die Variablen können anhand der Namen gruppiert werden: Innodb_buffer_pool_*, Innodb_log_* usw. Wir erläutern die Interna von InnoDB, wenn wir SHOW INNODB STATUS untersuchen. Diese Variablen gibt es seit MySQL 5.0, und sie haben eine wichtige Nebenwirkung: Sie erzeugen eine globale Sperre und durchlaufen den gesamten InnoDB-Puffer-Pool, bevor sie die Sperre wieder freigeben. In der Zwischenzeit werden Threads, die auf die Sperre treffen, blockiert. Dies verzerrt einige Statuswerte, wie etwa Threads_running, so dass sie höher erscheinen als normal (möglicherweise viel höher, je nachdem, wie ausgelastet Ihr Server ist). Der gleiche Effekt tritt auf, wenn Sie SHOW INNODB STATUS ausführen oder über die INFORMATION_SCHEMA-Tabellen auf diese Statistiken zugreifen (seit MySQL 5.0 werden SHOW STATUS und SHOW VARIABLES hinter den Kulissen auf Abfragen an die INFORMATION_ SCHEMA-Tabellen abgebildet). Diese Operationen können deshalb in diesen Versionen von MySQL teuer sein – eine zu häufige Überprüfung des Serverstatus (z.B. einmal pro Sekunde) verursacht einen deutlichen Overhead. Es hilft nicht, SHOW STATUS LIKE einzusetzen, weil dies den vollständigen Status abfragt und nachträglich filtert.
Plugin-spezifisches MySQL 5.1 und neuere Versionen unterstützen Plugin-fähige Storage-Engines und bieten einen Mechanismus, mit dessen Hilfe Storage-Engines ihre eigenen Status- und Konfigurationsvariablen am MySQL-Server registrieren können. Falls Sie eine Plugin-fähige Storage-Engine verwenden, werden Ihnen Plugin-spezifische Variablen begegnen.
Verschiedenes Es gibt noch verschiedene weitere Statusvariablen: Delayed_*, Not_flushed_delayed_rows
Diese Variablen sind Zähler und Maße für INSERT DELAYED-Abfragen. Last_query_cost
Diese Variable zeigt die Abfrageplankosten des Abfrageoptimierers für die letzte durchgeführte Abfrage. Wir haben die Kosten für Abfragepläne in Kapitel 4 besprochen. Ndb_*
Diese Variablen zeigen NDB Cluster-Konfigurationsinformationen, falls anwendbar. Slave_*
Diese Variablen gelten, wenn der Server ein Replikations-Slave ist. Die Variable Slave_open_temp_tables ist vor allem für die anweisungsbasierte Replikation wichtig. In »Fehlende temporäre Tabellen« auf Seite 429 erfahren Sie mehr über Replikation und temporäre Tabellen. SHOW STATUS | 615
Tc_log_*
Diese Zähler sind für einen Server, der als Koordinator für XA-Transaktionen auftritt. Mehr dazu erfahren Sie in »Verteilte (XA-) Transaktionen« auf Seite 283. Uptime
Diese Variable zeigt die Laufzeit (Uptime) eines Servers in Sekunden. Eine gute Methode, um ein Gefühl für Ihre Gesamtbelastung zu bekommen, besteht darin, die Werte innerhalb einer Gruppe miteinander verwandter Statusvariablen zu vergleichen – sich z.B. alle Select_*-Variablen oder alle Handler_*-Variablen anzuschauen. Mit innotop können Sie das leicht im Command-Summary-Modus erledigen, es geht aber auch manuell mit einem Befehl wie mysqladmin extended -r -i60 | grep Handler_. Folgendes zeigt innotop für die Select_*-Variablen auf einem von uns überprüften Server: ____________________ Command Summary ___________________ _ _ Name Value Pct Last Incr Pct Select_scan 756582 59.89% 2 100.00% Select_range 497675 39.40% 0 0.00% Select_full_join 7847 0.62% 0 0.00% Select_full_range_join 1159 0.09% 0 0.00% Select_range_check 1 0.00% 0 0.00%
Die ersten beiden Spalten mit Werten laufen seit dem Booten des Servers, in den beiden letzten Spalten werden die Werte seit dem letzten Auffrischen gezeigt (in diesem Fall seit 10 Sekunden). Die Prozentwerte beziehen sich auf die Gesamtheit der gezeigten Werte, nicht auf die Gesamtheit aller Abfragen. Obwohl dieser Server einen relativ geringen Prozentsatz an vollständigen Joins aufweist, lohnt es sich nachzuschauen, wieso es überhaupt welche gibt.
SHOW INNODB STATUS Die InnoDB-Storage-Engine liefert in der Ausgabe von SHOW ENGINE INNODB STATUS oder seinem einfacheren Synonym SHOW INNODB STATUS eine Menge an Informationen über ihre Interna. Im Gegensatz zu den meisten der SHOW-Befehle besteht seine Ausgabe aus einem einzelnen String, nicht aus Zeilen und Spalten. Der String ist in Abschnitte unterteilt, die jeweils Informationen über einen anderen Teil der InnoDB-Storage-Engine zeigen. Ein Teil der Ausgabe ist vor allem für InnoDB-Entwickler von Nutzen, das meiste davon ist jedoch dann interessant – oder sogar wesentlich –, wenn Sie versuchen, InnoDB zu verstehen und für High Performance einzustellen. InnoDB gibt 64-Bit-Zahlen oft in zwei Teilen aus: den 32 hohen Bits und den 32 niedrigen Bits. Ein Beispiel wäre eine Transaktions-ID, wie etwa TRANSACTION 0 3793469. Sie können den Wert der 64-Bit-Zahl berechnen, indem Sie die erste Zahl um 32 Bits nach links verschieben und sie dann zur zweiten Zahl addieren. Wir zeigen später einige Beispiele dafür.
616 |
Kapitel 13: Der MySQL-Serverstatus
Die Ausgabe enthält einige Durchschnittsstatistiken, wie etwa fsync( )-Aufrufe pro Sekunde. Diese zeigen die durchschnittliche Aktivität seit dem Generieren der letzten Ausgabe. Falls Sie also diese Werte untersuchen, müssen Sie wenigstens 30 Sekunden zwischen den Abfragen warten, damit die Stichproben überhaupt erst gesammelt werden können. Die Ausgaben werden nicht alle zum gleichen Zeitpunkt generiert. Die Durchschnittswerte in der Ausgabe werden daher auch nicht alle über das gleiche Zeitintervall berechnet. Darüber hinaus besitzt InnoDB ein internes Reset-Intervall, das unvorhersehbar ist und zwischen den Versionen variiert; untersuchen Sie die Ausgabe, um die Zeit festzustellen, über die die Durchschnittswerte erzeugt wurden, weil diese Zeit nicht unbedingt identisch ist mit der Zeit zwischen den Stichproben. Es gibt in der Ausgabe genügend Informationen, um die Durchschnittswerte für die meisten Statistiken auch von Hand zu berechnen, falls Sie das wollen. Allerdings ist ein Überwachungswerkzeug wie innotop – das inkrementelle Differenzen und Durchschnitte für Sie ermittelt – an dieser Stelle durchaus hilfreich.
Header Der erste Abschnitt ist der Header, der einfach den Anfang der Ausgabe, das aktuelle Datum und die Uhrzeit und die Zeit seit der letzten Ausgabe anzeigt. Zeile 2 zeigt das aktuelle Datum und die Uhrzeit. Zeile 4 zeigt den Zeitrahmen, über den die Durchschnitte berechnet wurden. Dabei handelt es sich entweder um die Zeit seit der letzten Ausgabe oder um die Zeit seit dem letzten internen Reset: 1 2 3 4
===================================== 070913 10:31:48 INNODB MONITOR OUTPUT ===================================== Per second averages calculated from the last 49 seconds
SEMAPHORES Falls Sie eine stark nebenläufige Last haben, sollten Sie Ihr spezielles Augenmerk auf den nächsten Abschnitt richten, SEMAPHORES. Dieser enthält zwei Arten von Daten: Event-Zähler und optional eine Liste der aktuellen Wartezustände. Falls Sie Ärger aufgrund von Engpässen haben, dann kann diese Information Ihnen helfen, die Flaschenhälse zu finden. Die hierbei nötigen Aktionen sind allerdings etwas komplexer, wir kommen jedoch später noch darauf zurück. Hier ist eine Beispielausgabe für diesen Abschnitt: ---------SEMAPHORES ---------OS WAIT ARRAY INFO: reservation count 13569, signal count 11421 --Thread 1152170336 has waited at ./../include/buf0buf.ic line 630 for 0.00 seconds the semaphore: 6 Mutex at 0x2a957858b8 created file buf0buf.c line 517, lock var 0 7 waiters flag 0 8 wait is ending 1 2 3 4 5
SHOW INNODB STATUS | 617
9 --Thread 1147709792 has waited at ./../include/buf0buf.ic line 630 for 0.00 seconds the semaphore: 10 Mutex at 0x2a957858b8 created file buf0buf.c line 517, lock var 0 11 waiters flag 0 12 wait is ending 13 Mutex spin waits 5672442, rounds 3899888, OS waits 4719 14 RW-shared spins 5920, OS waits 2918; RW-excl spins 3463, OS waits 3163
Zeile 4 liefert Informationen über das Warte-Array des Betriebssystems, bei dem es sich um ein Array aus »Slots« handelt. InnoDB reserviert in diesem Array Slots für Semaphore, mit denen das Betriebssystem den Threads signalisiert, dass sie weitermachen und die Arbeit verrichten können, auf die sie warten mussten. Diese Zeile zeigt, wie oft InnoDB Betriebssystem-Wartezustände benutzen musste. Der Reservierungszähler (reservation count) zeigt an, wie oft InnoDB Slots belegt hat, und der Signalzähler (signal count) misst, wie oft Threads über das Array gemeldet wurden. Betriebssystemwartezustände sind im Vergleich zum Warten auf einen Spin viel kostenintensiver, wie wir gleich sehen werden. Die Zeilen 5 bis 12 zeigen die InnoDB-Threads, die momentan auf einen Mutex warten. Das Beispiel zeigt zwei wartende Threads, die jeweils mit dem Text »-- Thread has waited…« beginnen. Dieser Abschnitt sollte leer sein, es sei denn, Ihr Server hat eine stark nebenläufige Belastung, die InnoDB veranlasst, auf Betriebssystemwartezustände zurückzugreifen. Wenn Sie nicht gerade mit dem InnoDB-Quellcode vertraut sind, ist das Nützlichste, wonach Sie hier Ausschau halten können, der Dateiname, unter dem der Thread wartet. Dieser liefert Ihnen einen Hinweis, wo sich in InnoDB die Hot-Spots verstecken. Falls Sie z.B. viele Threads sehen, die auf eine Datei namens buf0buf.ic warten, dann liegt ein Wettstreit um den Puffer-Pool vor. Die Ausgabe deutet an, wie lange der Thread schon wartet, und das »Waiters Flag« zeigt, wie viele Wartende es für den Mutex gibt. Der Text »wait is ending« bedeutet, dass der Mutex tatsächlich bereits frei ist, das Betriebssystem den Thread jedoch noch nicht für die Ausführung eingeteilt hat. Sie werden sich fragen, worauf genau InnoDB wartet. InnoDB benutzt Mutexe und Semaphore, um wichtige Codeabschnitte zu schützen, indem es sie auf jeweils einen einzigen Thread zu einem Zeitpunkt beschränkt, oder um schreibende Operationen zu beschränken, wenn es aktive Leser gibt usw. Es gibt im Code von InnoDB viele wichtige Abschnitte, und unter den richtigen Bedingungen könnte jeder von ihnen hier auftauchen. Gebräuchlich ist z.B. das Erlangen des Zugriffs auf den Puffer-Pool. Hinter der Liste der wartenden Threads zeigen die Zeilen 13 und 14 weitere Event-Zähler. Zeile 13 zeigt verschiedene Zähler, die mit Mutexen zu tun haben, und Zeile 14 ist für Lese-/Schreib-Shared- und exklusive Locks. In allen Fällen können Sie sehen, wie oft InnoDB auf Betriebssystemwartezustände zurückgegriffen hat. InnoDB verfügt über eine mehrphasige Wartestrategie. Zuerst probiert es für die Sperre ein Spin-Warten. Falls es nach einer vorkonfigurierten Anzahl von Spin-Runden (festgelegt mit der Konfigurationsvariablen innodb_sync_spin_loops) nicht erfolgreich war, greift es auf das teurere und komplexe Warte-Array zurück.2
2 Das Warte-Array wurde in MySQL 5.1 geändert und ist jetzt viel effizienter.
618 |
Kapitel 13: Der MySQL-Serverstatus
Spin-Warten ist relativ preiswert, verbrennt aber CPU-Zyklen, weil wiederholt geprüft wird, ob eine Ressource gesperrt werden kann. Das ist nicht so schlimm, wie es klingt, weil es typischerweise freie CPU-Zyklen gibt, während der Prozessor auf die Ein-/Ausgabe wartet. Und selbst wenn es keine freien CPU-Zyklen gibt, ist Spin-Warten oft weniger teuer als die Alternative. Allerdings reißt das Spinning den Prozessor an sich, obwohl ein anderer Thread möglicherweise tätig werden könnte. Die Alternative zum Spin-Warten besteht für das Betriebssystem darin, einen Kontextwechsel vorzunehmen, damit ein anderer Thread ausgeführt werden kann, während der Thread wartet, und dann den schlafenden Thread aufzuwecken, wenn über das Semaphor das entsprechende Signal kommt. Die Signalisierung über ein Semaphor ist effizient, der Kontextwechsel dagegen ist teuer. Die Kosten können sich leicht aufsummieren: Tausende von ihnen pro Sekunde verursachen eine Menge Overhead. Sie können versuchen, ein Gleichgewicht zwischen Spin-Warten und Betriebssystemwartezuständen zu erreichen, indem Sie die Systemvariable innodb_sync_spin_loops ändern. Kümmern Sie sich nicht um Spin-Warten, solange Sie nicht viele (vielleicht im Bereich von Hunderten oder Tausenden) Spin-Runden pro Sekunde sehen. In Kapitel 6 finden Sie weitere Hinweise, wie Sie diesen Teil von InnoDB einstellen.
LATEST FOREIGN KEY ERROR Der nächste Abschnitt, LATEST FOREIGN KEY ERROR, erscheint nur dann, wenn Ihr Server einen Fremdschlüsselfehler bemerkt. Es gibt im Quellcode viele Stellen, die diese Ausgabe generieren können. Die Ausgabe hängt stark von der Art des Fehlers ab. Manchmal sind es eine Transaktion und die Eltern- oder Kindzeilen, nach denen sie gesucht hat, während sie versuchte, einen Datensatz einzufügen, zu aktualisieren oder zu löschen. Dann wieder handelt es sich um einen Typfehler zwischen Tabellen, der auftritt, wenn InnoDB versucht, einen Fremdschlüssel hinzuzufügen oder zu löschen, oder eine Tabelle zu ändern, die bereits einen Fremdschlüssel besitzt. Die Ausgabe dieses Abschnitts eignet sich sehr gut, um die exakten Ursachen der oft sehr vagen Fremdschlüsselfehler von InnoDB zu finden. Wir wollen uns einige Beispiele anschauen. Zuerst erzeugen wir zwei Tabellen mit einem Fremdschlüssel zwischen ihnen und fügen einige Daten ein: CREATE TABLE parent ( parent_id int NOT NULL, PRIMARY KEY(parent_id) ) ENGINE=InnoDB; CREATE TABLE child ( parent_id int NOT NULL, KEY parent_id (parent_id), CONSTRAINT child_ibfk_1 FOREIGN KEY (parent_id) REFERENCES parent (parent_id) ) ENGINE=InnoDB; INSERT INTO parent(parent_id) VALUES(1); INSERT INTO child(parent_id) VALUES(1);
SHOW INNODB STATUS | 619
Es gibt zwei grundlegende Klassen für Fremdschlüsselfehler. Die erste Klasse von Fehlern wird durch das Hinzufügen, Aktualisieren oder Löschen von Daten auf eine Weise verursacht, die eine Verletzung des Fremdschlüssels verursacht. Hier sehen Sie z.B., was passiert, wenn wir die Zeile aus der Elterntabelle löschen: DELETE FROM parent; ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`test/child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (` parent_id`))
Die Fehlermeldung ist ziemlich deutlich. Sie erhalten ähnliche Meldungen für alle Fehler, die durch das Hinzufügen, Aktualisieren oder Löschen nicht passender Zeilen verursacht werden. Dies ist die Ausgabe von SHOW INNODB STATUS: 1 2 3 4 5 6 7 8 9 10 11 12
-----------------------LATEST FOREIGN KEY ERROR -----------------------070913 10:57:34 Transaction: TRANSACTION 0 3793469, ACTIVE 0 sec, process no 5488, OS thread id 1141152064 updating or deleting, thread declared inside InnoDB 499 mysql tables in use 1, locked 1 4 lock struct(s), heap size 1216, undo log entries 1 MySQL thread id 9, query id 305 localhost baron updating DELETE FROM parent Foreign key constraint fails for table `test/child`: , CONSTRAINT `child_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`parent_id`)
13 Trying to delete or update in parent table, in index `PRIMARY` tuple: 14 DATA TUPLE: 3 fields; 15 0: len 4; hex 80000001; asc ;; 1: len 6; hex 00000039e23d; asc 9 =;; 2: len 7; hex 000000002d0e24; asc - $;; 16 17 But in child table `test/child`, in index `parent_id`, there is a record: 18 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 19 0: len 4; hex 80000001; asc ;; 1: len 6; hex 000000000500; asc ;;
Zeile 4 zeigt Datum und Uhrzeit des letzten Fremdschlüsselfehlers. In den Zeilen 5 bis 9 stehen Details über die Transaktion, die den Fremdschlüsselfehler verursacht hat; mehr dazu folgt später. Die Zeilen 10 bis 19 zeigen die genauen Daten, die InnoDB versucht hatte zu ändern, als es den Fehler gefunden hat. Ein Großteil dieser Ausgabe besteht aus den Zeilendaten, konvertiert in druckbares Format; auch dazu sagen wir später mehr. So weit, so gut. Es gibt aber noch eine weitere Klasse von Fremdschlüsselfehlern, die viel schwieriger zu beheben ist. Folgendes geschieht, wenn wir versuchen, die Elterntabelle zu ändern: ALTER TABLE parent MODIFY parent_id INT UNSIGNED NOT NULL; ERROR 1025 (HY000): Error on rename of './test/#sql1570_9' to './test/parent' (errno: 150)
Das ist nicht besonders verständlich, allerdings wirft der SHOW INNODB STATUS-Text ein wenig Licht darauf:
620 |
Kapitel 13: Der MySQL-Serverstatus
1 2 3 4 5 6 7 8 9 10 11 12
-----------------------LATEST FOREIGN KEY ERROR -----------------------070913 11:06:03 Error in foreign key constraint of table test/child: there is no index in referenced table which would contain the columns as the first columns, or the data types in the referenced table do not match to the ones in table. Constraint: , CONSTRAINT child_ibfk_1 FOREIGN KEY (parent_id) REFERENCES parent (parent_id) The index in the foreign key in table is parent_id See http://dev.mysql.com/doc/refman/5.0/en/innodb-foreign-key-constraints.html for correct foreign key definition.
Der Fehler besteht in diesem Fall in einem anderen Datentyp. Spalten mit Fremdschlüsseln müssen exakt denselben Datentyp haben, einschließlich aller Modifikatoren (wie etwa UNSIGNED, was in diesem Fall das Problem war). Immer wenn Sie Fehler 1025 sehen und nicht verstehen, weshalb, hilft ein Blick in SHOW INNODB STATUS.
LATEST DETECTED DEADLOCK Genau wie der Fremdschlüsselabschnitt erscheint der Abschnitt LATEST DETECTED DEADLOCK nur, wenn Ihr Server einen Deadlock hat. Ein Deadlock ist ein Kreis im Waits-for-Graph, einer Datenstruktur aus Zeilensperren, die gehalten werden und auf die gewartet wird. Der Kreis kann beliebig groß sein. InnoDB bemerkt Deadlocks sofort, weil es jedes Mal, wenn eine Transaktion auf eine Zeilensperre warten muss, auf einen Kreis in dem Graph prüft. Deadlocks können ziemlich komplex sein. Dieser Abschnitt zeigt jedoch nur die letzten beiden beteiligten Transaktionen, die letzte Anweisung, die jeweils in der Transaktion ausgeführt wurde, und die Sperren, die den Kreis in dem Graph erzeugt haben. Sie sehen weder weitere Transaktionen, die ebenfalls an dem Kreis beteiligt sein könnten, noch sehen Sie die Anweisung, die tatsächlich früher in einer Transaktion die Sperren angefordert haben könnte. Nichtsdestotrotz können Sie normalerweise herausfinden, was den Deadlock verursacht hat, indem Sie sich diese Ausgabe anschauen. Es gibt eigentlich zwei Arten von InnoDB-Deadlocks. Die erste, an die die meisten Leute gewöhnt sind, ist ein echter Kreis im Waits-for-Graph. Die andere Art ist ein Waits-forGraph, der zu teuer ist, um ihn auf Kreise zu prüfen. Wenn InnoDB mehr als eine Million Sperren in dem Graph zu überprüfen hat oder wenn es bei der Überprüfung mehr als 200 Transaktionen durchläuft, dann gibt es auf und behauptet, es gäbe einen Deadlock. Bei diesen Zahlen handelt es sich um festkodierte Konstanten in der InnoDB-Quelle, die Sie nicht konfigurieren können (obwohl Sie sie natürlich ändern und InnoDB neu kompilieren können). Sie wissen, wenn das Überschreiben dieser Grenzen einen Deadlock verursacht, weil Sie in der Ausgabe die Meldung »TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH« sehen. InnoDB gibt nicht nur die Transaktionen aus und die Sperren, die sie halten und auf die sie warten, sondern auch die Datensätze selbst. Diese Informationen haben vor allem für
SHOW INNODB STATUS | 621
die InnoDB-Entwickler einen Sinn, es gibt jedoch momentan keine Möglichkeit, sie zu deaktivieren. Leider kann dieser Abschnitt so lang werden, dass er den gesamten Platz einnimmt, der für die Ausgabe reserviert ist, und verhindert, dass Sie die folgenden Abschnitte sehen. Sie können dem nur abhelfen, indem Sie einen kleinen Deadlock verursachen, der den großen ersetzt, oder indem Sie einen Patch eines der Autoren dieses Buches verwenden, der unter http://lists.mysql.com/internals/35174 zur Verfügung steht. Hier ist ein Beispiel-Deadlock: 1 2 3 4 5 6 7 8 9 10 11 12
-----------------------LATEST DETECTED DEADLOCK -----------------------070913 11:14:21 *** (1) TRANSACTION: TRANSACTION 0 3793488, ACTIVE 2 sec, process no 5488, OS thread id 1141287232 starting index read mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1216 MySQL thread id 11, query id 350 localhost baron Updating UPDATE test.tiny_dl SET a = 0 WHERE a <> 0 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 0 page no 3662 n bits 72 index `GEN_CLUST_INDEX` of table `test/tiny_dl` trx id 0 3793488 lock_mode X waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 6; hex 000000000501 ...[ Auslassung ] ...
13 14 15 16 *** (2) TRANSACTION: 17 TRANSACTION 0 3793489, ACTIVE 2 sec, process no 5488, OS thread id 1141422400 starting index read, thread declared inside InnoDB 500 18 mysql tables in use 1, locked 1 19 4 lock struct(s), heap size 1216 20 MySQL thread id 12, query id 351 localhost baron Updating 21 UPDATE test.tiny_dl SET a = 1 WHERE a <> 1 22 *** (2) HOLDS THE LOCK(S): 23 RECORD LOCKS space id 0 page no 3662 n bits 72 index `GEN_CLUST_INDEX` of table `test/tiny_dl` trx id 0 3793489 lock mode S 24 Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 25 0: ... [ Auslassung ] ... 26 27 *** (2) WAITING FOR THIS LOCK TO BE GRANTED: 28 RECORD LOCKS space id 0 page no 3662 n bits 72 index `GEN_CLUST_INDEX` of table `test/tiny_dl` trx id 0 3793489 lock_mode X waiting 29 Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 30 0: len 6; hex 000000000501 ...[ Auslassung ] ... 31 32 *** WE ROLL BACK TRANSACTION (2)
Zeile 4 zeigt, wann der Deadlock aufgetreten ist, und die Zeilen 5 bis 10 zeigen Informationen über die erste Transaktion, die an dem Deadlock beteiligt war. Wir erläutern die Bedeutung dieser Ausgabe im nächsten Abschnitt ausführlicher. Die Zeilen 11 bis 15 zeigen die Sperren, auf die Transaktion 1 gewartet hat, als der Deadlock eintrat. Wir haben auf Zeile 14 einige der Informationen weggelassen, die nur für
622 |
Kapitel 13: Der MySQL-Serverstatus
das Debugging von InnoDB hilfreich sind. Wichtig ist Zeile 12, die besagt, dass diese Transaktion eine exklusive (X) Sperre in GEN_CLUST_INDEX3 in der test.tiny_dl-Tabelle haben wollte. Die Zeilen 16 bis 21 zeigen den Status der zweiten Transaktion, in den Zeilen 22 bis 26 stehen die Sperren, die sie gehalten hat. Auf Zeile 25 werden mehrere Datensätze aufgeführt, die wir um der Kürze willen entfernt haben. Einer davon war der Datensatz, auf den die erste Transaktion gewartet hat. Die Zeilen 27 bis 31 schließlich zeigen die Sperren, auf die sie gewartet hat. Ein Kreis im Waits-for-Graph tritt auf, wenn jede Transaktion eine Sperre hält, die die andere haben möchte, und eine Sperre haben möchte, die die andere Transaktion hält. InnoDB zeigt nicht alle Sperren, die gehalten werden und auf die gewartet wird, es zeigt aber genug, damit Sie feststellen können, welche Indizes die Abfragen benutzt haben. Mit dieser Information lässt sich dann ermitteln, ob Sie die Deadlocks vermeiden können. Falls Sie beide Abfragen dazu bringen, den gleichen Index in die gleiche Richtung zu scannen, können Sie die Anzahl der Deadlocks oft verringern, weil Abfragen keinen Kreis verursachen, wenn sie Sperren in derselben Reihenfolge anfordern. Manchmal ist das einfach. Falls Sie z.B. eine Reihe von Datensätzen innerhalb einer Transaktion aktualisieren müssen, dann sortieren Sie sie im Speicher der Anwendung nach ihrem Primärschlüssel, und aktualisieren Sie sie in dieser Reihenfolge – sie können sich dann nicht verklemmen. Zu einer anderen Gelegenheit ist das möglicherweise wiederum nicht machbar (etwa wenn Sie zwei Prozesse haben, die an derselben Tabelle arbeiten müssen, allerdings mit unterschiedlichen Indizes). Zeile 32 zeigt, welche Transaktion als Deadlock-Opfer gewählt wurde. InnoDB versucht, die Transaktion auszuwählen, von der es annimmt, dass sie am einfachsten zurückgenommen werden kann, also diejenige mit den wenigsten Aktualisierungen. Diese Information sollte zum Zwecke der Analyse überwacht und aufgezeichnet werden. Das Maatkit-Werkzeug mk-deadlock-logger bietet hierzu eine bequeme Möglichkeit. Es hilft außerdem, das allgemeine Log zu untersuchen, alle Abfragen der beteiligten Threads zu suchen und festzustellen, wodurch der Deadlock wirklich verursacht wurde. Im nächsten Abschnitt erfahren Sie, wo Sie in der Deadlock-Ausgabe die Thread-ID finden.
TRANSACTIONS Dieser Abschnitt enthält einige zusammenfassende Informationen über InnoDB-Transaktionen, gefolgt von einer Liste der momentan aktiven Transaktionen. Hier sind die ersten Zeilen (der Header): 1 -----------2 TRANSACTIONS 3 ------------
3 Dies ist der Index, den InnoDB intern erzeugt, wenn Sie keinen Primärschlüssel angeben.
SHOW INNODB STATUS | 623
4 5 6 7
Trx id counter 0 80157601 Purge done for trx's n:o <0 80154573 undo n:o <0 0 History list length 6 Total number of lock structs in row lock hash table 0
Die Ausgabe hängt von der MySQL-Version ab, enthält aber wenigstens Folgendes: • Zeile 4: Dies ist der aktuelle Transaktionsidentifikator, eine Systemvariable, die bei jeder neuen Transaktion inkrementiert wird. • Zeile 5: Das ist die Transaktions-ID, in der InnoDB alte MVCC-Zeilenversionen aufgeräumt hat. Sie können feststellen, wie viele alte Versionen noch nicht aufgeräumt wurden, indem Sie sich die Differenz zwischen diesem Wert und der aktuellen Transaktions-ID anschauen. Es gibt keine wirklich feste Regel dafür, wie groß diese Zahl sicher werden kann. Wenn nichts irgendwelche Daten aktualisiert, dann bedeutet eine große Zahl nicht, dass es nichtaufgeräumte Daten gibt, weil alle Transaktionen eigentlich auf die gleiche Version der Datenbank schauen. Falls andererseits viele Zeilen aktualisiert werden, bleiben eine oder mehrere Versionen jeder Zeile im Speicher. Die beste Strategie zum Verringern des Overheads besteht darin, dafür zu sorgen, dass Transaktionen bestätigt werden, wenn sie fertig sind, anstatt sie lange offen zu lassen, weil selbst eine offene Transaktion, die nichts tut, InnoDB davon abhält, alte Zeilenversionen aufzuräumen. Ebenfalls in Zeile 5 erscheint die Datensatznummer des Undo-Logs, in der der Aufräumprozess von InnoDB gerade tätig ist, falls einer vorhanden ist. Wenn die Nummer »0 0« ist, wie in unserem Beispiel, dann ist der Aufräumprozess untätig. • Zeile 6: Hier finden Sie die Länge der History-Liste, also die Anzahl der nichtaufgeräumten Transaktionen im Undo-Raum der InnoDB-Datendateien. Wenn eine Transaktion Aktualisierungen und Commits durchführt, erhöht sich diese Zahl. Entfernt der Aufräumprozess die alten Versionen, dann verkleinert sie sich. Der Aufräumprozess aktualisiert auch den Wert in Zeile 5. • Zeile 7: Hier sehen Sie die Anzahl der Lock-Structs. Jedes Lock-Struct enthält normalerweise viele Zeilensperren, der Wert ist also nicht identisch mit der Anzahl der gesperrten Zeilen. Dem Header folgt eine Liste mit Transaktionen. Aktuelle Versionen von MySQL bieten keine Unterstützung für geschachtelte Transaktionen, es gibt also ein Maximum von einer Transaktion pro Clientverbindung zu einem Zeitpunkt, und jede Transaktion gehört nur zu einer Verbindung. Jede Transaktion umfasst in der Ausgabe wenigstens zwei Zeilen. Hier ist ein Beispiel für die Mindestinformationen, die Sie über eine Transaktion erhalten: 1 ---TRANSACTION 0 3793494, not started, process no 5488, OS thread id 1141152064 2 MySQL thread id 15, query id 479 localhost baron
Die erste Zeile beginnt mit der ID und dem Status der Transaktion. Diese Transaktion ist »not started« (nicht gestartet), was bedeutet, dass sie bestätigt wurde und keine weiteren Anweisungen aufgerufen hat, die Transaktionen beeinflussen; wahrscheinlich ist sie ein-
624 |
Kapitel 13: Der MySQL-Serverstatus
fach nur untätig. Anschließend kommen einige Prozess- und Thread-Informationen. Die zweite Zeile zeigt die MySQL-Prozess-ID, die mit der Id-Spalte in SHOW FULL PROCESSLIST identisch ist. Gefolgt wird diese Information von einer internen Abfragenummer und einigen Verbindungsinformationen (ebenfalls identisch mit denen in SHOW FULL PROCESSLIST). Die einzelnen Transaktionen können jedoch noch viel mehr Informationen ausgeben. Hier ist ein komplexeres Beispiel: 1 ---TRANSACTION 0 80157600, ACTIVE 4 sec, process no 3396, OS thread id 1148250464, thread declared inside InnoDB 442 2 mysql tables in use 1, locked 0 3 MySQL thread id 8079, query id 728899 localhost baron Sending data 4 select sql_calc_found_rows * from b limit 5 5 Trx read view will not see trx with id>= 0 80157601, sees <0 80157597
Zeile 1 in diesem Beispiel zeigt, dass die Transaktion seit vier Sekunden aktiv ist. Die möglichen Zustände sind »not started«, »active«, »prepared« (vorbereitet) und »committed in memory« (bestätigt im Speicher; d.h., sobald die Transaktion auf die Festplatte bestätigt wurde, ändert sich dieser Zustand in »not started«). Es können hier auch Informationen darüber stehen, was die Transaktion momentan macht; das ist in diesem Beispiel allerdings nicht der Fall. Es gibt mehr als 30 Stringkonstanten in der Quelle, die hier ausgegeben werden können, wie etwa »fetching rows«, »adding foreign keys« usw. Der Text »thread declared inside InnoDB 442« auf Zeile 1 bedeutet, dass der Thread eine Operation im InnoDB-Kernel ausführt und noch 442 »Tickets« übrig hat. Mit anderen Worten: Diese SQL-Abfrage darf noch 442-mal in den InnoDB-Kernel eintreten. Das Ticketsystem beschränkt die Thread-Nebenläufigkeit im Kernel, um auf einigen Plattformen eine Thread-Überlastung zu verhindern. Selbst wenn der Status des Threads »inside InnoDB« lautet, führt der Thread nicht unbedingt seine ganze Arbeit innerhalb von InnoDB aus. Die Abfrage könnte einige Operationen auch auf der Serverebene verarbeiten und mit dem InnoDB-Kernel einfach nur irgendwie interagieren. Der Status der Transaktion könnte auch »sleeping before joining InnoDB queue« (schlafend vor dem Eintritt in die InnoDB-Warteschlange) oder »waiting in InnoDB queue« (wartend in der InnoDB-Warteschlange) sein. Die nächste Zeile, die Sie sehen könnten, zeigt, wie viele Tabellen die aktuelle Anweisung benutzt und gesperrt hat. Normalerweise sperrt InnoDB Tabellen nicht, manchmal macht es das aber doch für einige Anweisungen. Gesperrte Tabellen können auch auftauchen, wenn der MySQL-Server sie auf einer höheren Ebene gesperrt hat als InnoDB. Falls die Transaktion irgendwelche Zeilen gesperrt hat, gibt es eine Zeile, die die Anzahl der Lock-Structs (auch dies wieder nicht identisch mit den Zeilensperren) und die Größe des Heaps zeigt; Beispiele dafür finden Sie in der bereits gezeigten Deadlock-Ausgabe. Seit MySQL 5.1 gibt diese Zeile außerdem die tatsächliche Anzahl der Zeilensperren aus, die die Transaktion hält.
SHOW INNODB STATUS | 625
Die Heap-Größe bezeichnet die Menge an Speicher, die für die Aufnahme der Zeilensperren benutzt wird. InnoDB implementiert Zeilensperren mit einer besonderen Tabelle aus Bitmaps, die theoretisch so wenig wie ein Bit pro gesperrter Zeile benutzen kann. Unsere Tests haben ergeben, dass sie im Allgemeinen nicht mehr als vier Bits pro Sperre verwendet. Die dritte Zeile in diesem Beispiel hat ein paar Informationen mehr als die zweite Zeile im vorangegangenen Beispiel: Am Ende der Zeile steht der Thread-Status »Sending data«. Dies ist identisch mit dem, was Sie in der Command-Spalte in SHOW FULL PROCESSLIST finden. Wenn die Transaktion aktiv eine Abfrage ausführt, kommt dann der Text der Abfrage (oder in manchen MySQL-Versionen nur ein Auszug daraus), in diesem Fall in Zeile 4. Zeile 5 zeigt die Lesesicht der Transaktion. Diese kennzeichnet den Bereich der Transaktionsidentifikatoren, der eindeutig sichtbar ist und der für die Transaktion aufgrund der Versionierung eindeutig sichtbar ist. In diesem Fall gibt es eine Lücke von vier Transaktionen zwischen den beiden Zahlen. Diese vier Transaktionen können sichtbar sein, müssen es aber nicht. Wenn InnoDB eine Abfrage ausführt, muss es die Sichtbarkeit aller Zeilen überprüfen, deren Transaktionsidentifikatoren in diese Lücke fallen. Falls die Transaktion auf eine Sperre wartet, sehen Sie außerdem direkt hinter der Abfrage die Lock-Informationen. Auch dafür finden Sie in der genannten Deadlock-Ausgabe Beispiele. Leider sagt die Ausgabe nicht, welche andere Transaktion die Sperre hält, auf die diese Transaktion wartet. Wenn es viele Transaktionen gibt, schränkt InnoDB möglicherweise die Anzahl derjenigen, die es anzeigt, ein, damit die Ausgabe nicht zu groß wird. Falls dies geschieht, sehen Sie die Ausschrift »…truncated…«.
FILE I/O Der FILE I/O-Abschnitt zeigt den Zustand der Ein-/Ausgabe-Helper-Threads zusammen mit Performance-Zählern: 1 2 3 4 5 6 7 8 9 10 11 12
-------FILE I/O -------I/O thread 0 state: waiting for i/o request (insert buffer thread) I/O thread 1 state: waiting for i/o request (log thread) I/O thread 2 state: waiting for i/o request (read thread) I/O thread 3 state: waiting for i/o request (write thread) Pending normal aio reads: 0, aio writes: 0, ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0 Pending flushes (fsync) log: 0; buffer pool: 0 17909940 OS file reads, 22088963 OS file writes, 1743764 OS fsyncs 0.20 reads/s, 16384 avg bytes/read, 5.00 writes/s, 0.80 fsyncs/s
Die Zeilen 4 bis 7 zeigen die Zustände der Ein-/Ausgabe-Helper-Threads. In den Zeilen 8 bis 10 steht die Anzahl der ausstehenden Operationen für die einzelnen Helper-Threads sowie die Anzahl der ausstehenden fsync( )-Operationen für die Log- und Puffer-Pool-
626 |
Kapitel 13: Der MySQL-Serverstatus
Threads. Die Abkürzung »aio« bedeutet »asynchronous I/O«. Zeile 11 zeigt die Anzahl der ausgeführten Lese- und Schreiboperationen sowie der ausgeführten fsync( )-Aufrufe. Diese Variablen eignen sich gut für die Überwachung mit einem der Trend- und Visualisierungssysteme, die wir im nächsten Kapitel vorstellen. Die absoluten Werte hängen von der Belastung Ihres Systems ab, weshalb es wichtig ist, die Änderungen der Werte im Verlauf der Zeit zu beobachten. Zeile 12 zeigt sekundenweise Durchschnittswerte über das Zeitintervall, das im Header-Abschnitt angegeben ist. Die ausstehenden Werte in den Zeilen 8 und 9 eignen sich gut, um eine Ein-/Ausgabegebundene Anwendung zu erkennen. Wenn die meisten dieser Ein-/Ausgaben ausstehende Operationen haben, dann ist die Last wahrscheinlich Ein-/Ausgabegebunden. Unter Windows können Sie die Anzahl der Ein-/Ausgabe-Helper-Threads mit der Konfigurationsvariablen innodb_file_io_threads einstellen, so dass Sie vielleicht mehr als einen Lese- und Schreib-Thread sehen. Sie sehen allerdings auf allen Plattformen wenigstens diese vier Threads: Eingabepuffer-Thread Verantwortlich für Eingabepuffer-Merges (d.h. Datensätze, die aus dem Eingabepuffer in den Tablespace übergehen) Log-Thread Verantwortlich für asynchrone Log-Entleerungen Lese-Thread Führt Read-Ahead-Operationen durch, um zu versuchen, Daten vorab zu holen, von denen InnoDB annimmt, dass es sie brauchen wird. Schreib-Thread Entleert schmutzige Puffer.
INSERT BUFFER AND ADAPTIVE HASH INDEX Dieser Abschnitt zeigt den Status des INSERT BUFFER AND ADAPTIVE HASH INDEX: 1 2 3 4 5 6 7 8
------------------------------------INSERT BUFFER AND ADAPTIVE HASH INDEX ------------------------------------Ibuf for space 0: size 1, free list len 887, seg size 889, is not empty Ibuf for space 0: size 1, free list len 887, seg size 889, 2431891 inserts, 2672643 merged recs, 1059730 merges Hash table size 8850487, used cells 2381348, node heap has 4091 buffer(s) 2208.17 hash searches/s, 175.05 non-hash searches/s
Zeile 4 zeigt Informationen über die Größe des Eingabepuffers, die Länger seiner »freien Liste« und seine Segmentgröße. Der Text »for space 0« scheint auf die Möglichkeit mehrerer Eingabepuffer hinzudeuten – eines pro Tablespace –, aber das wurde niemals implementiert. In aktuellen MySQL-Versionen gibt es diesen Text nicht mehr. Es gibt nur einen Eingabepuffer, so dass Zeile 5 wirklich redundant ist. Zeile 6 zeigt Statistiken darüber, wie viele Pufferoperationen InnoDB durchgeführt hat. Das Verhältnis der Merges zu den Einfügungen liefert einen guten Eindruck davon, wie effizient der Puffer ist.
SHOW INNODB STATUS | 627
Zeile 7 zeigt den Status des adaptiven Hash-Index. Zeile 8 gibt an, wie viele Hash-Indexoperationen InnoDB in dem Zeitrahmen durchgeführt hat, der im Header-Abschnitt angegeben wird. Das Verhältnis der Hash-Index-Lookups zu den Nicht-Hash-IndexLookups ist ein weiteres gutes Effizienzmaß, da Hash-Lookups schneller sind als NichtHash-Lookups. Diese Zeilen sind rein informativ; man kann den adaptiven Hash-Index nicht konfigurieren.
LOG Dieser Abschnitt zeigt Statistiken über das InnoDB-Transaktions-LOG-Subsystem: 1 2 3 4 5 6 7 8
--LOG --Log sequence number 84 3000620880 Log flushed up to 84 3000611265 Last checkpoint at 84 2939889199 0 pending log writes, 0 pending chkp writes 14073669 log i/o's done, 10.90 log i/o's/second
Zeile 4 zeigt die Nummer der aktuellen Log-Sequenz, Zeile 5 gibt die Stelle an, bis zu der die Logs entleert wurden. Bei der Log-Sequenznummer handelt es sich einfach um die Anzahl der Bytes, die in die Log-Dateien geschrieben wurden. Sie können damit also berechnen, wie viele Daten im Log-Puffer noch nicht in die Log-Dateien übertragen wurden. In diesem Fall sind es 9.615 Bytes (13000620880 – 13000611265). Zeile 6 zeigt den letzten Prüfpunkt. (Ein Prüfpunkt kennzeichnet einen Augenblick, in dem die Datenund Log-Dateien sich in einem bekannten Zustand befanden; dies kann für die Wiederherstellung verwendet werden.) Die Zeilen 7 und 8 zeigen ausstehende Log-Operationen und Statistiken, die Sie mit den Werten im Abschnitt FILE I/O vergleichen können, um festzustellen, wie viele Ihrer Ein-/Ausgaben durch Ihr Log-Subsystem verursacht werden und wie viele auf andere Gründe zurückzuführen sind.
BUFFER POOL AND MEMORY Dieser Abschnitt zeigt Statistiken über InnoDBs BUFFER POOL AND MEMORY (in Kapitel 6 erfahren Sie genauer, wie Sie den Puffer-Pool einstellen): 1 2 3 4 5 6 7 8 9 10 11 12 13
628 |
---------------------BUFFER POOL AND MEMORY ---------------------Total memory allocated 4648979546; in additional pool allocated 16773888 Buffer pool size 262144 Free buffers 0 Database pages 258053 Modified db pages 37491 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages read 57973114, created 251137, written 10761167 9.79 reads/s, 0.31 creates/s, 6.00 writes/s Buffer pool hit rate 999 / 1000
Kapitel 13: Der MySQL-Serverstatus
Zeile 4 zeigt den Gesamtspeicher, der von InnoDB alloziert wurde, und gibt an, welcher Anteil davon im zusätzlichen Speicher-Pool alloziert wurde. In den Zeilen 5 bis 8 stehen Puffer-Pool-Maße, gemessen in Seiten. Bei diesen Maßen handelt es sich um die Gesamtpuffer-Pool-Größe, die Anzahl der freien Seiten, die Anzahl der Seiten, die für das Speichern von Datenbankseiten reserviert wurden, und die Anzahl der »schmutzigen« Datenbankseiten. InnoDB verwendet einige Seiten im Puffer-Pool für Lock-Indizes, den adaptiven Hash-Index und andere Systemstrukturen, so dass die Anzahl der Datenbankseiten im Pool niemals mit der gesamten Pool-Größe identisch ist. Die Zeilen 9 und 10 zeigen die Anzahl der ausstehenden Lese- und Schreiboperationen (d.h. die Anzahl der logischen Lese- und Schreiboperationen, die InnoDB für den PufferPool durchführen muss). Diese Werte entsprechen nicht den Werten im Abschnitt FILE I/O, weil InnoDB viele logische Operationen zu einer einzigen physischen Ein-/Ausgabeoperation zusammenfassen könnte. LRU bedeutet »least recently used« (am längsten nicht verwendet). Das ist eine Methode zum Freigeben von Platz für häufig verwendete Seiten, indem weniger häufig verwendete Seiten aus dem Puffer-Pool entfernt werden. Die flush list enthält alte Seiten, die vom Prüfpunktprozess entfernt werden müssen. single page-Schreiboperationen sind unabhängige Seitenschreiboperationen, die nicht zusammengefasst werden. Zeile 8 in dieser Ausgabe zeigt, dass der Puffer-Pool 37491 schmutzige Seiten enthält, die irgendwann auf die Platte übertragen werden müssen (sie wurden im Speicher geändert, nicht jedoch auf der Festplatte). Zeile 10 jedoch gibt an, dass momentan keine Übertragungen (flushes) vorgesehen sind. Das ist kein Problem; InnoDB überträgt sie bei Bedarf. In Zeile 11 erkennen Sie, wie viele Seiten InnoDB gelesen, erzeugt und geschrieben hat. Die pages read- und pages written-Werte beziehen sich auf Daten, die von der Festplatte in den Puffer-Pool gelesen wurden oder umgekehrt. Der pages created-Wert bezeichnet Seiten, die InnoDB im Puffer-Pool alloziert, ohne ihren Inhalt aus der Datendatei zu lesen, weil es sich nicht um den Inhalt kümmert (sie könnten z.B. zu einer Tabelle gehört haben, die inzwischen verworfen wurde). Zeile 13 verrät uns die Trefferrate im Puffer-Pool, mit der die Rate angegeben wird, mit der InnoDB die benötigten Seiten im Puffer-Pool findet. Dies ist ein Maß für die Effizienz des Cache. Es misst die Treffer seit der letzten InnoDB-Statusausgabe. Falls also der Server seitdem ruhig gewesen ist, steht dort »No buffer pool page gets since the last printout«. Aufgrund des Designs von InnoDB können Sie die Puffer-Pool-Trefferrate von InnoDB nicht direkt mit der Trefferrate des Schlüsselpuffers von MyISAM vergleichen.
ROW OPERATIONS Dieser Abschnitt zeigt ROW OPERATIONS und verschiedene InnoDB-Statistiken: 1 2 3 4
-------------ROW OPERATIONS -------------0 queries inside InnoDB, 0 queries in queue
SHOW INNODB STATUS | 629
5 6 7 8 9 10 11
1 read views open inside InnoDB Main thread process no. 10099, id 88021936, state: waiting for server activity Number of rows inserted 143, updated 3000041, deleted 0, read 24865563 0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s ---------------------------END OF INNODB MONITOR OUTPUT ============================
Zeile 4 gibt an, wie viele Threads sich innerhalb des InnoDB-Kernels befinden (wir haben das in unserer Beschreibung des TRANSACTIONS-Abschnitts bereits besprochen). Abfragen in der Warteschlange (queries in queue) sind Threads, die InnoDB noch nicht in den Kernel eingeliefert hat, um die Anzahl der Threads zu begrenzen, die parallel ausgeführt werden. Wie wir bereits erwähnt haben, können Abfragen auch schlafen, bevor sie zum Warten in die Warteschlange gehen. Zeile 5 zeigt, wie viele Lesesichten InnoDB offen hat. Eine Lesesicht ist ein konsistenter MVCC-»Schnappschuss« des Inhalts der Datenbank seit dem Punkt, an dem die Transaktion begonnen hat. Sie können im TRANSACTIONS-Abschnitt feststellen, ob eine bestimmte Transaktion eine Lesesicht hat. Zeile 6 zeigt den Zustand des Haupt-Kernel-Threads. Hier sind die möglichen Statuswerte in MySQL 5.0.45 und 5.1.22: • archiving log (falls das Log-Archiv eingeschaltet ist) • doing background drop tables • doing insert buffer merge • flushing buffer pool pages • flushing log • making checkpoint • purging • reserving kernel mutex • sleeping • suspending • waiting for buffer pool flush to end • waiting for server activity Die Zeilen 7 und 8 zeigen Statistiken für die Anzahl der eingefügten, aktualisierten, gelöschten und gelesenen Zeilen sowie Durchschnittswerte pro Sekunde für diese Werte. Diese Zahlen sollten Sie überwachen, wenn Sie beobachten wollen, wie viel Arbeit InnoDB verrichtet. Die SHOW INNODB STATUS-Ausgabe endet mit den Zeilen 9 bis 13. Falls Sie diesen Text nicht sehen, haben Sie wahrscheinlich einen sehr großen Deadlock, der die Ausgabe abschneidet.
630 |
Kapitel 13: Der MySQL-Serverstatus
SHOW PROCESSLIST Die Prozessliste ist die Liste der Verbindungen oder Threads, die momenten mit MySQL verbunden sind. SHOW PROCESSLIST listet die Threads auf und liefert Informationen über den Zustand jedes einzelnen Threads. Zum Beispiel: mysql> SHOW FULL PROCESSLIST\G *************************** 1. row *************************** Id: 61539 User: sphinx Host: se02:58392 db: art136 Command: Query Time: 0 State: Sending data Info: SELECT a.id id, a.site_id site_id, unix_timestamp(inserted) AS inserted, forum_id, unix_timestamp(p *************************** 2. row *************************** Id: 65094 User: mailboxer Host: db01:59659 db: link84 Command: Killed Time: 12931 State: end Info: update link84.link_in84 set url_to = replace(replace(url_to,'&','&'),'%20','+'), url_prefix=repl
Es gibt verschiedene Werkzeuge (wie etwa innotop), die Ihnen eine fortlaufende Sicht der Prozessliste zeigen können. In den Command- und State-Spalten wird der »Status« des Threads tatsächlich angezeigt. Beachten Sie, dass der erste unserer Prozesse eine Abfrage ausführt und Daten sendet, während der zweite beendet (»killed«) wird, wahrscheinlich weil er zu lange brauchte, um abgeschlossen zu werden und jemand ihn absichtlich mit dem KILL-Befehl beendet hat. Ein Thread kann eine Weile in diesem Zustand bleiben, weil ein Kill möglicherweise nicht sofort abgeschlossen wird. Es könnte z.B. eine Weile dauern, die Transaktion des Threads zurückzunehmen. SHOW FULL PROCESSLIST (mit dem zusätzlichen Schlüsselwort FULL) zeigt den vollständigen
Text der einzelnen Abfragen, der ansonsten nach 100 Zeichen abgeschnitten wird.
SHOW MUTEX STATUS SHOW MUTEX STATUS liefert ausführliche InnoDB-Mutex-Informationen zurück und eignet
sich vor allem, um einen Einblick in Skalierbarkeits- und Nebenläufigkeitsprobleme zu erhalten. Wie bereits erklärt wurde, schützt jeder Mutex einen kritischen Bereich im Code.
SHOW MUTEX STATUS | 631
Die Ausgabe hängt von der MySQL-Version und den Compile-Optionen ab. Manchmal erhält man die Namen der Mutexe und jeweils mehrere Ausgabespalten, manchmal dagegen bekommt man nur einen Dateinamen, eine Zeile und eine Nummer. Möglicherweise müssen Sie ein Skript schreiben, um die Ausgabe zu sammeln, die sehr groß werden kann. Hier ist eine Zeile einer Beispielausgabe: *************************** 1. row *************************** Mutex: &(buf_pool->mutex) Module: buf0buf.c Count: 95 Spin_waits: 0 Spin_rounds: 0 OS_waits: 0 OS_yields: 0 OS_waits_time: 0
Wenn Sie die Ausgabe untersuchen, können Sie feststellen, welche Teile von InnoDB Engpässe darstellen. Das Vorhandensein vieler CPUs kann z.B. Engpässe verursachen. MySQL hat vor Kurzem erst viele InnoDB-Skalierbarkeitsprobleme auf Mehr-CPU-Systemen behoben. Einige Probleme mit Mutexes sind jedoch geblieben. Typische Probleme, die wir gesehen haben, sind AUTO_INCREMENT-Sperren, die global pro Tabelle sind und in InnoDB von einem Mutex geschützt werden, und der Eingabepuffer. Immer, wenn es einen Mutex gibt, besteht die Gefahr des Wettstreits. Folgende Spalten stehen in der Ausgabe: Mutex
Der Mutex-Name. Module
Die Quelldatei, in der der Mutex definiert ist. Count
Gibt an, wie oft etwas den Mutex angefordert hat. Spin_waits
Gibt an, wie oft InnoDB sich für Spin-Warten entschieden hat, bis der Mutex frei war. Erinnern Sie sich daran, dass InnoDB zuerst ein Spin-Warten probiert und dann auf ein Betriebssystem-Warten zurückgreift. Spin_rounds
Gibt an, wie oft InnoDB bei einem Spin-Warten geprüft hat, ob der Mutex frei war. OS_waits
Gibt an, wie oft InnoDB für den Mutex auf Betriebssystem-Warten zurückgegriffen hat. OS_yields
Gibt an, wie oft der Thread, der auf den Mutex wartet, an das Betriebssystem zurückgegeben hat, damit ein anderer Thread ausgeführt werden konnte. OS_waits_time
Wenn die Systemvariable timed_mutexes auf 1 gesetzt ist, gibt dies die Anzahl der Millisekunden an, die mit Warten verbracht wurde. 632 |
Kapitel 13: Der MySQL-Serverstatus
Sie finden die Hot-Spots, indem Sie die relative Größe der Zähler vergleichen. Es gibt drei Hauptstrategien zum Abmildern von Engpässen: Versuchen Sie, InnoDBs Schwachpunkte zu vermeiden, versuchen Sie, die Nebenläufigkeit zu beschränken, oder versuchen Sie, zwischen CPU-intensivem Spin-Warten und ressourcenintensivem Betriebssystem-Warten auszugleichen. Mehr Hinweise zum Einstellen der Nebenläufigkeit von InnoDB finden Sie in »InnoDB bei Nebenläufigkeit anpassen« auf Seite 321.
Status der Replikation MySQL besitzt mehrere Befehle zum Überwachen der Replikation. Auf einem MasterServer zeigt SHOW MASTER STATUS den Replikationsstatus und die Konfiguration des Masters: mysql> SHOW MASTER STATUS\G *************************** 1. row *************************** File: mysql-bin.000079 Position: 13847 Binlog_Do_DB: Binlog_Ignore_DB:
Die Ausgabe enthält die aktuelle Binärlogposition des Masters. Sie erhalten mit SHOW BINARY LOGS eine Liste der Binärlogs: mysql> SHOW BINARY LOGS +------------------+-----------+ | Log_name | File_size | +------------------+-----------+ | mysql-bin.000044 | 13677 | ... | mysql-bin.000079 | 13847 | +------------------+-----------+ 36 rows in set (0.18 sec)
Um die Events in den Binärlogs zu betrachten, benutzen Sie SHOW BINLOG EVENTS. Auf einem Slave-Server können Sie sich den Status und die Konfiguration des Slaves mit SHOW SLAVE STATUS zeigen lassen. Wir verzichten hier auf ein Beispiel für die Ausgabe, weil
sie sehr umfangreich ist, wollen aber einige Dinge dazu anmerken. Erstens können Sie den Status sowohl der Slave-Ein-/Ausgabe-Threads als auch der Slave-SQL-Threads sehen, inklusive eventueller Fehler. Außerdem können Sie sehen, wie weit der Slave in der Replikation zurückliegt. Und schließlich gibt es für die Zwecke des Backups sowie des Klonens von Slaves drei Gruppen von Binärlogkoordinaten in der Ausgabe: Master_Log_File/Read_Master_Log_Pos
Die Position, an der der Ein-/Ausgabe-Thread in den Binärlogs des Masters liest. Relay_Log_File/Relay_Log_Pos
Die Position, an der der SQL-Thread in den Relay-Logs des Slaves ausgeführt wird.
Status der Replikation | 633
Relay_Master_Log_File/Exec_Master_Log_Pos
Die Position, an der der SQL-Thread in den Binärlogs des Masters ausgeführt wird. Dies ist die gleiche logische Position wie Relay_Log_File/Relay_Log_Pos, allerdings in den Binärlogs des Slaves anstatt in denen des Masters. Mit anderen Worten: Falls Sie sich diese beiden Positionen in den Logs anschauen, finden Sie die gleichen LogEvents.
INFORMATION_SCHEMA Die INFORMATION_SCHEMA-Datenbank bezeichnet eine Gruppe von Systemsichten, die im SQL-Standard definiert sind. MySQL implementiert viele der Standardsichten und fügt weitere hinzu. In MySQL 5.1 entsprechen viele der Sichten den MySQL-SHOW-Befehlen, wie etwa SHOW FULL PROCESSLIST und SHOW STATUS. Es gibt jedoch auch Sichten, die keinen dazugehörenden SHOW-Befehl besitzen. Das Schöne an den INFORMATION_SCHEMA-Sichten ist, dass Sie sie mit normalem SQL abfragen können. Dies bietet Ihnen viel mehr Flexibilität als die SHOW-Befehle, die Ergebnismengen produzieren, die Sie nicht sammeln, zusammenführen oder anderweitig mit normalem SQL manipulieren können. Wenn all diese Daten in Systemsichten zur Verfügung stehen, haben Sie die Möglichkeit, interessante und sinnvolle Abfragen zu schreiben. Welche Tabellen enthalten z.B. eine Referenz auf die actor-Tabelle in der Sakila-Beispieldatenbank? Das lässt sich aufgrund der konsistenten Namenskonvention relativ leicht feststellen: mysql> SELECT TABLE_NAME FROM INFORMATION_SCHEMA.COLUMNS -> WHERE TABLE_SCHEMA='sakila' AND COLUMN_NAME='actor_id' -> AND TABLE_NAME <> 'actor'; +------------+ | TABLE_NAME | +------------+ | actor_info | | film_actor | +------------+
Für verschiedene Beispiele in diesem Buch mussten wir Tabellen mit mehrspaltigen Indizes suchen. Hier ist eine Abfrage dafür: mysql> SELECT TABLE_NAME, GROUP_CONCAT(COLUMN_NAME) -> FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE -> WHERE TABLE_SCHEMA='sakila' -> GROUP BY TABLE_NAME, CONSTRAINT_NAME -> HAVING COUNT(*) > 1; +---------------+--------------------------------------+ | TABLE_NAME | GROUP_CONCAT(COLUMN_NAME) | +---------------+--------------------------------------+ | film_actor | actor_id,film_id | | film_category | film_id,category_id | | rental | customer_id,rental_date,inventory_id | +---------------+--------------------------------------+
634 |
Kapitel 13: Der MySQL-Serverstatus
Sie können genau wie bei normalen Tabellen auch komplexere Abfragen schreiben. Bei MySQL Forge (http://forge.mysql.com) lassen sich Abfragen an diesen Sichten finden und auch veröffentlichen. Es gibt Beispiele zum Suchen von duplizierten oder redundanten Indizes, zum Suchen von Indizes mit einer sehr niedrigen Kardinalität und noch vieles mehr. Der größte Nachteil besteht darin, dass die Sichten manchmal im Vergleich zu den entsprechenden SHOW-Befehlen sehr langsam sind. Typischerweise holen sie alle Daten, speichern sie in einer temporären Tabelle und machen die temporäre Tabelle dann für die Abfrage verfügbar. Für die Überwachung, Fehlerbehebung und Feineinstellung ist es oft schneller, den SHOW-Befehl einzutippen, anstatt das vollständige SQL einzugeben, um die Daten aus den Sichten auszuwählen. Momentan können die Sichten auch noch nicht aktualisiert werden. Es ist zwar möglich, die Servereinstellungen aus ihnen abzufragen, Sie können sie aber nicht aktualisieren, um damit Einfluss auf die Konfiguration des Servers zu nehmen. In der Praxis bedeuten diese Einschränkungen, dass Sie für die Konfiguration weiterhin die SHOW- und SET-Befehle brauchen, auch wenn die INFORMATION_SCHEMA-Sichten für andere Aufgaben sehr nützlich sein können.
INFORMATION_SCHEMA | 635
KAPITEL 14
Werkzeuge für High Performance
Die MySQL-Serverdistribution enthält keine Werkzeuge für viele gebräuchliche Aufgaben, wie etwa das Überwachen des Servers oder das Vergleichen von Daten zwischen den Servern. Glücklicherweise hat die treue MySQL-Fangemeinde viele unterschiedliche Werkzeuge zur Verfügung gestellt, so dass Sie nicht unbedingt eigene Programme zusammenbasteln müssen. Viele Unternehmen bieten darüber hinaus kommerzielle Alternativen oder Ergänzungen zu den MySQL-eigenen Werkzeugen. Dieses Kapitel behandelt einige der beliebtesten und wichtigsten Produktivitätswerkzeuge für MySQL. Wir unterteilen die Werkzeuge in fogende Kategorien: Schnittstelle, Überwachung, Analyse und Dienstprogramme.
Schnittstellenwerkzeuge Schnittstellenwerkzeuge helfen Ihnen dabei, Abfragen auszuführen, Tabellen und Benutzer anzulegen und andere Routineaufgaben zu erledigen. Dieser Abschnitt liefert eine kurze Beschreibung einiger der beliebtesten Werkzeuge für diese Zwecke. Im Allgemeinen können Sie alle oder zumindest die meisten Jobs, für die sie benutzt werden, mit SQL-Abfragen oder -Befehlen ausführen. Die hier vorgestellten Werkzeuge dienen der Bequemlichkeit, helfen Ihnen, Fehler zu vermeiden, und beschleunigen Ihre Arbeit.
MySQL Visual Tools MySQL AB bietet eine Gruppe grafischer Werkzeuge, zu denen MySQL Query Browser, MySQL Administrator, MySQL Migration Toolkit und MySQL Workbench gehören. Diese Werkzeuge sind frei verfügbar, Sie können sie zusammen herunterladen und installieren. Sie laufen auf allen verbreiteten Desktop-Betriebssystemen. Früher wiesen diese Werkzeuge viele ärgerliche Fehler auf, aber in letzter Zeit hat man bei MySQL AB große Anstrengungen unternommen, um die Fehler in ihnen zu finden und zu beheben. Mit dem MySQL Query Browser kann man Abfragen ausführen, Tabellen und gespeicherte Prozeduren erzeugen, Daten exportieren und Datenbankstrukturen durchsuchen.
636 |
Es enthält eine Dokumentation der SQL-Befehle und Funktionen von MySQL. Dieses Werkzeug eignet sich besonders für Leute, die MySQL-Datenbanken entwickeln und abfragen. MySQL Administrator konzentriert sich auf die Serveradministration und ist daher besonders für Datenbankadministratoren gedacht und eher nicht für Entwickler oder Analysten. Es unterstützt die Automatisierung von Aufgaben, wie das Erzeugen von Backups, das Anlegen von Benutzern und das Zuweisen von Berechtigungen, sowie das Betrachten von Server-Logs und Statusinformationen. Es enthält eine einfache Überwachungsfunktionalität, wie etwa die grafische Darstellung von Statusvariablen, ist aber nicht so flexibel wie die interaktiven Überwachungswerkzeuge, die wir weiter hinten in diesem Kapitel noch vorstellen werden. Es zeichnet auch keine Statistiken für eine spätere Analyse auf, was viele andere Überwachungswerkzeuge tun. Das Paket enthält darüber hinaus das MySQL Migration Toolkit, mit dessen Hilfe man Datenbanken aus anderen Systemen nach MySQL migrieren kann, sowie das MySQL Workbench-Modellierungswerkzeug. Die Vorteile der MySQL-eigenen Werkzeuge bestehen darin, dass sie frei sind, inzwischen in ziemlich guter Qualität vorliegen und auf den meisten Desktop-Betriebssystemen laufen. Sie verfügen über einfache Funktionen, die für die meisten Aufgaben ausreichend sind. Besonders erwähnenswert sind die Benutzerverwaltung und die Backup-Funktionen in MySQL Administrator und die integrierte Dokumentation in MySQL Query Browser. Der größte Nachteil dieser Werkzeuge besteht darin, dass sie manchmal ein bisschen arg einfach sind, ohne die ganzen Feinheiten, die sogenannte Power-User schätzen und erwarten. Eine vollständige Beschreibung, einschließlich Screenshots, gibt es auf der MySQL-Website unter http://www.mysql.com/products/tools/. MySQL Workbench wurde vor Kurzem von Grund auf neu geschrieben und steht jetzt sowohl als freie als auch als kommerzielle Version zur Verfügung. Die freie Version ist nicht eingeschränkt, allerdings enthält die kommerzielle Version einige Plugins, die dabei helfen, Aufgaben zu automatisieren, damit sie weniger Handarbeit erfordern. Zurzeit ist die neue Version von MySQL Workbench noch im Beta-Stadium.
SQLyog SQLyog ist das beliebteste grafische Werkzeug für MySQL. Es ist hervorragend dafür geeignet, die Produktivität von Datenbankadministratoren und Entwicklern zu unterstützen. Die vollständige Liste aller Funktionen ist zu lang, um sie hier aufzuführen; hier sind aber zumindest einige Höhepunkte: • automatische Codevervollständigung zum schnelleren Schreiben von Abfragen • die Fähigkeit, sich über SSH-Tunnel an entfernten Servern anzumelden
Schnittstellenwerkzeuge | 637
• grafische Werkzeuge und Wizards zur Unterstützung von wesentlichen Aufgaben wie dem Aufbauen von Abfragen • die Fähigkeit, Aufgaben, wie Backups, Datenimporte und Datensynchronisation zeitlich einzutakten • Tastenkürzel • Schemavergleiche, die Zugriff auf Eigenschaften von Objekten, wie Tabellen und Sichten, bieten • Benutzerverwaltung SQLyog besitzt außerdem auch all die Standardfunktionen, die Sie wahrscheinlich erwarten, wie einen Schemaeditor. Dieses Werkzeug gibt es nur für Microsoft Windows – in einer voll ausgebauten Ausgabe gegen Bezahlung und in einer Fassung mit eingeschränkter Funktionalität kostenlos. Weitere Informationen über SQLyog finden Sie unter http://www.webyog.com.
phpMyAdmin phpMyAdmin ist ein beliebtes Administrationswerkzeug, das auf einem Webserver läuft und Ihnen eine browserbasierte Schnittstelle zu Ihren MySQL-Servern bietet. Es verfügt über eine Reihe netter Funktionen für Abfrage und Administration. Seine wichtigsten Vorteile sind seine Plattformunabhängigkeit, ein großer Funktionsumfang und der Zugriff über einen Browser. Der browserbasierte Zugriff ist praktisch, wenn Sie sich nicht in Ihrer gewohnten Umgebung aufhalten und ein Browser alles ist, was Sie haben. Sie können z.B. phpMyAdmin auf gehosteten Servern installieren, auf denen Sie nur FTPZugriff haben und deshalb nicht den mysql-Client oder ein anderes Programm für eine Shell ausführen können. phpMyAdmin ist sicherlich praktisch und kann in vielen Situationen ausreichend sein. Seien Sie jedoch vorsichtig, wenn Sie es auf Systemen installieren, die im Web erreichbar sind. Falls nämlich Ihr Server nicht richtig abgesichert ist, könnten Sie einem Angreifer kaum einen besseren Weg in Ihre Systeme bieten. Gegner von phpMyAdmin sagen, dass es zu viele Funktionen besitzt und zu groß und komplex ist. phpMyAdmin wird auf SourceForge.net vorgehalten, wo es stets zu den Top-Projekten gehört. Weitere Informationen gibt es unter http://sourceforge.net/ projects/phpmyadmin/.
Überwachungswerkzeuge Das Thema der Überwachung von MySQL verlangt fast schon ein eigenes Buch: Es handelt sich um eine große und komplizierte Aufgabe, da die unterschiedlichen Anwendungen auch unterschiedliche Anforderungen haben. Wir können Sie jedoch auf einige der besseren Werkzeuge und Ressourcen auf diesem Gebiet hinweisen.
638 | Kapitel 14: Werkzeuge für High Performance
»Überwachung« gehört zu den Begriffen, die gern mit mehreren Bedeutungen überfrachtet werden, wobei immer davon ausgegangen wird, dass der andere weiß, worum es geht. Unserer Erfahrung nach müssen die meisten MySQL-Abteilungen viele verschiedene Arten der Überwachung durchführen. Wir konzentrieren uns auf Werkzeuge zur nichtinteraktiven und zur interaktiven Überwachung. Die nichtinteraktive Überwachung umfasst normalerweise ein automatisiertes System, das Messungen vornimmt und den Administrator potenziell davon in Kenntnis setzt, wenn ein Parameter aus seinem sicheren Bereich herausfällt. Interaktive Überwachungswerkzeuge dagegen erlauben es Ihnen, einen Server in Echtzeit zu beobachten. Wir stellen in den folgenden Abschnitten diese beiden Kategorien von Werkzeugen getrennt voneinander vor. Vielleicht sind Sie auch noch an anderen Unterscheidungen zwischen den Werkzeugen interessiert, wie etwa an der passiven Überwachung (etwa durch innotop) im Vergleich zu der aktiven Überwachung, bei der Alarme ausgelöst oder Aktionen initiiert werden (z.B. durch Nagios), oder vielleicht suchen Sie nach einem Werkzeug, das ein InformationsWarehouse erzeugt, anstatt nur die aktuellen Statistiken anzuzeigen. Wir werden auf die Qualitäten der einzelnen Werkzeuge hinweisen.
Nichtinteraktive Überwachungssysteme Viele Überwachungssysteme sind nicht speziell dafür gedacht, den MySQL-Server zu überwachen. Stattdessen handelt es sich um allgemeine Systeme, die regelmäßig den Status verschiedener Ressourcen überprüfen sollen, von Maschinen über Router bis hin zu Software (wie etwa MySQL). Normalerweise haben sie eine Art von Plugin-Architektur und werden oft mit fertigen Plugins für MySQL geliefert. Einige dieser Systeme können den Status der Systeme aufzeichnen, die sie überwachen, und sie über Webschnittstellen grafisch darstellen. Viele senden auch einen Alarm oder lösen eine Aktion aus, wenn etwas, das sie überwachen, ausfällt oder eine Sicherheitsgrenze überschreitet. Im Allgemeinen installieren Sie ein solches System auf seinem eigenen Server und setzen es ein, um andere Server zu überwachen. Falls Sie damit wichtige Systeme überwachen, wird es schnell zu einem wesentlichen Bestandteil Ihrer Infrastruktur, so dass Sie wahrscheinlich weitere Schritte unternehmen müssen (z.B. das Überwachungssystem selbst redundant zu machen und mit einem Failover auszustatten). Ein automatisiertes Überwachungssystem, das die History aufzeichnet und Trends zeigt, kann lebensrettend sein, wenn eine MySQL-Instanz unter zunehmender Last immer langsamer wird oder andere Probleme zu verzeichnen hat. Das Beheben von Problemen verlangt oft, dass Sie wissen, was sich geändert hat, was wiederum erfordert, dass Sie die Geschichte Ihres Servers kennen und deshalb diese Geschichte aufzeichnen. Ein System, das Sie alarmiert, wenn etwas komisch aussieht, kann Sie noch vor dem Eintreten der Katastrophe warnen und Ihnen im Ernstfall helfen, Ihre Anstrengungen zu konzentrieren.
Überwachungswerkzeuge | 639
Selbst gebaute Systeme Viele Organisationen beginnen damit, ihre eigenen Überwachungs- und Alarmierungssysteme aufzubauen. Das funktioniert normalerweise ganz gut, wenn nur wenige Systeme und Leute beteiligt sind. Wird Ihre Organisation jedoch größer und komplexer und müssen sich mehr Leute an der Systemadministration beteiligen, dann gehen selbst gebaute Systeme oft in die Knie. Sie überfluten möglicherweise die Mailboxen mit Tausenden von E-Mails, wenn es einmal einen Netzwerkausfall gibt, oder versagen einfach stillschweigend und informieren niemanden über auftretende Probleme. Doppelte oder redundante Benachrichtigungen stellen bei selbst gebauten Systemen ein häufig auftretendes Problem dar und können die Arbeit stark behindern. Falls Sie es in Erwägung ziehen, ein Überwachungswerkzeug zu schreiben – selbst so ein einfaches wie einen cron-Job, der eine Abfrage überprüft und jemandem eine E-Mail schreibt, wenn es ein Problem gibt –, dann sollten Sie gründlich darüber nachdenken. Wahrscheinlich ist es besser, wenn Sie Ihre Zeit und Energie dafür nutzen, den Umgang mit einem der Systeme zu erlernen, die in den folgenden Abschnitten erwähnt sind. Obwohl diese Systeme eine steile Lernkurve aufweisen und vielleicht die Anfangsinvestition nicht zu lohnen scheinen, sparen sie Ihnen auf lange Sicht Zeit und Energie. Eines dieser Systeme zu implementieren, auch wenn es zu Anfang vielleicht nicht so gut funktioniert, ist schließlich der Implementierung eines eigenen Systems vorzuziehen. Und schließlich werden Sie genügend Erfahrungen und Kompetenz erworben haben, um ein Standardüberwachungssystem erfolgreich zu benutzen.
Nagios Nagios (http://www.nagios.org) ist ein Open-Source-Überwachungs- und Alarmierungssystem, das Dienste, die Sie definieren, regelmäßig überprüft und die Ergebnisse mit vorgegebenen oder expliziten Grenzen vergleicht. Wenn die Ergebnisse außerhalb der Grenzen landen, kann Nagios ein Programm ausführen und/oder jemanden über das Problem in Kenntnis setzen. Das Kontakt- und Alarmierungssystem von Nagios erlaubt es Ihnen, Alarme auf unterschiedliche Kontakte zu verteilen, Alarme zu ändern oder sie je nach Tageszeit und anderen Bedingungen an unterschiedliche Stellen zu senden. Außerdem beachtet es planmäßige Ausfallzeiten. Nagios versteht darüber hinaus Abhängigkeiten zwischen Diensten, belästigt Sie also nicht wegen einer nicht laufenden MySQLInstanz, wenn es merkt, dass der Server unerreichbar ist, weil der dazwischenliegende Router ausgefallen ist, oder wenn es merkt, dass der Host selbst nicht läuft. Nagios kann jede ausführbare Datei als Plugin laufen lassen, vorausgesetzt, sie akzeptiert die richtigen Argumente und liefert die richtige Ausgabe. Daher gibt es Nagios-Plugins in vielen Sprachen, einschließlich der Shell, Perl, Python, Ruby und anderen Skriptsprachen. Es gibt sogar eine Website, http://www.nagiosexchange.org, die dem Veröffentlichen und Kategorisieren von Plugins gewidmet ist. Und falls Sie kein Plugin finden können, das genau das tut, was Sie wollen, dann können Sie ganz leicht ein eigenes herstellen. Ein Plugin muss nur die Standardargumente akzeptieren, mit einem passenden Status enden und optional die Ausgabe so ausgeben, dass Nagios sie erfassen kann. 640 | Kapitel 14: Werkzeuge für High Performance
Nagios kann fast alles überwachen, was Sie messen können, und das auf vielen Betriebssystemen, über verschiedene Methoden (einschließlich aktiver Tests, entfernt ausgeführter Plugins und passiver Tests, die lediglich Statusdaten akzeptieren, die von anderen Systemen »herübergeschoben« werden). Es besitzt auch eine Webschnittstelle, über die Sie den Status überprüfen, Grafiken und Visualisierungen Ihres Netzwerks und seines Status anschauen, geplante Ausfallzeiten eintakten und vieles weitere erledigen können. Der größte Nachteil von Nagios ist seine gewaltige Komplexität. Selbst nachdem man es gut kennengelernt hat, ist es schwierig zu warten. Es bewahrt außerdem seine gesamte Konfiguration in Dateien auf, die eine besondere Syntax haben, bei der man leicht Fehler machen kann. Die Änderung dieser Dateien beim Anwachsen Ihrer Systeme ist sehr aufwendig. Schließlich sind seine Fähigkeiten zur grafischen Darstellung, zur Darstellung von Trends und zur Visualisierung begrenzt. Nagios kann Leistungs- und andere Daten in einem MySQL-Server speichern und grafische Darstellungen darauf generieren, ist aber nicht so flexibel wie andere Systeme. Es gibt mehrere Bücher über Nagios; uns gefällt Wolfgang Barths Nagios: System- und Netzwerk-Monitoring (Open Source Press).
Alternativen zu Nagios Nagios ist zwar die beliebteste allgemeine Überwachungs- und Alarmierungssoftware,1 es gibt aber verschiedene Open-Source-Alternativen: Zenoss Zenoss ist in Python geschrieben und besitzt eine browserbasierte Benutzeroberfläche, die Ajax verwendet, um sie schneller und produktiver zu machen. Es kann automatisch Ressourcen im Netzwerk entdecken, und es vereint Überwachung, Alarmierung, Trenddarstellung, grafische Funktionen und das Aufzeichnen historischer Daten in einem einheitlichen Werkzeug. Zenoss verwendet standardmäßig SNMP, um Daten von entfernten Maschinen zu sammeln, kann aber auch SSH einsetzen. Außerdem unterstützt es Nagios-Plugins. Weitere Informationen finden Sie unter http://www.zenoss.com. Hyperic HQ Hyperic HQ ist ein Java-basiertes Überwachungssystem, das sich eher an die sogenannte Unternehmensüberwachung richtet als die meisten anderen Systeme in seiner Klasse. Genau wie Zenoss kann es automatisch Ressourcen entdecken und unterstützt Nagios-Plugins. Seine logische Organisation und Architektur sind aber anders, und es ist ein bisschen »sperriger«. Ob Ihnen dies passt, hängt von Ihren Anforderungen ab sowie davon, was Sie überwachen wollen. Weitere Informationen finden Sie unter http://www.hyperic.com.
1 Wahrscheinlich wollen Sie nie wieder über Überwachungssysteme nachdenken, nachdem Sie einmal Nagios installiert und konfiguriert haben.
Überwachungswerkzeuge | 641
OpenNMS OpenNMS ist in Java geschrieben. Es gibt dafür eine aktive Entwicklergemeinde. Es verfügt über die üblichen Funktionen wie Überwachung und Alarmierung, bietet aber auch grafische Fähigkeiten und Trenddarstellungen. Seine Ziele sind High Performance und Skalierbarkeit, Automatisierung und Flexibilität. Genau wie Hyperic ist es für die Unternehmensüberwachung großer, kritischer Systeme gedacht. Weitere Informationen finden Sie unter http://www.opennms.org. Groundwork Open Source Groundwork Open Source basiert eigentlich auf Nagios. Es kombiniert Nagios und verschiedene andere Werkzeuge zu einem System mit einer Portalschnittstelle. Am besten beschreibt man es wahrscheinlich so: Es ist das System, das Sie sich zusammenbauen würden, wenn Sie ein Experte in Nagios, Cacti und einer Reihe anderer Werkzeuge wären und viel Zeit hätten, diese nahtlos ineinander zu integrieren. Mehr dazu finden Sie unter http://www.groundworkopensource.com. Zabbix Zabbix ist ein Open-Source-Überwachungssystem, das in vielerlei Hinsicht vergleichbar mit Nagios ist, aber einige wichtige Unterschiede aufweist. Zum Beispiel speichert es alle Konfigurationsdaten und weitere Informationen in einer Datenbank, nicht in Konfigurationsdateien. Es speichert außerdem mehr Datenarten als Nagios und kann deshalb bessere Trend- und History-Berichte generieren. Seine Fähigkeiten zur grafischen Darstellung und Visualisierung sind denen von Nagios überlegen. Viele Leute finden außerdem, es sei einfacher zu konfigurieren und flexibler. Darüber hinaus soll es stärkeren Lasten standhalten als Nagios. Andererseits ist die Benutzergemeinde von Zabbix kleiner als die von Nagios, und seine Alarmierungsfähigkeiten sind nicht so gut entwickelt. Mehr Informationen finden Sie unter http://www.zabbix.com.
MySQL Monitoring and Advisory Service Die MySQL-eigene Überwachungslösung ist speziell dafür gedacht, MySQL-Instanzen zu überwachen. Sie kann außerdem einige wesentliche Aspekte der Host-Maschine im Auge behalten. Sie ist nicht Open Source und erfordert den Erwerb von MySQL Enterprise. Ein wichtiger Vorteil dieses Dienstes gegenüber Nagios besteht darin, dass er eine vorgefertigte Menge an Regeln (»Advisor«) bietet, die viele Aspekte der Serverleistung, des Status und der Konfiguration untersuchen. Die Advisors können Lösungen für die Probleme vorschlagen, die sie bemerken, so dass der Systemadministrator nicht ganz allein herausfinden muss, was schiefgegangen ist. Der Dienst verfügt über eine gut gestaltete Anzeige, auf der Statusinformationen für alle Ihre Server auf einmal zu sehen sind. Man könnte dieselben Statistiken zwar auch mit Nagios oder einem anderen System überwachen, es würde aber eine gehörige Menge an Arbeit erfordern, um die notwendigen Plugins zu schreiben und Nagios so zu konfigurieren, dass es die ganzen Maße überwacht, die der MySQL Monitoring and Advisory Service bereits von sich aus bietet.
642 | Kapitel 14: Werkzeuge für High Performance
Nachteilig an diesem Produkt ist, dass Sie den Rest Ihres Netzwerks nicht damit überwachen können; es ist nur für die Überwachung von MySQL gedacht. Außerdem muss auf jedem System, das es überwacht, ein Agent installiert sein. Das passt manchen MySQLAdministratoren nicht, die ihre Systeme lieber auf das Nötigste beschränken. Weitere Informationen gibt es unter http://www.mysql.com/products/enterprise/advisors. html.
MONyog MONyog (http://www.webyog.com) ist ein leichtgewichtiges, agentenloses Überwachungssystem, das einen anderen Ansatz verfolgt als die zuvor erwähnten Werkzeuge. Es soll auf einem Desktop-System laufen, wo es auf einem unbenutzten Port einen HTTPListener startet. Sie können mit Ihrem Browser an diesem Port Informationen über Ihre MySQL-Server sehen. Diese Informationen werden mithilfe einer Kombination aus JavaScript und Flash aufbereitet. Die zugrunde liegende Implementierung verwendet eine JavaScript-Engine, und die gesamte Konfiguration wird über ein JavaScript-Objektmodell erledigt. MONyog ist eigentlich sowohl interaktiv als auch nichtinteraktiv, so dass Sie seine Fähigkeiten für beide Arten der Überwachung in Betracht ziehen können.
RRDTool-basierte Systeme Auch wenn es streng genommen kein Überwachungssystem ist, ist das RRDTool (http://www.rrdtool.org) wichtig genug, um es hier zu erwähnen. Viele Organisationen benutzen irgendeine Art von Skript oder Programm – oft selbst gemacht –, um Informationen von Servern zu extrahieren und sie in RDD-Dateien (Round-Robin Database) zu speichern. RRD-Dateien bilden eine elegante Lösung für viele Situationen, in denen Daten aufgezeichnet und grafisch dargestellt werden müssen. Sie sammeln automatisch eingehende Daten, interpolieren fehlende Daten, falls die eingehenden Daten nicht erwartungsgemäß eintreffen, und besitzen leistungsfähige Grafikwerkzeuge, die wunderbare, charakteristische Diagramme generieren. Es sind verschiedene RRDTool-basierte Systeme verfügbar. Der Multi Router Traffic Grapher oder MRTG (http://oss.oetiker.ch/mrtg/) ist das vollkommene RRDTool-basierte System. Es ist tatsächlich zum Aufzeichnen des Netzwerkverkehrs gedacht, kann aber auch erweitert werden, um andere Dinge aufzuzeichnen und grafisch darzustellen. Munin (http://munin.projects.linpro.no) ist ein System, das Daten für Sie sammelt, diese in das RRDTool packt und dann Diagramme der Daten in unterschiedlicher Auflösung generiert. Es erzeugt statische HTML-Dateien aus der Konfiguration, Sie können sie also leicht durchblättern und sich die Trends anschauen. Es ist leicht, ein Diagramm zu definieren; Sie erzeugen dazu ein Plugin-Skript, dessen Kommandozeilenhilfsausgabe eine besondere Syntax besitzt, die Munin als Anweisungen zum Erzeugen des Diagramms
Überwachungswerkzeuge | 643
erkennt. Zu Munins Nachteilen gehört die Notwendigkeit, einen Agenten auf jedem System zu laden, das es überwacht, sowie die vereinfachende Eine-für-alle-Konfiguration und die Diagrammoptionen, die für manche Anforderungen möglicherweise nicht flexibel genug sind. Cacti (http://www.cacti.net) ist ein weiteres beliebtes System zum Erzeugen von grafischen Darstellungen und Anzeigen von Trends. Es holt die Daten aus den Systemen, speichert sie in RRD-Dateien und erzeugt dann aus den Daten Diagramme, wozu es das RRDTool über eine PHP-Webschnittstelle verwendet. Diese bildet auch die Schnittstelle für die Konfiguration und Verwaltung (Konfigurationsdaten werden in einem MySQLServer gespeichert). Das System ist Template-getrieben, d.h., Sie definieren Templates und wenden sie dann auf Ihre Systeme an. Es kann die Daten mit SNMP oder eigenen Skripten holen. Cricket (http://cricket.sourceforge.net) ist ein Cacti-artiges System, das in Perl geschrieben ist, allerdings ein dateibasiertes Konfigurationssystem besitzt. Ganglia (http://ganglia. sourceforge.net) ähnelt Cacti ebenfalls, soll aber Cluster und Netze aus Systemen überwachen. Sie können daher die gesammelten Daten vieler Server anschauen und auf Wunsch bis zu den einzelnen Servern heruntergehen. (Cacti und Cricket können keine gesammelten Daten anzeigen.) Mit diesen Systemen kann man Daten auf MySQL-Systemen sammeln, aufzeichnen und grafisch darstellen sowie Berichte darüber erstellen. Sie bieten verschiedene Grade an Flexibilität und eignen sich jeweils für etwas unterschiedliche Zwecke. Ihnen allen fehlen wirklich flexible Mittel, um jemanden zu alarmieren, wenn etwas schiefgegangen ist, und einige von ihnen kennen nicht einmal das Konzept »falsch«. Manche Leute betrachten das als Vorteil, weil sie glauben, dass es besser sei, die Aufgaben des Aufzeichnens, des grafischen Darstellens und Alarmierens voneinander zu trennen; um genau zu sein, soll Munin sogar auf Nagios als Alarmierungssystem zurückgreifen. Für andere Leute ist das jedoch ein Nachteil. Ein weiterer Nachteil sind die Zeit und der Aufwand, die Sie investieren müssen, um ein System zu installieren und zu konfigurieren, das fast Ihren Anforderungen entspricht, aber eben nicht ganz. Schließlich müssen Sie Ihre künftigen Bedürfnisse berücksichtigen. RRD-Dateien erlauben es Ihnen nicht, die Daten mit SQL oder anderen Standardmethoden abzufragen. Sie speichern die Daten auch nicht standardmäßig für immer mit hoher Granularität. Viele MySQL-Administratoren sind nicht willens, diese Einschränkungen zu akzeptieren, und entscheiden sich dafür, historische Daten stattdessen in einer relationalen Datenbank abzulegen. Viele Datenbankadministratoren wünschen sich außerdem besser angepasste und flexiblere Möglichkeiten, um Daten aufzuzeichnen, so dass sie schließlich doch ihre eigenen Systeme schreiben oder ein vorhandenes System ändern. Ob RRDTool-basierte Systeme für Ihre Organisation eine gute Lösung darstellen, hängt von Ihrem persönlichen Geschmack ab sowie von den Erfordernissen Ihres Unternehmens und davon, ob Sie die Vorkenntnisse haben, um das System zu administrieren.
644 | Kapitel 14: Werkzeuge für High Performance
Interaktive Werkzeuge Interaktive Werkzeuge sind solche Werkzeuge, die Sie bei Bedarf starten und mit denen Sie sich kontinuierlich anschauen können, was auf Ihrem Server geschieht. Wir konzentrieren uns auf innotop (http://innotop.sourceforge.net). Es gibt aber noch andere, wie etwa mtop (http://mtop.sourceforge.net), mytop (http://jeremy.zawodny.com/mysql/mytop/) sowie webbasierte Klone von mytop.
innotop Baron Schwartz, einer der Autoren dieses Buches, hat innotop geschrieben. Ungeachtet seines Namens beschränkt es sich nicht darauf, die Interna von InnoDB zu überwachen. Dieses Werkzeug wurde durch mytop inspiriert, bietet aber eine viel größere Funktionalität. Es verfügt über viele Modi, um alle Arten von MySQL-Interna zu überwachen, einschließlich aller Informationen, die in SHOW INNODB STATUS zur Verfügung stehen, die es in seinen einzelnen Komponenten analysiert. Sie können mit ihm mehrere MySQL-Instanzen gleichzeitig überwachen, und es lässt sich gut konfigurieren und erweitern. Zu seinen Funktionen gehören: • eine Transaktionsliste, die die aktuellen InnoDB-Transaktionen anzeigt • eine Abfrageliste, die die momentan laufenden Abfragen anzeigt • eine Liste der momentanen Sperren und Wartezustände auf Sperren • Zusammenfassungen des Serverstatus und Variablen zum Anzeigen der relativen Größenordnungen der Werte • Modi zum Anzeigen von Informationen über die InnoDB-Interna, wie etwa seine Puffer, Deadlocks, Fremdschlüsselfehler, Ein-/Ausgabe-Aktivitäten, Zeilenoperationen, Semaphore usw. • Replikationsüberwachung, bei der Master- und Slave-Zustände zusammen angezeigt werden • ein Modus zum Betrachten beliebiger Servervariablen • Servergruppierung, um die Organisation vieler Server zu vereinfachen • ein nichtinteraktiver Modus zum Einsatz in Kommandozeilenskripten innotop lässt sich leicht installieren. Sie können es entweder aus dem Repository Ihres Betriebssystempakets installieren oder es von http://innotop.sourceforge.net herunterladen, entpacken und die normale make install-Routine ausführen: perl Makefile.PL make install
Nachdem Sie es installiert haben, rufen Sie innotop auf der Kommandozeile auf. Es führt Sie durch den Vorgang des Verbindens mit einer MySQL-Instanz. Es kann Ihre ~/.my.cnfOptionsdateien lesen, so dass Sie nichts weiter tun müssen, als den Hostnamen Ihres Servers einzutippen und mehrmals Enter zu drücken. Nach dem Verbinden sind Sie im
Überwachungswerkzeuge | 645
T-Modus (InnoDB Transaction) und sollten eine Liste der InnoDB-Transaktionen sehen (siehe Abbildung 14-1).
Abbildung 14-1: innotop im T-(Transaction)Modus
innotop setzt standardmäßig Filter ein, um störende Daten zu reduzieren (wie immer in innotop können Sie eigene Filter definieren oder die eingebauten Filter anpassen). In Abbildung 14-1 wurden die meisten der Transaktionen herausgefiltert, um nur die aktiven Transaktionen anzuzeigen. Mit der Taste i deaktivieren Sie den Filter und zeigen so viele Transaktionen an, wie auf den Bildschirm passen. innotop gibt in diesem Modus einen Header und eine Haupt-Thread-Liste aus. Der Header zeigt einige der allgemeinen InnoDB-Informationen, wie etwa die Länge der HistoryListe, die Anzahl der nichtaufgeräumten InnoDB-Transaktionen, den Prozentsatz an schmutzigen Puffern im Puffer-Pool usw. Zuallererst sollten Sie auf das Fragezeichen (?) drücken, um den Hilfe-Bildschirm angezeigt zu bekommen. Der Inhalt dieses Bildschirms hängt davon ab, in welchem Modus innotop sich befindet. Es werden allerdings immer alle aktiven Tasten gezeigt, damit Sie alle möglichen Aktionen anschauen können. Abbildung 14-2 zeigt die Hilfe im T-Modus. Wir werden uns die anderen Modi nicht näher anschauen. Sie sollten jedoch erkennen, dass innotop viele Funktionen besitzt. Wir betrachten hier lediglich eine grundlegende Anpassung, um Ihnen zu zeigen, wie Sie die gewünschten Elemente überwachen können. Eine der Stärken von innotop besteht in seiner Fähigkeit, benutzerdefinierte Ausdrücke zu interpretieren, wie etwa Uptime/ Questions, um einen Wert für Abfragen-pro-Sekunde abzuleiten. Das Ergebnis wird entweder seit dem Serverstart und/oder inkrementell seit der letzten Messung ermittelt. Auf diese Weise können Sie ganz einfach Ihre eigenen Spalten zu dieser tabellarischen Anzeige hinzufügen. So besitzt z.B. der Q-(Query List)Modus einen Header, der die allgemeinen Serverinformationen angibt. Wir wollen uns anschauen, wie Sie diesen Header so ändern, dass der Füllstand des Schlüssel-Caches überwacht wird. Starten Sie innotop, und drücken Sie Q, um in den Q-Modus einzutreten. Das Ergebnis sehen Sie in Abbildung 14-3.
646 | Kapitel 14: Werkzeuge für High Performance
Abbildung 14-2: innotop-Hilfe-Bildschirm
Abbildung 14-3: innotop im Q-(Query List)Modus
Der Screenshot wurde allerdings gekürzt, weil wir in dieser Übung nicht an der Abfrageliste interessiert sind, sondern uns nur um den Header kümmern. Der Header zeigt Statistiken für »Now« (womit die inkrementelle Aktivität gemessen wird, seit innotop sich das letzte Mal mit neuen Daten vom Server versorgt hat) und »Total« (was alle Aktivitäten misst, seit der MySQL-Server vor 25 Tagen gestartet wurde). Jede Spalte im Header wurde aus einer Gleichung ermittelt, die Werte aus SHOW STATUS und SHOW VARIABLES umfasst. Die Header, die in Abbildung 14-3 gezeigt werden, sind vorgegeben, Sie können sich aber auch leicht eigene herstellen. Dazu müssen Sie lediglich eine Spalte in die Header-»Tabelle« einfügen. Drücken Sie die ^-Taste, um den Tabelleneditor zu starten, geben Sie dann am Prompt q_header ein, um den TabellenHeader zu bearbeiten (Abbildung 14-4). Eine Tab-Vervollständigung ist eingebaut, es reicht also, wenn Sie q und dann die Tabulatortaste drücken, um das Wort zu vervollständigen.
Überwachungswerkzeuge | 647
Abbildung 14-4: Einen Header hinzufügen (Start)
Danach sehen Sie die Tabellendefinition für den Q-Modus-Header (Abbildung 14-5). Die Tabellendefinition zeigt die Spalten der Tabelle. Die erste Spalte ist ausgewählt. Wir könnten die Auswahl verschieben, umsortieren, die Spalten bearbeiten und Weiteres (drücken Sie ?, um eine vollständige Liste Ihrer Möglichkeiten zu erhalten), wollen aber nur eine neue Spalte anlegen. Drücken Sie die Taste n, und geben Sie den Spaltennamen ein (Abbildung 14-6).
Abbildung 14-5: Einen Header hinzufügen (Auswahl)
Abbildung 14-6: Einen Header hinzufügen (Spalte benennen)
Geben Sie nun den Header der Spalte ein, der oben in der Spalte auftauchen wird (Abbildung 14-7). Wählen Sie schließlich die Quelle der Spalte. Dabei handelt es sich um einen Ausdruck, den innotop intern in eine Funktion kompiliert. Sie können die Namen aus SHOW VARIABLES und SHOW STATUS verwenden, als wären es Variablen in einer Gleichung. Wir verwenden Klammern und Perl-artige »or«-Vorgaben, um die Division durch null zu vermeiden, ansonsten ist diese Gleichung ziemlich einfach. Außerdem benutzen wir eine 648 | Kapitel 14: Werkzeuge für High Performance
innotop-Transformation namens percent( ), um die resultierende Spalte als Prozentwert zu formatieren; mehr dazu erfahren Sie in der innotop-Dokumentation. Abbildung 14-8 zeigt den Ausdruck.
Abbildung 14-7: Einen Header hinzufügen (Text für Spalte)
Abbildung 14-8: Einen Header hinzufügen (Ausdruck zum Berechnen)
Wenn Sie Enter drücken, sehen Sie die wieder die Tabellendefinition, allerdings mit der neuen Spalte am Ende. Drücken Sie mehrmals +, um sie in der Liste nach oben neben die key_buffer_hit-Spalte zu verschieben, und drücken Sie dann q, um den Tabelleneditor zu verlassen. Voilà: Ihre neue Spalte, zwischen KCacheHit und BpsIn (Abbildung 14-9). Es ist leicht, innotop so anzupassen, dass Sie die gewünschten Dinge überwachen. Sie können sogar Plugins schreiben, falls es nicht das tut, was Sie wollen. Weitere Dokumentationen finden Sie unter http://innotop.sourceforge.net.
Abbildung 14-9: Einen Header hinzufügen (Ergebnis)
Analysewerkzeuge Analysewerkzeuge helfen Ihnen dabei, die Server zu untersuchen und Bereiche zu finden, die von einer Optimierung oder Einstellung profitieren könnten. Diese Werkzeuge eignen sich großartig, um bei Performance-Problemen einen Einstieg in eine Lösung zu finden. Falls eines von ihnen ein offensichtliches Problem erkennt, können Sie Ihre Anstrengungen auf diese Stellen konzentrieren und das Problem wahrscheinlich schneller lösen.
Analysewerkzeuge | 649
HackMySQL-Werkzeuge Daniel Nichter betreibt eine Website namens HackMySQL, auf der er einige nützliche MySQL-Werkzeuge anbietet. mysqlreport ist ein Perl-Skript, das die SHOW STATUS-Ausgabe des Servers untersucht, sie in einen leicht lesbaren Bericht umwandelt und ausdruckt. Sie können diesen Bericht viel schneller lesen, als Sie SHOW STATUS untersuchen können, und er ist ziemlich gründlich. Hier ist ein Überblick über die wesentlichen Teile des Berichts (seit Version 3.23): • Der Abschnitt »Key« zeigt, wie Ihre Schlüssel (Indizes) benutzt werden. Falls diese Werte nicht in Ordnung sind, müssen Sie wahrscheinlich die Einstellungen des Schlüssel-Caches anpassen. • Der Abschnitt »Questions« zeigt, welche Arten von Abfragen Ihr Server ausführt, um Ihnen eine Vorstellung davon zu vermitteln, wo sich die Last konzentriert. • Der Abschnitt »SELECT and Sort« zeigt, welche Arten von Abfrageplänen und Sortierstrategien Ihr Server am häufigsten ausführt. Dieser Abschnitt kann Probleme mit der Indizierung oder mit schlecht optimierten Abfragen aufdecken. • Der Abschnitt »Query Cache« zeigt, wie gut Ihr Abfrage-Cache funktioniert. Falls er nicht gut funktioniert, müssen Sie die Einstellungen ändern oder – falls Ihre Arbeit nicht von einer Speicherung im Cache profitiert – sogar den Cache deaktivieren. • Mehrere Abschnitte zeigen Informationen über Tabellen, Sperren, Verbindungen und den Netzwerkverkehr. Hier auftretende Probleme deuten normalerweise auf einen schlecht eingestellten Server hin. • Drei Abschnitte zeigen Maße und Einstellungen für die InnoDB-Performance. Hier auftretende Probleme deuten auf schlechte Servereinstellungen, Hardwareprobleme oder Probleme mit der Abfrage- oder Schema-Optimierung hin. Weitere Informationen gibt es unter http://hackmysql.com/mysqlreport, darunter eine ausführliche Anleitung, wie man Berichte zu interpretieren hat. Sie sollten sich wirklich die Zeit nehmen und lernen, wie man die Berichte lesen muss, vor allem, wenn Sie oft Probleme auf Servern lösen, mit denen Sie nicht vertraut sind. Mit ein wenig Praxis können Sie einen Bericht einfach überfliegen und die Probleme sofort erkennen. mysqlsla (der MySQL Statement Log Analyzer) ist ein weiteres nützliches Werkzeug. Damit können Sie das allgemeine Log aller Abfragen, die auf dem Server ausgeführt werden, das Slow-Query-Log (d.h. Abfragen, die länger als die konfigurierte Maximalzeit zum Ausführen brauchen) oder jedes andere Log analysieren. Es akzeptiert verschiedene Log-Formate und kann viele Logs auf einmal analysieren. In »Feinere Kontrolle über das Logging« auf Seite 70 erfahren Sie mehr über die Analyse der MySQL-Log-Dateien. Andere Programme auf der Site können Ihnen helfen, die Indexbenutzung eines Servers zu analysieren und den MySQL-bezogenen Netzwerkverkehr zu untersuchen.
650 | Kapitel 14: Werkzeuge für High Performance
Maatkit-Analysewerkzeuge Maatkit ist eine weitere Kreation von Baron Schwartz. Dabei handelt es sich um eine Sammlung von Kommandozeilenwerkzeugen, die in Perl geschrieben und dazu gedacht ist, wichtige Funktionalitäten zu bieten, die die MySQL-Produkte nicht liefern. Maatkit steht unter http://maatkit.sourceforge.net zur Verfügung und enthält eine Mischung aus Analysewerkzeugen und Dienstprogrammen. Eines der Analysewerkzeuge ist mk-query-profiler, das Abfragen ausführen kann, während es die Statusvariablen Ihres Servers beobachtet. Es gibt einen ausführlichen, leicht lesbaren Bericht über die Unterschiede vor und nach einer Abfrage aus. Dieser Bericht vermittelt Ihnen ein tieferes Verständnis für die Auswirkungen Ihrer Abfrage auf die Leistung als nur die Ausführungszeit allein. Sie können Abfragen über eine Pipe in die Standardeingabe von mk-query-profiler leiten, eine oder mehrere Dateien mit Abfragen angeben oder es einfach auffordern, Ihren Server zu beobachten, ohne Abfragen auszuführen (das kann hilfreich sein, wenn Sie eine externe Anwendung ausführen). Anstelle von Abfragen können Sie das Programm auch Shell-Befehle ausführen lassen. Der Bericht von mk-query-profiler ist in Abschnitte unterteilt. Der Profiler gibt standardmäßig eine Batch-Zusammenfassung aus. Sie können aber auch einen Bericht über die einzelnen Abfragen oder über ausgewählte Abfragen in dem Batch erhalten, die Sie leicht mit dem ebenfalls enthaltenen Werkzeug mk-profile-compact vergleichen können. Dies sind die wichtigsten Abschnitte des Berichts: • Der Abschnitt »Overall stats« listet Grundlagen auf, wie die Ausführungszeit, die Anzahl der Befehle und den Netzwerkverkehr. • Der Abschnitt »Table and index accesses« zeigt, wie viele verschiedene Arten von Ausführungsplänen der Batch verursacht hat. Falls Sie viele Tabellenscans sehen, bedeutet das wahrscheinlich, dass die Indizes nicht gut an die Abfragen angepasst sind. • Der Abschnitt »Row operations« zeigt, wie viele Low-Level-Handler-Operationen und/oder InnoDB-Operationen der Batch verursacht hat. Schlechte Abfragepläne verursachen eine große Anzahl von Low-Level-Operationen. • Der Abschnitt »I/O operations« zeigt, wie viel Speicher- und Festplattenverkehr der Batch verursacht hat. Ein begleitender Abschnitt zeigt InnoDB-spezifische Datenoperationen. Alles in allem liefert dieser Bericht einen ausführlichen Einblick, wie viel und welche Art von Arbeit der Server durchführt, was deutlich mehr Wert hat, als nur zu messen, wie lange die Abfragen dauern. Das kann Ihnen z.B. helfen, sich zwischen zwei Abfragen zu entscheiden, die unter niedriger Last auf einer kleinen Datenmenge fast genauso lange laufen, die aber unter hoher Last mit vielen Daten ganz anders ausgeführt werden könnten. Damit können Sie außerdem validieren, ob Ihre Optimierungen funktionieren. In diesem Sinne ist es wie ein kleines Benchmark-Werkzeug.
Analysewerkzeuge | 651
Es gibt in dem Toolkit noch einige weitere Analysewerkzeuge: mk-visual-explain Rekonstruiert den Abfrageausführungsplan aus EXPLAIN und zeigt ihn als Baum an, was viele Leute als besser lesbar empfinden. Das ist vor allem dann hilfreich, wenn die Abfragepläne komplexer werden; wir haben bereits EXPLAIN-Ausgaben gesehen, die Hunderte von Zeilen beanspruchten, und es ist fast unmöglich, das tatsächlich zu verstehen. mk-visual-explain eignet sich besonders als Lehrwerkzeug oder wenn Sie versuchen zu lernen, wie man die EXPLAIN-Ausgabe liest. mk-duplicate-key-checker Identifiziert doppelte oder redundante Indizes und Fremdschlüssel, die sehr schlecht für die Performance sein können. Mehr dazu erfahren Sie in »Redundante und duplizierte Indizes« auf Seite 136. mk-deadlock-logger Hält Ausschau nach InnoDB-Deadlocks und zeichnet sie in einer Datei oder Tabelle auf. mk-heartbeat Misst exakt den Replikationsrückstand, ohne dass SHOW SLAVE STATUS (das meist nicht korrekt ist) geprüft werden muss. Es verschiebt standardmäßig die Durchschnitte über die letzten 1, 5 und 15 Minuten. Dies ist eine vollständigere und besser konfigurierbare Implementierung des Heartbeat-Skripts, das in der ersten Ausgabe dieses Buches erwähnt wurde.
MySQL-Dienstprogramme Es sind verschiedene Werkzeuge veröffentlicht wurden, um die Lücken in der Funktionalität zu füllen, die der MySQL-Server und die ihn begleitenden Kommandozeilenwerkzeuge hinterlassen haben. In diesem Abschnitt werden einige davon besprochen.
MySQL Proxy Das MySQL Proxy-Projekt wird von MySQL AB entwickelt und gepflegt, ist unter der GPL lizenziert und wird wahrscheinlich künftig mit dem MySQL-Server verteilt. Momentan ist es noch kein Jahr alt und wird rasant weiterentwickelt.2 Sie finden es zurzeit im Abschnitt Community von http://www.mysql.com, und eine Dokumentation steht im MySQL-Handbuch zur Verfügung. Das Kernkonzept ist eine zustandsbehaftete Anwendung, die das MySQL-Client/ServerProtokoll versteht und zwischen einem Client und einem Server sitzen kann, wo sie transparent ihre Nachrichten weiterleitet. Eine Clientverbindung kann sich genau so mit ihr verbinden, als wenn sie ein Server wäre. Der Proxy erzeugt dann eine Verbindung zu einem echten MySQL-Server und tritt als Vermittler auf. 2 MySQL Proxy entwickelt sich so schnell, dass diese Informationen wahrscheinlich schon wieder überholt sind, wenn Sie dieses Buch lesen.
652 | Kapitel 14: Werkzeuge für High Performance
Diese Funktionalität allein könnte für viele Anwendungen genutzt werden (z.B. Lastausgleich und Failover), der Proxy geht aber noch einen Schritt weiter. Er versteht das Client/Server-Protokoll, kann also die Abfragen und Antworten untersuchen. Außerdem enthält er einen Lua-Interpreter. Sie können also eigene Skripten schreiben und mit Abfragen und Antworten fast alles tun, was Sie sich vorstellen können. Hier sind einige der Möglichkeiten: • Abfragen umschreiben oder filtern. Sie können z.B. Befehle an den Proxy selbst übergeben, indem Sie ein Skript schreiben, das diese erkennt und dann etwas tut, anstatt die Abfrage an den Server zu übergeben. • Neue Ergebnismengen generieren, die vom MySQL-Server zu kommen scheinen, oder diejenigen verwerfen, die der Server generiert hat. • Dynamisch den MySQL-Server anpassen, je nachdem, was er beobachtet. Beispielsweise kann der Proxy das Slow-Query-Log aktivieren oder deaktivieren, oder er kann die Abfragestatistiken verfolgen und als Antwort auf eine Abfrage Antwortzeithistogramme anzeigen. • Abfragen einspeisen, wenn eine Transaktion bestätigt wird (um z.B. einen globalen Transaktionsidentifikator zu erzeugen). Für all diese Möglichkeiten gibt es funktionierenden Code, den Sie aus Online-Artikeln und Open-Source-Repositories herunterladen können. Die Möglichkeiten sind nahezu unbegrenzt, und kreative Benutzer finden mit Sicherheit Anwendungen für den Proxy, an die wir bisher noch nicht gedacht haben. Falls Sie Probleme haben, sich vorzustellen, was Sie damit anfangen können, dann sollten Sie einige Artikel von Giuseppe Maxia oder Jan Kneschke lesen.
Dormando’s Proxy for MySQL Ein anderes GPL-Proxy-Projekt, das etwa zum gleichen Zeitpunkt auftauchte wie MySQL Proxy (und eigentlich zuerst Lua-Skriptfähigkeiten bot) ist Dormando’s Proxy for MySQL. Es war teilweise eine Antwort auf das MySQL Proxy-Projekt, das zu dieser Zeit noch nicht veröffentlicht war und dessen letztendliche Lizenzierung unsicher war. Ebenso wie MySQL Proxy ändert es sich rasend schnell, so dass Sie sich die neueste Version anschauen sollten, um seinen tatsächlichen Status zu erfahren. Seine Website finden Sie unter http://www.consoleninja.net/code/dpm/.
Maatkit-Dienstprogramme Wir haben Maatkit bereits bei den Analysewerkzeugen erwähnt, es enthält aber auch noch eine Reihe von nützlichen Skripten. Die wichtigsten Werkzeuge sind mk-tablechecksum und mk-table-sync, über die wir bereits in »Feststellen, ob Slaves konsistent mit dem Master sind« auf Seite 413 ein paar Worte verloren haben. Abgesehen von den bereits aufgeführten Werkzeugen enthält Maatkit:
MySQL-Dienstprogramme | 653
mk-archiver Führt Aufräum- und Archivierungsarbeiten durch, um Ihre Tabellen frei von unerwünschten Daten zu halten. Dieses Werkzeug soll Daten verschieben, ohne die OLTP-Abfragen zu beeinflussen, Sie können es aber auch benutzen, um ein DataWarehouse aufzubauen oder um veraltete Daten zu suchen und zu entfernen. Es kann Daten in eine Datei und/oder eine andere Tabelle auf eine beliebigen MySQLInstanz schreiben. Es besitzt einen Plugin-Mechanismus, über den man Jobs leicht anpassen kann. Zum Beispiel könnten Sie ein Plugin einsetzen, um Summary-Tabellen in einem Data-Warehouse zu erzeugen, während die Daten in eine Log-Tabelle eingefügt werden. mk-find Ähnlich dem Unix-Befehl find, jedoch für MySQL-Datenbanken und -Tabellen. mk-parallel-dump Führt Multithread-fähige logische Backups durch, wobei die einzelnen Tabellen in Stücke der gewünschten Größe unterteilt werden, um schnellere Backups auf Systemen mit vielen CPUs oder Festplatten zu erreichen. Sie können es als Multithreadfähigen Wrapper für jedes Werkzeug benutzen, es eignet sich also auch zum Durchführen von Multithread-fähigen CHECK TABLE- oder OPTIMIZE TABLE-Operationen (zum Beispiel). Viele Arten von Jobs profitieren von der Parallelisierung auf Systemen mit mehr als einer CPU und Festplatte. mk-parallel-restore Das Partnerprogramm von mk-parallel-dump: lädt Dateien parallel in MySQL. Dieses Werkzeug kann separierte Dateien direkt über LOAD DATA INFILE laden oder SQLDateien an das mysql-Clientprogramm delegieren. Es ist ein cleverer Wrapper um viele Ladeoperationen, wie etwa das Laden komprimierter Dateien durch benannte Pipes. mk-show-grants Kanonisiert, negiert, separiert und sortiert GRANT-Anweisungen für eine einfache Kommandozeilenmanipulation. Eine interessante Anwendung besteht darin, Ihre Datenbankberechtigungen in einem Versionskontrollsystem zu speichern, ohne störende Änderungssätze zu erhalten. mk-slave-delay Sorgt dafür, dass ein Slave hinter seinen Master zurückfällt. Das ist praktisch für die Wiederherstellung nach einer Katastrophe. Wenn auf dem Master eine zerstörerische SQL-Anweisung ausgeführt wird, dann können Sie den Slave stoppen, bevor er diese Anweisung anwendet, das Binärlog bis zu der Anwendung noch einmal abspielen und den Slave zum Master befördern. Das geht typischerweise schneller als das Neuladen des letzten Backups und das Einspielen der Binärlogs eines ganzen Tages.
654 | Kapitel 14: Werkzeuge für High Performance
mk-slave-prefetch Implementiert die Techniken, die in »Den Cache für den Slave-Thread vorbereiten« auf Seite 437 besprochen wurden. Bei manchen Arbeitslasten kann es dazu beitragen, die Replikation auf dem Slave schneller durchzuführen. mk-slave-restart Startet einen Slave nach einem Fehler neu. mk-table-checksum Erzeugt effizient Prüfsummen von Tabelleninhalten auf einem oder mehreren Servern parallel oder verbreitet Prüfsummenabfragen durch Replikation, um die Integrität Ihrer Slaves zu verifizieren. mk-table-sync Sucht effizient die Unterschiede zwischen Tabellen und generiert eine minimale Menge an SQL-Befehlen, um sie aufzulösen. Kann auch mittels Replikation operieren. Baron fügt häufig neue Werkzeuge hinzu, so dass diese Liste wahrscheinlich nicht mehr aktuell ist. Downloads und aktuelle Dokumentationen gibt es immer unter http://maatkit.sourceforge.net.
Weitere Informationsquellen Falls Sie feststellen, dass Sie ständig sich wiederholende oder fehleranfällige Handarbeit mit MySQL durchführen, dann können Sie sicher sein, dass jemand bereits ein Werkzeug oder ein Skript geschrieben hat, das Ihnen die Last abnehmen kann. Dieses Werkzeug zu finden, ist ein anderes Problem. Wir haben viele unser bevorzugten Werkzeuge über den Planet MySQL-Blog-Sammler (http://www.planetmysql.org) und die MySQL Forge-Community-Site (http://forge.mysql.com) kennengelernt. Dies sind großartige Ressourcen, um mehr über MySQL im Allgemeinen zu erfahren. Es gibt darüber hinaus Mailinglisten, IRC-Kanäle und Foren, in denen Sie oft Antworten von freundlichen Gurus bekommen können (durchsuchen Sie aber zuerst die Archive!). Konferenzen stellen einen weiteren wichtigen Ort dar, an dem wir mehr über die MySQL-Werkzeuge und -Techniken gelernt haben. Selbst wenn Sie nicht an den Konferenzen teilnehmen können, so ist es doch oft möglich, sich Folien herunterzuladen oder online Videos anzuschauen. Weitere Informationen über einige der komplexeren Werkzeuge, wie etwa Nagios, finden Sie in Büchern, die sich speziell diesen Werkzeugen widmen. Diese Quellen gehen viel stärker in die Tiefe, als wir das hier können.
Weitere Informationsquellen | 655
ANHANG A
Große Dateien übertragen
Das Kopieren, Komprimieren und Dekomprimieren von riesigen Dateien (oft über ein Netzwerk) gehört zu den üblichen Aufgaben beim Administrieren von MySQL, Initialisieren von Servern, Klonen von Slaves und Durchführen von Backups und Wiederherstellungsoperationen. Die schnellsten und besten Methoden, um diese Aufgaben auszuführen, sind nicht immer die offensichtlichsten, und der Unterschied zwischen guten und schlechten Methoden kann ganz beträchtlich sein. Dieser Anhang zeigt einige Beispiele dafür, wie Sie mithilfe gebräuchlicher Unix-Dienstprogramme ein großes Backup-Image von einem Server auf einen anderen kopieren. Es ist üblich, mit einer unkomprimierten Datei zu beginnen, wie etwa dem InnoDB-Tablespace und den Log-Dateien eines Servers. Die Datei soll außerdem natürlich dekomprimiert werden, wenn Sie mit dem Kopieren der Datei an das Ziel fertig sind. Das andere verbreitete Szenario sieht so aus, dass man mit einer komprimierten Datei beginnt, zum Beispiel mit einem Backup-Image, und mit einer dekomprimierten Datei aufhört. Falls Sie eine eingeschränkte Netzwerkkapazität haben, dann ist es normalerweise keine schlechte Idee, die Dateien in komprimierter Form über das Netzwerk zu schicken. Möglicherweise brauchen Sie auch noch eine sichere Übertragung, damit Ihre Daten nicht kompromittiert werden; für Backup-Images ist das eine übliche Anforderung.
Dateien kopieren Die Aufgabe besteht also darin, das Folgende effizient durchzuführen: 1. Komprimieren Sie (optional) die Daten. 2. Senden Sie sie an eine andere Maschine. 3. Dekomprimieren Sie die Daten an ihrem endgültigen Ziel. 4. Prüfen Sie, dass die Dateien nach dem Kopieren nicht beschädigt wurden.
Wir haben verschiedene Methoden zum Erreichen dieser Ziele Benchmark-Tests unterworfen. Im restlichen Teil dieses Anhangs zeigen wir Ihnen, wie wir das gemacht haben und welches sich als der schnellste Weg herausstellte. 656 |
Für viele der Zwecke, die wir in diesem Buch besprochen haben, wie etwa Backups, sollten Sie überlegen, auf welcher Maschine Sie die Dekomprimierung durchführen wollen. Falls Sie über die nötige Netzwerkbandbreite verfügen, können Sie Ihre Backup-Images unkomprimiert kopieren und die CPU-Ressourcen auf Ihrem MySQL-Server für die Abfragen sparen.
Ein naives Beispiel Wir beginnen mit einem naiven Beispiel dafür, wie man eine unkomprimierte Datei sicher von einer Maschine zur nächsten sendet, sie unterwegs komprimiert und dann wieder dekomprimiert. Auf dem Quellserver, den wir server1 nennen, führen wir Folgendes aus: server1$ gzip -c /backup/mydb/mytable.MYD > mytable.MYD.gz server1$ scp mytable.MYD.gz root@server2:/var/lib/myql/mydb/
Anschließend auf server2: server2$ gunzip /var/lib/mysql/mydb/mytable.MYD.gz
Dies ist wahrscheinlich der einfachste Ansatz. Er ist allerdings nicht besonders effizient, weil er die Schritte, die an der Komprimierung, dem Kopieren und der Dekomprimierung der Datei beteiligt sind, serialisiert. Jeder Schritt erfordert außerdem Lesen von der Festplatte und Schreiben auf die Festplatte, was langsam ist. Was passiert wirklich während der einzelnen Befehle? gzip führt sowohl Lese- als auch Schreiboperationen auf server1 aus, scp liest von server1 und schreibt auf server2, und gunzip liest und schreibt auf server2.
Eine Ein-Schritt-Methode Effizienter ist es, die Datei in einem Schritt zu komprimieren und zu kopieren und auf der anderen Seite zu dekomprimieren. Dieses Mal benutzen wir SSH, das sichere Protokoll, auf dem SCP basiert. Diesen Befehl führen wir auf server1 aus: server1$ gzip -c /backup/mydb/mytable.MYD | ssh root@server2 "gunzip -c - > /var/lib/mysql/mydb/mytable.MYD"
Normalerweise funktioniert das viel besser als die erste Methode, weil die FestplattenEin-/Ausgaben deutlich reduziert werden: Die Festplattenaktivität wird auf das Lesen von server1 und das Schreiben auf server2 beschränkt. Dadurch kann die Festplatte sequenziell arbeiten. Sie können dazu auch die in SSH eingebaute Komprimierung benutzen. Wir haben Ihnen aber auch schon gezeigt, wie Sie mit Pipes komprimieren und dekomprimieren. Pipes bieten Ihnen eine viel größere Flexibilität. Falls Sie z.B. die Datei auf der anderen Seite gar nicht dekomprimieren wollen, müssen Sie auch keine SSH-Komprimierung benutzen.
Dateien kopieren | 657
Sie können diese Methode verbessern, indem Sie einige Optionen ändern. Indem Sie z.B. –1 hinzufügen, beschleunigen Sie die gzip-Komprimierung. Dadurch verringert sich normalerweise die Komprimierungsrate nicht sehr stark, sie kann aber schneller werden, was wichtig ist. Sie können auch andere Komprimierungsalgorithmen verwenden. Falls Sie z.B. eine sehr starke Komprimierung haben wollen und es Ihnen egal ist, wie lange das dauert, setzen Sie einfach bzip2 anstelle von gzip ein. Wollen Sie eine sehr schnelle Komprimierung, dann verwenden Sie stattdessen eine LZO-basierte Archivierung. Die komprimierten Daten sind zwar vielleicht bis zu 20 % größer, die Komprimierung geht aber auch fünfmal schneller.
Verschlüsselungs-Overhead vermeiden SSH ist nicht die schnellste Methode, um Daten über das Netzwerk zu übertragen, weil es zusätzlichen Aufwand für das Ver- und Entschlüsseln erfordert. Falls Sie keine Verschlüsselung benötigen, dann kopieren Sie einfach die »rohen« Bits mit netcat über das Netzwerk. Für nichtinteraktive Operationen, also das, was wir machen, rufen Sie dieses Werkzeug als nc auf. Hier ist ein Beispiel. Zuerst lauschen wir an Port 12345 (jeder unbenutzte Port eignet sich) auf server2 auf die Datei und dekomprimieren alles, was an diesen Port geschickt wird, in die gewünschte Datendatei: server2$ nc -l -p 12345 | gunzip -c - > /var/lib/mysql/mydb/mytable.MYD
Auf server1 starten wir dann eine weitere Instanz von netcat und senden sie an den Port, an dem das Ziel lauscht. Die Option -q weist netcat an, die Verbindung zu schließen, nachdem es das Ende der eingehenden Datei gesehen hat. Dies sorgt dafür, dass die lauschende Instanz die Zieldatei schließt und sich beendet: server1$ gzip -c - /var/lib/mysql/mydb/mytable.MYD | nc -q 1 server2 12345
Eine noch einfachere Technik besteht darin, tar zu benutzen, damit Dateinamen über die Leitung geschickt werden, wodurch eine weitere Fehlerquelle eliminiert wird und die Dateien automatisch an die richtigen Orte geschrieben werden. Die Option z weist tar an, die gzip-Komprimierung und -Dekomprimierung zu benutzen. Dieser Befehl muss auf server2 ausgeführt werden: server2$ nc -l -p 12345 | tar xvzf -
Und hier ist der Befehl für server1: server1$ tar cvzf - /var/lib/mysql/mydb/mytable.MYD | nc -q 1 server2 12345
Sie können diese Befehle in einem einzigen Skript zusammenfassen, das viele Dateien in der Netzwerkverbindung effizient komprimiert und kopiert und dann auf der anderen Seite wieder dekomprimiert.
658 |
Anhang A: Große Dateien übertragen
Weitere Möglichkeiten Eine weitere Möglichkeit ist rsync. rsync ist bequem, weil es es einfach macht, Quelle und Ziel zu spiegeln, und weil es unterbrochene Dateiübertragungen wieder aufnehmen kann. Allerdings funktioniert es nicht so gut, wenn sein Binärdifferenzalgorithmus nicht ausgenutzt werden kann. Sie sollten es möglicherweise für Fälle vorsehen, bei denen Sie wissen, dass der größte Teil der Datei nicht verschickt werden muss – z.B. zum Beenden einer abgebrochenen nc-Kopieroperation. Sie sollten das Kopieren von Dateien ausprobieren, wenn gerade keine Krise vorliegt, da es sicher eine Weile dauern wird, bis Sie die schnellste Methode gefunden haben. Welche Methode am besten funktioniert, hängt von Ihrem System ab. Die größten Einflussfaktoren sind die Anzahl der Festplattenlaufwerke, Netzwerkkarten und CPUs sowie die Frage, wie schnell sie relativ zueinander sind. Es ist keine schlechte Idee, vmstat -n 5 zu überwachen, um festzustellen, ob die Festplatte oder die CPU den Geschwindigkeitsengpass bilden. Falls Sie untätige CPUs haben, können Sie den Vorgang wahrscheinlich beschleunigen, indem Sie mehrere Kopieroperationen parallel ausführen. Falls andererseits die CPU die Schwachstelle bildet und Sie eine große Festplatten- und Netzwerkkapazität haben, dann lassen Sie die Komprimierung weg. Wie beim Erzeugen von Dumps und beim Wiederherstellen bietet es sich oft an, diese Operationen aus Geschwindigkeitsgründen parallel auszuführen. Überwachen Sie auch hier wieder die Leistung des Servers, um festzustellen, ob sich irgendwo eine ungenutzte Kapazität versteckt. Wenn Sie es bei der Parallelisierung übertreiben, bremsen Sie die Arbeit möglicherweise wieder aus.
Dateikopier-Benchmarks Zum Vergleich zeigt Tabelle A-1 Ihnen hier, wie schnell wir eine Beispieldatei über eine normale 100 Mbit/s-Ethernet-Verbindung in einem LAN kopieren konnten. Die Datei war unkomprimiert 738 MByte groß und wurde mit den vorgegebenen Optionen von gzip auf 100 MByte komprimiert. Auf den Quell- und Zielmaschinen stand ausreichend Speicher zur Verfügung, Auch die CPU-Ressourcen und die Festplattenkapazität waren hinreichend großzügig bemessen, das Netzwerk dagegen bildete einen Engpass. Tabelle A-1: Benchmarks für das Kopieren von Dateien über ein Netzwerk Methode
Zeit (Sekunden)
rsync ohne Komprimierung
71
scp ohne Komprimierung
68
nc ohne Komprimierung
67
rsync mit Komprimierung (-z)
63
gzip, scp und gunzip
60 (44 + 10 + 6)
ssh mit Komprimierung
44
nc mit Komprimierung
42
Dateikopier-Benchmarks | 659
Sehen Sie, wie sehr es geholfen hat, die Datei zu komprimieren, wenn sie über das Netzwerk geschickt wurde – bei den drei langsamsten Methoden wurde die Datei nicht komprimiert. Ihre Werte werden jedoch sicher anders aussehen. Falls Sie langsame CPUs und Festplatten und eine Gigabit-Ethernet-Verbindung haben, dann wird das Lesen und Komprimieren der Daten den Engpass darstellen, und es ist vermutlich besser, die Komprimierung zu überspringen. Übrigens ist es oft viel schneller, wenn man eine schnelle Komprimierung benutzt, wie etwa gzip --fast, anstatt die vorgegebenen Komprimierungsstufen zu verwenden, die viel CPU-Zeit benötigen, um die Datei nur ein wenig stärker zu komprimieren. Bei unserem Test verwendeten wir die vorgegebene Komprimierungsstufe. Der letzte Schritt beim Übertragen der Daten besteht darin, zu überprüfen, dass das Kopieren die Dateien nicht beschädigt hat. Es gibt hierfür eine Vielzahl an Methoden, wie etwa md5sum, allerdings ist es sehr teuer, noch einmal einen vollständigen Scan der Datei durchzuführen. Dies ist ein weiterer Grund, weshalb die Komprimierung sinnvoll ist: die Komprimierung selbst umfasst wenigstens einen zyklischen Redundanztest (Cyclic Redundancy Check; CRC), der alle Fehler erfassen sollte. Sie haben also gleich eine Fehlerüberprüfung inklusive.
660 |
Anhang A: Große Dateien übertragen
ANHANG B
EXPLAIN benutzen
In diesem Anhang erfahren Sie, wie Sie EXPLAIN aufrufen, um Informationen über den Abfrageausführungsplan zu erhalten, und wie Sie die Ausgabe interpretieren. Der Befehl EXPLAIN bildet die wichtigste Methode, um festzustellen, wie der Abfrageoptimierer in Hinblick auf die Ausführung der Abfragen entschieden hat. Diese Funktion unterliegt vielen Einschränkungen und sagt nicht immer die Wahrheit; ihre Ausgabe ist aber die beste verfügbare Information und sollte näher untersucht werden, damit Sie qualifizierte Aussagen darüber treffen können, wie Ihre Abfragen ausgeführt werden.
EXPLAIN aufrufen Um EXPLAIN zu benutzen, fügen Sie einfach das Wort EXPLAIN direkt vor das Stichwort SELECT in Ihre Abfrage ein. MySQL setzt ein Flag auf der Abfrage. Wenn es die Abfrage ausführt, sorgt dieses Flag dafür, dass Informationen über jeden Schritt im Ausführungsplan zurückgeliefert werden, anstatt die Abfrage auszuführen. Es werden eine oder mehrere Zeilen zurückgeliefert, die alle Teile des Ausführungsplans und die Ausführungsreihenfolge zeigen. Hier ist das einfachste mögliche Ergebnis von EXPLAIN: mysql> EXPLAIN SELECT 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: NULL type: NULL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: NULL Extra: No tables used
Es gibt pro Tabelle in der Abfrage eine Zeile in der Ausgabe. Wenn die Abfrage zwei Tabellen vereint, gibt es zwei Zeilen in der Ausgabe. Eine unter einem Alias erreichbare
| 661
Tabelle zählt als separate Tabelle. Falls Sie also eine Tabelle mit sich selbst zusammenführen, erhalten Sie zwei Zeilen in der Ausgabe. Die Bedeutung von »Tabelle« wird hier ziemlich breit aufgefasst: Damit kann eine Unterabfrage, das Ergebnis einer UNION usw. gemeint sein. Sie werden später sehen, wieso das so ist. Es gibt zwei wichtige Variationen von EXPLAIN: • EXPLAIN EXTENDED scheint sich wie ein normales EXPLAIN zu verhalten, weist aber den Server an, den Abfrageplan »rückwärts in eine SELECT-Anweisung einzubauen«. Sie sehen diese generierte Anweisung, wenn Sie unmittelbar danach SHOW WARNINGS ausführen. Die Anweisung kommt direkt aus dem Ausführungsplan, nicht aus der ursprünglichen SQL-Anweisung, die an dieser Stelle auf eine Datenstruktur reduziert wurde. In den meisten Fällen ist sie nicht identisch mit der Originalanweisung. Wenn Sie sie untersuchen, können Sie genau feststellen, wie der Abfrageoptimierer die Anweisung umgewandelt hat. EXPLAIN EXTENDED gibt es seit MySQL 5.0. In MySQL 5.1 fügt es die zusätzliche Spalte filtered hinzu (mehr dazu später). • EXPLAIN PARTITIONS zeigt die Partitionen, auf die die Abfrage zugreift, falls sie verfügbar sind. Es gibt diese Variante nur in Versionen ab MySQL 5.1. Näheres zu Partitionen erfahren Sie in »Partitionierte Tabellen« auf Seite 278. Es ist ein häufig gemachter Fehler zu glauben, dass MySQL eine Abfrage nicht ausführt, wenn Sie EXPLAIN hinzufügen. Um genau zu sein: Wenn die Abfrage eine Unterabfrage in der FROM-Klausel enthält, dann führt MySQL tatsächlich die Unterabfrage aus, setzt ihre Ergebnisse in eine temporäre Tabelle und beendet anschließend die Optimierung der äußeren Abfrage. Es muss erst alle diese Unterabfragen verarbeiten, bevor es die äußere Abfrage vollständig optimieren kann, was für EXPLAIN erforderlich ist. Das bedeutet, dass EXPLAIN für den Server wirklich viel Arbeit bedeuten kann, wenn die Anweisung teure Unterabfragen oder Sichten enthält, die den TEMPTABLE-Algorithmus benutzen. Denken Sie daran, dass EXPLAIN eine Schätzung ist, nicht mehr. Manchmal ist es eine gute Schätzung, dann wieder kann es sehr weit von der Wahrheit entfernt sein. Hier sind einige seiner Einschränkungen: • EXPLAIN verrät Ihnen nicht alles darüber, wie Trigger, gespeicherte Funktionen oder UDFs Ihre Abfrage beeinflussen. • Es funktioniert für gespeicherte Prozeduren nicht, obwohl Sie die Abfragen manuell extrahieren und sie individuell mit EXPLAIN verarbeiten können. • Es sagt Ihnen nichts über die Ad-hoc-Optimierungen, die MySQL während der Abfrageausführung durchführt. • Einige der Statistiken, die es zeigt, sind Schätzungen, die sehr ungenau ausfallen können. • Es zeigt Ihnen nicht alles, was es über den Ausführungsplan einer Abfrage zu wissen gibt. (Die MySQL-Entwickler fügen nach Möglichkeit weitere Informationen hinzu.)
662 | Anhang B: EXPLAIN benutzen
• Es unterscheidet nicht zwischen Dingen, die denselben Namen tragen. Beispielsweise benutzt es »filesort« für im Speicher ablaufende Sortierungen und für temporäre Dateien, und es zeigt »Using temporary« für temporäre Tabellen auf der Festplatte und im Speicher an. • Es kann irreführend sein. Es kann z.B. einen vollständigen Indexscan für eine Abfrage mit einem kleinen LIMIT zeigen. (EXPLAIN von MySQL 5.1 zeigt genauere Informationen über die Anzahl der zu untersuchenden Zeilen; frühere Versionen beachteten ein LIMIT dagegen nicht.)
Umschreiben von Nicht-SELECT-Abfragen MySQL erklärt nur SELECT-Abfragen, keine Aufrufe von gespeicherten Routinen oder INSERT-, UPDATE-, DELETE- oder andere Anweisungen. Sie können einige Nicht-SELECTAbfragen jedoch so umschreiben, dass sie EXPLAIN-fähig werden. Dazu müssen Sie die Anweisung lediglich in ein äquivalentes SELECT umwandeln, das auf die gleichen Spalten zugreift. Alle erwähnten Spalten müssen in einer SELECT-Liste, einer Join-Klausel oder einer WHERE-Klausel vorliegen. Nehmen Sie z.B. an, dass Sie die folgende UPDATE-Anweisung so umschreiben wollen, dass sie EXPLAIN-fähig ist: UPDATE sakila.actor INNER JOIN sakila.film_actor USING (actor_id) SET actor.last_update=film_actor.last_update;
Die folgende EXPLAIN-Anweisung ist nicht äquivalent zu UPDATE, weil sie nicht verlangt, dass der Server die last_update-Spalte aus einer der Tabellen holt: mysql> EXPLAIN SELECT film_actor.actor_id -> FROM sakila.actor -> INNER JOIN sakila.film_actor USING (actor_id)\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: index possible_keys: PRIMARY key: PRIMARY key_len: 2 ref: NULL rows: 200 Extra: Using index *************************** 2. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ref possible_keys: PRIMARY key: PRIMARY key_len: 2
EXPLAIN aufrufen | 663
ref: sakila.actor.actor_id rows: 13 Extra: Using index
Dieser Unterschied ist sehr wichtig. Die Ausgabe zeigt, dass MySQL z.B. abdeckende Indizes verwendet, die es nicht benutzen kann, wenn es die last_updated-Spalte holt und aktualisiert. Die folgende Anweisung kommt dem Original schon viel näher: mysql> EXPLAIN SELECT film_actor.last_update, actor.last_update -> FROM sakila.actor -> INNER JOIN sakila.film_actor USING (actor_id)\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: actor type: ALL possible_keys: PRIMARY key: NULL key_len: NULL ref: NULL rows: 200 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: film_actor type: ref possible_keys: PRIMARY key: PRIMARY key_len: 2 ref: sakila.actor.actor_id rows: 13 Extra:
Das Umschreiben von Abfragen ist keine exakte Wissenschaft, oft reicht es jedoch, damit Sie verstehen, was eine Abfrage tut. Sie müssen allerdings verstehen, dass es so etwas wie eine »äquivalente« Leseabfrage zum Zeigen des Plans für eine Schreibabfrage nicht gibt. Eine SELECT-Abfrage muss nur eine Kopie der Daten finden und an Sie zurückliefern. Jede Abfrage, die Daten verändert, muss alle Kopien davon finden und modifizieren, und zwar in allen Indizes. Oft ist das viel teurer als das, was eine äquivalente SELECT-Abfrage zu sein scheint.
Die Spalten in EXPLAIN Die Ausgabe in EXPLAIN enthält immer die gleichen Spalten (mit Ausnahme von EXPLAIN EXTENDED, das in MySQL 5.1 die Spalte filtered hinzufügt, und EXPLAIN PARTITIONS, bei dem es die Spalte partitions zusätzlich gibt). Variabel dagegen sind die Anzahl und der Inhalt der Zeilen. Um unsere Beispiele jedoch übersichtlich zu halten, zeigen wir in diesem Anhang nicht immer alle Spalten.
664 | Anhang B: EXPLAIN benutzen
In den folgenden Abschnitten zeigen wir Ihnen die Bedeutung der einzelnen Spalten in einem EXPLAIN-Ergebnis. Denken Sie daran, dass die Zeilen in der Ausgabe in der Reihenfolge erscheinen, in der MySQL die Teile der Abfrage tatsächlich ausführt. Diese Reihenfolge ist nicht immer identisch mit der Anordnung im ursprünglichen SQL.
Die id-Spalte Diese Spalte enthält immer eine Zahl, die das SELECT identifiziert, zu dem die Zeile gehört. Wenn es in der Anweisung keine Unterabfragen oder Vereinigungen gibt, dann gibt es nur ein SELECT; jede Zeile zeigt also eine 1 in dieser Spalte. Ansonsten sind die inneren SELECT-Anweisungen im Allgemeinen sequenziell nummeriert, entsprechend ihrer Positionen in der Originalanweisung. MySQL unterteilt SELECT-Abfragen in einfache und komplexe Typen. Die komplexen Typen können in drei großen Klassen zusammengefasst werden: einfache Unterabfragen, sogenannte abgeleitete Tabellen (Unterabfragen in der FROM-Klausel)1 und UNIONs. Hier ist eine einfache Unterabfrage: mysql> EXPLAIN SELECT (SELECT 1 FROM sakila.actor LIMIT 1) FROM sakila.film; +----+-------------+-------+... | id | select_type | table |... +----+-------------+-------+... | 1 | PRIMARY | film |... | 2 | SUBQUERY | actor |... +----+-------------+-------+...
Unterabfragen in der FROM-Klausel UNIONs erhöhen die Komplexität der id-Spalte. Hier ist eine einfache Unterabfrage in der FROM-Klausel: mysql> EXPLAIN SELECT film_id FROM (SELECT film_id FROM sakila.film) AS der; +----+-------------+------------+... | id | select_type | table |... +----+-------------+------------+... | 1 | PRIMARY | <derived2> |... | 2 | DERIVED | film |... +----+-------------+------------+...
Wie Sie wissen, wird diese Abfrage mit einer temporären Tabelle ausgeführt. MySQL verweist auf die temporäre Tabelle intern über ihren Alias (der) in der äußeren Abfrage, den Sie in komplizierteren Abfragen in der ref-Spalte finden. Hier ist schließlich eine UNION-Abfrage: mysql> EXPLAIN SELECT 1 UNION ALL SELECT 1; +------+--------------+------------+... | id | select_type | table |... +------+--------------+------------+... | 1 | PRIMARY | NULL |... 1 Die Aussage, »Eine Unterabfrage in der FROM-Klausel ist eine abgeleitete Tabelle«, ist wahr, während »Eine abgeleitete Tabelle ist eine Unterabfrage in der FROM-Klausel« falsch ist. Der Begriff »abgeleitete Tabelle « besitzt in SQL eine breitere Bedeutung.
Die Spalten in EXPLAIN | 665
| 2 | UNION | NULL |... | NULL | UNION RESULT | |... +------+--------------+------------+...
Beachten Sie die zusätzliche Spalte in der Ausgabe für das Ergebnis des UNION. UNIONErgebnisse werden immer in eine temporäre Tabelle gesetzt; MySQL liest die Ergebnisse dann zurück aus der temporären Tabelle. Die temporäre Tabelle erscheint im ursprünglichen SQL nicht, ihre id-Spalte ist daher NULL. Im Gegensatz zum vorangegangenen Beispiel (das eine Unterabfrage in der FROM-Klausel veranschaulicht) wird die temporäre Tabelle, die aus dieser Abfrage resultiert, als letzte Zeile in den Ergebnissen gezeigt, nicht als erste Zeile. Bis hierher ist das alles noch ganz einfach, allerdings können Mischungen aus diesen drei Kategorien von Anweisungen dafür sorgen, dass die Ausgabe komplizierter wird, wie wir gleich sehen werden.
Die select_type-Spalte Diese Spalte zeigt, ob die Zeile ein einfaches oder ein komplexes SELECT ist (und falls sie zur zweiten Gruppe gehört, um welchen der drei komplexen Typen es sich handelt). Der Wert SIMPLE bedeutet, dass die Abfrage keine Unterabfragen oder UNIONs enthält. Besitzt die Abfrage einen dieser komplexen Unterbestandteile, dann wird der äußerste Teil mit PRIMARY bezeichnet, und die anderen Teile erhalten dann folgende Bezeichnungen: SUBQUERY Ein SELECT, das in einer Unterabfrage in der SELECT-Liste enthalten ist (mit anderen Worten, nicht in der FROM-Klausel), wird als SUBQUERY bezeichnet. DERIVED
Der Wert DERIVED wird für ein SELECT verwendet, das in einer Unterabfrage in der FROM-Klausel enthalten ist, die MySQL rekursiv ausführt und in eine temporäre Tabelle setzt. Der Server nennt dies intern eine »abgeleitete Tabelle«, weil die temporäre Tabelle aus der Unterabfrage abgeleitet wird. UNION
Das zweite und nachfolgende SELECTs in einem UNION werden als UNION bezeichnet. Das erste SELECT wird so bezeichnet, als würde es als Teil der äußeren Abfrage ausgeführt werden. Aus diesem Grund zeigte das vorangegangene Beispiel das erste SELECT in dem UNION als PRIMARY. Wäre das UNION in einer Unterabfrage der FROMKlausel enthalten, dann würde sein erstes SELECT als DERIVED bezeichnet werden. UNION RESULT Das SELECT, das verwendet wird, um Ergebnisse aus der temporären Tabelle des UNION zu holen, wird als UNION RESULT bezeichnet.
Zusätzlich zu diesen Werten können ein SUBQUERY und ein UNION als DEPENDENT und UNCACHEABLE bezeichnet werden. DEPENDENT bedeutet, dass das SELECT von Daten abhängt, die in einer äußeren Abfrage gefunden werden; UNCACHEABLE heißt, dass etwas in dem SELECT mit einem Item_cache verhindert, dass die Ergebnisse im Cache gespeichert werden.
666 | Anhang B: EXPLAIN benutzen
(Item_cache ist nicht dokumentiert; es ist nicht identisch mit dem Abfrage-Cache, obwohl es mit einigen der gleichen Arten von Konstrukten, wie etwa der RAND( )-Funktion, vereitelt werden kann.)
Die table-Spalte Diese Spalte zeigt, auf welche Tabelle die Zeile zugreift. Meist ist das einfach: Es ist die Tabelle oder ihr Alias, falls das SQL einen angibt. Sie lesen diese Spalte von oben nach unten, um die Join-Reihenfolge zu sehen, die der Join-Optimierer von MySQL für die Abfrage gewählt hat. Sie können z.B. feststellen, dass MySQL eine andere Join-Reihenfolge gewählt hat, als für die folgende Abfrage angegeben ist: mysql> EXPLAIN SELECT film.film_id -> FROM sakila.film -> INNER JOIN sakila.film_actor USING(film_id) -> INNER JOIN sakila.actor USING(actor_id); +----+-------------+------------+... | id | select_type | table |... +----+-------------+------------+... | 1 | SIMPLE | actor |... | 1 | SIMPLE | film_actor |... | 1 | SIMPLE | film |... +----+-------------+------------+...
Erinnern Sie sich an die Left-Deep-Tree-Diagramme, die wir in »Der Ausführungsplan« auf Seite 185 gezeigt haben? Die Ausführungspläne von MySQL sind immer Left-Deep Trees. Wenn Sie den Plan auf die Seite legen, dann können Sie die Blattknoten in der Reihenfolge herauslesen; sie entsprechen direkt den Zeilen in EXPLAIN. Der Plan für die vorangegangene Abfrage sieht so aus wie Abbildung B-1.
Abbildung B-1: Wie der Abfrageausführungsplan den Zeilen in EXPLAIN entspricht
Abgeleitete Tabellen und Vereinigungen Die table-Spalte wird viel komplizierter, wenn es eine Unterabfrage in der FROM-Klausel oder einer UNION gibt. In diesen Fällen gibt es eigentlich keine »Tabelle«, auf die man verweist, weil die temporäre Tabelle, die MySQL erzeugt, nur existiert, während die Abfrage ausgeführt wird.
Die Spalten in EXPLAIN | 667
Wenn es in der FROM-Klausel eine Unterabfrage gibt, dann hat die table-Spalte die Form <derivedN>, wobei N die id der Unterabfrage ist. Das ist immer eine »Vorwärtsreferenz« – mit anderen Worten: N verweist auf eine spätere Zeile in der EXPLAIN-Ausgabe. Wenn es ein UNION gibt, enthält die UNION RESULT-table-Spalte eine Liste der ids, die an der UNION teilnehmen. Das ist immer eine »Rückwärtsreferenz«, weil das UNION RESULT nach all den Zeilen kommt, die an der UNION teilnehmen. Wenn es mehr als etwa 20 ids in der Liste gibt, wird die table-Spalte abgeschnitten, damit sie nicht zu lang wird, und Sie können nicht mehr alle Werte sehen. Glücklicherweise ist es trotzdem möglich, Rückschlüsse auf die enthaltenen Zeilen zu ziehen, weil man die id der ersten Zeile sehen kann. Alles, was zwischen dieser Zeile und dem UNION RESULT kommt, wird irgendwie einbezogen.
Ein Beispiel für komplexe SELECT-Typen Hier ist eine Nonsense-Abfrage, die als relativ kompaktes Beispiel für einige der komplexen SELECT-Typen dient: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
EXPLAIN SELECT actor_id, (SELECT 1 FROM sakila.film_actor WHERE film_actor.actor_id = der_1.actor_id LIMIT 1) FROM ( SELECT actor_id FROM sakila.actor LIMIT 5 ) AS der_1 UNION ALL SELECT film_id, (SELECT @var1 FROM sakila.rental LIMIT 1) FROM ( SELECT film_id, (SELECT 1 FROM sakila.store LIMIT 1) FROM sakila.film LIMIT 5 ) AS der_2;
Die LIMIT-Klauseln dienen nur der Bequemlichkeit, falls Sie die Abfrage ohne EXPLAIN ausführen und die Ergebnisse sehen wollen. Hier ist das Ergebnis des EXPLAIN: +------+----------------------+------------+... | id | select_type | table |... +------+----------------------+------------+... | 1 | PRIMARY | <derived3> |... | 3 | DERIVED | actor |... | 2 | DEPENDENT SUBQUERY | film_actor |... | 4 | UNION | <derived6> |... | 6 | DERIVED | film |... | 7 | SUBQUERY | store |... | 5 | UNCACHEABLE SUBQUERY | rental |... | NULL | UNION RESULT | |... +------+----------------------+------------+...
668 | Anhang B: EXPLAIN benutzen
Wir haben sorgfältig darauf geachtet, dass jeder Teil der Abfrage auf eine andere Tabelle zugreift, damit Sie sehen können, was wohin geht. Es ist trotzdem schwer zu erkennen! Sehen wir uns das Ganze also mal von oben her an: • Die erste Zeile ist eine Vorwärtsreferenz auf der_1, das die Abfrage als <derived3> bezeichnet hat. Es stammt aus Zeile 2 im ursprünglichen SQL. Um festzustellen, welche Zeilen in der Ausgabe sich auf SELECT-Anweisungen beziehen, die Teil von <derived3> sind, schauen Sie weiter… • …in die zweite Zeile, deren id 3 lautet. Sie ist 3, weil sie Teil des dritten SELECT in der Abfrage ist, und es ist als DERIVED-Typ aufgeführt, weil sie in eine Unterabfrage in der FROM-Klausel eingeschachtelt ist. Sie stammt aus den Zeilen 6 und 7 im ursprünglichen SQL. • Die id der dritten Zeile lautet 2. Sie stammt aus Zeile 3 im ursprünglichen SQL. Beachten Sie, dass sie nach einer Zeile mit einer höheren id-Nummer kommt, wodurch suggeriert wird, dass sie danach ausgeführt wird, was sinnvoll ist. Sie wird als DEPENDENT SUBQUERY aufgeführt. Das bedeutet, dass ihr Ergebnis von den Ergebnissen einer äußeren Abfrage abhängt (auch bekannt als korrelierte Unterabfrage). Die äußere Abfrage ist in diesem Fall das SELECT, das in Zeile 2 beginnt und Daten aus der_1 holt. • Die vierte Zeile wird als ein UNION aufgeführt, was bedeutet, dass es das zweite oder ein späteres SELECT in einem UNION ist. Ihre Tabelle ist <derived6>, d.h., sie bezieht Daten aus einer Unterabfrage in der FROM-Klausel und hängt sie für das UNION an eine temporäre Tabelle. Um die EXPLAIN-Zeilen zu finden, die den Abfrageplan für diese Unterabfrage zeigen, müssen Sie auch hier nach vorn schauen. • Die fünfte Zeile ist die der_2-Unterabfrage, die in den Zeilen 13, 14 und 15 im ursprünglichen SQL definiert ist und die EXPLAIN als <derived6> bezeichnet. • Die sechste Zeile ist eine normale Unterabfrage in der SELECT-Liste von <derived6>. Ihre id ist 7, was wichtig ist… • …weil sie größer als 5 ist, die id der siebenten Zeile. Wieso ist das wichtig? Weil es die Grenzen der <derived6>-Unterabfrage zeigt. Wenn EXPLAIN eine Zeile ausgibt, deren SELECT-Typ DERIVED ist, dann repräsentiert dies den Beginn eines »geschachtelten Bereichs«. Wenn die id einer nachfolgenden Zeile kleiner ist (in diesem Fall ist 5 kleiner als 6), dann bedeutet dies, dass der geschachtelte Bereich geschlossen wurde. Wir wissen jetzt, dass die siebente Zeile Teil der SELECT-Liste ist, die Daten aus <derived6> geholt hat – d.h. Teil der SELECT-Liste der vierten Zeile (Zeile 11 im ursprünglichen SQL). Dieses Beispiel ist recht einfach zu verstehen, ohne dass man die Bedeutung und die Regeln von geschachtelten Bereichen kennen muss. Manchmal ist es jedoch nicht so einfach. Die andere bemerkenswerte Sache über diese Zeile in der Ausgabe ist, dass sie aufgrund der Benutzervariablen als UNCACHEABLE SUBQUERY aufgeführt wird. • Die letzte Zeile schließlich ist das UNION RESULT. Es repräsentiert das Stadium des Lesens der Zeilen aus der temporären Tabelle der UNION. Sie können in dieser Zeile beginnen und sich rückwärts arbeiten, wenn Sie wollen; sie liefert Ergebnisse aus den Zeilen mit den ids 1 und 4, die wiederum Referenzen auf <derived3> und <derived6> darstellen.
Die Spalten in EXPLAIN | 669
Wie Sie sehen, können die Kombinationen aus diesen komplizierten SELECT-Typen in einer EXPLAIN-Ausgabe resultieren, die relativ schwierig zu lesen ist. Es wird einfacher, wenn man die Regeln verstanden hat, dennoch muss man es üben. Das Lesen der Ausgabe von EXPLAIN verlangt oft, vorwärts und rückwärts in der Liste zu springen. Schauen Sie sich z.B. wieder die erste Zeile in der Ausgabe an. Es gibt durch einfaches Hinschauen keine Möglichkeit festzustellen, dass sie Teil einer UNION ist. Sie sehen das erst, wenn Sie die letzte Zeile der Ausgabe gelesen haben.
Die type-Spalte Das MySQL-Handbuch besagt, dass diese Spalte den »Join-Typ« zeigt, allerdings glauben wir, dass es genauer ist, wenn man vom Zugriffstyp spricht – mit anderen Worten. Wie hat MySQL beschlossen, die Zeilen in der Tabelle zu finden? Hier sind die wichtigsten Zugriffsmethoden, aufgezählt von den schlechtesten zu den besten: ALL
Das ist das, was die meisten Leute einen Tabellenscan nennen. Im Allgemeinen bedeutet es, dass MySQL die Tabelle von Anfang bis Ende durchlaufen muss, um die Zeile zu finden. (Es gibt Ausnahmen, wie etwa Abfragen mit LIMIT oder Abfragen, die »Using distinct/not exists« in der Extra-Spalte zeigen.) index
Dies ist identisch mit einem Tabellenscan, allerdings scannt MySQL die Tabelle in der Indexreihenfolge anstatt in der Reihenfolge der Zeilen. Der größte Vorteil besteht darin, dass eine Sortierung vermieden wird, und der größte Nachteil sind die Kosten für das Lesen einer ganzen Tabelle in der Indexreihenfolge. Normalerweise bedeutet dies einen zufälligen Zugriff auf die Zeilen, was sehr teuer ist. Falls Sie auch noch »Using index« in der Extra-Spalte sehen, bedeutet dies, dass MySQL einen abdeckenden Index benutzt (siehe Kapitel 3) und nur die Daten des Index scannt und nicht jede Zeile in der Indexreihenfolge liest. Das ist weniger teuer, als die Tabelle in der Indexreihenfolge zu scannen. range
Ein Bereichsscan ist ein eingeschränkter Indexscan. Er beginnt an irgendeiner Stelle in dem Index und liefert Zeilen zurück, die einem Bereich aus Werten entsprechen. Das ist besser als ein vollständiger Indexscan, weil es nicht durch den gesamten Index läuft. Offensichtliche Bereichsscans sind Abfragen mit einem BETWEEN oder > in der WHERE-Klausel. Wenn MySQL einen Index verwendet, um Listen aus Werten zu suchen, wie etwa IN( )- und OR-Listen, zeigt es dies ebenfalls als Bereichsscan an. Es handelt sich
jedoch um ganz unterschiedliche Zugriffsarten, die wesentliche Unterschiede in der Leistung aufweisen. Mehr dazu erfahren Sie im Kasten »Was ist eine Bereichsbedingung?« auf Seite 143. Für diesen Typ gelten die gleichen Überlegungen wie für den index-Typ.
670 | Anhang B: EXPLAIN benutzen
ref
Dies ist ein Indexzugriff (manchmal auch als Index-Lookup bezeichnet), der Zeilen zurückliefert, die einem einzelnen Wert entsprechen. Es können hierbei allerdings auch mehrere Zeilen gefunden werden, d.h., es handelt sich um einen Mix aus Lookup und Scan. Diese Art von Indexzugriff kann nur bei einem nichteindeutigen Index oder bei einem nichteindeutigen Präfix eines eindeutigen Index auftreten. Er wird ref genannt, weil der Index mit einem Referenzwert verglichen wird. Der Referenzwert ist entweder eine Konstante oder ein Wert aus einer vorherigen Tabelle in einer Mehrtabellenabfrage. Der Zugriffstyp ref_or_null stellt eine Variante von ref dar. Er bedeutet, dass MySQL eine zweite Suche durchführen muss, um NULL-Einträge zu finden, nachdem es den ersten Lookup durchgeführt hat. eq_ref
Dies ist ein Index-Lookup, von dem MySQL weiß, dass er höchstens einen einzigen Wert zurückliefert. Sie finden diese Zugriffsmethode, wenn MySQL beschließt, einen Primärschlüssel oder einen eindeutigen Index zu benutzen, um die Abfrage zu befriedigen, indem es diesen mit einem Referenzwert vergleicht. MySQL kann diesen Zugriffstyp sehr gut optimieren, weil es weiß, dass es keine Bereiche passender Zeilen schätzen oder nach weiteren passenden Zeilen suchen muss, sobald es eine gefunden hat. const, system
MySQL verwendet diese Zugriffstypen, wenn es einen Teil der Abfrage wegoptimieren und in eine Konstante umwandeln kann. Falls Sie z.B. den Primärschlüssel einer Zeile auswählen, indem Sie ihren Primärschlüssel in die WHERE-Klausel setzen, kann MySQL die Abfrage in eine Konstante konvertieren. Damit wird die Tabelle dann gewissermaßen aus der Join-Ausführung entfernt. NULL
Diese Zugriffsmethode bedeutet, dass MySQL die Abfrage während der Optimierungsphase auflösen kann und während des Ausführungsstadiums nicht einmal auf die Tabelle oder den Index zugreift. Beispielsweise kann die Auswahl des Minimalwertes einer indizierten Spalte erfolgen, indem man sich den Index allein anschaut. Während der Ausführung ist kein Tabellenzugriff erforderlich.
Die possible_keys-Spalte Diese Spalte zeigt, welche Indizes für die Abfrage benutzt werden könnten. Diese Angabe beruht auf den Spalten, auf die die Abfrage zugreift, sowie auf den verwendeten Vergleichsoperatoren. Diese Liste wird bereits früh in der Optimierungsphase erzeugt, so dass einige der aufgeführten Indizes nach nachfolgenden Optimierungsphasen nutzlos sein könnten.
Die Spalten in EXPLAIN | 671
Die key-Spalte Diese Spalte zeigt, welchen Index MySQL benutzen will, um den Zugriff auf die Tabelle zu optimieren. Wenn der Index nicht in possible_keys erscheint, wählt MySQL ihn aus einem anderen Grund – so könnte es z.B. einen abdeckenden Index wählen, obwohl es keine WHERE-Klausel gibt. Mit anderen Worten: possible_keys enthüllt, welche Indizes dazu beitragen können, Zeilen-Lookups effizient zu gestalten, während key zeigt, welchen Index der Optimierer wählt, um die Abfragekosten zu minimieren (siehe »Die Abfrageoptimierung« auf Seite 176 für weitere Informationen über die Kostenmaße des Optimierers). Hier ist ein Beispiel: mysql> EXPLAIN SELECT actor_id, film_id FROM sakila.film_actor\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: film_actor type: index possible_keys: NULL key: idx_fk_film_id key_len: 2 ref: NULL rows: 5143 Extra: Using index
Die key_len-Spalte Diese Spalte zeigt die Anzahl der Bytes, die MySQL in einem Index benutzt. Wenn MySQL nur einige der Spalten des Index verwendet, können Sie mithilfe dieses Wertes berechnen, welche Spalten es benutzt. Erinnern Sie sich daran, dass MySQL nur das am weitesten links gelegene Präfix des Index verwenden kann. Zum Beispiel deckt der Primärschlüssel von sakila.film_actor zwei SMALLINT-Spalten ab. Ein SMALLINT ist zwei Byte groß, so dass jedes Tupel in dem Index vier Byte umfasst. Hier ist eine Beispielabfrage: mysql> EXPLAIN SELECT actor_id, film_id FROM sakila.film_actor WHERE actor_id=4; ...+------+---------------+---------+---------+... ...| type | possible_keys | key | key_len |... ...+------+---------------+---------+---------+... ...| ref | PRIMARY | PRIMARY | 2 |... ...+------+---------------+---------+---------+...
Aus der key_len-Spalte im Ergebnis können Sie schließen, dass die Abfrage IndexLookups nur mit der ersten Spalte durchführt, actor_id. Wenn Sie die Spaltenbenutzung berechnen, müssen Sie daran denken, die Zeichensätze in den Zeichenspalten zu berücksichtigen: mysql> CREATE TABLE -> a char(3) -> b int(11) -> c char(1)
t ( NOT NULL, NOT NULL, NOT NULL,
672 | Anhang B: EXPLAIN benutzen
-> PRIMARY KEY (a,b,c) -> ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; mysql> INSERT INTO t(a, b, c) -> SELECT DISTINCT LEFT(TABLE_SCHEMA, 3), ORD(TABLE_NAME), -> LEFT(COLUMN_NAME, 1) -> FROM INFORMATION_SCHEMA.COLUMNS: mysql> EXPLAIN SELECT a FROM t WHERE a='sak' AND b = 112; ...+------+---------------+---------+---------+... ...| type | possible_keys | key | key_len |... ...+------+---------------+---------+---------+... ...| ref | PRIMARY | PRIMARY | 13 |... ...+------+---------------+---------+---------+...
Die Länge von 13 Byte in dieser Abfrage ist die Summe der Längen der Spalten a und b. Spalte a umfasst drei Zeichen, die in utf8 jeweils bis zu drei Byte erfordern, Spalte b ist ein vier Byte langer Integer. MySQL zeigt Ihnen nicht immer, wie viel eines Index wirklich benutzt wird. Falls Sie z.B. eine LIKE-Abfrage mit einem Präfix-Mustervergleich durchführen, zeigt es, dass die vollständige Breite der Spalte benutzt wird. Die key_len-Spalte zeigt die maximal mögliche Länge der indizierten Felder, nicht die tatsächliche Anzahl der Bytes der Daten in der verwendeten Tabelle. MySQL zeigt im vorangegangenen Beispiel immer 13 Byte, selbst wenn Spalte a keine Werte enthält, die mehr als ein Zeichen lang sind. Mit anderen Worten: key_len wird nach einem Blick auf die Definition der Tabelle berechnet, nicht nach einem Blick auf die Daten in der Tabelle.
Die ref-Spalte Diese Spalte zeigt, welche Spalten oder Konstanten aus früheren Tabellen benutzt werden, um Werte in dem Index nachzusehen, der in der key-Spalte genannt ist. Hier ist ein Beispiel, das eine Kombination aus Join-Bedingungen und Aliasen zeigt. Die ref-Spalte spiegelt übrigens wider, dass die film-Tabelle im Abfragetext mit dem Alias f bezeichnet wird: mysql> EXPLAIN -> SELECT STRAIGHT_JOIN f.film_id -> FROM sakila.film AS f -> INNER JOIN sakila.film_actor AS fa -> ON f.film_id=fa.film_id AND fa.actor_id = 1 -> INNER JOIN sakila.actor AS a USING(actor_id); ...+-------+...+--------------------+---------+------------------------+... ...| table |...| key | key_len | ref |... ...+-------+...+--------------------+---------+------------------------+... ...| a |...| PRIMARY | 2 | const |... ...| f |...| idx_fk_language_id | 1 | NULL |... ...| fa |...| PRIMARY | 4 | const,sakila.f.film_id |... ...+-------+...+--------------------+---------+------------------------+...
Die Spalten in EXPLAIN | 673
Die rows-Spalte Diese Spalte zeigt die Anzahl der Zeilen an, die MySQL laut seiner Schätzung lesen muss, um die gewünschten Zeilen zu finden. Diese Anzahl ist pro Schleife im Nested-Loop-JoinPlan. Das heißt, es handelt sich nicht nur um die Anzahl der Zeilen, von denen MySQL glaubt, dass es sie aus der Tabelle lesen muss, sondern um die durchschnittliche Anzahl der Zeilen, von denen MySQL glaubt, dass es sie lesen muss, um die Zeilen zu finden, die das an dieser Stelle in der Abfrageausführung wirksame Kriterium erfüllen. (Zu den Kriterien gehören Konstanten, die im SQL angegeben sind, sowie die aktuellen Spalten aus früheren Tabellen in der Join-Reihenfolge.) Diese Schätzung kann ziemlich ungenau sein, je nach den Tabellenstatistiken und der Selektivität der Indizes. Sie spiegelt auch nicht die LIMIT-Klauseln in MySQL 5.0 und früheren Versionen wider. Zum Beispiel werden bei der folgenden Abfrage keine 1.022 Zeilen untersucht: mysql> EXPLAIN SELECT * FROM sakila.film LIMIT 1\G ... rows: 1022
Sie können die Anzahl der Zeilen, die die gesamte Abfrage untersucht, grob berechnen, indem Sie alle rows-Werte miteinander multiplizieren. Beispielsweise würde die folgende Abfrage ungefähr 2.600 Zeilen untersuchen: mysql> EXPLAIN -> SELECT f.film_id -> FROM sakila.film AS f -> INNER JOIN sakila.film_actor AS fa USING(film_id) -> INNER JOIN sakila.actor AS a USING(actor_id); ...+------+... ...| rows |... ...+------+... ...| 200 |... ...| 13 |... ...| 1 |... ...+------+...
Denken Sie daran, dass dies die Anzahl der Zeilen ist, von denen MySQL glaubt, dass es sie untersuchen muss, nicht die Anzahl der Zeilen in der Ergebnismenge. Bedenken Sie außerdem, dass es viele Optimierungen gibt, wie etwa Join-Puffer und Caches, die nicht in die Anzahl der gezeigten Zeilen einbezogen werden. Wahrscheinlich muss MySQL in Wirklichkeit nicht jede vorhergesagte Zeile lesen. MySQL weiß außerdem nichts über das Betriebssystem oder die Hardware-Caches.
Die filtered-Spalte Diese Spalte ist neu in MySQL 5.1 und erscheint, wenn Sie EXPLAIN EXTENDED benutzen. Sie zeigt eine pessimistische Schätzung des Prozentsatzes an Zeilen, die eine Bedingung in der Tabelle erfüllen, wie etwa eine WHERE-Klausel oder eine Join-Bedingung. Wenn Sie die rows-Spalte mit diesem Prozentwert multiplizieren, dann sehen Sie die Anzahl der Zeilen, 674 | Anhang B: EXPLAIN benutzen
die MySQL schätzungsweise mit den früheren Tabellen im Abfrageplan zusammenführen muss. Momentan nutzt der Optimierer diese Schätzung nur für die Zugriffsmethoden ALL, index, range und index_merge. Um die Ausgabe dieser Spalte zu verdeutlichen, haben wir eine solche Tabelle erzeugt: CREATE TABLE t1 ( id INT NOT NULL AUTO_INCREMENT, filler char(200), PRIMARY KEY(id) );
Wir fügten in diese Tabelle dann 1.000 Zeilen ein, mit zufälligem Text in der fillerSpalte. Damit sollte verhindert werden, dass MySQL einen abdeckenden Index für die Abfrage benutzt, die wir ausführen wollen: mysql> EXPLAIN EXTENDED SELECT * FROM t1 WHERE id < 500\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: ALL possible_keys: PRIMARY key: NULL key_len: NULL ref: NULL rows: 1000 filtered: 49.40 Extra: Using where
MySQL könnte einen Bereichszugriff verwenden, um alle Zeilen aus der Tabelle abzufragen, deren IDs kleiner als 500 sind, würde dies aber nicht tun, da dies nur etwa die Hälfte der Zeilen eliminieren würde. Es glaubt, dass ein Tabellenscan weniger teuer wäre. Schließlich benutzt es einen Tabellenscan und eine WHERE-Klausel, um Zeilen auszufiltern. Es weiß aufgrund der Kostenabschätzung für den Bereichszugriff, wie viele Zeilen die WHERE-Klausel aus dem Ergebnis entfernt. Daher erscheint der Wert 49.40 % in der filtered-Spalte.
Die Extra-Spalte Diese Spalte enthält zusätzliche Informationen, die nicht in andere Spalten passen. Das MySQL-Handbuch dokumentiert die meisten der vielen Werte, die hier auftauchen können; wir haben viele von ihnen bereits im Laufe dieses Buches erwähnt. Dies sind die wichtigsten Werte, die Ihnen häufig begegnen können: »Using index« Dies zeigt an, dass MySQL einen abdeckenden Index benutzt, um den Zugriff auf die Tabelle zu vermeiden (siehe »Abdeckende Indizes« auf Seite 128). Verwechseln Sie abdeckende Indizes nicht mit dem Zugriffstyp index.
Die Spalten in EXPLAIN | 675
»Using where« Dies bedeutet, dass der MySQL-Server Zeilen filtert, nachdem die Storage-Engine sie abgefragt hat. Viele WHERE-Bedingungen, die Spalten in einem Index einbeziehen, können von der Storage-Engine überprüft werden, wenn (und falls) sie den Index liest, deshalb zeigen nicht alle Abfragen mit einer WHERE-Klausel »Using where«. Manchmal ist das Vorhandensein von »Using where« ein Hinweis darauf, dass die Abfrage von einer anderen Indizierung profitieren könnte. »Using temporary« Dies bedeutet, dass MySQL eine temporäre Tabelle benutzt, wenn es das Ergebnis der Abfrage sortiert. »Using filesort« Dies bedeutet, dass MySQL eine externe Sortierung verwendet, wenn es die Ergebnisse sortiert, anstatt die Zeilen in der Indexreihenfolge aus der Tabelle zu lesen. MySQL besitzt zwei Filesort-Algorithmen, über die Sie in »Für Filesorts optimieren« auf Seite 325 mehr erfahren. Beide Typen können im Speicher oder auf der Festplatte durchgeführt werden. EXPLAIN verrät Ihnen nicht, welche Art von Filesort MySQL benutzt, und sagt Ihnen auch nicht, ob die Sortierung im Speicher oder auf der Festplatte durchgeführt werden wird. »range checked for each record (index map: N)« Dieser Wert bedeutet, dass es keinen guten Index gibt und dass die Indizes für jede Zeile in einem Join neu bewertet werden. N ist eine Bitmap der Indizes, die in possible_keys gezeigt werden, und ist redundant.
Visuelles EXPLAIN Die MySQL-Entwickler haben gesagt, dass sie die Ausgabe von EXPLAIN gern als Baum formatiert sehen, wodurch eine genauere Darstellung des Ausführungsplans ermöglicht wird. Auch die MySQL-Benutzer haben sich eine solche Verbesserung gewünscht. So, wie es ist, ist EXPLAIN eine etwas umständliche Methode, um den Ausführungsplan zu verdeutlichen; eine Baumstruktur passt nicht besonders gut in eine tabellarische Ausgabe. Die Umständlichkeit wird durch die große Anzahl möglicher Werte für die Extra-Spalte sowie durch UNION betont. Ein UNION ist ganz anders als jede andere Art von Join, das MySQL durchführen kann, und passt ebenfalls nicht gut in EXPLAIN. Mit einem guten Verständnis der Regeln und Besonderheiten von EXPLAIN ist es möglich, sich rückwärts an einen baumformatierten Ausführungsplan heranzuarbeiten. Das ist allerdings relativ mühselig und sollte am besten einem automatisierten Werkzeug überlassen werden. Maatkit (siehe Kapitel 14) enthält mit mk-visual-explain ein solches Werkzeug.
676 | Anhang B: EXPLAIN benutzen
ANHANG C
Sphinx mit MySQL benutzen
Sphinx (http://www.sphinxsearch.com) ist eine kostenlose Open-Source-Volltextsuchmaschine, die extra so gestaltet wurde, dass sie sich gut in Datenbanken integriert. Sie besitzt DBMS-artige Funktionen, unterstützt verteiltes Suchen und skaliert gut. Darüber hinaus bietet sie eine effiziente Speichernutzung und Festplatten-Ein-/Ausgabe, was besonders wichtig ist, da dies bei großen Operationen oft die begrenzenden Faktoren sind. Sphinx funktioniert gut zusammen mit MySQL. Es kann benutzt werden, um eine Vielzahl von Abfragen, wie etwa Volltextsuchen, zu beschleunigen. Sie können damit aber auch neben anderen Anwendungen schnelle Gruppierungs- und Sortieroperationen durchführen. Darüber hinaus gibt es eine zusätzliche Storage-Engine, die es einem Programmierer oder Administrator erlaubt, auf Sphinx direkt über MySQL zuzugreifen. Sphinx eignet sich besonders für bestimmte Abfragen, die von der allgemeinen MySQLArchitektur für große Datenmengen nicht sehr gut optimiert werden. Kurz gesagt, Sphinx kann die Funktionalität und Performance von MySQL verbessern. Die Quelle der Daten für einen Sphinx-Index ist normalerweise das Ergebnis einer MySQL-SELECT-Abfrage. Sie können einen Index aber auch aus einer unbegrenzten Anzahl von Quellen verschiedenster Art aufbauen. Jede Instanz von Sphinx kann eine unbegrenzte Anzahl von Indizes durchsuchen. Sie können z.B. einige der Dokumente in einem Index aus einer MySQL-Instanz ziehen, die auf einem entfernten Server läuft, einige aus einer PostgreSQL-Instanz, die auf einem weiteren Server läuft, und einige aus der Ausgabe eines lokalen Skripts über einen XML-Pipe-Mechanismus. In diesem Anhang untersuchen wir einige der Fälle, in denen die Fähigkeiten von Sphinx die Leistung verbessern, zeigen eine Zusammenfassung der Schritte, die für die Installation und Konfiguration von Sphinx erforderlich sind, erläutern detailliert seine Eigenschaften und diskutieren mehrere Beispiele für tatsächliche Implementierungen.
| 677
Überblick: Eine typische Sphinx-Suche Wir beginnen mit einem einfachen, aber vollständigen Beispiel für die Benutzung von Sphinx, damit wir einen Ausgangspunkt für die weitere Diskussion bekommen. Wir verwenden PHP wegen seiner Popularität, obwohl es auch für eine Vielzahl anderer Sprachen APIs gibt. Nehmen Sie an, dass wir eine Volltextsuche für eine Preisvergleichs-Engine implementieren. Wir stellen folgende Anforderungen: • Pflege eines durchsuchbaren Volltextindex in einer Produkttabelle, die in MySQL gespeichert ist • Es sollen Volltextsuchen über Produkttitel und -beschreibungen erlaubt sein. • Es soll möglich sein, bei Bedarf die Suchen auf eine bestimmte Kategorie einzuschränken. • Das Ergebnis soll nicht nur nach Relevanz, sondern auch nach Einzelpreis oder Einstelldatum sortiert werden können. Wir beginnen damit, eine Datenquelle und einen Index in der Sphinx-Konfigurationsdatei einzurichten: source products { type sql_host sql_user sql_pass sql_db sql_query
= = = = = =
mysql localhost shopping mysecretpassword shopping SELECT id, title, description, \ cat_id, price, UNIX_TIMESTAMP(added_date) AS added_ts \ FROM products sql_attr_uint = cat_id sql_attr_float = price sql_attr_timestamp = added_ts
} index products { source = products path = /usr/local/sphinx/var/data/products docinfo = extern }
Dieses Beispiel geht davon aus, dass die MySQL-shopping-Datenbank eine productsTabelle mit den Spalten enthält, die wir in unserer SELECT-Abfrage anfordern, um den Sphinx-Index zu füllen. Der Sphinx-Index wird ebenfalls products genannt. Nach dem Erzeugen einer neuen Quelle und eines Index führen wir das Programm indexer aus, um die ersten Volltextindexdatendateien zu erzeugen, und starten dann den searchdDaemon (neu), um die Änderungen zu erfassen:
678 |
Anhang C: Sphinx mit MySQL benutzen
$ $ $ $
cd /usr/local/sphinx/bin ./indexer products ./searchd --stop ./searchd
Der Index ist nun bereit, Abfragen zu beantworten. Wir können ihn mit dem in Sphinx enthaltenen Beispielskript test.php testen: $ php -q test.php -i products ipod Query 'ipod ' retrieved 3 of 3 matches in 0.010 sec. Query stats: 'ipod' found 3 times in 3 documents Matches: 1. doc_id=123, weight=100, cat_id=100, price=159.99, added_ts=2008-01-03 22:38:26 2. doc_id=124, weight=100, cat_id=100, price=199.99, added_ts=2008-01-03 22:38:26 3. doc_id=125, weight=100, cat_id=100, price=249.99, added_ts=2008-01-03 22:38:26
Der letzte Schritt besteht darin, die Suche zu unserer Webanwendung hinzuzufügen. Wir müssen die Sortier- und Filteroptionen basierend auf der Benutzereingabe einrichten und die Ausgabe hübsch formatieren. Da Sphinx nur Dokument-IDs und konfigurierte Attribute an den Client zurückliefert – es speichert keine der originalen Textdaten –, müssen wir außerdem selbst zusätzliche Zeilendaten aus MySQL ziehen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
SetSortMode ( SPH_SORT_ATTR_ASC, $sortby ); else $cl->SetSortMode ( SPH_SORT_ATTR_DESC, $sortby ); $offset = ($_REQUEST["page"]-1)*$rows_per_page; $cl->SetLimits ( $offset, $rows_per_page ); // die Abfrage ausführen, die Ergebnisse entgegennehmen $res = $cl->Query ( $_REQUEST["query"], "products" ); // Suchfehler verarbeiten if ( !$res ) { print "Suchfehler:" . $cl->GetLastError ( ); die; } // weitere Spalten aus MySQL holen $ids = join ( ",", array_keys ( $res["matches"] );
Überblick: Eine typische Sphinx-Suche | 679
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
$r = mysql_query ( "SELECT id, title FROM products WHERE id IN ($ids)" ) or die ( "MySQL-Fehler: " . mysql_error( ) ); while ( $row = mysql_fetch_assoc($r) ) { $id = $row["id"]; $result["matches"][$id]["sql"] = $row; } // Anzeige der Ergebnisse in der Reihenfolge, die von Sphinx geliefert wird $n = 1 + $offset; foreach ( $result["matches"] as $id=>$match ) { printf ( "%d. %s, USD %.2f
\n", $n++, $id, $match["sql"]["title"], $match["attrs"]["price"] ); } ?>
Auch wenn der hier gezeigte Code-Schnipsel recht einfach ist, sollen einige Dinge hervorgehoben werden: • Der Aufruf SetLimits( ) teilt Sphinx mit, dass nur die Anzahl der Zeilen geholt werden soll, die der Client auf einer Seite anzeigen möchte. Es ist nicht aufwendig, diese Grenze in Sphinx aufzustellen (im Gegensatz zur Suchfunktion, die in MySQL eingebaut ist). Die Anzahl der Ergebnisse, die ohne die Grenze zurückgeliefert worden wäre, steht ohne weitere Kosten in $result['total_found'] zur Verfügung. • Da Sphinx die title-Spalte nur indiziert und nicht speichert, müssen wir diese Daten von MySQL holen. • Wir beziehen die Daten von MySQL mit einer einzigen kombinierten Abfrage für den gesamten Dokumentenstapel mittels der Klausel WHERE id IN (...), anstatt eine Abfrage für jeden Vergleich auszuführen (was ineffizient wäre). • Wir fügen die Zeilen, die aus MySQL geholt wurden, in unsere Volltextsuchergebnismenge ein, um die ursprüngliche Sortierreihenfolge beizubehalten. Darauf gehen wir gleich genauer ein. • Wir zeigen die Zeilen mit den Daten an, die sowohl von Sphinx als auch von MySQL geholt wurden. Der Code zum Einbringen der Zeilen, der PHP-spezifisch ist, verlangt nach einer genaueren Erklärung. Wir könnten nicht einfach über die Ergebnismenge aus der MySQLAbfrage iterieren, weil die Zeilenreihenfolge sich von der in der WHERE id IN (...)-Klausel angegebenen Reihenfolge unterscheiden kann (und das in den meisten Fällen auch tun wird). PHP-Hashes (assoziative Arrays) jedoch behalten die Reihenfolge bei, in der die Treffer in sie eingefügt wurden, so dass das Iterieren über $result["matches"] Zeilen in der richtigen Sortierreihenfolge liefert, die von Sphinx zurückgegeben wurde. Um daher die Treffer in der von Sphinx gelieferten richtigen Reihenfolge zu halten (anstatt in der halb zufälligen Reihenfolge, die von MySQL geliefert wurde), fügen wir die MySQLAbfrageergebnisse nacheinander in den Hash ein, den PHP aus der Sphinx-Ergebnismenge der Treffer speichert. 680 |
Anhang C: Sphinx mit MySQL benutzen
Es gibt darüber hinaus in Bezug auf das Zählen der Treffer und das Anwenden einer LIMIT-Klausel einige wichtige Unterschiede in der Implementierung und Performance zwischen MySQL und Sphinx. Erstens ist LIMIT in Sphinx billig. Betrachten Sie eine LIMIT 500,10-Klausel. MySQL holt 510 halb zufällige Zeilen (das ist langsam) und wirft 500 weg, während Sphinx die IDs zurückliefert, die Sie benutzen, um die 10 Zeilen zu beziehen, die Sie tatsächlich aus MySQL brauchen. Zweitens liefert Sphinx immer die exakte Anzahl der Treffer zurück, die es tatsächlich in der Ergebnismenge gefunden hat, unabhängig davon, was in der LIMIT-Klausel steht. MySQL kann das nicht effizient erledigen (Näheres dazu finden Sie in »SQL_CALC_FOUND_ROWS optimieren« auf Seite 208).
Wieso sollten Sie Sphinx benutzen? Sphinx kann eine MySQL-basierte Anwendung auf vielerlei Weise ergänzen, indem es die Leistung dort verstärkt, wo MySQL keine gute Lösung darstellt, und Funktionalität bietet, die MySQL nicht besitzt. Typische Benutzungsszenarien sind: • Schnelle, effiziente, skalierbare, relevante Volltextsuchen • Optimieren von WHERE-Bedingungen in wenig selektiven Indizes oder Spalten ohne Indizes • Optimieren von ORDER BY ... LIMIT N-Abfragen und GROUP BY-Abfragen • Paralleles Generieren von Ergebnismengen • Vertikale und horizontale Skalierung • Ansammeln partitionierter Daten Wir werden in den folgenden Abschnitten diese Szenarien untersuchen. Diese Liste ist allerdings nicht vollständig. Sphinx-Benutzer finden regelmäßig neue Anwendungen. Zum Beispiel war einer der wichtigsten Anwendungsfälle von Sphinx – das schnelle Durchsuchen und Filtern von Datensätzen – eine Benutzererfindung und gehörte nicht zu den ursprünglichen Entwurfszielen vonSphinx.
Effiziente und skalierbare Volltextsuche MySQLs Volltextsuchfähigkeit1 ist bei kleineren Datenmengen schnell, wird jedoch mit zunehmender Datengröße schlechter. Bei Millionen von Datensätzen und Gigabytes an indiziertem Text können die Abfragezeiten zwischen einer Sekunde und mehr als 10 Minuten liegen, was für eine High-Performance-Webanwendung nicht akzeptabel ist. Es ist zwar möglich, die MySQL-Volltextsuchen zu skalieren, indem man die Daten auf viele Orte verteilt, allerdings verlangt das dann, dass Sie die Suchen parallel ausführen und die Ergebnisse in Ihrer Anwendung zusammenführen.
1 Siehe »Volltextsuche« auf Seite 263.
Wieso sollten Sie Sphinx benutzen?
| 681
Sphinx arbeitet deutlich schneller als die in MySQL eingebauten Volltextindizes. Es kann z.B. mehr als 1 GByte Text innerhalb von 10 bis 100 Millisekunden durchsuchen – und das skaliert linear auf 10–100 GByte pro CPU. Sphinx bietet darüber hinaus die folgenden Vorteile: • Es kann Daten indizieren, die mit InnoDB und anderen Engines gespeichert wurden, nicht nur mit MyISAM. • Es kann Indizes in Daten erzeugen, die aus vielen Quelltabellen kombiniert werden, und ist nicht nur auf die Spalten in einer einzigen Tabelle beschränkt. • Es kann dynamisch Suchergebnisse aus mehreren Indizes kombinieren. • Zusätzlich zum Indizieren von textuellen Spalten können seine Indizes eine unbeschränkte Anzahl an numerischen Attributen enthalten, die analog zu »zusätzlichen Spalten« sind. Sphinx-Attribute können Integer, Fließkommazahlen und Unix-Zeitstempel sein. • Es kann Volltextsuchen mit zusätzlichen Bedingungen in den Attributen optimieren. • Sein phrasenbasierter Ranking-Algorithmus hilft ihm dabei, relevantere Ergebnisse zurückzuliefern. Falls Sie z.B. eine Tabelle mit Song-Texten nach »I love you, dear« durchsuchen, dann erscheint ein Lied, das genau diese Phrase enthält, an vorderster Stelle vor Liedern, die nur mehrmals »love« oder »dear« enthalten. • Es erleichtert die Skalierung. Mehr über die Skalierung erfahren Sie in Kapitel 9 sowie in »Skalierung« auf Seite 687 weiter hinten in diesem Anhang.
WHERE-Klauseln effizient anwenden Manchmal müssen Sie SELECT-Abfragen an sehr großen Tabellen (die Millionen von Datensätzen enthalten) durchführen, mit mehreren WHERE-Bedingungen in Spalten, die eine schwache Indexselektivität aufweisen (d.h., die zu viele Zeilen für eine bestimmte WHERE-Bedingung zurückliefern) oder überhaupt nicht indiziert werden konnten. Verbreitete Beispiele sind Suchen nach Benutzern in einem sozialen Netzwerk und Suchen nach Objekten auf einer Auktions-Site. Typische Suchoberflächen erlauben es dem Benutzer, WHERE-Bedingungen auf 10 oder mehr Spalten anzuwenden, wobei die Ergebnisse nach anderen Spalten sortiert werden müssen. In »Indizierung – eine Fallstudie« auf Seite 140 finden Sie ein Beispiel für eine solche Anwendung und die erforderlichen Indizierungsstrategien. Mit den richtigen Schema- und Abfrageoptimierungen kann MySQL solche Abfragen ganz akzeptabel verarbeiten, solange die WHERE-Klauseln nicht zu viele Spalten enthalten. Wenn allerdings die Anzahl der Spalten zunimmt, wächst die Anzahl der Indizes, die erforderlich sind, um alle möglichen Suchen zu unterstützen, exponentiell. Das Abdecken aller möglichen Kombinationen für nur vier Spalten lässt MySQL an seine Grenzen stoßen. Es wird außerdem sehr langsam und teuer, die Indizes zu pflegen. Das bedeutet, dass es praktisch unmöglich ist, alle erforderlichen Indizes für viele WHERE-Bedingungen zu haben, und dass Sie die Abfragen ohne Indizes ausführen müssen.
682 |
Anhang C: Sphinx mit MySQL benutzen
Wichtiger ist: Selbst wenn Sie Indizes hinzufügen können, nützen sie nicht viel, wenn sie nicht selektiv sind. Das klassische Beispiel ist eine gender-Spalte, die kaum hilfreich ist, weil sie typischerweise die Hälfte aller Zeilen auswählt. MySQL greift im Allgemeinen auf einen vollständigen Tabellenscan zurück, wenn der Index nicht ausreichend selektiv ist. Sphinx kann solche Abfragen viel schneller durchführen als MySQL. Sie können einen Sphinx-Index mit nur den erforderlichen Spalten aus den Daten aufbauen. Sphinx erlaubt dann zwei Arten von Zugriff auf die Daten: eine indizierte Suche in einem Schlüsselwort oder einen vollständigen Scan. In beiden Fällen wendet Sphinx Filter an, die sein Äquivalent einer WHERE-Klausel darstellen. Im Gegensatz zu MySQL, das intern entscheidet, ob ein Index oder ein vollständiger Scan benutzt werden soll, erlaubt Sphinx Ihnen die Wahl der zu verwendenden Zugriffsmethode. Um einen vollständigen Scan mit Filtern zu benutzen, geben Sie einen leeren String als Suchabfrage an. Um eine indizierte Suche zu verwenden, fügen Sie Ihren Volltextfeldern Pseudoschlüsselwörter hinzu, wenn Sie den Index aufbauen, und suchen dann nach diesen Schlüsselwörtern. Falls Sie z.B. nach Elementen in Kategorie 123 suchen wollten, würden Sie während der Indizierung das Schlüsselwort »Kategorie123« in das Dokument einfügen und dann eine Volltextsuche nach »Kategorie123« durchführen. Sie können entweder mit der Funktion CONCAT( ) Schlüsselwörter zu einem der vorhandenen Felder hinzufügen oder ein besonderes Volltextfeld für die Pseudoschlüsselwörter anlegen, um die Flexibilität zu erhöhen. Normalerweise sollten Sie Filter für nichtselektive Werte benutzen, die mehr als 30 % der Zeilen abdecken, sowie falsche Schlüsselwörter für selektive Werte, die 10 % oder weniger auswählen. Wenn die Werte in der 10–30-%-Grauzone liegen, dann sollten Sie Benchmarks einsetzen, um die beste Lösung zu ermitteln. Sphinx führt sowohl indizierte Suchen als auch Scans schneller aus als MySQL. Manchmal führt Sphinx sogar einen vollständigen Scan schneller aus, als MySQL einen Index liest.
Die obersten Ergebnisse anzeigen Webanwendungen müssen häufig die obersten N Ergebnisse anzeigen. Wie wir in »LIMIT und OFFSET optimieren« auf Seite 207 besprochen haben, ist das in MySQL schwer zu optimieren. Am schlimmsten ist es, wenn die WHERE-Bedingung viele Zeilen findet (z.B. 1 Million) und die ORDER BY-Spalten nicht indiziert sind. MySQL nutzt den Index, um alle passenden Zeilen zu identifizieren, liest die Datensätze mit halb zufälligen Plattenzugriffen nacheinander in den Sortierpuffer ein, sortiert sie dann alle mit einem Filesort und verwirft die meisten von ihnen. Es speichert das gesamte Ergebnis temporär und verarbeitet es, wobei es die LIMIT-Klausel und den beanspruchten RAM ignoriert. Und falls die Ergebnismenge nicht in den Sortierpuffer passt, kommt sie wieder auf die Festplatte, wodurch weitere Festplattenzugriffe erforderlich werden. Dies ist ein Extremfall. Vermutlich glauben Sie, dass er in der Realität nur selten vorkommt, dabei treten die Probleme, die hiermit verdeutlicht werden, oft auf. Die
Wieso sollten Sie Sphinx benutzen?
| 683
Beschränkungen von MySQL hinsichtlich der Indizes für die Sortierung – es wird nur der am weitesten links gelegene Teil des Index benutzt, lockere Indexscans werden nicht unterstützt, und es ist nur eine einzige Bereichsbedingung erlaubt –, bedeuten, dass viele wirkliche Abfragen nicht von Indizes profitieren. Und selbst wenn sie es können, dann ist die Verwendung von halb zufälligen Festplatten-Ein-/Ausgaben zum Abfragen von Zeilen ein Performance-Killer. Paginierte Ergebnismengen, die normalerweise Abfragen der Form SELECT ... LIMIT N, M verlangen, sind ein weiteres Leistungsproblem in MySQL. Sie lesen N + M Zeilen von der Festplatte, wodurch sie eine große Zahl von zufälligen Ein-/Ausgaben verursachen und Speicherressourcen verschwenden. Sphinx kann solche Abfragen deutlich beschleunigen, indem zwei der größten Probleme eliminiert werden: Speicherbenutzung Die RAM-Auslastung von Sphinx ist immer streng begrenzt. Diese Grenze lässt sich konfigurieren. Sphinx unterstützt einen Offset und eine Größe der Ergebnismenge ähnlich der MySQL-Syntax LIMIT N, M, besitzt aber auch die Option max_matches. Diese steuert das Äquivalent der »Sortierpuffer«-Größe, sowohl auf einer serverweisen als auch auf einer abfrageweisen Basis. Der RAM-Footprint von Sphinx bewegt sich garantiert innerhalb der angegebenen Grenzen. Ein-/Ausgabe Falls Attribute im RAM gespeichert werden, führt Sphinx überhaupt keine Ein/Ausgaben durch. Und selbst falls Attribute auf der Festplatte abgelegt werden, führt Sphinx sequenzielle Ein-/Ausgaben durch, um sie zu lesen, was viel schneller geht, als der halb zufällige Bezug von Zeilen von der Festplatte, den MySQL durchführt. Sie können die Suchergebnisse durch eine Kombination aus Relevanz (Gewicht), Attributwerten und (bei Benutzung von GROUP BY) Aggregatfunktionswerten sortieren. Die Syntax der Sortierklausel ist ähnlich einer SQL-ORDER BY-Klausel: SetSortMode ( SPH_SORT_EXTENDED, 'price DESC, @weight ASC' ); // weiterer Code und Query( )-Aufruf... ?>
In diesem Beispiel ist price ein benutzerdefiniertes Attribut, das im Index gespeichert ist, und @weight ist ein besonderes Attribut, das zur Laufzeit erzeugt wird und die berechnete Relevanz der einzelnen Ergebnisse enthält. Sie können die Sortierung auch nach einem arithmetischen Ausdruck vornehmen, in dem Attributwerte, gebräuchliche mathematische Operatoren und Funktionen vorkommen: SetSortMode ( SPH_SORT_EXPR, '@weight + log(pageviews)*1.5' ); // weiterer Code und Query( )-Aufruf... ?>
684 |
Anhang C: Sphinx mit MySQL benutzen
Optimieren von GROUP BY-Abfragen Die Unterstützung für alltägliche SQL-artige Klauseln wäre unvollständig ohne die GROUP BY-Funktionalität, weshalb Sphinx auch diese bietet. Im Gegensatz zur allgemeinen Implementierung von MySQL hat sich Sphinx darauf spezialisiert, eine praktische Teilmenge der GROUP BY-Aufgaben effizient zu lösen. Diese Teilmenge umfasst das Generieren von Berichten aus großen (1–100 Million Zeilen) Datenmengen, wenn einer der folgenden Fälle zutrifft: • Das Ergebnis ist nur eine »kleine« Anzahl gruppierter Zeilen (wobei »klein« sich in Größenordnungen von 100.000 bis 1 Million Zeilen bewegt). • Es ist eine sehr schnelle Ausführungsgeschwindigkeit erforderlich, geschätzte COUNT(*)-Ergebnisse sind akzeptabel, wenn viele Gruppen aus Daten bezogen werden, die über einen Cluster aus Maschinen verteilt sind. Das ist nicht so restriktiv, wie es klingen mag. Das erste Szenario behandelt praktisch alle vorstellbaren zeitbasierten Berichte. Beispielsweise liefert ein ausführlicher stundenweiser Bericht für einen Zeitraum von 10 Jahren weniger als 90.000 Datensätze zurück. Das zweite Szenario könnte mit einfachen Worten so ausgedrückt werden: »Suche so schnell und genau wie möglich die 20 wichtigsten Datensätze in einer 100 Millionen Zeilen umfassenden Sharded-Tabelle«. Diese zwei Arten von Abfragen können allgemeine Abfragen beschleunigen, Sie können sie aber auch für Volltextsuchanwendungen benutzen. Viele Anwendungen müssen nicht nur Volltexttreffer anzeigen, sondern einige sammeln auch Ergebnisse. Zum Beispiel zeigen viele Suchergebnisseiten, wie viele Treffer in jeder Produktkategorie gefunden wurden, oder stellen ein Diagramm mit den Zählern der zutreffenden Dokumente im Zeitverlauf dar. Eine weitere gebräuchliche Anforderung besteht darin, die Ergebnisse zu gruppieren und den relevantesten Treffer aus jeder Kategorie zu zeigen. Die Group-byUnterstützung von Sphinx erlaubt es Ihnen, Gruppierung und Volltextsuchen zu kombinieren, wodurch der Overhead eliminiert wird, der beim Gruppieren in Ihrer Anwendung oder in MySQL auftritt. Auch das Gruppieren in Sphinx verwendet wie das Sortieren einen festen Speicher. Es ist etwas (10 % bis 50 %) effizienter als ähnliche MySQL-Abfragen in Datenmengen, die in den RAM passen. In diesem Fall stammt ein Großteil der Stärke von Sphinx aus seiner Fähigkeit, die Last zu verteilen und die Latenz stark zu verringern. Für riesige Datenmengen, die niemals in den RAM passen würden, können Sie einen speziellen festplattenbasierten Index für das Reporting aufbauen, wobei Sie Inline-Attribute benutzen (diese werden später definiert). Abfragen an solchen Indizes werden fast so schnell ausgeführt, wie die Festplatte die Daten lesen kann – mit etwa 30–100 MByte/s auf moderner Hardware. In diesem Fall kann die Leistung um ein Vielfaches besser sein als die von MySQL, obwohl die Ergebnisse nur angenähert werden.
Wieso sollten Sie Sphinx benutzen?
| 685
Der wichtigste Unterschied zum MySQL-GROUP BY besteht darin, dass Sphinx unter bestimmten Umständen angenäherte Ergebnisse liefert. Dafür gibt es zwei Gründe: • Das Gruppieren verwendet eine feste Menge Speicher. Wenn es zu viele Gruppen gibt, die im RAM vorgehalten werden müssen, und die Treffer in einer bestimmten »unglücklichen« Reihenfolge vorliegen, dann könnten die gruppenweisen Zähler kleiner ausfallen als die tatsächlichen Werte. • Eine verteilte Suche sendet nur die zusammengefassten Ergebnisse, nicht die Treffer selbst, von Knoten zu Knoten. Wenn es duplizierte Datensätze in unterschiedlichen Knoten gibt, dann könnten gruppenweise getrennte Zähler größer sein als die tatsächlichen Werte, weil die Information, die die Duplikate entfernen kann, nicht zwischen den Knoten übertragen wird. In der Praxis ist es oft akzeptabel, schnelle, angenäherte Group-by-Zähler zu haben. Falls es nicht akzeptabel ist, dann ist es oft möglich, exakte Ergebnisse zu bekommen, indem man den Daemon und die Client-Anwendung sorgfältig anpasst. Sie können auch das Äquivalent von COUNT(DISTINCT ) generieren. Zum Beispiel können Sie damit die Anzahl der einzelnen Verkäufer pro Kategorie in einer Auktions-Site berechnen. Schließlich erlaubt Sphinx es Ihnen, Kriterien zu wählen, um das jeweils »beste« Dokument innerhalb jeder Gruppe zu ermitteln. So können Sie z.B. das relevanteste Dokument aus jeder Domain wählen, indem Sie nach der Domain gruppieren und die Ergebnismenge anhand domain-weiser Trefferzähler sortieren. In MySQL ist dies ohne eine komplexe Abfrage nicht möglich.
Parallele Ergebnismengen generieren Sphinx erlaubt es Ihnen, gleichzeitig mehrere Ergebnisse aus denselben Daten zu generieren, und zwar ebenfalls mit einer festen Menge Speicher. Verglichen mit dem traditionellen SQL-Ansatz, entweder zwei Abfragen auszuführen (und zu hoffen, dass einige der Daten zwischen den Durchläufen im Cache bleiben) oder eine temporäre Tabelle für jede Suchergebnismenge zu erzeugen, bringt dies eine deutliche Verbesserung mit sich. Nehmen Sie z.B. an, dass Sie über einen bestimmten Zeitraum Berichte pro Tag, pro Woche und pro Monat benötigen. Um diese mit MySQL zu generieren, müssten Sie drei Abfragen mit unterschiedlichen GROUP BY-Klauseln durchführen, wobei die Datenquelle dreimal verarbeitet wird. Sphinx dagegen kann die zugrunde liegenden Daten einmal verarbeiten und alle drei Berichte parallel zusammenstellen. Sphinx erledigt dies mit einem Mehrfache-Abfragen-Mechanismus. Anstatt die Abfragen nacheinander auszuführen, werden mehrere Abfragen gestapelt und in einer einzigen Anforderung übermittelt: SetSortMode ( SPH_SORT_EXTENDED, "price desc" );
686 |
Anhang C: Sphinx mit MySQL benutzen
$cl->AddQuery ( $cl->SetGroupBy $cl->AddQuery ( $cl->RunQueries ?>
"ipod" ); ( "category_id", SPH_GROUPBY_ATTR, "@count desc" ); "ipod" ); ( );
Sphinx analysiert die Anforderung, identifiziert die Abfrageteile, die es kombinieren kann, und parallelisiert die Abfragen, wo dies möglich ist. Zum Beispiel könnte Sphinx bemerken, dass sich nur die Sortier- und Gruppiermodi unterscheiden und die Abfragen ansonsten gleich sind. Das ist in dem gerade gezeigten Beispielcode der Fall, bei dem die Sortierung nach price, die Gruppierung dagegen nach category_id geschieht. Sphinx erzeugt mehrere Sortierwarteschlangen, um diese Abfragen zu verarbeiten. Wenn es die Abfragen ausführt, bezieht es die Zeilen einmal und übermittelt sie an alle Warteschlangen. Verglichen mit dem Ausführen der Abfragen nacheinander, werden damit mehrere redundante Volltextsuch- oder vollständige ScanOperationen eliminiert. Beachten Sie, dass das Generieren paralleler Ergebnismengen zwar eine gebräuchliche und wichtige Optimierung darstellt, aber nur ein spezieller Fall des allgemeineren Mehrfache-Abfragen-Mechanismus ist. Es ist nicht die einzige mögliche Optimierung. Die Faustregel lautet, nach Möglichkeit Abfragen in einer Anforderung zu kombinieren, was es Sphinx im Allgemeinen erlaubt, interne Optimierungen durchzuführen. Selbst wenn Sphinx die Abfragen nicht parallelisieren kann, spart es Rundreisen durch das Netzwerk. Und falls Sphinx in Zukunft weitere Optimierungen bringt, nutzen Ihre Abfragen sie automatisch ohne weitere Änderungen.
Skalierung Sphinx skaliert sowohl horizontal als auch vertikal recht gut. Sphinx kann vollständig über viele Maschinen verteilt werden. Alle Anwendungsfälle, die wir erwähnt haben, können davon profitieren, die Arbeit über mehrere CPUs zu verteilen. Der Sphinx-Such-Daemon (searchd) unterstützt besondere verteilte Indizes, die wissen, welche lokalen und entfernten Indizes abgefragt und zusammengefasst werden sollen. Das bedeutet, dass eine horizontale Skalierung eine triviale Konfigurationsänderung darstellt. Sie verteilen die Daten einfach über die Knoten, konfigurieren den Master-Knoten so, dass er mehrere entfernte Abfragen parallel zu den lokalen ausführt, und das war es schon. Sie können auch vertikal skalieren, also etwa mehr Kerne oder CPUs auf einer Maschine benutzen, um die Latenz zu verbessern. Dazu führen Sie z.B. einfach mehrere Instanzen von searchd auf einer einzelnen Maschine aus und fragen sie über einen verteilten Index von einer anderen Maschine aus ab. Alternativ können Sie eine einzelne Instanz so konfigurieren, dass sie mit sich selbst kommuniziert, so dass die parallelen »entfernten« Abfragen tatsächlich auf einer einzigen Maschine, aber auf unterschiedlichen CPUs oder Kernen laufen.
Wieso sollten Sie Sphinx benutzen?
| 687
Es kann also mit Sphinx eine einzelne Abfrage dazu gebracht werden, mehr als eine CPU zu benutzen (mehrere nebenläufige Abfragen benutzen automatisch mehrere CPUs). Das ist ein wesentlicher Unterschied zu MySQL, wo eine Abfrage immer eine CPU erhält, unabhängig davon, wie viele verfügbar sind. Sphinx benötigt außerdem keine Synchronisation zwischen nebenläufig ausgeführten Abfragen. Dadurch werden Mutexe (ein Synchronisierungsmechanismus) vermieden, die auf Mehr-CPU-Systemen einen berüchtigen MySQL-Leistungsengpass bilden. Ein weiterer wichtiger Aspekt der vertikalen Skalierung ist das Skalieren der FestplattenEin-/Ausgaben. Unterschiedliche Indizes (einschließlich der Teile eines größeren verteilten Index) können leicht auf unterschiedliche physische Festplatten oder RAID-Volumes gepackt werden, um die Latenz und den Durchsatz zu verbessern. Dieser Ansatz hat teilweise die gleichen Vorteile wie die partitionierten Tabellen in MySQL 5.1, bei denen ebenfalls Daten auf mehrere Stellen aufgeteilt werden. Allerdings sind verteilte Indizes sogar noch ein bisschen besser als partitionierte Tabellen. Sphinx verwendet verteilte Indizes sowohl zum Verteilen der Last als auch zum parallelen Verarbeiten aller Teile einer Abfrage. Im Gegensatz dazu kann die MySQL-Partitionierung einige (aber nicht alle) Abfragen optimieren, indem es Partitionen wegkürzt, jedoch wird die Abfrageverarbeitung nicht parallelisiert. Und obwohl sowohl Sphinx als auch die MySQL-Partitionierung den Abfragedurchsatz verbessern, können Sie bei ein-/ausgabegebundenen Abfragen von Sphinx eine lineare Latenzverbesserung bei allen Abfragen erwarten, während die MySQL-Partitionierung die Latenz nur bei den Abfragen verbessert, bei denen der Optimierer ganze Partitionen wegkürzen kann. Der Ablauf bei der verteilten Suche ist recht einfach: 1. Es werden entfernte Abfragen auf allen entfernten Servern ausgelöst. 2. Es werden sequenzielle lokale Indexsuchen durchgeführt. 3. Die Teilsuchergebnisse von den entfernten Servern werden gelesen. 4. Alle Teilergebnisse werden zur endgültigen Ergebnismenge zusammengefasst und
an den Client zurückgeliefert. Falls es Ihre Hardware-Ressourcen zulassen, können Sie auch mehrere Indizes auf der gleichen Maschine parallel durchsuchen. Falls es mehrere physische Festplattenlaufwerke und mehrere CPU-Kerne gibt, können die nebenläufigen Suchen durchgeführt werden, ohne einander zu stören. Sie können so tun, als wären einige der Indizes entfernt, und searchd so konfigurieren, dass es sich selbst kontaktiert, um eine parallele Abfrage auf der gleichen Maschine auszulösen: index distributed_sample { type = distributed local = chunk1 # liegt auf HDD1 agent = localhost:3312:chunk2 # liegt auf HDD2, searchd kontaktiert sich selbst }
688 |
Anhang C: Sphinx mit MySQL benutzen
Aus Sicht des Clients unterscheiden sich verteilte Indizes nicht von lokalen Indizes. Dies erlaubt es Ihnen, »Bäume« aus verteilten Indizes zu erzeugen, indem Sie Knoten als Proxies für Gruppen anderer Knoten verwenden. Beispielsweise könnte der Knoten auf der ersten Ebene stellvertretend die Abfragen für eine Reihe von Knoten der zweiten Ebene entgegennehmen, die wiederum entweder sich selbst lokal durchsuchen oder die Abfragen an andere Knoten weitergeben – bis zu einer beliebigen Tiefe.
Verteilte Daten ansammeln Der Aufbau eines skalierbaren Systems beinhaltet oft das Sharding (Partitionieren) der Daten über unterschiedliche physische MySQL-Server. Wir haben das ausführlich in »Datenzerlegung (Sharding)« auf Seite 454 besprochen. Wenn die Daten mit einer hohen Granularität verteilt wurden, dann bedeutet das simple Holen einiger Zeilen mit einem selektiven WHERE (das schnell sein sollte), dass viele Server kontaktiert werden müssen, dass auf Fehler geprüft werden muss und dass die Ergebnisse in der Anwendung zusammengefasst werden müssen. Sphinx vermindert dieses Problem, weil die gesamte notwendige Funktionalität bereits im Such-Daemon implementiert ist. Betrachten Sie ein Beispiel, bei dem eine 1-TByte-Tabelle mit einer Milliarde Blog-Einträgen anhand der Benutzer-ID über 10 physische MySQL-Server verteilt wird, so dass die Nachrichten eines bestimmten Benutzers immer auf denselben Server gelangen. Solange Abfragen auf einen einzigen Benutzer beschränkt werden, ist alles in Ordnung: Wir wählen den Server auf der Grundlage der Benutzer-ID und arbeiten damit wie gehabt. Nehmen Sie nun an, dass wir eine Archivseite implementieren müssen, auf der die Nachrichten der Freunde des Benutzers zu sehen sind. Wie zeigen wir Seite 50 mit den Einträgen 981 bis 1000, sortiert nach Datum, an? Höchstwahrscheinlich liegen die Daten der verschiedenen Freunde auf unterschiedlichen Servern. Bei nur 10 Freunden besteht eine Wahrscheinlichkeit von etwa 90 %, dass mehr als 8 Server benutzt werden. Diese Wahrscheinlichkeit steigt auf 99 %, wenn es 20 Freunde sind. Das heißt, für die meisten Abfragen müssen wir alle Server ansprechen. Noch schlimmer: Wir müssen 1.000 Nachrichten von jedem Server ziehen und diese in der Anwendung sortieren. Entsprechend den Anregungen, die wir in Kapitel 10 und anderswo geliefert haben, streichen wir die benötigten Daten auf die Nachrichten-ID und den Zeitstempel zusammen, aber auch das sind noch 10.000 Datensätze, die in der Anwendung sortiert werden müssen. Die meisten modernen Skriptsprachen verbrauchen allein für diesen Sortierschritt eine Menge CPU-Zeit. Darüber hinaus müssen wir entweder die Datensätze sequenziell von den einzelnen Servern holen (was langsam sein wird) oder irgendwelchen Code schreiben, um die parallelen Abfrage-Threads unter einen Hut zu bringen (was schwierig zu implementieren und zu warten ist). In solchen Situationen bietet es sich an, Sphinx zu benutzen, anstatt das Rad noch einmal zu erfinden. Wir müssen in diesem Fall lediglich mehrere Sphinx-Instanzen einrichten, die Nachrichtenattribute, auf die häufig zugegriffen wird, aus den einzelnen Tabellen Wieso sollten Sie Sphinx benutzen?
| 689
spiegeln – in diesem Beispiel also die Nachrichten-ID, die Benutzer-ID und den Zeitstempel – und die Master-Sphinx-Instanz nach den Einträgen 981 bis 1000, sortiert nach Nachrichtendatum, in ungefähr drei Zeilen Code abfragen. Das ist eine viel geschicktere Art der Skalierung.
Überblick über die Architektur Sphinx besteht aus mehreren einzelnen Programmen. Die beiden wichtigsten sind: indexer Das ist ein Programm, das Dokumente aus angegebenen Quellen holt (z.B. aus den MySQL-Abfrageergebnissen) und einen Volltextindex aus ihnen erzeugt. Dabei handelt es sich um einen Hintergrund-Batch-Job, den die Sites üblicherweise regelmäßig durchführen. searchd Das ist ein Daemon, der die Suchabfragen aus den Indizes bedient, die der indexer erzeugt. Dies bietet die Laufzeitunterstützung für Anwendungen. Die Sphinx-Distribution enthält außerdem native searchd-Client-APIs in einer Reihe von Programmiersprachen (zurzeit sind das PHP, Python, Perl, Ruby und Java) und SphinxSE, einen Client, der als Plugin-fähige Storage-Engine für MySQL-Versionen ab 5.0 implementiert ist. Die APIs und SphinxSE erlauben es einer Clientanwendung, eine Verbindung zu searchd herzustellen, ihm die Suchabfrage zu übergeben und die Suchergebnisse zurückzugeben. Jeder Sphinx-Volltextindex kann mit einer Tabelle in einer Datenbank verglichen werden; anstatt aus Zeilen in einer Tabelle besteht der Sphinx-Index aus Dokumenten. (Sphinx besitzt außerdem eine eigene Datenstruktur namens Mehrwert-Attribut, auf die wir später noch eingehen.) Jedes Dokument besitzt einen eindeutigen 32-Bit- oder 64-BitInteger-Identifikator, der aus der Datenbanktabelle bezogen werden soll, die indiziert wird (z.B. aus einer Primärschlüsselspalte). Darüber hinaus enthält jedes Dokument ein oder mehrere Volltextfelder (die jeweils einer Textspalte aus der Datenbank entsprechen) und numerische Attribute. Genau wie eine Datenbanktabelle hat der Sphinx-Index die gleichen Felder und Attribute für alle seine Dokumente. Tabelle C-1 zeigt die Analogie zwischen einer Datenbanktabelle und einem Sphinx-Index. Tabelle C-1: Datenbankstruktur und entsprechende Sphinx-Struktur Datenbankstruktur
Sphinx-Struktur
CREATE TABLE documents ( id int(11) NOT NULL auto_increment, title varchar(255), content text, group_id int(11), added datetime, PRIMARY KEY (id) );
index documents document ID title field, full-text indexed content field, full-text indexed group_id attribute, sql_attr_uint added attribute, sql_attr_timestamp
690 |
Anhang C: Sphinx mit MySQL benutzen
Sphinx speichert nicht die Textfelder aus der Datenbank, sondern nur ihren Index, um einen Suchindex zu erstellen.
Überblick über die Installation Die Installation von Sphinx ist einfach und umfasst folgende Schritte: 1. Aufbau der Programme aus den Quellen: $ configure && make && make install
2. Erzeugen einer Konfigurationsdatei mit Definitionen für Datenquellen und Volltext-
indizes 3. Erste Indizierung 4. Aufruf von searchd
Anschließend steht die Suchfunktionalität sofort für Clientprogramme zur Verfügung: Query ( 'test query', 'myindex' ); // benutze hier $res Suchergebnis ?>
Jetzt muss nur noch indexer regelmäßig ausgeführt werden, um die Volltextindexdaten zu aktualisieren. Indizes, die searchd momentan bedient, bleiben während der Reindizierung voll funktionstüchtig: indexer erkennt, dass sie gerade in Benutzung sind, erzuegt stattdessen eine »Schatten«-Indexkopie und benachrichtigt searchd, um diese Kopie bei Fertigstellung aufzugreifen. Volltextindizes werden im Dateisystem gespeichert (an der Stelle, die in der Konfigurationsdatei angegeben ist) und liegen in einem besonderen »monolithischen« Format vor, das sich nicht gut für inkrementelle Backups eignet. Die normale Methode, die Indexdaten zu aktualisieren, besteht darin, sie von Grund auf neu aufzubauen. Das ist jedoch aus folgenden Gründen kein so großes Problem, wie es scheinen mag: • Die Indizierung ist schnell. Sphinx kann einfachen Text (ohne HTML-Auszeichnungen) auf moderner Hardware mit einer Rate von 4–8 MByte/s indizieren. • Sie können die Daten in mehreren Indizes partitionieren, wie im nächsten Abschnitt gezeigt wird, und indizieren nur den aktualisierten Teil bei jedem Durchlauf von indexer von Grund auf neu. • Es besteht kein Grund, die Indizes zu »defragmentieren« – sie sind für optimale Ein/Ausgaben geschaffen, was die Suchgeschwindigkeit verbessert. • Numerische Attribute können ohne einen vollständigen Neuaufbau aktualisiert werden. Eine künftige Version wird ein zusätzliches Index-Backend bieten, das Index-Updates in Echtzeit unterstützt.
Überblick über die Architektur
| 691
Typische Verwendung der Partitionierung Wir wollen nun die Partitionierung etwas ausführlicher diskutieren. Das einfachste Partitionierungsschema ist der main + delta-Ansatz, bei dem zwei Indizes erzeugt werden, um eine Dokumentensammlung zu indizieren. Main indiziert die komplette Dokumentenmenge, während delta nur Dokumente indiziert, die sich seit der letzten Erstellung des Hauptindex geändert haben. Dieses Schema passt perfekt auf viele Datenmodifikationsmuster. Foren, Blogs, E-Mailund News-Archive sowie vertikale Suchmaschinen stellen gute Beispiele dar. Die meisten der Daten in diesen Datenlagern ändern sich nicht mehr, nachdem sie dort eingetroffen sind; nur ein Bruchteil der Dokumente wird regelmäßig geändert oder ergänzt. Das bedeutet, dass der Delta-Index klein ist und so oft wie nötig neu aufgebaut werden kann (z.B. einmal alle 1–15 Minuten). Dies ist äquivalent zum Indizieren nur der neu eingefügten Zeilen. Sie müssen die Indizes nicht neu aufbauen, um die Attribute zu ändern, die mit den Dokumenten verknüpft sind – das können Sie online über searchd erledigen. Sie können Zeilen als gelöscht markieren, indem Sie einfach ein »Gelöscht«-Attribut im Hauptindex setzen. Daher verarbeiten Sie Updates, indem Sie dieses Attribut in Dokumenten im Hauptindex markieren und dann den Delta-Index neu aufbauen. Die Suche nach allen Dokumenten, die nicht als »gelöscht« markiert sind, liefert die korrekten Ergebnisse zurück. Beachten Sie, dass die indizierten Daten von den Ergebnissen einer SELECT-Anweisung kommen können; sie müssen nicht aus einer einzigen SQL-Tabelle stammen. Es gibt keine Beschränkungen für die SELECT-Anweisungen. Das bedeutet, dass Sie die Ergebnisse in der Datenbank vorverarbeiten können, bevor sie indiziert werden. Zu den gebräuchlichen Beispielen für eine Vorverarbeitung gehören Joins mit anderen Tabellen, das spontane Erzeugen von zusätzlichen Feldern, das Ausschließen einiger Felder aus der Indizierung und das Manipulieren von Werten.
Besondere Eigenschaften Neben dem »einfachen« Indizieren und Durchsuchen von Datenbankinhalten bietet Sphinx weitere besondere Funktionen. Hier ist eine (unvollständige) Liste der wichtigsten: • Die Such- und Ranking-Algorithmen ziehen Wortpositionen und die Nähe der Abfragephrase zum Dokumentinhalt in Betracht. • Sie können numerische Attribute, einschließlich multiwertiger Attribute, an Dokumente binden. • Sie können nach Attributwerten sortieren, filtern und gruppieren. • Sie können Dokumentschnipsel mit hervorgehobenen Suchschlüsselwörtern erzeugen.
692 |
Anhang C: Sphinx mit MySQL benutzen
• Sie können das Suchen über mehrere Maschinen verteilen. • Sie können Abfragen optimieren, die mehrere Ergebnismengen aus den gleichen Daten erzeugen. • Sie können mittels SphinxSE aus MySQL heraus auf die Suchergebnisse zugreifen. • Sie können die Last justieren, die Sphinx dem Server auferlegt. Wir haben einige dieser Funktionen bereits früher behandelt. In diesem Abschnitt geht es um ein paar der verbleibenden Funktionen.
Phrasennähe-Ranking Sphinx, wie auch andere Open-Source-Volltextsuchsysteme, merkt sich die Wortpositionen innerhalb der einzelnen Dokumente. Aber im Gegensatz zu den meisten anderen benutzt es die Positionen, um die Rangfolge der Treffer festzustellen und relevantere Ergebnisse zurückzuliefern. Zum letztendlichen Rang eines Dokuments können eine Reihe von Faktoren beitragen. Um den Rang zu berechnen, benutzen die meisten anderen Systeme nur die Schlüsselworthäufigkeit: Wie oft treten die einzelnen Schlüsselwörter auf? Die klassische BM25Wichtungsfunktion2, die praktisch alle Volltextsuchsysteme benutzen, baut darauf auf, dass solchen Wörtern mehr Gewicht gegeben wird, die entweder häufig in dem speziellen Dokument auftauchen, das durchsucht wird, oder die selten im gesamten Dokument auftauchen. Das BM25-Ergebnis wird normalerweise als fertiger Rangwert zurückgegeben. Im Gegensatz dazu berechnet Sphinx auch die Abfragephrasennähe, bei der es sich einfach um die Länge der längsten wortwörtlichen Abfragenteilphrase handelt, die im Dokument enthalten ist, gezählt in Wörtern. Zum Beispiel erzeugt die Phrase »John Doe Jr«, abgefragt in einem Dokument mit dem Text »John Black, John White Jr und Jane Dunne« eine Phrasennähe von 1, weil keine zwei Wörter in der Abfrage zusammen in der Abfragereihenfolge auftauchen. Die gleiche Abfrage an »Mr. John Doe Jr und Freunde« ergibt eine Nähe von 3, weil drei Abfragewörter im Dokument in der Abfragereihenfolge auftauchen. Das Dokument »John Gray, Jane Doe Jr« ergibt dank der Teilphrase »Doe Jr« eine Nähe von 2. Standardmäßig klassifiziert Sphinx Treffer zuerst mittels Phrasennähe und erst dann mit der klassischen BM25-Wichtung. Das bedeutet, dass wortwörtliche Abfragezitate garantiert ganz oben stehen. Anschließend kommen Zitate, die um ein Wort abweichen, usw. Wann und wie beeinflusst die Phrasennähe die Ergebnisse? Betrachten Sie das Durchsuchen von 1.000.000 Textseiten nach der Phrase »To be or not to be«. Sphinx setzt die Seiten mit wörtlichen Zitaten an die Spitze der Suchergebnisse, während BM25-basierte Systeme zuerst die Seiten mit den häufigsten Erwähnungen von »to«, »be«, »or« und »not« zurückliefern – Seiten mit dem exakten Zitat, aber nur wenigen Erwähnungen von »to« werden tief in den Ergebnissen vergraben. 2 Näheres unter http://en.wikipedia.org/wiki/Okapi_BM25.
Besondere Eigenschaften | 693
Die meisten großen Websuchmaschinen stellen die Rangfolgen heutzutage auch mit Schlüsselwortpositionen fest. Die Suche nach einer Phrase mit Google setzt wahrscheinlich perfekte oder fast perfekte Treffer an die Spitze, gefolgt von den Dokumenten mit den »Worthäufungen«. Allerdings erfordert die Analyse von Schlüsselwortpositionen zusätzliche CPU-Zeit, und manchmal müssen Sie sie aus Leistungsgründen überspringen. Es gibt auch Fälle, in denen das Phrasen-Ranking unerwünschte, unerwartete Ergebnisse liefert. Beispielsweise funktioniert das Suchen nach Tags in einer Wolke besser ohne Schlüsselwortpositionen: Es ist egal, ob die Tags aus der Abfrage im Dokument nebeneinander stehen. Aus Gründen der Flexibilität stellt Sphinx mehrere Ranking-Modi zur Auswahl. Neben dem vorgegebenen Modus aus Nähe plus BM25, können Sie aus einer Reihe von anderen Modi wählen, unter anderem Wichtung nur mit BM25, vollständig deaktivierte Wichtung (was eine hübsche Optimierung bietet, falls Sie nicht nach dem Rang sortieren) usw.
Unterstützung für Attribute Jedes Dokument kann eine unbegrenzte Anzahl numerischer Attribute enthalten. Attribute werden vom Benutzer angegeben und können zusätzliche Informationen enthalten, die für eine bestimmte Aufgabe erforderlich sind. Beispiele sind die Autoren-ID eines Blog-Eintrags, der Preis eines Inventarstücks, eine Kategorien-ID usw. Attribute ermöglichen effiziente Volltextsuchen mit zusätzlicher Filterung, Sortierung und Gruppierung der Suchergebnisse. Theoretisch könnten sie in MySQL gespeichert und jedes Mal dort herausgezogen werden, wenn eine Suche durchgeführt wird. In der Praxis jedoch wäre dieses Vorgehen unakzeptabel langsam, falls eine Volltextsuche Hunderte oder Tausende von Zeilen findet (was nicht viel wäre). Sphinx unterstützt zwei Möglichkeiten, Attribute zu speichern: inline in den Dokumentlisten oder extern in einer getrennten Datei. Die Inline-Speicherung verlangt, dass alle Attributwerte viele Male im Index gespeichert werden, und zwar jedes Mal wenn eine Dokument-ID gespeichert wird. Dies bläst den Index auf und erhöht die Anzahl der Ein-/ Ausgaben, verringert aber die RAM-Nutzung. Das externe Speichern der Attribute erfordert, dass sie beim Start von searchd in den RAM geladen werden. Normalerweise passen die Attribute in den RAM, so dass sie üblicherweise extern gespeichert werden. Dadurch wird die Filterung, Sortierung und Gruppierung sehr schnell, da der Zugriff der Daten jetzt nur noch eine Frage des schnellen Blicks in den Speicher ist. Außerdem können nur die extern gespeicherten Attribute zur Laufzeit aktualisiert werden. Die Inline-Speicherung sollte nur dann zum Einsatz kommen, wenn es nicht genügend freien RAM gibt, um die Attributdaten aufzunehmen. Sphinx unterstützt darüber hinaus Mehrwert-Attribute (Multivalued-Attributes; MVAs). Der MVA-Inhalt besteht aus einer beliebig langen Liste von Integer-Werten, die mit dem jeweiligen Dokument verknüpft sind. Beispiele für gute Anwendungen von MVAs sind Listen mit Tag-IDs, Produktkategorien und Zugriffskontrolllisten.
694 |
Anhang C: Sphinx mit MySQL benutzen
Filterung Der Zugriff auf die Attribute in der Volltext-Engine erlaubt es Sphinx, potenzielle Treffer während der Suche so früh wie möglich zu filtern und abzuweisen. Technisch gesehen tritt der Filtertest nach der Überprüfung auf, dass das Dokument alle erforderlichen Schlüsselwörter enthält, aber bevor bestimmte aufwendige Berechnungen (wie etwa das Ranking) erledigt werden. Aufgrund dieser Optimierungen kann die Verwendung von Sphinx, um die Volltextsuche mit der Filterung und Sortierung zu kombinieren, 10- bis 100-mal schneller sein als die Verwendung von Sphinx für die Suche und die anschließende Filterung der Ergebnisse in MySQL. Sphinx unterstützt zwei Arten von Filtern, die analog zu einfachen WHERE-Bedingungen in SQL sind: • Ein Attributwert passt auf einen angegebenen Bereich aus Werten (analog zu einer BETWEEN-Klausel oder numerischen Vergleichen). • Ein Attributwert passt zu einer angegebenen Menge von Werten (analog zu einer IN( )-Liste). Falls die Filter eine feste Nummer von Werten haben (»Mengen«-Filter anstelle von »Bereichs«-Filtern) und falls solche Werte selektiv sind, ist es sinnvoll, die Integer-Werte durch »falsche Schlüsselwörter« zu ersetzen und sie als Volltextinhalt zu indizieren und nicht als Attribute. Dies gilt sowohl für normale Attribute als auch für MVAs. Wir werden später Beispiele dafür sehen. Sphinx kann Filter außerdem einsetzen, um vollständige Scans zu optimieren. Sphinx merkt sich die minimalen und maximalen Attributwerte für kurze zusammenhängende Zeilenblöcke (standardmäßig 128 Zeilen) und kann schnell ganze Blöcke auf der Grundlage der Filterbedingungen wegwerfen. Die Zeilen werden in der Reihenfolge aufsteigender Dokument-IDs gespeichert, so dass diese Optimierung am besten für Spalten funktioniert, die mit der ID korrelieren. Falls Sie z.B. einen Zeitstempel für das Zeileneinfügen haben, der mit der ID anwächst, dann ist ein vollständiger Scan mit Filterung dieses Zeitstempels sehr schnell.
Die Plugin-fähige SphinxSE-Storage-Engine Volltextsuchergebnisse, die von Sphinx empfangen werden, erfordern fast immer zusätzliche Arbeit mit MySQL – zumindest um die Textspalten zu holen, die der Sphinx-Index nicht speichert. In der Folge müssen Sie oft Suchergebnisse von Sphinx mit anderen MySQL-Tabellen in einem JOIN zusammenführen. Sie können dieses Ziel zwar auch erreichen, indem Sie die Dokument-IDs des Ergebnisses in einer Abfrage an MySQL senden, allerdings führt diese Strategie weder zum saubersten noch zum schnellsten Code. Wenn es um große Volumina geht, sollten Sie die Verwendung von SphinxSE in Betracht ziehen, einer Plugin-fähigen Storage-Engine, die Sie in MySQL-Versionen ab 5.0 kompilieren oder ab MySQL 5.1 als Plugin laden können.
Besondere Eigenschaften | 695
SphinxSE erlaubt es Programmierern, aus MySQL heraus searchd abzufragen und auf Suchergebnisse zuzugreifen. Die Benutzung ist so einfach wie das Erzeugen einer speziellen Tabelle mit einer ENGINE=SPHINX-Klausel (und einer optionalen CONNECTION-Klausel zum Suchen des Sphinx-Servers, falls er sich nicht an der erwarteten Stelle befindet) und das Ausführen von Abfragen an dieser Tabelle: mysql> CREATE TABLE search_table ( -> id INTEGER NOT NULL, -> weight INTEGER NOT NULL, -> query VARCHAR(3072) NOT NULL, -> group_id INTEGER, -> INDEX(query) -> ) ENGINE=SPHINX CONNECTION="sphinx://localhost:3312/test"; Query OK, 0 rows affected (0.12 sec) mysql> SELECT * FROM search_table WHERE query='test;mode=all' \G *************************** 1. row *************************** id: 123 weight: 1 query: test;mode=all group_id: 45 1 row in set (0.00 sec)
Jedes SELECT übergibt eine Sphinx-Abfrage als query-Spalte in der WHERE-Klausel. Der Sphinx-searchd-Server liefert die Ergebnisse zurück. Die SphinxSE-Storage-Engine übersetzt diese dann in MySQL-Ergebnisse und gibt sie an die SELECT-Anweisung zurück. Abfragen können JOINs mit anderen Tabellen enthalten, die mit anderen Storage-Engines gespeichert sind. Die SphinxSE-Engine unterstützt außerdem die meisten Suchoptionen, die über die API verfügbar sind. Sie können Optionen wie Filterung und Beschränkungen angeben, indem Sie zusätzliche Klauseln in den Abfragestring aufnehmen: mysql> SELECT * FROM search_table WHERE query='test;mode=all; -> filter=group_id,5,7,11;maxmatches=3000';
Abfrageweise und wortweise Statistiken, die von der API zurückgegeben werden, stehen ebenfalls über SHOW STATUS zur Verfügung: mysql> SHOW ENGINE SPHINX STATUS \G *************************** 1. row *************************** Type: SPHINX Name: stats Status: total: 3, total found: 3, time: 8, words: 1 *************************** 2. row *************************** Type: SPHINX Name: words Status: test:3:5 2 rows in set (0.00 sec)
Selbst wenn Sie SphinxSE benutzen, besagt die Faustregel, dass es searchd erlaubt wird, das Sortieren, Filtern und Gruppieren vorzunehmen – d.h. lieber alle erforderlichen Klauseln in den Abfragestring aufzunehmen, anstatt WHERE, ORDER BY oder GROUP BY zu 696 |
Anhang C: Sphinx mit MySQL benutzen
benutzen. Das ist besonders wichtig für WHERE-Bedingungen. Der Grund besteht darin, dass SphinxSE nur ein Client für searchd ist, keine voll ausgestattete eingebaute Suchbibliothek. Sie müssen daher alles Mögliche an die Sphinx-Engine übergeben, um die beste Performance zu erzielen.
Erweiterte Leistungsüberwachung Sowohl die Indizierung als auch die Suchoperationen könnten dem Suchserver bzw. dem Datenbankserver eine deutliche zusätzliche Last auferlegen. Zum Glück gibt es eine Reihe von Einstellungen, die es Ihnen erlauben, die Last einzuschränken, die von Sphinx kommt. Eine unerwünschte datenbankseitige Last kann von indexer-Abfragen verursacht werden, die entweder MySQL vollständig mit ihren Sperren blockieren oder einfach zu schnell auftreten und Ressourcen von anderen nebenläufigen Abfragen abziehen. Der erste Fall ist ein berüchtigtes Problem mit MyISAM, wo lange laufende Leseoperationen die Tabellen sperren und andere ausstehende Lese- und Schreibvorgänge blockieren – Sie können nicht einfach SELECT * FROM big_table auf einem Produktionsserver ausführen, weil Sie riskieren, alle anderen Operationen zu unterbrechen. Um dieses Problem zu beheben, bietet Sphinx bereichsweise Abfragen. Anstatt eine einzige riesige Abfrage zu konfigurieren, können Sie eine Abfrage angeben, die schnell die indizierbaren Zeilenbereiche berechnet, und eine weitere Abfrage, die die Daten Schritt für Schritt in kleinen Häppchen herauszieht: sql_query_range sql_range_step sql_query
= SELECT MIN(id),MAX(id) FROM documents = 1000 = SELECT id, title, body FROM documents \ WHERE id>=$start AND id<=$end
Diese Funktion ist außerordentlich hilfreich für das Indizieren von MyISAM-Tabellen, sollte aber auch in Betracht gezogen werden, wenn InnoDB-Tabellen zum Einsatz kommen. InnoDB sperrt zwar nicht einfach die Tabelle und blockiert andere Abfragen, wenn es ein großes SELECT * ausführt, nutzt aber aufgrund seiner MVCC-Architektur viele Maschinenressourcen. Eine Multiversionierung für 1.000 Transaktionen, die jeweils 1.000 Zeilen abdecken, kann weniger teuer sein als eine einzige lange laufende MillionenZeilen-Transaktion. Der zweite Grund für ausufernde Last tritt ein, wenn indexer in der Lage ist, die Daten schneller zu verarbeiten, als MySQL sie liefert. Sie sollten in diesem Fall ebenfalls Bereichsabfragen verwenden. Die Option sql_ranged_throttle zwingt indexer, für einen bestimmten Zeitraum (angegeben in Millisekunden) zwischen den aufeinanderfolgenden Bereichsabfragen zu schlafen, wodurch zwar die Indizierungszeit erhöht wird, aber die Last auf MySQL abnimmt. Interessanterweise gibt es einen Sonderfall, bei dem Sie Sphinx dazu bringen können, genau den gegenteiligen Effekt zu erreichen: das heißt, die Indizierungszeit zu verbessern, indem Sie MySQL mehr Last aufladen. Wenn die Verbindung zwischen der indexer-Kiste Besondere Eigenschaften | 697
und der Datenbankkiste 100 Mbps beträgt und die Zeilen gut komprimiert werden (was typisch für Textdaten ist), kann das MySQL-Komprimierungsprotokoll die Gesamtindizierungszeit verbessern. Allerdings muss jetzt sowohl auf der MySQL- als auch auf der indexer-Seite mehr CPU-Zeit aufgewandt werden, um die Zeilen, die über das Netzwerk übertragen werden, zu komprimieren bzw. zu dekomprimieren. Allerdings könnte die Gesamtindizierungszeit wegen des stark reduzierten Netzwerkverkehrs bis zu 20–30 % niedriger sein. Auch Such-Cluster können unter gelegentlicher Überlast leiden. Deshalb bietet Sphinx einige Methoden, um zu verhindern, dass searchd ausfällt. Erstens begrenzt eine max_children-Option einfach die Gesamtanzahl der nebenläufig ausgeführten Abfragen und weist bei Erreichen dieser Grenze die Clients an, es später noch einmal zu probieren. Dann gibt es noch Begrenzungen auf Abfrageebene. Sie können festlegen, dass die Abfrageverarbeitung entweder bei einem bestimmten Schwellenwert gefundener Treffer stoppt oder bei einem angegebenen Schwellenwert abgelaufener Zeit anhält. Dazu verwenden Sie die API-Aufrufe SetLimits( ) bzw. SetMaxQueryTime( ). Dies wird pro Abfrage festgelegt, Sie können also sicherstellen, dass wichtigere Abfragen immer vollständig ausgeführt werden. Schließlich können periodische indexer-Durchläufe Spitzen zusätzlicher Ein-/Ausgaben verursachen, die wiederum searchd ausbremsen. Um das zu vermeiden, gibt es Optionen, die die indexer-Festplatten-Ein-/Ausgaben begrenzen. max_iops setzt eine minimale Verzögerung zwischen Ein-/Ausgabe-Operationen durch, die dafür sorgt, dass nicht mehr als max_iops-Festplattenoperationen pro Sekunde durchgeführt werden. Aber selbst eine einzige Operation könnte zu viel sein; stellen Sie sich z.B. einen 100MByte umfassenden read( )-Aufruf vor. Die Option max_iosize kümmert sich genau darum. Sie garantiert, dass die Länge der jeweiligen Lese- oder Schreiboperationen unter einer angegebenen Grenze bleibt. Größere Operationen werden automatisch in kleinere aufgeteilt, und diese kleineren werden dann von den max_iops-Einstellungen gesteuert.
Praktische Implementierungsbeispiele Jede dieser beschriebenen Funktionen wurde erfolgreich in der Produktion umgesetzt. In den folgenden Abschnitten schauen wir uns diese realen Beispiele für den Sphinx-Einsatz an, wobei wir kurz die Sites und einige Implementierungsdetails beschreiben.
Volltextsuche auf Mininova.org Die beliebte Torrent-Suchmaschine Mininova.org stellt ein gutes Beispiel dafür dar, wie eine »reine« Volltextsuche optimiert wird. Sphinx ersetzte mehrere MySQL-Slaves, die die in MySQL eingebauten Volltextindizes benutzten, aber nicht in der Lage waren, die Last zu bewältigen. Nach der Ersetzung waren die Suchserver nicht mehr ausgelastet; der aktuelle Lastdurchschnitt liegt jetzt im Bereich von 0,3–0,4. 698 |
Anhang C: Sphinx mit MySQL benutzen
Dies sind die Datenbankgröße und die Lastwerte: • Die Site besitzt eine kleine Datenbank mit ungefähr 300.000–500.000 Datensätzen und einem Index von etwa 300–500 MByte Größe. • Die Last auf der Site ist ziemlich hoch: Momentan gehen etwa 8–10 Millionen Suchanfragen pro Tag ein. Die Daten bestehen hauptsächlich aus Dateinamen, die von Benutzern übermittelt werden, oft ohne richtige Interpunktion. Aus diesem Grund wird anstelle einer Indizierung ganzer Wörter eine Präfixindizierung verwendet. Der resultierende Index ist deshalb mehrere Male größer, als er normalerweise sein würde, ist aber klein genug, damit er schnell aufgebaut werden kann und seine Daten effektiv im Cache gespeichert werden können. Die Suchergebnisse für die 1.000 am häufigsten vorkommenden Abfragen werden auf der Anwendungsseite im Cache abgelegt. Ungefähr 20–30 % aller Abfragen werden aus dem Cache bedient. Wegen der »Long Tail«-Abfrageverteilung würde ein großer Cache hier nicht mehr helfen. Um Hochverfügbarkeit zu gewährleisten, verwendet die Site zwei Server mit vollständigen Repliken des Volltextindex. Die Indizes werden alle paar Minuten von Grund auf neu aufgebaut. Die Indizierung dauert weniger als eine Minute, weshalb es nicht nötig ist, komplexere Schemata zu implementieren. Folgende Lehren kann man aus diesem Beispiel ziehen: • Die Cache-Speicherung der Suchergebnisse in der Anwendung ist sehr hilfreich. • Selbst für ausgelastete Anwendungen ist es unter Umständen nicht nötig, einen riesigen Cache zu haben. 1.000 bis 10.000 Einträge können ausreichen. • Für Datenbanken in der Größenordnung von 1 GByte Größe ist es in Ordnung, regelmäßig den Index neu zu erstellen, anstatt kompliziertere Schemata einzusetzen, und das selbst bei ausgelasteten Sites.
Volltextsuche auf BoardReader.com Mininova ist ein Beispiel für ein Projekt mit extrem hoher Last – es gibt zwar nicht so viele Daten, aber es werden viele Abfragen an diese Daten gerichtet. BoardReader (http://www.boardreader.com) war ursprünglich genau das Gegenteil: eine Forensuchmaschine, die viel weniger Suchabfragen an einer viel größeren Datenmenge durchgeführt hat. Sphinx ersetzte eine kommerzielle Volltextsuchmaschine, die bis zu 10 Sekunden pro Abfrage benötigte, um eine 1-GByte-Sammlung zu durchsuchen. Mithilfe von Sphinx konnte BoardReader ausgezeichnet skaliert werden, sowohl was die Datengröße betrifft als auch in Bezug auf den Durchsatz an Abfragen.
Praktische Implementierungsbeispiele | 699
Hier sind einige allgemeine Informationen: • Es gibt mehr als 1 Milliarde Dokumente und mehr als 1,5 TByte an Text in der Datenbank. • Es gibt ungefähr 500.000 Page-Views und zwischen 700.000 und 1 Million Suchanfragen pro Tag. Momentan besteht der Such-Cluster aus sechs Servern, die jeweils vier logische CPUs (zwei Dual-Core Xeons), 16 GByte RAM und 0,5 TByte Festplattenplatz enthalten. Die Datenbank selbst ist auf einem separaten Cluster gespeichert. Der Such-Cluster wird nur für die Indizierung und die Suche verwendet. Jeder der vier Server führt vier searchd-Instanzen aus, so dass alle vier Kerne zum Einsatz kommen. Eine der vier Instanzen sammelt die Ergebnisse von den anderen drei Instanzen ein. Das macht insgesamt 24 searchd-Instanzen. Die Daten sind gleichmäßig über alle verteilt. Jede searchd-Kopie führt mehrere Indizes über ungefähr 1/24 der Gesamtdaten (etwa 60 GByte). Die Suchergebnisse von den sechs »erstrangigen« searchd-Knoten werden wiederum von einem weiteren searchd eingesammelt, das auf dem Frontend-Webserver läuft. Diese Instanz führt mehrere rein verteilte Indizes, die auf die sechs Such-Cluster-Server verweisen, aber keine lokalen Daten enthalten. Weshalb braucht man vier searchd-Instanzen pro Knoten? Wieso reicht nicht eine searchd-Instanz pro Server, die man so konfiguriert, dass sie vier Index-Teile führt und sich selbst kontaktiert, so als wäre sie ein entfernter Server (um mehrere CPUs zu benutzen, wie wir früher schon einmal vorgeschlagen haben)? Es hat seine Vorteile, vier Instanzen anstelle einer einzigen zu haben. Erstens wird die Startzeit verringert. Es gibt mehrere Gigabyte an Attributdaten, die in den RAM geladen werden müssen; das Starten mehrerer Daemons zur gleichen Zeit erlaubt es uns, dies zu parallelisieren. Zweitens wird die Verfügbarkeit verbessert. Im Fall von searchd-Ausfällen oder Updates ist nur 1/24 des gesamten Index nicht verfügbar, und nicht 1/6. Innerhalb der einzelnen 24 Instanzen im Such-Cluster benutzen wir eine zeitbasierte Partitionierung, um die Last weiter zu verringern. Viele Abfragen müssen nur auf den neuesten Daten ausgeführt werden, weshalb die Daten in drei disjunkte Indexmengen unterteilt werden: Daten aus der letzten Woche, aus den letzten drei Monaten und aus dem gesamten Zeitraum. Diese Indizes werden instanzweise über unterschiedliche physische Festplatten verteilt. Auf diese Weise hat jede Instanz ihre eigene CPU und ihr eigenes physisches Festplattenlaufwerk und stört die anderen nicht. Lokale cron-Jobs aktualisieren regelmäßig die Indizes. Sie ziehen die Daten über das Netzwerk von MySQL, erzeugen die Indexdateien aber lokal. Die Verwendung mehrerer, explizit getrennter »Raw«-Festplatten hat sich als schneller erwiesen als ein einziges RAID-Volume. Raw-Festplatten bieten Kontrolle darüber, welche Dateien auf welche physische Festplatte gelangen. Das ist bei RAID nicht der Fall, wo der Controller entscheidet, welcher Block auf welche physische Festplatte gelangt. Raw-
700 |
Anhang C: Sphinx mit MySQL benutzen
Festplatten garantieren außerdem voll parallele Ein-/Ausgaben in den unterschiedlichen Indexteilen, während gleichzeitig durchgeführte Suchen in RAID die Ursache für eine Ein-/Ausgabe-Staffelung bilden. Wir wählten RAID 0, das keine Redundanz bietet, weil Festplattenausfällt uns egal sind; wir können die Indizes leicht in den Suchknoten neu erstellen. Wir hätten auch mehrere RAID-1-Volumes (Spiegel-Volumes) benutzen können, um den gleichen Durchsatz wie mit Raw-Festplatten zu erreichen, jedoch bei verbesserter Zuverlässigkeit. Eine andere interessante Sache, die man von BoardReader lernen kann, ist, wie SphinxVersions-Updates durchgeführt werden. Offensichtlich kann nicht der gesamte Cluster heruntergefahren werden. Deshalb ist eine Abwärtskompatibilität wichtig. Diese wird zum Glück von Sphinx geboten – neuere searchd-Versionen können normalerweise ältere Indexdateien lesen und sind immer in der Lage, mit älteren Clients über das Netzwerk zu kommunizieren. Beachten Sie, dass die erstrangigen Knoten, die die Suchergebnisse sammeln, für die zweitrangigen Knoten, die den Großteil der eigentlichen Suchen erledigen, wie Clients aussehen. Daher werden zuerst die zweitrangigen Knoten aktualisiert, dann die erstrangigen und schließlich das Web-Frontend. Folgende Lehren kann man aus diesem Beispiel ziehen: • Das Motto für Sehr Große Datenbanken lautet: partitionieren, partitionieren, partitionieren, parallelisieren. • Organisieren Sie in großen Suchfarmen searchd in Bäumen mit mehreren Stufen. • Bauen Sie nach Möglichkeit optimierte Indizes mit nur einem Bruchteil der Gesamtdaten. • Ordnen Sie Dateien explizit Festplatten zu, anstatt sich auf den RAID-Controller zu verlassen.
Selects optimieren auf Sahibinden.com Sahibinden.com, eine führende türkische Online-Auktions-Site hatte eine Reihe von Performance-Problemen, unter anderem mit der Volltextsuche. Nach dem Wechsel auf Sphinx und dem Profilieren einiger Abfragen stellte sich heraus, dass Sphinx viele der häufig auftretenden anwendungsspezifischen Abfragen mit Filtern schneller ausführen konnte als MySQL – selbst wenn es in einer der teilnehmenden Spalten in MySQL einen Index gab. Abgesehen davon ergab die Benutzung von Sphinx für Nichtvolltextsuchen einen vereinheitlichten Anwendungscode, der einfacher zu schreiben und zu pflegen war. MySQL erbrachte die benötigte Leistung gar nicht, weil die Selektivität in den einzelnen Spalten nicht ausreichte, um den Suchraum signifikant zu verkleinern. Um genau zu sein, war es fast unmöglich, alle erforderlichen Indizes zu erzeugen und zu pflegen, weil zu viele Spalten sie benötigten. Die Tabellen mit den Produktinformationen hatten ungefähr 100 Spalten, von denen die Webanwendung technisch gesehen jede einzelne zum Filtern oder Sortieren verwenden konnte.
Praktische Implementierungsbeispiele | 701
Aktive Einfügungen und Aktualisierungen der »heißen« Produkttabelle wurden unakzeptabel langsam wegen der vielen Index-Updates. Aus diesem Grund war Sphinx die natürliche Wahl für alle SELECT-Abfragen in den Produktinformationstabellen, nicht nur für die Volltextsuchabfragen. Dies sind die Datenbankgröße und die Lastwerte für die Site: • Die Datenbank enthält ungefähr 400.000 Datensätze und 500 MByte an Daten. • Die Last beträgt etwa 3 Millionen Abfragen pro Tag. Um normale SELECT-Abfragen mit WHERE-Bedingungen zu emulieren, fügte derSphinxIndizierungsvorgang besondere Schlüsselwörter in den Volltextindex ein. Die Schlüsselwörter hatten die Form _ _CATN_ _ _, wobei N durch die entsprechende Kategorien-ID ersetzt wurde. Diese Ersetzung geschah während der Indizierung mit der Funktion CONCAT( ) in der MySQL-Abfrage, so dass die Quelldaten nicht verändert wurden. Die Indizes mussten so oft wie möglich neu erstellt werden. Wir entschieden uns dafür, sie jede Minute neu aufzubauen. Eine vollständige Neuindizierung dauerte 9–15 Sekunden auf einer der vielen CPUs, so dass das vorgestellte main + delta-Schema nicht nötig war. Es stellte sich heraus, dass die PHP-API einen wesentlichen Zeitraum (7–9 Millisekunden pro Abfrage) damit verbrachte, das Ergebnis zu parsen, wenn es viele Attribute aufwies. Normalerweise wäre dieser Overhead kein Problem, weil die Volltextsuchkosten, speziell bei großen Sammlungen, viel größer wären als die Kosten für das Parsen. Allerdings benötigten wir in diesem speziellen Fall auch Nichtvolltextabfragen auf einer kleinen Sammlung. Um diesem Problem zu begegnen, wurden die Indizes in Paare unterteilt: »in einen »leichtgewichtigen« Index mit den 34 am häufigsten benutzten Attributen und einen »vollständigen« mit allen 99 Attributen«. Andere mögliche Lösungen hätten darin bestanden, SphinxSE zu benutzen oder eine Funktion zu implementieren, um nur die angegebenen Spalten nach Sphinx zu holen. Die Lösung mit den zwei Indizes ließ sich jedoch am schnellsten implementieren, und der Zeitaufwand war von Belang. Folgende Lehren lassen sich aus diesem Beispiel ziehen: • Manchmal wird ein vollständiger Scan in Sphinx besser ausgeführt als eine IndexLeseoperation in MySQL. • Benutzen Sie für selektive Bedingungen ein »falsches Schlüsselwort«, anstatt nach einem Attribut zu filtern, damit die Volltextsuchmaschine mehr Arbeit erledigen kann. • APIs in Skriptsprachen können in bestimmten extremen, aber durchaus realen Fällen einen Engpass bilden.
702 |
Anhang C: Sphinx mit MySQL benutzen
GROUP BY auf BoardReader.com optimieren Eine Verbesserung des BoardReader-Dienstes erforderte das Zählen der Hyperlinks und das Erstellen verschiedener Berichte aus den Verknüpfungsdaten. Beispielsweise musste einer der Berichte die obersten N Second-Level-Domains zeigen, die während der letzten Woche verknüpft worden waren. Ein anderer zählte die obersten N Second- und ThirdLevel-Domains, die mit einer bestimmten Site verknüpft waren, wie etwa YouTube. Die Abfragen zum Erzeugen dieser Berichte wiesen folgende gemeinsame Eigenschaften auf: • Sie gruppieren immer nach Domain. • Sie sortieren nach dem Zähler pro Gruppe oder nach dem Zähler für einzelne Werte pro Gruppe. • Sie verarbeiten viele Daten (bis zu Millionen Datensätzen), die Ergebnismenge mit den besten Gruppen ist aber immer klein. • Ungefähre Ergebnisse sind akzeptabel. Während der Prototyp-Testphase brauchte MySQL bis zu 300 Sekunden, um diese Abfragen auszuführen. Theoretisch wäre es mittels Partitionierung der Daten, Verteilen dieser Partitionen über die Server und manuellem Sammeln der Ergebnisse in der Anwendung möglich, die Abfragen auf etwa 10 Sekunden zu optimieren. Allerdings ist eine solche Architektur kompliziert umzusetzen; selbst die Implementierung des Partitionierens ist alles andere als einfach. Weil wir die Suchlast mit Sphinx erfolgreich verteilt hatten, beschlossen wir, ebenfalls mit Sphinx ein annähernd verteiltes GROUP BY zu implementieren. Dies erforderte eine Vorverarbeitung der Daten vor der Indizierung, um alle interessanten Teilstrings in unabhängige »Wörter« umzuwandeln. Hier ist eine Beispiel-URL vor und nach der Vorverarbeitung: source_url processed_url
= http://my.blogger.com/my/best-post.php = my$blogger$com, blogger$com, my$blogger$com$my, my$blogger$com$my$best, my$blogger$com$my$best$post.php
Dollar-Zeichen ($) sind lediglich eine vereinheitlichte Ersetzung der URL-Trennzeichen, damit die Suchen in jedem URL-Teil ausgeführt werden können, sei es Domain oder Pfad. Diese Art der Vorverarbeitung extrahiert alle »interessanten« Teilstrings in einzelne Schlüsselwörter, die am schnellsten zu durchsuchen sind. Technisch gesehen hätten wir auch Phrasenabfragen oder Präfixindizierung benutzen können, die aber zu größeren Indizes und langsamerer Leistung hätten führen können. Die Links werden zur Indizierungszeit mit einer speziell hergestellten MySQL-UDF vorverarbeitet. Wir haben außerdem Sphinx mit der Fähigkeit ausgestattet, einzelne Werte zu zählen. Anschließend konnten wir die Abfragen vollständig auf den Such-Cluster verschieben, sie verteilen und die Abfragelatenz deutlich verringern.
Praktische Implementierungsbeispiele | 703
Dies sind die Datenbankgröße und die Lastwerte: • Es gibt etwa 150–200 Millionen Datensätze, die nach der Vorverarbeitung zu etwa 50–100 GByte Daten werden. • Die Last beträgt ungefähr 60.000–100.000 GROUP BY-Abfragen pro Tag. Die Indizes für das verteilte GROUP BY wurden auf den gleichen Such-Cluster aus 6 Maschinen und 24 logischen CPUs gebracht, den wir bereits beschrieben haben. Diese geringe Last ergänzt die Hauptlast auf der 1,5-TByte-Textdatenbank. Sphinx ersetzte MySQLs exakte, langsame und auf einer CPU ausgeführte Berechnungen durch angenäherte, schnelle, verteilte Berechnungen. Alle Faktoren, die Näherungsfehler bringen, sind vorhanden: Die eingehenden Daten enthalten oft zu viele Zeilen, um in den »Sortierpuffer« zu passen (wir benutzten eine feste RAM-Grenze von 100-K-Zeilen), wir verwenden COUNT(DISTINCT), und die Ergebnismengen werden über das Netzwerk eingesammelt. Trotzdem sind die Ergebnisse für die ersten 10 bis 1000 Gruppen – die tatsächlich für die Berichte benötigt werden – von 99 % bis 100 % korrekt. Die indizierten Daten unterscheiden sich stark von den Daten, die für eine normale Volltextsuche benutzt werden würden. Es gibt eine riesige Anzahl an Dokumenten und Schlüsselwörtern, auch wenn die Dokumente sehr klein sind. Die Nummerierung der Dokumente ist nichtsequenziell, weil sie eine besondere Nummerierungskonvention einsetzt (Quellserver, Quelltabelle und Primärschlüssel), die nicht in 32 Bits passt. Die riesige Menge an Such-»Schlüsselwörtern« hat außerdem häufige CRC32-Kollisionen verursacht (Sphinx benutzt CRC32, um Schlüsselwörter auf interne Wort-IDs abzubilden). Aus diesen Gründen waren wir gezwungen, intern überall 64-Bit-Identifikatoren zu benutzen. Die momentane Leistung ist zufriedenstellend. Für die komplexesten Domains werden die Abfragen normalerweise in 0,1 bis 1,0 Sekunden abgeschlossen. Folgende Lehren können wir aus diesem Beispiel ziehen: • Für GROUP BY-Abfragen kann man zugunsten der Geschwindigkeit auf eine gewisse Präzision verzichten. • Bei riesigen Textsammlungen oder mittelgroßen Spezialsammlungen könnten 64Bit-Identifikatoren erforderlich sein.
Geteilte JOIN-Abfragen auf Grouply.com optimieren Die MVA-Unterstützung von Sphinx ist relativ neu. Dennoch haben die Benutzer bereits clevere Einsatzfälle dafür gefunden. Grouply.com baute eine Sphinx-basierte Lösung zum Durchsuchen seiner Datenbank, die mehrere Millionen Datensätze umfasst, um diese nach Nachrichten zu durchsuchen, die mit Tags versehen sind. Die Datenbank ist zur besseren Skalierbarkeit über viele physische Server verteilt, so dass es notwendig sein könnte, Tabellen abzufragen, die sich auf unterschiedlichen Servern befinden. Beliebige groß angelegte Joins sind unmöglich, weil zu viele Server, Datenbanken und Tabellen beteiligt sind.
704 |
Anhang C: Sphinx mit MySQL benutzen
Grouply.com benutzt die MVA-Attribute von Sphinx, um die Nachrichten-Tags zu speichern. Die Tag-Liste wird über die PHP-API von einem Sphinx-Cluster geholt. Das ersetzt mehrere sequenzielle SELECTs von mehreren MySQL-Servern. Um auch die Anzahl der SQL-Abfragen zu verringern, werden bestimmte Daten, die nur der Präsentation dienen (z.B. eine kleine Liste der Benutzer, die zuletzt eine Nachricht gelesen haben), ebenfalls in einem eigenen MVA-Attribut gespeichert, und es wird über Sphinx auf diese Daten zugegriffen. Zwei wichtige Innovationen hier sind die Verwendung von Sphinx, um JOIN-Ergebnisse vorzubereiten, und die Benutzung seiner verteilten Fähigkeiten, um Daten, die über viele Shards verteilt sind, zusammenzufassen. Mit MySQL allein wäre das fast unmöglich. Ein effizientes Merging würde erfordern, dass die Daten auf so wenige physische Server und Tabellen wie möglich partitioniert werden würden, was aber sowohl der Skalierbarkeit als auch der Erweiterbarkeit entgegenstände. Folgende Lehren können wir aus diesem Beispiel ziehen: • Sphinx kann verwendet werden, um stark partitionierte Daten effizient zu sammeln. • MVAs eignen sich, um vorbereitete JOIN-Ergebnisse zu speichern und zu optimieren.
Schlussfolgerung Wir haben in diesem Anhang das Sphinx-Volltextsuchsystem nur kurz vorgestellt. Um der Kürze willen haben wir auf die Vorstellung vieler anderer Sphinx-Funktionen (wie etwa die Unterstützung für die HTML-Indizierung, Bereichsabfragen zur besseren MyISAM-Unterstützung, Morphologie und Unterstützung von Synonymen, Präfix- und Infix-Indizierung sowie CJK-Indizierung) verzichtet. Nichtsdestotrotz soll Ihnen dieser Anhang eine Vorstellung davon vermitteln, wie man mit Sphinx effizient viele reale Probleme lösen kann. Es beschränkt sich nicht auf die Volltextsuche, sondern kann viele schwierige Probleme lösen, die traditionell in SQL gelöst werden würden. Sphinx ist weder ein Allheilmittel noch ein Ersatz für MySQL. In vielen Fällen jedoch (die in modernen Webanwendungen inzwischen verbreitet sind) dient es als außerordentlich nützliche Ergänzung zu MySQL. Sie können es einsetzen, um einfach einen Teil der Arbeit abzuwälzen oder um neue Möglichkeiten für Ihre Anwendung zu schaffen. Laden Sie es von http://www.sphinxsearch.com herunter – und vergessen Sie nicht, Ihre eigenen Ideen mitzuteilen!
Schlussfolgerung | 705
ANHANG D
Sperren debuggen
In jedem System, das Sperren (Locks) benutzt, um den gemeinsamen Zugriff auf Ressourcen zu kontrollieren, kann es schwer sein, Fehler zu finden, wenn eine Wettkampfsituation eintritt. Vielleicht versuchen Sie, eine Spalte zu einer Tabelle hinzuzufügen oder wollen einfach nur eine Abfrage ausführen, und stellen plötzlich fest, dass Ihre Abfragen blockiert werden, weil etwas anderes die Tabelle oder die Zeilen sperrt, die Sie zu benutzen versuchen. In diesem Anhang zeigen wir Ihnen, was Sie tun können, wenn Sie in MySQL in eine solche Lage kommen. Oft werden Sie einfach nur herausfinden wollen, weshalb Ihre Abfrage blockiert ist. Manchmal jedoch wird es Sie interessieren, was sie blockiert, damit Sie wissen, welchen Prozess Sie beenden müssen. Hier erfahren Sie, wie Sie beide Ziele erreichen.
Lock-Wartezustände auf Serverebene Das Warten auf eine Sperre kann auf der Serverebene, aber auch auf der Storage-EngineEbene passieren.1 (Auch Sperren auf Anwendungsebene können ein Problem darstellen, allerdings konzentrieren wir uns auf MySQL.) Der MySQL-Server selbst verwendet mehrere Arten von Sperren. Sie erkennen in der Ausgabe von SHOW PROCESSLIST, ob eine Abfrage auf eine Sperre auf der Serverebene (ServerLevel-Lock) wartet. Zusätzlich zu den Server-Level-Locks implementiert jede StorageEngine, die Sperren auf Zeilenebene (Row-Level-Locks) unterstützt, wie InnoDB, ihre eigenen Sperren, zumindest zurzeit noch. In MySQL 5.0 und früheren Versionen erkennt der Server solche Sperren nicht, und sie sind auch vor den Benutzern und den Datenbankadministratoren größtenteils verborgen. Künftige Versionen zeigen möglicherweise mehr dieser Sperren auf der Serverebene, wahrscheinlich durch INFORMATION_SCHEMA-Tabellen.
1 Schauen Sie sich Abbildung 1-1 in Kapitel 1 an, falls Sie Ihr Gedächtnis bezüglich der Trennung von Server und Storage-Engines auffrischen wollen.
706 |
Dies sind die Arten von Sperren, die der MySQL-Server verwendet: Tabellen-Locks Tabellen können mit expliziten Lese- und Schreib-Locks gesperrt werden. Es gibt für diese Sperren verschiedene Varianten, wie etwa lokale Lese-Locks. Mehr darüber erfahren Sie im Abschnitt LOCK TABLES des MySQL-Handbuchs. Zusätzlich zu diesen expliziten Sperren holen sich Abfragen während ihrer Lebensdauer implizite Locks auf Tabellen. Globale Sperren Es gibt einen globalen Lese-Lock, der mit FLUSH TABLES WITH READ LOCK beschafft werden kann. Name-Locks Name-Locks sind eine Art von Tabellen-Lock, die der Server erzeugt, wenn er eine Tabelle umbenennt oder verwirft. String-Locks Sie können einen beliebigen String serverweit mit GET_LOCK( ) und seinen dazugehörenden Funktionen sperren und wieder freigeben. Wir untersuchen die einzelnen Arten von Sperren in den folgenden Abschnitten genauer.
Tabellen-Locks Tabellen-Locks sind entweder explizit oder implizit. Sie erzeugen explizite Locks mit LOCK TABLES. Falls Sie z.B. den folgenden Befehl in einer mysql-Sitzung ausführen, haben Sie einen expliziten Lock auf sakila.film: mysql> LOCK TABLES sakila.film READ;
Führen Sie den folgenden Befehl in einer anderen Sitzung aus, bleibt die Abfrage hängen und wird nicht abgeschlossen: mysql> LOCK TABLES sakila.film WRITE;
Sie können den wartenden Thread in der ersten Verbindung sehen: mysql> SHOW PROCESSLIST\G *************************** 1. row *************************** Id: 7 User: baron Host: localhost db: NULL Command: Query Time: 0 State: NULL Info: SHOW PROCESSLIST *************************** 2. row *************************** Id: 11 User: baron Host: localhost db: NULL
Lock-Wartezustände auf Serverebene | 707
Command: Query Time: 4 State: Locked Info: LOCK TABLES sakila.film WRITE 2 rows in set (0.01 sec)
Beachten Sie, dass der Zustand von Thread 11 Locked ist. Es gibt nur eine Stelle im Code des MySQL-Servers, wo ein Thread in diesen Zustand eintritt: wenn er versucht, einen Tabellen-Lock zu beschaffen und ein anderer Thread die Tabelle bereits gesperrt hat. Wenn Sie das also sehen, dann wissen Sie, dass der Thread auf einen Lock im MySQLServer wartet, und nicht in der Storage-Engine. Explizite Locks sind jedoch nicht die einzige Art von Lock, die eine solche Operation blockieren könnten. Wie wir bereits erwähnt haben, blockiert der Server implizit die Tabellen während der Abfragen. Eine einfache Möglichkeit, dies nachzuweisen, ist mit einer lange laufenden Abfrage, die Sie leicht mit der Funktion SLEEP( ) erzeugen können: mysql> SELECT SLEEP(30) FROM sakila.film LIMIT 1;
Falls Sie erneut versuchen, sakila.film zu sperren, während diese Abfrage läuft, wird diese Operation gestoppt, weil es einen impliziten Lock gibt, also genau wie bei dem expliziten Lock. Sie können sich die Effekte wieder in der Prozessliste anschauen: mysql> SHOW PROCESSLIST\G *************************** 1. row *************************** Id: 7 User: baron Host: localhost db: NULL Command: Query Time: 12 State: Sending data Info: SELECT SLEEP(30) FROM sakila.film LIMIT 1 *************************** 2. row *************************** Id: 11 User: baron Host: localhost db: NULL Command: Query Time: 9 State: Locked Info: LOCK TABLES sakila.film WRITE
In diesem Beispiel blockiert der implizite Lese-Lock für die SELECT-Abfrage den expliziten Schreib-Lock, der mit LOCK TABLES angefordert wurde. Implizite Locks können sich auch gegenseitig blockieren. Sie werden sich fragen, worin der Unterschied zwischen impliziten und expliziten Locks besteht. Intern weisen sie die gleiche Art von Struktur auf, und sie werden auch vom gleichen MySQL-Servercode kontrolliert. Extern können Sie explizite Locks selbst mit LOCK TABLES und UNLOCK TABLES steuern.
708 |
Anhang D: Sperren debuggen
Kommt man zu anderen Storage-Engines als MyISAM, dann gibt es jedoch einen sehr wichtigen Unterschied zwischen ihnen. Wenn Sie explizit einen Lock erzeugen, dann passiert das, was Sie wollen; implizite Locks dagegen sind verborgen und »magisch«. Der Server erzeugt und löst implizite Locks automatisch, wenn das nötig ist, und informiert die Storage-Engine darüber. Storage-Engines »konvertieren« diese Locks, damit sie passen. Beispielsweise enthält InnoDB Regeln darüber, welche Art von InnoDB-TabellenLock es für einen bestimmten Tabellen-Lock auf Serverebene erzeugen soll. Dadurch ist möglicherweise schwer zu verstehen, welche Locks InnoDB tatsächlich hinter den Kulissen erzeugt. In MySQL 5.0 und 5.1 verwaltet der Server die Tabellen-Locks auf Serverebene auf Deadlock-freie Art, indem er sie alle gleichzeitig erzeugt und freigibt, und zwar alle in der gleichen intern definierten Reihenfolge. In MySQL 6.0 ist es möglich, weitere Locks hinzuzufügen, ohne die vorhandenen Locks freizugeben, so dass es passieren kann, dass Deadlocks in Locks auf Tabellenebene auftreten. Allerdings ist diese Funktionalität zurzeit noch nicht vollständig, so dass das endgültige Verhalten nicht bekannt ist.
Feststellen, wer einen Lock hält Wenn Sie viele Prozesse im Zustand Locked vorfinden, könnte Ihr Problem darin bestehen, dass Sie versuchen, MyISAM oder eine ähnliche Storage-Engine für eine stark nebenläufige Last zu verwenden. Dies kann das manuelle Ausführen einer Operation blockieren, wie etwa das Hinzufügen eines Index zu einer Tabelle. Wenn eine UPDATEAbfrage in die Warteschlange aufgenommen wird und auf eine Sperre auf einer MyISAMTabelle wartet, dann dürfte nicht einmal eine SELECT-Abfrage laufen. (Mehr über die Lock-Warteschlangen und -Prioritäten von MySQL erfahren Sie im MySQL-Handbuch.) In manchen Fällen ist es ganz klar, dass eine Verbindung schon sehr lange eine Sperre auf einer Tabelle hält und einfach beendet werden muss (oder dass ein Benutzer ermahnt werden muss, nicht die Arbeit aufzuhalten!). Aber wie können Sie feststellen, um welche Verbindung es sich handelt? Es gibt momentan keinen SQL-Befehl, der Ihnen zeigen kann, welcher Thread die Tabellen-Locks hält, die Ihre Abfrage blockieren. Wenn Sie SHOW PROCESSLIST ausführen, können Sie die Prozesse sehen, die auf Locks warten, jedoch nicht, welche Prozesse diese Locks halten. Glücklicherweise gibt es einen debug-Befehl (der nicht durch SQL ausgeführt werden kann), der Informationen über die Locks in das Fehler-Log des Servers schreiben kann. Führen Sie diesen Befehl mit mysqladmin aus: $ mysqladmin debug
Die Ausgabe enthält viele Debugging-Informationen, am Ende sehen Sie dann so etwas wie den folgenden Ausschnitt. Wir haben diese Ausgabe erzeugt, indem wir die Tabelle in einer Verbindung sperrten und dann versuchten, sie in einer anderen Verbindung ebenfalls zu sperren:
Lock-Wartezustände auf Serverebene | 709
Thread database.table_name Locked/Waiting
Lock_type
7 8
Read lock without concurrent inserts Highest priority write lock
sakila.film sakila.film
Locked - read Waiting - write
Sie können sehen, dass Thread 8 auf die Sperre wartet, die Thread 7 hält. Der Befehl mysqladmin debug gibt noch weitere Informationen aus, falls MySQL mit aktiviertem Debugging kompiliert wurde. Die Locks und einige andere nützliche Informationen werden aber auf jeden Fall angezeigt.
Der globale Lese-Lock Der MySQL-Server implementiert auch einen globalen Lese-Lock. Sie können diesen Lock folgendermaßen beschaffen: mysql> FLUSH TABLES WITH READ LOCK;
Falls Sie jetzt versuchen, in einer anderen Sitzung eine Tabelle zu sperren, bleibt diese hängen: mysql> LOCK TABLES sakila.film WRITE;
Wie können Sie feststellen, dass diese Abfrage auf den globalen Lese-Lock wartet und nicht auf einen Lock auf Tabellenebene wie zuvor? Schauen Sie sich die Ausgabe von SHOW PROCESSLIST an: mysql> SHOW PROCESSLIST\G ... *************************** 2. row *************************** Id: 22 User: baron Host: localhost db: NULL Command: Query Time: 9 State: Waiting for release of readlock Info: LOCK TABLES sakila.film WRITE
Beachten Sie, dass der Zustand der Abfrage Waiting for release of readlock ist. Das ist der Beweis: Die Abfrage wartet auf den globalen Lese-Lock und nicht auf einen Lock auf Tabellenebene. MySQL bietet keine Möglichkeit, um herauszufinden, wer den globalen Lese-Lock hält.
Name-Locks Name-Locks sind eine Art von Tabellen-Lock, die der Server erzeugt, wenn er eine Tabelle umbenennt oder verwirft. Ein Name-Lock kollidiert mit einem normalen Tabellen-Lock, ob nun implizit oder explizit. Falls wir z.B. wie gehabt LOCK TABLES benutzen und dann in einer anderen Sitzung versuchen, die gesperrte Tabelle umzubenennen, bleibt die Abfrage hängen, aber dieses Mal nicht im Zustand Locked: mysql> RENAME TABLE sakila.film2 TO sakila.film;
710 |
Anhang D: Sperren debuggen
Wie zuvor ist die Prozessliste die Stelle, an der Sie die gesperrte Abfrage sehen können, die sich im Zustand Waiting for table befindet: mysql> SHOW PROCESSLIST\G ... *************************** 2. row *************************** Id: 27 User: baron Host: localhost db: NULL Command: Query Time: 3 State: Waiting for table Info: rename table sakila.film to sakila.film 2
Auch in der Ausgabe von SHOW OPEN TABLES können Sie die Wirkungen eines Name-Locks sehen: mysql> SHOW OPEN TABLES; +----------+-----------+--------+-------------+ | Database | Table | In_use | Name_locked | +----------+-----------+--------+-------------+ | sakila | film_text | 3 | 0 | | sakila | film | 2 | 1 | | sakila | film2 | 1 | 1 | +----------+-----------+--------+-------------+ 3 rows in set (0.00 sec)
Beachten Sie, dass beide Namen (der ursprüngliche und der neue Name) gesperrt sind. sakila.film_text ist gesperrt, weil es einen Trigger in sakila.film gibt, der darauf verweist – was eine andere Möglichkeit verdeutlicht, wie Locks sich selbst an Stellen mogeln können, wo Sie sie nicht erwarten würden. Falls Sie sakila.film abfragen, veranlasst der Trigger Sie, implizit sakila.film_text anzufassen und es damit implizit zu sperren. Es stimmt, dass der Trigger für das Umbenennen wirklich nicht ausgelöst werden muss und technisch gesehen der Lock daher nicht erforderlich ist, aber so ist es nun einmal: die Sperren von MySQL sind manchmal nicht so feinkörnig, wie Sie es sich wünschen. MySQL bietet keine Möglichkeit, festzustellen, wer Name-Locks hält. Das ist aber normalerweise kein Problem, da sie im Allgemeinen nur für eine sehr kurze Zeit gehalten werden. Wenn es zu einem Konflikt kommt, liegt das meist daran, dass ein Name-Lock auf einen normalen Tabellen-Lock wartet. Das können Sie, wie bereits gezeigt, mit mysqladmin debug sehen.
Benutzer-Locks Die letzte Art von Lock, die im Server implementiert ist, ist der Benutzer-Lock, bei dem es sich im Prinzip um einen benannten Mutex handelt. Sie geben den zu sperrenden String an sowie die Anzahl an Sekunden, nach denen der Lock-Versuch abgebrochen werden soll:
Lock-Wartezustände auf Serverebene | 711
mysql> SELECT GET_LOCK('my lock', 100); +--------------------------+ | GET_LOCK('my lock', 100) | +--------------------------+ | 1 | +--------------------------+ 1 row in set (0.00 sec)
Dieser Versuch war unmittelbar erfolgreich, so dass dieser Thread jetzt einen Lock auf diesem benannten Mutex hat. Falls ein anderer Thread versucht, den gleichen String zu sperren, bleibt er bis zum Timeout hängen. Dieses Mal zeigt die Prozessliste einen anderen Zustand: mysql> SHOW PROCESSLIST\G *************************** 1. row *************************** Id: 22 User: baron Host: localhost db: NULL Command: Query Time: 9 State: User lock Info: SELECT GET_LOCK('my lock', 100)
Der Zustand User lock weist eindeutig auf diese Art von Lock hin. MySQL bietet keine Möglichkeit festzustellen, wer einen Benutzer-Lock hält.
Lock-Wartezustände in Storage-Engines Probleme mit Locks auf der Serverebene sind meist ein bisschen einfacher zu beheben als mit Locks in Storage-Engines. Storage-Engine-Locks unterscheiden sich von einer Storage-Engine zur anderen, und außerdem bieten die Engines nicht immer ein Mittel, um ihre Locks zu untersuchen. Wir konzentrieren uns in diesem Anhang hauptsächlich auf InnoDB, weil es momentan die beliebteste Storage-Engine ist, die ihre eigenen Locks implementiert.
InnoDB-Lock-Wartezustände InnoDB liefert in der Ausgabe von SHOW INNODB STATUS einige Informationen über Locks. Wenn eine Transaktion auf einen Lock wartet, dann erscheint der Lock im Abschnitt TRANSACTIONS der SHOW INNODB STATUS-Ausgabe. Falls Sie z.B. die folgenden Befehle in einer Sitzung ausführen, erhalten Sie einen Schreib-Lock auf der ersten Zeile in der Tabelle: mysql> SET AUTOCOMMIT=0; mysql> BEGIN; mysql> SELECT film_id FROM sakila.film LIMIT 1 FOR UPDATE;
Wenn Sie nun den gleichen Befehl in einer anderen Sitzung ausführen, wird Ihre Abfrage von dem Lock in der ersten Sitzung blockiert. Sie können diese Wirkung in SHOW INNODB STATUS sehen (wir haben die Ausgabe der besseren Übersichtlichkeit halber abgekürzt):
712 |
Anhang D: Sperren debuggen
1 2 3 4 5
LOCK WAIT 2 lock struct(s), heap size 1216 MySQL thread id 8, query id 89 localhost baron Sending data SELECT film_id FROM sakila.film LIMIT 1 FOR UPDATE ------- TRX HAS BEEN WAITING 9 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 0 page no 194 n bits 1072 index `idx_fk_language_id` of table `sakila/film` trx id 0 61714 lock_mode X waiting
Die letzte Zeile zeigt, dass die Abfrage auf einen exklusiven (lock_mode X) Lock auf Seite 194 des idx_fk_language_id-Index der Tabelle wartet. Schließlich wird der Lock-WarteTimeout überschritten, und die Abfrage liefert einen Fehler zurück: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
Ohne zu sehen, wer die Locks hält, ist es leider schwierig festzustellen, welche Transaktion das Problem verursacht. Oft kann man gezielt raten, indem man sich anschaut, welche Transaktionen schon sehr lange offen sind; alternativ können Sie den InnoDBLock-Monitor aktivieren, der bis zu 10 der Locks zeigt, die jede Transaktion hält. Um den Monitor zu aktivieren, erzeugen Sie mit der InnoDB-Storage-Engine eine magisch benannte Tabelle:2 mysql> CREATE TABLE innodb_lock_monitor(a int) ENGINE=INNODB;
Wenn Sie diese Abfrage auslösen, beginnt InnoDB damit, in Intervallen eine etwas erweiterte Version der Ausgabe von SHOW INNODB STATUS auf der Standardausgabe anzuzeigen (das Intervall variiert, normalerweise ist es jedoch mehrmals pro Minute). Auf den meisten Systemen wird diese Ausgabe an das Fehler-Log des Servers umgeleitet; wenn Sie dies untersuchen, dann sehen Sie, welche Transaktionen welche Sperren halten. Um den Lock-Monitor zu stoppen, verwerfen Sie die Tabelle. Hier ist der relevante Ausschnitt der Ausgabe des Lock-Monitors: ---TRANSACTION 0 61717, ACTIVE 3 sec, process no 5102, OS thread id 1141152080 3 lock struct(s), heap size 1216 MySQL thread id 11, query id 108 localhost baron show innodb status TABLE LOCK table `sakila/film` trx id 0 61717 lock mode IX RECORD LOCKS space id 0 page no 194 n bits 1072 index `idx_fk_language_id` of table `sakila/film` trx id 0 61717 lock_mode X 7 Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 8 ... weggelassen ... 9 10 RECORD LOCKS space id 0 page no 231 n bits 168 index `PRIMARY` of table `sakila/film` trx id 0 61717 lock_mode X locks rec but not gap 11 Record lock, heap no 2 PHYSICAL RECORD: n_fields 15; compact format; info bits 0 12 ... weggelassen ... 1 2 3 4 5 6
Beachten Sie, dass Zeile 3 die MySQL-Thread-ID zeigt, die identisch ist mit dem Wert in der Id-Spalte in der Prozessliste. Zeile 5 zeigt, dass die Transaktion einen impliziten exklusiven Tabellen-Lock (IX) auf der Tabelle hat. Die Zeilen 6 bis 8 zeigen den Lock auf 2 InnoDB beachtet mehrere »magische« Tabellennamen als Anweisungen. Momentane Praxis ist es, dynamisch einstellbare Servervariablen zu verwenden, allerdings gibt es InnoDB schon lange, weshalb es immer noch das alte Verhalten zeigt.
Lock-Wartezustände in Storage-Engines | 713
dem Index. Wir haben die Information in Zeile 8 weggelassen, weil dies ein (ziemlich ausführlicher) Dump des gesperrten Datensatzes war. Die Zeilen 9 bis 11 zeigen den entsprechenden Lock auf dem Primärschlüssel (ein FOR UPDATE-Lock muss die Zeile und nicht nur den Index sperren). Es ist undokumentiert, aber wenn der Lock-Monitor aktiviert ist, erscheinen die zusätzlichen Informationen auch in der Ausgabe von SHOW INNODB STATUS, so dass Sie eigentlich nicht in das Fehler-Log des Servers schauen müssen, um die Lock-Informationen zu sehen.
Zu einer sinnvolleren Lock-Ausgabe Der Lock-Monitor ist aus verschiedenen Gründen nicht optimal. Das Hauptproblem besteht darin, dass die Lock-Information sehr umfangreich ist, da sie Hex- und ASCIIDumps der Datensätze enthalten kann, die gesperrt sind. Sie füllt das Fehler-Log und kann leicht die feste Größe der Ausgabe von SHOW INNODB STATUS überschreiten. Das bedeutet, dass Sie möglicherweise gar nicht die Informationen erhalten, nach denen Sie in späteren Abschnitten der Ausgabe suchen (mehr dazu finden Sie in »LATEST DETECTED DEADLOCK« auf Seite 621). InnoDB besitzt außerdem eine festkodierte Begrenzung der Anzahl der Locks, die es pro Transaktion ausgibt – nach dem Ausgeben von 10 Locks ist Schluss, d.h., unter Umständen bekommen Sie überhaupt keine Informationen über die Sperre, die Sie sehen wollen. Die Krönung ist, dass das, was Sie suchen, in der Ausgabe schwer zu finden ist, selbst wenn es nicht fehlt. (Probieren Sie es einfach einmal auf einem ausgelasteten Server; Sie werden schon sehen!) Zwei Dinge können die Lock-Ausgabe besser benutzbar machen. Das erste ist ein Patch, den einer der Autoren dieses Buches für InnoDB und den MySQL-Server geschrieben hat. Der Patch entfernt die umfangreichen Datensatz-Dumps aus der Ausgabe, fügt die LockInformationen standardmäßig in die Ausgabe von SHOW INNODB STATUS ein (so dass der Lock-Monitor nicht aktiviert werden muss) und fügt dynamisch einstellbare Servervariablen hinzu, um den Umfang und die Anzahl der pro Transaktion auszugebenden Locks zu steuern. Sie finden den Patch für MySQL 5.0 unter http://lists.mysql.com/internals/ 35174. Die zweite Möglichkeit besteht darin, innotop zu benutzen, um die Ausgabe zu parsen und zu formatieren. Sein Lock-Modus zeigt Locks, die hübsch nach Verbindung und Tabelle zusammengefasst sind, damit Sie auf einen Blick feststellen können, welche Transaktionen die Locks auf einer bestimmten Tabelle halten. Das ist allerdings keine narrensichere Methode, um festzustellen, welche Transaktion eine Sperre blockiert, da dies außerdem erfordern würde, die im Dump gespeicherten Datensätze zu untersuchen, um den exakten Datensatz zu ermitteln, der gesperrt ist. Allerdings ist es viel besser als die üblichen Alternativen und reicht für viele Zwecke aus. Die InnoDB-Entwickler haben uns gesagt, dass sie für eine künftige Version daran arbeiten, die InnoDB-Informationen in INFORMATION_SCHEMA-Tabellen zu exportieren, allerdings ist dieser Code noch nicht allgemein veröffentlicht worden. Künftig wird das wahrscheinlich die bevorzugte Methode sein, um Lock-Informationen zu präsentieren. 714 |
Anhang D: Sperren debuggen
Falcon-Lock-Wartezustände Die transaktionsfähige Falcon-Storage-Engine, die zurzeit Teil des MySQL 6.0-AlphaRelease ist, exportiert ihre Transaktionsinformationen in eine INFORMATION_SCHEMATabelle. Sie können dies nutzen, um den Grund für einen Lock-Wartezustand zu finden. Dazu verwenden Sie einfach einen SQL-Befehl: mysql> SELECT a.THREAD_ID AS blocker, a.STATEMENT AS blocking_query, -> b.THREAD_ID AS blocked, b.STATEMENT AS blocked_query -> FROM INFORMATION_SCHEMA.FALCON_TRANSACTIONS AS a -> INNER JOIN INFORMATION_SCHEMA.FALCON_TRANSACTIONS AS b ON -> a.ID = b.WAITING_FOR -> WHERE b.WAITING_FOR > 0; +---------+----------------+---------+------------------------------+ | blocker | blocking_query | blocked | blocked_query | +---------+----------------+---------+------------------------------+ | 4 | | 5 | SELECT * FROM tbl FOR UPDATE | +---------+----------------+---------+------------------------------+
Diese Art der diagnostischen Informationen sollten den MySQL-Datenbankadministratoren in Zukunft das Leben deutlich erleichtern!
Lock-Wartezustände in Storage-Engines | 715
Index
Symbole '...' (Anführungszeichen), für Hostnamen und Benutzernamen 585 * (Asterisk), Passwörter beginnend mit 579 ? (Fragezeichen), Parameter in vorbereiteten Anweisungen 243 % (Prozentzeichen), vorgegebener Hostname 575, 584
A abdeckende Indizes 128–132, 181 Abfrage 75 abfragebasierte Aufteilung 478 Abfrage-Cache 3, 176, 220–225 Abfragen ausschließen 233 aktivieren 228 Cache-Misses, Gründe für 226 Cache-Treffer prüfen auf 221 verbessern 230, 233 deaktivieren 228, 233 Entfernen aller Abfragen und Ergebnisse aus 230 Ergebnismengen, Größe der 228 Fragmentierung 225, 229 für Abfrage nicht benutzen 77 Größe 291 Einfluss auf die Leistung 223 potenzielle 227 reservierte 228 kürzen 230, 233 Nützlichkeit ermitteln 225–228 Spaltenberechtigungen nicht benutzen 582 Speicherbenutzung durch 223–225, 228 Sperren beeinflussen 229 Statusvariablen für 612 Trefferrate 226
verbessern 228–230 zusätzlicher Overhead durch 222 Abfrage-Logs 69–75 Abfragen analysieren 163–168, 650–652 ausführen 172, 191 Ausführungsplan 185, 191 (siehe auch EXPLAIN-Befehl) Ausführungszeit 165 beschleunigen (siehe Sphinx-Werkzeug) Client/Server-Protokoll für 173–176 COUNT( )-Abfragen 179, 202–204 DISTINCT-Klauseln 205 GROUP BY-Klausel 205, 685, 703 IN( )-Listenvergleiche 182 indexabdeckende Abfragen 129 Joins Ausführungsstrategie 183–185 Dekomposition 170 Optimierungen 179, 186–189, 205 STRAIGHT_JOIN-Option 211 konsistente Formatierung, Wichtigkeit 221 LIMIT-Klausel 207, 208 MAX( )-Abfragen 179, 201 MIN( )-Abfragen 179, 201 neustrukturieren 169–171 in mehrere Abfragen aufteilen 169 zurückgelieferte Zeilen verringern 170 OFFSET-Klausel 207 Optimierer 3, 177–182 benutzte Indexstatistiken 183 benutzte Tabellenstatistiken 183 Beschränkungen 178, 192–202 dynamische Optimierungen 179 frühe Beendigung durch 181 Hinweisoptionen für 208, 210, 218 Join-Optimierungen 186–189 Join-Strategie 183–185
Index | 717
Optimierungen 179–182 Sortieroptimierungen 190 statische Optimierungen 178 Systemvariablen beeinflussen 213 Optimierungen für bestimmte Arten von 202–209 Parser für 177 partitionierter Tabellen 282 Präprozessor für 177 Profiling 77–80 restrukturieren, Join-Dekomposition 170 Software für 636 Sortierreihenfolgen beeinflussen 260–262 Überwachung 645 UNION-Klausel 197, 209, 274 Unterabfragen 181, 205 Unterabfragen, korrelierte 193–197 untersuchte Zeilen 165–168 vorbereitete Anweisungen für 243 Einschränkungen von 247 optimieren 244 SQL-Schnittstelle für 245 WHERE-Klausel 182, 682 Zeichensätze beeinflussen 260–262 Zugriffsmethoden 166 zurückgelieferte Ergebnisse 192 zurückgelieferte Zeilen 164, 166, 170 Zustände 175 Abfrageplankosten, Statusvariable für 615 Aborted_clients-Statusvariable 326, 610 Aborted_connects-Statusvariable 327, 610 ab-Werkzeug 46 Accounts 571 anonyme, deaktivieren 585 Arten 576 Berechtigungen anzeigen 574 entfernen 574 hinzufügen 574, 576 in Grant-Tabellen gespeicherte 572 Rechte Arten 571 zum Ausführen von MySQL 592 für Replikation, erzeugen 378 ACID-Test 7 adaptive Hash-Indizes 111, 627 Administration, Software für 637, 638
718 | Index
Administrator-Account Datenbank 577 System 576 AES_DECRYPT( )-Funktion 605 AES_ENCRYPT( )-Funktion 605 Aker, Brian (Funktionen für memcached) 248 Aktien, geeignete Storage-Engines 30 aktive Caches 506 aktive Überwachung 639 aktuelle Wartezustände, Status 617 algebraische Äquivalenzregeln für Abfrageoptimierungen 179 ALTER TABLE-Befehl 149 Tabellenkonvertierungen 33 Verbessern der Leistung 156–159 Analysewerkzeuge 649–652 ANALYZE TABLE-Befehl 146 Analyzing-Zustand der Abfrage 176 Anführungszeichen ('...') für Hostnamen und Benutzernamen 585 Angestellten-Accounts 577 anonyme Benutzer deaktivieren 585 Anpassbarkeit von Werten 257 Antwortzeit (siehe Latenz) Anweisungen, vorbereitete (siehe vorbereitete Anweisungen) anweisungsbasierte Replikation 373, 386 Anweisungs-Handle 243 Anwendungen Joins ausgeführt in 170 Leistungsaspekte Caching 505–512 Erweitern von MySQL 512 optimale Nebenläufigkeit finden 504 Probleme finden 498–501 Webserverprobleme 502–505 Profiling 59–68, 498 Anwendungsebene, Caching 507–509 Anwendungsebene, Verschlüsselung 603–605 Apache-Webserver 502–504 Arbeitslast-basierte Anpassungen 323–330 Arbeitslast-Partitionierung (siehe funktionelle Partitionierung) Arbeitssatz an Daten 339 Architekturen mit gemeinsam genutztem Speicher 490 Archive-Storage-Engine 23, 32
Archivierung von Daten Dienstprogramm für 654 für Skalierbarkeit 471–474 Replikation für 404 Asterisk (*), Passwörter beginnend mit 579 Atomizität in ACID-Test 7 Attributeunterstützung in Sphinx 694 auf der Festplatte befindliche Caches 508 auf Localhost beschränkte Verbindungen 594 Aufräumen von Daten, für Skalierbarkeit 471–474 Aufteilung der veralteten Daten 478 Auftragsverarbeitung, geeignete Storage-Engines 30 Ausführungsplan für Abfrage 185, 191 (siehe auch EXPLAIN-Befehl) Authentifizierung 3, 570 auto_increment_increment-Variable 396 auto_increment_offset-Variable 396 AUTOCOMMIT-Modus 11 automatisch generierte Schemata 103 automatische Host-Blockade 600 Autorisierung 571 (siehe auch Rechte)
B Background Patrol Read 348 Backup & Recovery (Preston) 515 BACKUP DATABASE-Befehl 565 Backup-Accounts 578 Backups 515–521 aufzunehmende Daten 521, 525–528 Bedeutung 515, 520 von Binärlogs 519, 531–534 Dateisystemschnappschüsse 538–545 Daten- und Dateikonsistenz 528–530 Empfehlungen 519 Geschwindigkeit 558 »heiße« Backups 516 in separierte Dateien 536, 550 inkrementelle 526 »kalte« Backups 516 Kopieren von Dateien zwischen Maschinen 520 Lage der 593 logische Backups 519, 523 erzeugen 534–537, 559, 566 reparieren 548–551
LVM-Schnappschüsse 538–545 offline 521 online 522 paralleles Dump und Wiederherstellen 537 RAID als 518 Replikation für 375, 518, 530 Replikations-Slave-Initialisierung mittels 383 rohe Backups 519, 524, 547 schnappschussbasierte Backups 519 durch Shared-Hosting-Provider 520 Sicherheit von 519 Skriptunterstützung 566–569 SQL-Dumps 534 testen 519, 525, 551 für Testzwecke 520 zur Überprüfung 520 Überwachung 519 Wahl der Storage-Engine basierend auf 28 »warme« Backups 516 Werkzeuge 558–566, 654 zur Wiederherstellung im Katastrophenfall 520 (siehe auch Wiederherstellung) Barth, Wolfgang (»Nagios System- und Netzwerk-Monitoring«) 641 Battery Backup Unit (BBU) 353, 356 Baum- (Pyramide) topologie, Replikation 402 B-Baum-Indizes 104–108 Beschränkungen 108 wann benutzen? 107 BBU (Battery Backup Unit) 353, 356 Befehlszähler 611 BENCHMARK( )-Funktion 49 Benchmarking 35 Abfragen für 42 Anzahl der Durchläufe 45 Aufwärmen des Systems vor 41 automatisieren 45 Beeinflussung durch andere Jobs 44 Beispiele 48–58 Datenmenge für 42 entwerfen 42 Ergebnisse analysieren 45 dokumentieren 43 Genauigkeit des 43 ungewöhnliche 45 Fehler während 41 Full-Stack 36, 46
Index | 719
gebräuchliche Fehler vermeiden 41 Gründe 36 Latenz 38 von Migrationen 44 Nebenläufigkeitsmessungen 39 Parameter ändern 44 realistische Szenarien 41 Single-Component 36, 37, 47 Skalierbarkeitsmessungen 39 Standard-Benchmarks, wann benutzen? 42 Transaktionen pro Zeiteinheit (Durchsatz) 38 vor dem Konfigurieren des Servers 291 Werkzeuge ab-Werkzeug 46 BENCHMARK( )-Funktion 49 Database Test Suite 47, 55–58 http_load-Werkzeug 46, 48 JMeter 46 MySQL Benchmark Suite (sql-bench) 47, 58 mysqlslap-Werkzeug 47 Super Smack 48 sysbench-Werkzeug 47, 50–55 Werkzeuge für 46–49 Wiederholbarkeit 43 Ziele bestimmen 37–40 Benutzer (siehe Accounts) benutzerdefinierte Funktionen (User-Defined Functions, UDFs) 248, 513 benutzerdefinierte Variablen 213–218 Benutzer-Locks 711 Benutzernamen Eindeutigkeit 571, 585 in Befehlen schützen 585 Berechtigungen 571 für Datenbankadministrator-Accounts 577 der Benutzer anzeigen 574 Einfluss auf Leistung 582 entfernen 574, 576, 587 zum Entfernen von Berechtigungen 588 Fehlerbehebung 583–591 für INFORMATION_SCHEMA-Tabellen 582 gespeichert in Grant-Tabellen 572 für gespeicherte Routinen 579 globale 572 hinzufügen 574, 576–578 zum Hinzufügen von Berechtigungen 588 für Logging-Accounts 577
720 | Index
Menge an 582 objektspezifische 571 für Operationen-Accounts 578 für Replikations-Accounts 378 für Sichten 581 für Systemadministrator-Account 576 für temporäre Tabellen 584 für Trigger 580 für Überwachungs-Accounts 578 unsichtbare 588–591 veraltete 591 wie MySQL sie prüft 573 für Datenbanken mit Wildcards 586 (siehe auch Rechte) Bereichsabfragen in Sphinx 697 Bereichsbedingung 143 Bereichspartitionierung 279 Betriebssystem aktualisieren, Wichtigkeit 592 Profiling 82–85 Sicherheit 592 Sichern von Dateien im 526 Speicheranforderungen für 295 Status überwachen 366–372 für auslagernden Server 372 für CPU-gebundene Server 370 für Ein/Ausgabe-gebundene Server 371 iostat-Werkzeug 368–370 für untätige Server 372 vmstat-Werkzeug 367 wählen 360 Betriebssystemwartezustände 617 BIGINT-Typ 88 Binärlog-Events, Replikation 376 Binärlogs aufräumen 533 Format der 532 für Replikation 376, 384 sichern 519, 531–534 Status der 633 Statusvariablen für 611 übertragen, InnoDB 319 bind_address-Variable 594 Binlog_cache_disk_use-Statusvariable 327, 611 Binlog_cache_use-Statusvariable 327, 611 binlog_do_db-Variable 392 binlog_ignore_db-Variable 392, 405 binlog-Dump-Prozess 376 Bit-gepackte Datentypen 98–100
BIT-Typ 98 Blackhole-Storage-Engine 24, 32 »Blob-Streaming«-Infrastruktur, PBXT 26 BLOB-Typen 93, 323–326 Boolesche Volltextsuchen 266 Bücher und Veröffentlichungen Backup & Recovery (Preston) 515 High Performance Web Sites (Souders) 502 MySQL Stored Procedure Programming (Harrison, Feuerstein) 234 MySQL-Dokumentation 610 Nagios System- und Netzwerk-Monitoring (Barth) 641 BUFFER POOL AND MEMORY-Abschnitt, SHOW INNODB STATUS-Ausgabe 628 Bytes_received-Statusvariable 327 Bytes_sent-Statusvariable 327
C Cache auf Controller (RAID-Cache) 351 CACHE INDEX-Befehl 297 Cache-Einheiten 340 Cache-Miss-Rate 341 Caches, CPU 336 Cache-Tabellen 152–156 Caching 505–512 aktive Caches 506 Anwendungsebene 507–509 Einfluss auf Lesen und Schreiben 338 Kontrollstrategien 509 Objekthierarchien 511 passive Caches 506 Speicheranforderungen 295–303 unter der Anwendungsebene 506 Vorabgenerieren von Inhalt für 512 (siehe auch spezielle Caches) Caching-Proxy-Server 503 Cacti-Werkzeug 358, 644 CD-ROM-Anwendungen, geeignete StorageEngines 31 CHANGE MASTER TO-Befehl 380, 383, 415, 417 CHAR_LENGTH( )-Funktion 261 CHARACTER SET-Klausel 258 character_set_client-Variable 256 character_set_connection-Variable 256 character_set_database-Variable 258
character_set_result-Variable 257 CHAR-Typ 90–93 CHECK TABLE-Befehl 146 chroot-Umgebung, MySQL in 606 CIPHER-Option 598 Client/Server-Protokoll 173–176 Cluster-Indizes 118–128 InnoDB-Implementierung von 21, 119, 121– 128 Nachteile 120 Vorteile 120 Cluster-Systeme 474 Codewiederverwendung 234 Cole, Jeremy (SHOW PROFILE-Patch) 80 Collate-Klauseln 258 columns_priv-Tabelle 573 Com_*-Statusvariablen 327 Com_admin_commands-Statusvariable 611 Com_change_db-Statusvariable 611 Com_select-Statusvariable 226–227, 611 COMMIT-Befehl 7 (siehe auch AUTOCOMMIT-Modus) concurrent_insert-Variable 320 CONNECTION_ID( )-Funktion, Caching nicht benutzt für 221 Connections-Statusvariable 327, 609 Continuous Data Protection 565 CONVERT( )-Funktion 257 Copying to tmp table-Status der Abfrage 176 COUNT( )-Funktion, Optimierungen 179, 202–204 CPU-Benchmark, sysbench-Werkzeug 51 CPU-gebundene Server 370 CPUs Anzahl 333–336, 451 Architektur von 334 Geschwindigkeit von 333–334 Sättigung von 332–336 Werkzeuge für Profiling der Auslastung 68 CREATE TEMPORARY TABLE-Befehl 22 CREATE USER-Berechtigung 588 Created_tmp*-Statusvariablen 612 Created_tmp_disk_tables-Statusvariable 328 Created_tmp_tables-Statusvariable 328 Cricket-Werkzeug 644 CSV-Storage-Engine 23, 32 CURRENT_DATE( )-Funktion, Caching nicht benutzt für 221
Index | 721
CURRENT_USER( )-Funktion, Caching nicht benutzt für 221 Cursor 242
D Database Test Suite 47, 55–58 Data-Dictionary 303 Dateideskriptoren, Statusvariablen für 612 Dateien Datenbank 355–357 komprimieren und dekomprimieren 656–659 Kopieren großer Dateien 656–659 kopieren, Benchmarks für 659 lesen und übertragen 311–314 Serverkonfiguration 287–288 Dateikonsistenz bei Backups 529 Dateisysteme Verschlüsselung 602 wählen 361–363 Dateisystemschnappschüsse 538–545 (siehe auch LVM) Datenarchivierung (siehe Archivierung von Daten) Datenbank Dateien für 355–357 Host für Logins beschränken 592 migrieren auf MySQL 637 Rechte 586 Speicherort 15 Datenbankadministrator-Accounts 577 Datenbanken mit Wildcards, Berechtigungen für 586 Datendateien (siehe Dateien) Datenfragmentierung 148 Datenkonsistenz bei Backups 528 Daten-Sharding 454–456 Abfragen über Shards hinweg 458 dynamische Zuweisung der Daten zu Shards 463 Einheit des Sharding 457 explizite Zuweisung der Daten zu Shards 465 feste und dynamische Zuweisung 465 feste Zuweisung der Daten zu Shards 462 global eindeutige IDs nötig für 467–469 Größe der Shards 459 Partitionierungsfunktion 462 Partitionierungsschlüssel 456–458 Sammeln von Daten mit Sphinx 689, 704
722 | Index
Shards auf Knoten anordnen 460 Shards neu ausgleichen 466 in Sphinx 692 Werkzeuge 469 zeitbasierte Datenpartitonierung 473 Datensynchronisation (siehe Replikation) Datentypen automatisch generierte Schemata wählen 103 optimale 87 Bit-gepackt 98–100 Datum und Uhrzeit 88, 97 ganze Zahlen 88 Größe der 87, 93 für Identifikatorspalten 100 nullable 87 reelle Zahlen 89 Strings 90–96 wählen 88 Datenverschlüsselung (siehe Verschlüsselung) Datenverteilung, Replikation für 375 DATETIME-Typ 88, 97 Datums- und Uhrzeit-Datentypen 97 optimale wählen 88 Unterstützung für hohe Auflösung 98 Dauerhaftigkeit (Durability) in ACID-Test 8 dbt2-Werkzeug, Database Test Suite 55–58 db-Tabelle 573 Deadlocks 10 Status 621–623 Überwachung 652 dearchivieren 472 debug-Befehl, mysqladmin 709–710 Debugging (siehe Fehlerbehebung) DECIMAL-Typ 89 DEFAULT-Schlüsselwort für Konfigurationsvariablen 289 für MyISAM-Wiederherstellung 305 Dekomprimieren von Dateien 656–659 DELAY_KEY_WRITE-Option 19 delay_key_write-Variable 304, 321 Delayed_*-Statusvariablen 615 DELAYED-Option 210 DELETE-Befehl, EXPLAIN-Befehl mit 663 Denormalisierung 149, 151–152 DETERMINISTIC-Option 236 directio( )-Funktion 312 dirty read 9 Diskussionsforen, geeignete Storage-Engines 30 DISTINCT-Klausel optimieren 205
Distribution-Master 401 DMZs 596 DNS-Lookup aus Leistungsgründen vermeiden 358 Dokumentation für MySQL 610 Dokumentzeiger 263 Doppelpufferung 341 mit fsync( ) 312 mit O_SYNC-Flag 313 Dormando’s Proxy for MySQL-Werkzeug 653 DOUBLE-Typ 89 Doublewrite-Puffer, InnoDB 318 DRBD, Festplattenreplikationswerkzeug 490 DROP USER-Befehl 574 drop-Befehl, mysqladmin 591 duplizierte Indizes 136–138 Durchsatz 38, 333, 343 dynamische Optimierungen für Abfragen 179
E Edge Side Includes (ESI) 503 Ein/Ausgabe für InnoDB anpassen 306–320 Log-Datei- und Puffereinstellungen 308–314 Transaktions-Log-Einstellungen 307–308 für InnoDB einstellen Tablespace-Einstellungen 314–317 für InnoDB, Verfeinerung Doublewrite-Puffer-Einstellungen 318 für MyISAM anpassen 304–306 sequenzielle 337 zufällige 337 Ein/Ausgabe zusammenfassen 339 Ein/Ausgabe-gebundene Server 371 Ein/Ausgabe-Sättigung 332 Ein/Ausgabe-Slave-Thread 376 Ein/Ausgabe Caches beeinflussen 338 für InnoDB feineinstellen Binärlog-Einstellungen 319 Eingabepuffer, Status des 627 ENCRYPT( )-Funktion 601 Engines (siehe Storage-Engines) Entwertung beim Lesen, Cache-Kontrollstrategie 510 Entwicklung, Software für 637 ENUM-Typ 94, 101
Escape-Folgen 258 ESI (Edge Side Includes) 503 Events 240 Binärlog-Events 376 (siehe auch gespeicherter Code) Event-Zähler 617 exklusive Locks (Schreib-Locks) 5 expire_logs_days-Variable 319, 389, 533 EXPLAIN EXTENDED-Befehl 662, 674 EXPLAIN PARTITIONS-Befehl 662, 664 EXPLAIN-Befehl aufrufen 661 Ausgabe von 661, 664–676 in Baumstruktur 676 Extra-Spalte 675 filtered-Spalte 674 id-Spalte 665 key_len-Spalte 672 key-Spalte 672 partitions-Spalte 664 possible_keys-Spalte 671 ref-Spalte 673 rows-Spalte 674 select_type-Spalte 666 table-Spalte 667–670 type-Spalte 670–671 für Nicht-SELECT-Abfragen 663 Leistung des 662 mk-visual-explain-Skript für 676 explizite Entwertung, Cache-Kontrollstrategie 510 explizites Locking 13 ext2-Dateisystem 361, 363 ext3-Dateisystem 361–363 extended-Befehl, mysqladmin 76, 298, 609–610, 616 externe XA-Transaktionen 285
F Failback 493–497 Failover 375, 493–497 Falcon-Storage-Engine 25, 32 Integer-Typen speichern 89 Lock-Warten 715 MVCC unterstützt durch 13 Sperren in 15 fdatasync( )-Funktion 311 Federated-Storage-Engine 23, 32, 475
Index | 723
Federation 448, 475 Fehlerbehebung Anwendungsleistung Caching 505–512 Probleme finden 498–501 Webserverprobleme 502–505 Berechtigungen 583–591 Datenfragmentierung 148 Indexbeschädigung 146 Indexfragmentierung 148 MySQL-Upgrades, Probleme verursacht durch 218 Prozesse 83–85 Replikation Abhängigkeiten in nichtreplizierten Daten 428 alle Updates nicht repliziert 430 auf beide Master schreiben 432 begrenzte Bandbreite 440 Datenänderungen auf Slave 427 Datenbeschädigung oder -verlust 421–424 fehlende temporäre Tabellen 429 Fehler bei nichttransaktionsfähigen Tabellen 424 Festplattenplatz wird weniger 440 InnoDB-sperrende-SELECTs 430–432 Mischen transaktionsfähiger und nichttransaktionsfähiger Tabellen 425 nichtdeterministische Anweisungen 426 nichteindeutige Server-IDs 427 Pakete vom Master, übergroße 439 Setup-Probleme 391 Slave-Rückstand, übermäßiger 434–439 undefinierte Server-IDs 428 unterschiedliche Storage-Engines auf Master und Slave 426 Wettstreit um Sperren 430–432 Sperren 706–715 Tabellenbeschädigung 146 Verbindungen 83–85 Verbindungsfehler 583 Fehlertoleranz 446–447 Festplatten mehrere Festplatten-Volumes 355–357 Speicher-zu-Festplatte-Verhältnis für 341 wählen 342–344 (siehe auch RAID)
724 | Index
Feuerstein, Steven (»MySQL Stored Procedure Programming«) 234 FILE I/O-Abschnitt, SHOW INNODB STATUSAusgabe 626 FILE-Berechtigung 578 fileio-Benchmark, sysbench-Werkzeug 51 Filesorts optimieren 190, 325 Filterung in Sphinx 695 Firewalls 594 FLOAT-Typ 89 FLUSH HOSTS-Befehl 600 FLUSH PRIVILEGES-Befehl 575 FLUSH QUERY CACHE-Befehl 230 FLUSH STATUS-Befehl 77 FLUSH TABLES WITH READ LOCK-Befehl 522, 710 flush-hosts-Befehl, mysqladmin 600 FNV (Fowler/Noll/Vo) UDF 113 FOR UPDATE-Option 212 FORCE INDEX-Option 212 Fowler/Noll/Vo (FNV) UDF 113 Fragezeichen (?), Parameter in vorbereiteten Anweisungen 243 Fragmentierung von Daten 148 FreeBSD-Betriebssystem 360 Fremdschlüssel 21, 161, 272 Fehler in 619–621 redundante, prüfen auf 652 .frm-Dateien 15, 157 fsync( )-Funktion 311, 617 ft_min_word_len-Parameter 271 Full-Stack-Benchmarking 36, 46 funktionelle Partitionierung 452, 486 Funktionen benutzerdefinierte (User-Defined, UDFs) 248, 513 gespeicherte 246 nichtdeterministische, Caching nicht benutzt für 221
G Galbraith, Patrick (Funktionen für memcached) 248 ganze Zahlen, Datentypen für 88 gdb-Werkzeug 84 General-Query-Log 69, 75 gepackte (präfix-komprimierte) Indizes 135 gespeicherte Funktionen 246
gespeicherte Prozeduren 236, 246 gespeicherte Routinen, mit ihnen benutzte Berechtigungen 579 gespeicherter Code Bibliothek 234 Events 240 gespeicherte Funktionen 246 gespeicherte Prozeduren 236, 246 Kommentare in 241 Nachteile 235 Sprachkonstrukte 234 Trigger 238–240 Vorteile 234 GET_LOCK( )-Funktion 711 Gewichtet, Lastausgleichsalgorithmus 484 Gleichheit verbreiten 198 globale Berechtigungen 572, 587 globale Lese-Locks 710 globale Locks 707 globale Version/sitzungsbasierte Aufteilung 479 GNU/Linux-Betriebssystem 360–361 gprof-Werkzeug 85 GRANT-Befehl 574, 576 analysieren 654 nicht replizieren 392 Grant-Tabellen 572 direkt modifizieren 574–575 wie MySQL sie benutzt 573 Granularität der Sperren 5 Grimmer, Lenz (mylvmbackup-Werkzeug) 538, 563 Groundwork Open Source-Werkzeug 642 GROUP BY-Klausel, optimieren 205, 685, 703 Gruppen, simulierte 577 Gruppen-Commit 284 gunzip-Werkzeug 657, 659 gzip-Komprimierung aktivieren 503 gzip-Werkzeug 657–659
H HackMySQL-Werkzeug 650 Handler_*-Statusvariablen 612 Handler_read_rnd_next-Statusvariable 328 Handler-Operationen, Statusvariablen für 612 Hardware aufrüsten 450 für Slave-Server 345 (siehe auch CPUs)
Harrison, Guy (»MySQL Stored Procedure Programming«) 234 Hash-Code 108 Hashed, Lastausgleichsalgorithmus 484 Hash-Funktionen 109, 111–112, 601 Hash-Indizes 108–113, 161 adaptive Hash-Indizes 111, 627 Einschränkungen 110 emulieren 111 Kollisionen verarbeiten 113 Hashing von Passwörtern 578, 601 Hash-Joins emulieren 199 hauptsächlich gelesene Tabellen, geeignete Storage-Engines 29 have_openssl-Variable 597 HEAP-Tabellen (siehe Memory-Storage-Engine) Heartbeat Record 412 »heiße« Backups 516 Helper-Threads, Status 626 HFS Plus-Dateisystem 363 Hibernate Shards 470 High Availability Linux-Projekt 493 »High Performance Web Sites« (Souders) 502 HIGH_PRIORITY-Option 210 HiveDB 470 Hochverfügbarkeit (siehe Verfügbarkeit, hohe) Host-Blockade, automatisch 600 Hostnamen in Befehlen schützen 585 Localhost 583 vorgegebene 575 host-Tabelle 573 http_load-Werkzeug 46, 48 Hyperic HQ-Werkzeug 641
I ibbackup (InnoDB Hot Backup-Werkzeug) 561, 566 .ibd-Dateien 315 Identifikatorspalten, Datentypen für 100 ifconfig-Werkzeug 366 IGNORE INDEX-Option 212 implizites Locking 13 IN( )-Listenvergleiche, Optimierungen 182 indexabdeckende Abfragen 129 indexer-Programm in Sphinx 690 Index-Merge-Algorithmen optimieren 197 Indexscans, lockere, nicht unterstützt 199
Index | 725
Indexschreiboperationen verschieben 304 Indexstatistiken 183 Indizes 103, 139 abdeckende Indizes 128–132, 181 B-Baum-Indizes 104–108 Beschränkungen 108 wann benutzen? 107 Beispiel 140–145 Bereichsbedingungen und 143 Beschädigung 146 Datensätze in Primärschlüsselreihenfolge einfügen 125–128 Fragmentierung 148 Hash-Indizes 108–113 adaptive Hash-Indizes 111, 627 Einschränkungen 110 emulieren 111 Kollisionen verarbeiten 113 Leistung abdeckende Indizes 128–132 Cluster-Indizes 118–128 duplizierte Indizes 136–138 gepackte Indizes 135 Indexscans für Sortierungen 133–135 Locking und 138–140 Präfix-Indizes 115–118 redundante Indizes 136–138, 652 schnell aufbauen 159 Sortierung 145 Spalten isolieren 114 in MyISAM 19 räumliche Indizes 113 Selektivität 115–118 Statistiken aktualisieren 146 Surrogatschlüssel 125 Volltextindizes 114 Indizierung, Volltext- (siehe Volltextindizierung) info( )-Funktion 146 INFORMATION_SCHEMA-Datenbank 609, 634 Funden veralteter Berechtigungen mit 591 Systemvariablenzugriff in 608 Tabellenberechtigungen für 582 inkrementelle Backups 526 InnoDB Hot Backup-Werkzeug (ibbackup) 561, 566 InnoDB Recovery Toolkit 557 Innodb_*-Statusvariablen 615
726 | Index
Innodb_buffer_pool_pages_dirty-Statusvariable 300 innodb_buffer_pool_size-Variable 292 innodb_commit_concurrency-Variable 322 innodb_concurrency_tickets-Variable 322 innodb_data_file_path-Variable 315 innodb_data_home_dir-Variable 315 innodb_doublewrite-Variable 318 innodb_file_io_threads-Variable 314 innodb_file_per_table-Variable 303, 315 innodb_flush_log_at_trx_commit-Variable 310 innodb_flush_method-Variable 311 innodb_force_recovery-Variable 557 innodb_log_buffer_size-Variable 309 innodb_log_file_size-Variable 292 innodb_max_dirty_pages_pct-Variable 300 innodb_max_purge_lag-Variable 317 innodb_open_files-Variable 303 Innodb_os_log_written-Statusvariable 309 innodb_thread_concurrency-Variable 322 innodb_thread_sleep_delay-Variable 322 InnoDB-Storage-Engine 20, 32 adaptive Hash-Indizes 111 AUTO_INCREMENT, Tabellen-Locking benutzt für 162 Cluster-Indizes 21, 119, 121–128, 161 COUNT(*)-Abfragen nicht optimiert 162 Data-Dictionary 303 Dateisystemschnappschüsse für 543 Datenladen, nicht optimiert 162 Ein/Ausgabe anpassen 306–320 Log-Datei- und Puffereinstellungen 308–314 Transaktions-Log-Einstellungen 307–308 Ein/Ausgabe einstellen Tablespace-Einstellungen 314–317 Ein/Ausgabe feineinstellen Binärlog-Einstellungen 319 Ein/Ausgabe verfeinern Doublewrite-Puffer-Einstellungen 318 Fremdschlüsselbeschränkungen in 21, 161 Isolationsebenen in 12, 21 lock-freie Backups 542–544 Locking in 13, 15, 21 Lock-Wartezustände 712–714 MVCC unterstützt durch 13, 14, 161 Nebenläufigkeit anpassen 21, 321 optimiertes Caching 161 Probleme durch Beschädigungen 555
Puffer-Pool 292, 299 redundante Indizes 137 Rohdateien reparieren 547 Row-Locks 161 Statusinformationen für 616–630 adaptiver Hash-Index 627 aktuelle Wartezustände 617 Deadlocks 621–623 Eingabepuffer 627 Event-Zähler 617 Fremdschlüsselfehler 619–621 I/O-Helper-Threads 626 Leistungszähler 626 Mutexe 631 Pufferpool 628 Transaktionen 623–626 Transaktions-Logs 628 Zeilenoperationen 629 Statusvariablen 615 Tablespaces 21 Transaktionen beeinflussen Abfrage-Cache 222, 231 Überwachung 645 ungepackte Indizes 162 64-Bit-Zahlen, Ausgabeformat für 616 Wiederherstellung für 554–557 innotop-Werkzeug 610, 617, 645–649 INSERT BUFFER AND ADAPTIVE HASH INDEX-Abschnitt, SHOW INNODB STATUS-Ausgabe 627 INSERT-Befehl DELAYED, Statusvariablen für 615 EXPLAIN-Befehl mit 663 mit SELECT, für Tabellenumwandlungen 33 interaktive Überwachungswerkzeuge 645–649 interne Nebenläufigkeitsprobleme 335 interne XA-Transaktionen 284 interne Zeilenfragmentierung 148 Introducer für Zeichensätze 258 INT-Typ 88 iostat-Werkzeug 368–370 IP-Adressen speichern 102 Isolation in ACID-Test 7 Isolationsebenen 8 Einstellung 12 mit InnoDB 12, 21 MVCC-Unterstützung für 15 (siehe auch Sperren)
J JFS-Dateisystem 361, 363 JMeter-Werkzeug 46 join_buffer_size-Variable 288 Joins Ausführungsstrategie für 183–185 Dekomposition von 170 Optimierungen für 179, 186–189, 205 STRAIGHT_JOIN-Option 211
K »kalte« Backups 516 Kapazität 446 Katastrophe, Wiederherstellung 520 Keep-Alive-Konfiguration, Apache 502–503 Key_*-Statusvariablen 612 KEY_BLOCK_SIZE-Option 299 Key_blocks_used-Statusvariable 328 key_buffer_size-Variable 289–290, 296 Key_reads-Statusvariable 328 Kommandozeilenskripten, nichtinteraktiver Modus für 645 Kommentare in gespeichertem Code 241 Komprimieren von Dateien 656–659 Konfiguration des Servers (siehe Serverkonfiguration) Konsistenz (Consistency) in ACID-Test 7 Konsistenz der Daten bei Backups 528 konstante Ausdrücke, Optimierungen für 180 Kopieren von Dateien Benchmarks 659 große Dateien 656–659 korrelierte Unterabfragen optimieren 193–197
L Last 447 Last_query_cost-Statusvariable 615 Lastausgleich 475–487 Algorithmen 484 Anwendungskonfiguration 480 Datenpartitionierung 486 direkt verbinden mit 477–481 DNS-Namen ändern 480 Filtern für 486 funktionelle Partitionierung 486 IP-Adressen verschieben 481 Master und mehrere Slaves 486 Replikation 375, 477
Index | 727
Server hinzufügen oder entfernen, Effekt 485 Vermittlerlösungen 482–485 Werkzeuge 477, 482 Ziele 475 Latenz 343 Benchmarking 38 für Netzwerke 358 niedrige, schnelle CPUs für 333 LATEST DETECTED DEADLOCK-Abschnitt, SHOW INNODB STATUS-Ausgabe 621–623 LATEST FOREIGN KEY ERROR-Abschnitt, SHOW INNODB STATUS-Ausgabe 619–621 Laufwerke (siehe Festplatten) leichtes Profiling 61 Leistung 332, 446, 450 von Abfragen Abfrage-Cache 176 Abfragen neu strukturieren 169–171 Datenzugriff analysieren 163–168 Optimierer für 3, 177–202, 210, 218 Optimierungen für bestimmte Arten von 202–209 (siehe auch Abfrage-Cache) des ALTER TABLE-Befehls 156–159 der Anwendung Caching für 505–512 Erweitern von MySQL für 512 optimale Nebenläufigkeit finden 504 Probleme finden 498–501 Webserverprobleme 502–505 automatisch generierte Schemata beeinflussen 103 von Backup und Wiederherstellung 558 Berechtigungen beeinflussen 582 der Cursor 242 von Dateikopien 659 DNS-Abfrage vermeiden 358 Entwicklungskomplexität durch Verbesserung erhöht 156 des EXPLAIN-Befehls 662 von Fremdschlüsseln 272 von gespeichertem Code 234–241 von Indizes abdeckende Indizes 128–132 Cluster-Indizes 118–128 duplizierte Indizes 136–138 gepackte Indizes 135
728 | Index
Indexscans für Sortierungen 133–135 Isolieren von Spalten 114 Locking und 138–140 Präfix-Indizes 115–118 redundante Indizes 136–138 schnell aufbauen 159 Sortierung 145 von Merge-Tabellen 275 von OLTP messen 38 von partitionierten Tabellen 282 der Replikation 441–443 Serverkonfiguration, Vorteile für 286 der dynamisch gesetzten Servervariablen 289–291 von Sichten 250, 252 der Sperren 5 in Sphinx steuern 697 Swapping-Einfluss 364 von temporären Tabellen 324 von UDFs 248 von verteilten (XA) Transaktionen 284–285 der Volltextsuche 268–272 von vorbereiteten Anweisungen 244 Werkzeuge Analysewerkzeuge 649–652 Dormando’s Proxy for MySQL-Werkzeug 653 Maatkit-Werkzeuge 653 MySQL Proxy 652 Schnittstellenwerkzeuge 636–638 Überwachungswerkzeuge 638–649 von Zeichensätzen und Sortierreihenfolgen 260–262 (siehe auch Benchmarking; Optimierung; Profiling) Leistungszähler 626 LENGTH( )-Funktion 261 Lese-Locks 4 Lesen (siehe Ein/Ausgabe) lighttpd, Leichtgewichtsserver 503 LIMIT-Klausel optimieren 207–208 Linux Virtual Server (LVS) 477 Linux-Betriebssystem (siehe GNU/LinuxBetriebssystem) LinuxThreads-Thread-Bibliothek 364 LOAD DATA FROM MASTER-Befehl 384 LOAD DATA INFILE-Befehl 258, 536 LOAD INDEX-Befehl 297 LOAD TABLE FROM MASTER-Befehl 384
Localhost-Hostname 583 LOCK IN SHARE MODE-Option 212 LOCK TABLES-Befehl 13, 304, 707 Locked-Zustand der Abfrage 175 lockere Indexscans, nicht unterstützt 199 Lock-Wartezustände Liste anzeigen 645 log_bin_trust_function_creators-Variable 236 log_queries_not_using_indexes-Variable 69–70 log_slave_updates-Variable 379, 390, 532 LOG-Abschnitt, SHOW INNODB STATUSAusgabe 628 Log-Dateien Größe 308 lesen und übertragen 311–314 von Datendateien trennen 356 Logging Abfrage-Logs 69–75 Binärlogs (siehe Binärlogs) Relay-Log 376 Storage-Engines geeignet für 28 Transaktions-Logging 11, 307–308, 628 Logging-Accounts 577 Logical Unit Numbers (LUNs) 354 Logical Volume Manager (siehe LVM-Schnappschüsse) Logins beschränken 592 logische Backups 519, 523 erzeugen 534–537, 559, 566 reparieren 548–551 logische Lesevorgänge 338 logische Nebenläufigkeitsprobleme 335 Log-Puffer Größe 309 übertragen 309 Logserver für Wiederherstellung 553 Replikation für 407–408 lokale Caches 507 lokale Shared-Memory-Caches 508 Lokalitätseigenschaft 336 long 70 long_query_time-Variable 69–71 LONGBLOB-Typ 93 LONGTEXT-Typ 93 low_priority_updates-Variable 321 LOW_PRIORITY-Option 210 lsof-Werkzeug 84 LUNs (Logical Unit Numbers) 354
LVM- (Logical Volume Manager) Schnappschüsse 538–545 entfernen 541 erzeugen 540 Initialisieren des Slave mittels 383 Konfiguration für 539 lock-freie InnoDB-Backups mittels 542–544 mounten 541 für Online-Backups 542 planen für 544 LVS (Linux Virtual Server) 477
M Maatkit-Werkzeuge 651, 653 (siehe auch spezielle mk-*-Werkzeuge) Maria-Storage-Engine 26, 32 master.info-Datei 388 Master-Topologien für Replikation (siehe Replikation, Topologien für) MATCH AGAINST-Klausel 264 materialisierte Sichten 254 MAX( )-Funktion, Optimierungen für 179, 201 max_allowed_packet-Variable 173, 412, 439 max_connection_errors-Variable 600 max_length_for_sort_data-Variable 325 max_sort_length-Variable 326 Max_used_connections-Statusvariable 328 Maxia, Giuseppe Bibliothek der gespeicherten Routinen von 234 Sandbox-Skript von 382 McCullagh, Paul (Entwickler von PBXT) 26 MD5( )-Funktion 49, 91, 102, 112, 434, 601 MEDIUMBLOB-Typ 93 MEDIUMINT-Typ 88 MEDIUMTEXT-Typ 93 mehrere Festplatten-Volumes 355–357 memcached 508 benutzerdefinierte Funktionen für 248 für global eindeutige IDs 468–469 memlock-Variable 366 memory-Benchmark, sysbench-Werkzeug 54 Memory-Mapping 306 Memory-Storage-Engine 22, 32 Daten nicht persistent 161 dynamische Zeilen nicht unterstützt 160 Hash-Indizes 109, 161 Indexstatistiken nicht unterstützt 161 Locking in 15
Index | 729
Tabellen-Locks 160 temporäre Tabellen auf Festplatte benutzt von 94 MERGE-Algorithmus für Sichten 250 Merge-Storage-Engine 15, 20, 32 Merge-Tabellen 273–278 Migration von Datenbanken, Software für 637 MIN( )-Funktion, Optimierungen für 179, 201 mk-archiver-Werkzeug 405, 654 mk-deadlock-logger-Werkzeug 652 mk-duplicate-key-checker-Werkzeug 652 mk-find-Werkzeug 654 mk-heartbeat-Werkzeug 412, 652 mk-parallel-dump-Werkzeug 537, 562, 566, 654 mk-parallel-restore-Werkzeug 563–654 mk-profile-compact-Werkzeug 651 mk-query-profiler-Werkzeug 651 mk-show-grants-Werkzeug 654 mk-slave-delay-Werkzeug 654 mk-slave-prefetch-Werkzeug 655 mk-slave-restart-Werkzeug 655 mk-table-checksum-Werkzeug 413, 655 mk-table-sync-Werkzeug 415, 655 mk-visual-explain-Werkzeug 652, 676 MONyog-Werkzeug 643 mpstat-Werkzeug 366 MRTG (Multi Router Traffic Grapher) 358, 643 mtop-Werkzeug 645 Multi Router Traffic Grapher (MRTG) 358, 643 Multimaster-Replikation 395, 406 Multivalued Attributes (MVAs) 694 Multiversion Concurrency Control (siehe MVCC) Munin-Werkzeug 643 Mustererkennung, Beschränkungen der 586 mutex-Benchmark, sysbench-Werkzeug 55 Mutexe, Status der 631 MVAs (Multivalued Attributes) 694 MVCC (Multiversion Concurrency Control) 13–15, 161 my.cnf-Datei 287 .MYD-Dateien 18 .MYI-Dateien 18 myisam_block_size-Variable 299 myisam_recover-Variable 305 myisam_use_mmap-Variable 306 MyISAM-Storage-Engine 18–20, 32 COUNT( )-Funktion, Leistung der 203 Datenwiederherstellung nicht automatisiert 160 730 | Index
Ein/Ausgabe anpassen 304–306 gepackte Indizes 135 Indizes 19 Caching 160 schnell aufbauen 159 Schreiboperationen verschieben 304 Indizes in 121–124 kompakte Speicherung benutzt von 160 komprimierte Tabellen 20 Locking in 15, 19 Memory-Mapping 306 Nebenläufigkeit verbessern 320 räumliche Indizes 113 redundante Indizes 137 Reparatur von Tabellen 19 Reparieren von Rohdateien 547 Schlüssel-Caches (Schlüsselpuffer) 290, 296–299, 612 Tabellen-Locks 160 Transaktionen nicht unterstützt 160 verzögertes Schlüsselschreiben 19 Volltextindizierung 114, 263 Wiederherstellen nach Beschädigung 305 mylvmbackup-Werkzeug 538, 563, 566 MySQL Alternativen zu 514 Architektur 1–3 Benchmarking (siehe Single-ComponentBenchmarking) in chroot-Umgebung 606 Dokumentation für 610 Entwickler kontaktieren 513 erweitern 512 Profiling 68–75 Quellcode für 605 Rechte zum Ausführen 592 Upgrades Änderungen an Optimierer in 218 testen mit Replikation 375 verbinden mit, Fehlerbehebung 83–85, 583 MySQL Administrator 637 MySQL Benchmark Suite (sql-bench) 47, 58 MySQL Forge-Community-Site 635, 655 MySQL Master-Master Replication ManagerWerkzeug 495 MySQL Migration Toolkit 637 MySQL Monitoring and Advisory Service 642 MySQL Proxy 82, 482, 513, 652 MySQL Query Browser 636
MySQL Statement Log Analyzer (mysqlsla) 75, 650 »MySQL Stored Procedure Programming« (Harrison, Feuerstein) 234 MySQL Visual Tools 636 MySQL Workbench 637 MySQL, Version 4.1, Passwort-Hashing-Schema 578 MySQL, Version 5.0 Berechtigungen, Änderungen in 579–582 gespeicherte Routinen 579 Patch zum Entfernen ausführlicher DatensatzDumps 714 SHOW STATUS-Befehl 609 Trigger 580 MySQL, Version 5.1 INFORMATION_SCHEMA-Datenbank 609 Patch für langsame Abfragezeiten 71 Volltextsuche, Änderungen in 268 mysql_slow_log_filter-Werkzeug 75 mysql_slow_log_parser-Werkzeug 75 mysqladmin-Dienstprogramm debug-Befehl 709–710 drop-Befehl 591 extended-Befehl 76, 298, 609–610, 616 flush-hosts-Befehl 600 shutdown-Befehl 567 variables-Befehl 608 mysqladmin-Dienstprogramm extended-Befehl 610 mysqladmin-Dienstprogramm extended-Befehl 616 mysql-bin.index-Datei 388, 533 mysqlbinlog-Werkzeug 532 mysql-Datenbank (siehe Datenbank) mysqldumpslow-Werkzeug 74 mysqldump-Werkzeug Initialisieren des Slave mittels 383 logische Backups mittels 534, 559, 566 Neusynchronisieren des Slave mittels 414 Tabellenumwandlungen mittels 33 mysqlhotcopy-Werkzeug 383, 560, 566 mysqlmanager-Werkzeug 288 mysqlpdump-Werkzeug 537 mysql-relay-bin.index-Datei 388 mysqlreport-Werkzeug 326, 650 MySQL-Server (siehe Server) mysqlsla (MySQL Statement Log Analyzer) 75, 650 mysqlslap-Werkzeug 47
mysqlsniffer-Werkzeug 82 mytop-Werkzeug 645
N »Nagios System- und Netzwerk-Monitoring« (Barth) 641 Nagios-Werkzeug 640 Name-Locks 707, 710 NAS (Network-Attached Storage) 355 Native POSIX Threads Library (NPTL) 364 natürlichsprachige Volltextsuche 264–266 nc-Werkzeug 658–659 NDB Cluster-Storage-Engine 24, 32, 474 Konfiguration, Statusvariablen für 615 Locking in 15 T-Bäume benutzt für Indizes 104 Ndb_*-Statusvariablen 615 NDB-API 514 NDB-Modul für Apache 514 Nebenläufigkeit 4–6 feineinstellen 320–323 interne Nebenläufigkeitsprobleme 335 logische Nebenläufigkeitsprobleme 335 MVCC (Multiversion Concurrency Control) 13–15, 161 optimale 504 Sperren ermitteln Ebene der 15 Wählen der Storage-Engine basierend auf 27 (siehe auch Sperren) Nebenläufigkeit im Betrieb 40 Nebenläufigkeitsmessungen 39 netstat-Werkzeug 83, 366 Network-Attached Storage (NAS) 355 Netzwerk Konfiguration des 358–360 Latenz des 499 Sicherheit für 593–601 Überwachung 358 Zugriff deaktivieren 594 Next-Key-Locking-Strategie, InnoDB 21 Nichtdeterministische Funktionen, Caching nicht benutzt für 221 Nichter, Daniel (HackMySQL-WerkzeugWebsite) 650 nichtinteraktive Überwachungswerkzeuge 639–644 nichtinteraktiver Modus 645 nonrepeatable read 9 Normalisierung 149–152
Index | 731
Not_flushed_delayed_rows-Statusvariable 615 NOW( )-Funktion, Caching nicht benutzt für 221 NOW_USEC( )-UDF 249 NPTL (Native POSIX Threads Library) 364 NTFS-Dateisystem 363 nullable-Datentypen 87
O O_DIRECT-Flag 312 O_DSYNC-Flag 314 O_SYNC-Flag 313 Object-Relational Mapping (ORM) 103 Objekthierarchien für Caching 511 objektspezifische Berechtigungen 571 Objektversionierung 510 Offline-Backups 521 OFFSET-Klausel optimieren 207 OLAP (Online Analytical Processing) von OLTP trennen 404 OLD_PASSWORD( )-Funktion 579 old_passwords-Variable 579 OLTP (Online Transaction Processing) auf unterschiedlichen Slaves von OLAP trennen 404 Leistung messen 38 sysbench-Werkzeug für 53–54 Online Analytical Processing (OLAP) 404 Online Transaction Processing (siehe OLTP) Online-Backups 522 Open_*-Statusvariablen 612 Open_files-Statusvariable 328 Open_tables-Statusvariable 328 Opened_tables-Statusvariable 302, 328 OpenNMS-Werkzeug 642 OpenSSL-Bibliothek 597 Operationen-Accounts 578 OProfile-Werkzeug 85 optimale Nebenläufigkeit 504 optimieren 75 Optimierung Betriebssystem wählen 360 BLOB-Spalten 323–326 Cache-Tabellen 152–156 von CPUs 332–336 Dateisystem wählen 361–363 Datentypen 87 Bit-gepackt 98–100 Datum und Uhrzeit 88, 97
732 | Index
ganze Zahlen 88 Größe der 87, 93 für Identifikatorspalten 100 nullable 87 reelle Zahlen 89 Strings 90–96 wählen 88 Denormalisierung 149, 151–152 von Filesorts 190, 325 Fragmentierung reduzieren 148 Indexbeschädigung reparieren 146 Indexstatistiken aktualisieren 146 von mehreren Festplatten-Volumes 356 der Netzwerkkonfiguration 358–360 Normalisierung 149–152 von RAID 345–353 der Slave-Hardware 345 von Sortierungen 145, 190 von Speicher-zu-Festplatte-Verhältnis 341–344 Summary-Tabellen 152–156 Tabellenbeschädigung reparieren 146 TEXT-Spalten 323–326 der Volltextsuche 270–272 Zählertabellen 155 (siehe auch Verfügbarkeit, hohe; Lastausgleich; Leistung; Skalieren; Serverkonfiguration) OPTIMIZE TABLE-Befehl 148 optimizer_prune_level-Variable 213 optimizer_search_depth-Variable 213 ORM (Object-Relational Mapping) 103
P Paket-Sniffer für Profiling 82 Paketverlust 358 parallele Ausführung, nicht unterstützt 199 parallele Ergebnismengen, Sphinx-Werkzeug für 686 paralleles Dump und Wiederherstellen 537 Partionierung, funktionelle 486 partitionierte Daten (siehe Daten-Sharding) partitionierte Tabellen 273, 278–283 Abfragen an, optimieren 282 Arten 279 Beispiele 280 Beschränkungen 281 für Skalierbarkeit 473 Vorteile 278
Partitionierung, funktionelle 452–453 Partitionierungsschlüssel für Daten-Sharding 456–458 passive Caches 506 passive Überwachung 639 Passwörter Hashing 578, 601 Sicherheit von 576 passwortlosen Zugriff verbieten 584 PBXT- (Primebase XT) Storage-Engine 26, 32 Locking in 15 MVCC unterstützt durch 13 Percentile Response Times (Antwortzeiten-Perzentile) 38 perror-Dienstprogramm 303 persistente Verbindungen, verglichen mit Verbindungs-Pooling 501 Phantom-Read 9 phpMyAdmin-Werkzeug 594, 638 Phrasennähe-Ranking in Sphinx 693 Phrasensuchen 267 physische Größe von Festplatte 344 physische Leseoperationen 338 Planet MySQL-Blog-Aggregator 655 Plugin-spezifische Statusvariablen 615 Präfix-Indizes 115–118 präfix-komprimierte (gepackte) Indizes 135 Preforking 502 Preston, Curtis (»Backup & Recovery«) 515 Primärschlüsselreihenfolge, Einfügen von Datensätzen in 125–128 Primebase XT-Storage-Engine (siehe PBXTStorage-Engine) /proc-Dateisystem 84 procs_priv-Tabelle 573 Produktionsumgebung isolieren 593 Profiling 35, 59 Abfragen 77–80 Anwendungsebene 59–68, 498 Beispiel für 62–68 Maße für 60 Betriebssystem 82–85 CPU-Auslastung, Werkzeuge für 68 MySQL 68–75 MySQL-Server 76–81 ohne Code oder Patches 82 Paket-Sniffer für 82 Proxies für 82
Webserver-Logs für 82 (siehe auch SHOW INNODB STATUSBefehl; SHOW MUTEX STATUSBefehl) Proxies für Profiling 82 Prozeduren, gespeicherte 236, 246 Prozentzeichen (%), vorgegebener Hostname 575, 584 Prozesse, Fehlerbehebung 83–85 Prüfsummenabfragen 655 Pufferpool 292, 299 Pufferpool, Status von 628 punktgenaue Wiederherstellung 551 PURGE MASTER LOGS-Befehl 411 Putty-Werkzeug 599 Pyramiden- (Baum-) topologie, Replikation 402
Q Qcache_*-Statusvariablen 329, 612 Qcache_hits-Statusvariable 226 Qcache_inserts-Statusvariable 227 Qcache_not_cached -Statusvariable 226 query 228 query_cache_limit-Variable 228 query_cache_min_res_unit-Variable 223, 227–229 query_cache_size-Variable 228, 233, 288, 291 query_cache_type-Variable 228 query_cache_wlock_invalidate-Variable 229 Questions-Statusvariable 611
R R1Soft 565 RAID (Redundant Arrays of Inexpensive Disks) 345–353 Absturztestskript für 353 Ausfall von 348 als Backups 518 BBU (Battery Backup Unit) 353, 356 Hardware gegen Software abwägen 349 Konfiguration 350–353 Level 346–348 Stripe-Chunk-Größe für 350 Überwachung 348 RAID-Cache 351 räumliche (R-Baum) Indizes 113 R-Baum- (räumliche) Indizes 113
Index | 733
READ COMMITTED-Isolationsebene 9, 15 READ UNCOMMITTED-Isolationsebene 9, 15 read_buffer_size-Variable 291–292 read_only-Variable 385 read_rnd_buffer_size-Variable 291 Read-Around-Writes 298 Rechte 571 für Angestellten-Accounts 577 Arten 571 zum Ausführen von MySQL 592 für Backup-Accounts 578 für mysql-Datenbank 586 (siehe auch Autorisierung; Berechtigungen) records_in_range( )-Funktion 146 Redundant Arrays of Inexpensive Disks (siehe RAID) redundante Indizes 136–138, 652 Redundanz hinzufügen, für Hochverfügbarkeit 489 reelle Zahlen, Datentypen für 89 ReiserFS-Dateisystem 361, 363 relay_log_space_limit-Variable 385 relay_log-Variable 379 Relay-Log 376 relay-log.info-Datei 389 RELOAD-Berechtigung 578 REPAIR TABLE-Befehl 146 Reparieren von Daten 516 REPEATABLE READ-Isolationsebene 9, 15 replicate_*-Variablen 392 replicate_ignore_db-Variable 405 Replikation 373–377 Accounts für, erzeugen 378 anweisungsbasierte 373, 386 Anwendungen von 375 für Backups 375, 518, 530 benutzte MySQL-Versionen mischen 374 Beschränkungen 440 CPUs beeinflussen 333 Dateien benutzt durch 388 einrichten 377–385, 391 Fehlerbehebung Abhängigkeiten in nichtreplizierten Daten 428 alle Updates nicht repliziert 430 auf beide Master schreiben 432 begrenzte Bandbreite 440 Datenänderungen auf Slave 427
734 | Index
Datenbeschädigung oder -verlust 421–424 fehlende temporäre Tabellen 429 Fehler bei nichttransaktionsfähigen Tabellen 424 Festplattenplatz wird weniger 440 InnoDB-sperrende-SELECTs 430–432 Mischen transaktionsfähiger und nichttransaktionsfähiger Tabellen 425 nichtdeterministische Anweisungen 426 nichteindeutige Server-IDs 427 Pakete vom Master, übergroße 439 Setup-Probleme 391 Slave-Rückstand, übermäßiger 434–439 undefinierte Server-IDs 428 unterschiedliche Storage-Engines auf Master und Slave 426 Wettstreit um Sperren 430–432 Filterung für 391 für Skalierbarkeit 452 Geschwindigkeit messen 249 gespeicherte Routinen und 236 Kapazitätsplanung 408–410 Konfiguration 378 empfohlene 384 sichern 526 für Lastausgleich 477 Leistung der 441–443 Nichtnutzung von Servern einplanen 410 Sandbox-Skript für Experimente 382 Skalieren der Lesevorgänge 374 Skalieren der Schreibvorgänge 374, 409 Slave-Rückstand messen 412 Slave-Server Ändern des Masters 415 Hardware für 345 Konsistenz mit Master feststellen 413 als Master anderer Slaves 390 neustarten 655 starten 380–382 verzögern 654 vom Master neu synchronisieren 414 von einem anderen Server aus initialisieren 382 vorab holen für 655 zum Master befördern 416–420 Status 633 synchrone, für Hochverfügbarkeit 492
Topologien 393 anpassen 403 Baum- (Pyramiden-) 402 Datenarchivierung 404 Logserver 407–408 Master und mehrere Slaves 393 Master, Distribution-Master und Slaves 400–402 Master-Master mit Slaves 398 Master-Master, im aktiv-aktiv-Modus 395 Master-Master, im aktiv-passiv-Modus 397, 420 Multimaster 395, 406 Ring 399 schreibgeschützte Slaves 405 selektive Replikation 403 Trennen von Funktionen 404 Volltextsuchen 405 Überwachung 411, 645, 652 in Versionen vor 4.0 377 verzögerte, für Wiederherstellung 553 zeilenbasierte 373, 387 Zukunft der 443 replizierte-Festplatten-Architekturen 490 REQUIRE ISSUER-Option 598 REQUIRE SUBJECT-Option 598 RESET QUERY CACHE-Befehl 230 REVOKE-Befehl 574, 576, 587 für globale Berechtigungen 587 nicht replizieren 392 Richter, Georg (Patch für langsame Abfragezeiten) 71 rohe Backups 519, 524, 547 ROLLBACK-Befehl 7 Round-Robin, Lastausgleichsalgorithmus 484 ROW OPERATIONS-Abschnitt, SHOW INNODB STATUS-Ausgabe 629 Row-Locks 6, 15, 161 RRDTool-basierte Systeme 643 rsync-Werkzeug 659
S SAN (Storage Area Network) 354 Sandbox-Skript 382 sar-Werkzeug 366 Scannen von Indizes 133–135 Schema (siehe Datenbank)
Schlüssel (siehe Indizes) Schlüsselblockgröße 298 Schlüssel-Caches (Schlüsselpuffer) 290, 296–299 schnappschussbasierte Backups 519 Schnappschüsse, Dateisystem 538–545 schnellste Antwort, Lastausgleichsalgorithmus 484 Schnittstellenwerkzeuge 636–638 Schreiben (siehe Ein/Ausgabe) schreibgeschützte Slaves 405 schreibgeschützte Tabellen, geeignete StorageEngines 29 Schreibkapazität erhöhen 456 Schreib-Locks 4 Schwartz, Baron innotop-Werkzeug 645 Maatkit-Werkzeuge 651 Schwarze Bretter, geeignete Storage-Engines 30 scp-Werkzeug 657, 659 searchd-Programm, in Sphinx 690 Seconds_Behind_Master-Statusvariable 381, 412 Secure Sockets Layer (siehe SSL) secure_auth-Variable 579 SELECT INTO OUTFILE-Befehl 258, 536–537 Select_full_join-Statusvariable 329, 613 Select_full_range_join-Statusvariable 329, 613 Select_range_check-Statusvariable 329, 613 Select_range-Statusvariable 613 Select_scan-Statusvariable 609, 613 SELECT-Befehl mit UPDATE, nicht unterstützt 202 Statusvariablen für 613 (siehe auch Abfragen) SELECT-Berechtigung 571, 585 selektive Replikation 403 Selektivität von Indizes 115–118 SEMAPHORES-Abschnitt, SHOW INNODB STATUS-Ausgabe 617 Senden des Datenzustands der Abfrage 176 sequenzielle Ein/Ausgabe 337 seqwr-Benchmark, sysbench-Werkzeug 55 SERIALIZABLE-Isolationsebene 9, 15 Server Beschränken von Logins auf 592 Gruppieren von Servern 645 Profiling 76–81 überprüfen 593 Wartezustände in 706–712
Index | 735
Serveradministration, Software für 637–638 Serverkonfiguration Arbeitslast-basierte Anpassungen 323–330 Beispieldateien 293 Benchmarking vor 291 benutzte Einheiten 289 benutzte Syntax 288 BLOB-Spalten 323–326 Data-Dictionary 303 Dateien für 287, 288 dynamisch ändern 288–291 Filesorts 325 Geltungsbereich der Einstellungen 288 graduell ändern 292 in Backup sichern 526 Leistungsvorteile von 286 Nebenläufigkeit feineinstellen 320–323 Pufferpool 292 für Replikation 378, 384 Schlüssel-Caches 296–299 für Speicherbenutzung 293–296 Standardeinstellungen 286 Statusvariablen 326–330 Tabellen-Cache 301 TEXT-Spalten 323–326 Thread-Cache 301 verbindungsweise Einstellungen 330 (siehe auch Ein/Ausgabe) Serverstatus (siehe Status des MySQL-Servers) /server-status/-URL 84 Servervariablen anschauen 645 SET CHARACTER SET-Befehl 257 SET NAMES-Befehl 257 SET TRANSACTION ISOLATION LEVELBefehl 12 SET-Befehl 289 SET-Typ 99, 101 SHA1( )-Funktion 49, 102, 112, 601 Sharding (siehe Daten-Sharding) Shared-Hosting-Provider, Backups durch 520 Shared-Locks (Lese-Locks) 5 SHOW BINARY LOGS-Befehl 633 SHOW BINLOG EVENTS-Befehl 411, 633 SHOW CHARACTER SET-Befehl 259 SHOW COLLATION-Befehl 259 SHOW CREATE TABLE-Befehl 585 SHOW DATABASES-Berechtigung 587, 589 SHOW ENGINE INNODB STATUS-Befehl 616
736 | Index
SHOW FULL PROCESSLIST-Befehl 175 SHOW GLOBAL STATUS-Befehl 326, 609 SHOW GLOBAL VARIABLES-Befehl 289 SHOW GRANTS-Befehl 574, 588–591 SHOW INNODB STATUS-Befehl 616–630, 712 BUFFER POOL AND MEMORY-Abschnitt 628 FILE I/O-Abschnitt 626 INSERT BUFFER AND ADAPTIVE HASH INDEX-Abschnitt 627 LATEST DETECTED DEADLOCK-Abschnitt 621–623 LATEST FOREIGN KEY ERROR-Abschnitt 619–621 LOG-Abschnitt 628 ROW OPERATIONS-Abschnitt 629 SEMAPHORES-Abschnitt 617 TRANSACTIONS-Abschnitt 623–626, 712 SHOW MASTER LOGS-Befehl 408 SHOW MASTER STATUS-Befehl 379, 411, 633 SHOW MUTEX STATUS-Befehl 631 SHOW PROCESSLIST-Befehl 77, 83, 631, 706 SHOW PROFILE-Patch 80 SHOW SESSION STATUS-Befehl 77 SHOW SLAVE STATUS-Befehl 380, 412 SHOW STATUS-Befehl 76, 609–616, 650 SHOW TABLE STATUS-Befehl 16 SHOW USER STATISTICS-Befehl 435 SHOW VARIABLES-Befehl 608 shutdown-Befehl, mysqladmin 567 SHUTDOWN-Berechtigung 571 Sicherheit auf Localhost beschränkte Verbindungen 594 automatische Host-Blockade 600 von Backups 519 des Betriebssystems 592 chroot-Umgebung, MySQL in 606 Dateisystemverschlüsselung 602 Datenverschlüsselung 601–605 DMZs 596 Firewalls 594 Hashing von Passwörtern 578, 601 des Netzwerks 593–601 von Passwörtern 576 Quellcodemodifizierung für 605 SSL 597, 614 TCP-Wrapper 599 Tunnelung 596, 599
Verbindungsverschlüsselung 596–599 Verschlüsselung auf Anwendungsebene 603–605 (siehe auch Zugriffskontrolle; Authentifizierung; Autorisierung; Berechtigungen) Sichten 250 Berechtigungen 581 Beschränkungen 254 Leistung von 250, 252 materialisierte 254 MERGE-Algorithmus für 250 TEMPTABLE-Algorithmus für 251 update-fähige 252 Single-Component-Benchmarking 36–37, 47 Single-Pass-Sortieralgorithmus 190 sitzungsbasierte Aufteilung 478 Skalierbarkeit 445–446, 448 aktive Daten von inaktiven Daten trennen 472 Cluster-Bildung für 474 Datenumfang verringern 471–474 horizontal skalieren (Scaling out) 448, 452 Daten-Sharding für 454–471 Partitionierung für 452–453 Replikation 452 Lastausgleich für 475–487 Planen für 449 vertikal skalieren (Scaling up) 448, 451 Zwischenmaßnahmen vor dem Skalieren 450 Skalierbarkeitsmessungen 39 skip_grant_tables-Variable 586 skip_name_resolve-Variable 358, 582 skip_networking-Variable 594 skip_slave_start-Variable 385 Skriptunterstützung für Backups 566–569 Slave_*-Statusvariablen 615 slave_compressed_protocol-Variable 440 Slaves (siehe Replikation, Slave-Server) SLEEP( )-Funktion 708 Sleep-Zustand der Abfrage 175 Slow_launch_threads-Statusvariable 329 Slow_queries-Statusvariable 613 slow_query_log_file-Variable 69 slow_query_log-Variable 69 Slow-Query-Log 69–75 SMALLBLOB-Typ 93 SMALLINT-Typ 88 SMALLTEXT-Typ 93 Smokeping-Werkzeug 358
SNAP Innovation GmbH 26 Software-RAID 349 Solaris-Betriebssystem 360 Solid Information Technology 25 solidDB-Storage-Engine 15, 25, 32 sort_buffer_size-Variable 288, 291–292, 330, 614 Sort_merge_passes-Statusvariable 326, 329, 614 Sort_range-Statusvariable 614 Sort_scan-Statusvariable 614 Sortierreihenfolgen 255–258 Client/Server-Kommunikation 256 Escape-Folgen, Behandlung von 258 in Anweisungen festlegen 258, 260 unterstützte, in Anweisungen ermitteln 259 vorgegebene 256 wählen 259 Wirkungen auf Abfragen 260–262 Sortierung Filesorts 190, 325 Indexscans für 133–135 optimieren 145, 190 Statusvariablen für 614 Sorting result-Zustand der Abfrage 176 Souders, Steve (»High Performance Web Sites«) 502 Spaltenberechtigungen, Abfrage-Cache nicht benutzt mit 582 Speicher Abfrage-Cache-Benutzung von 223–225 ausgleichen mit Festplattenressourcen 336–344 Benutzungsspitze 295 Betriebssystemanforderungen für 295 Cache-Anforderungen für 295–303 für MySQL verfügbare Menge 294 Serverkonfiguration für 293–296 Speicherkapazität der Festplatte 343 Speicher-zu-Festplatte-Verhältnis 341–342 Sperren mit Archive-Engine 23 Benutzer-Locks 711 Concurrency-Ebene der 15 Debugging 706–715 explizite 13 Falcon 715 feststellen, wer sie hält 709, 713 Funktionen nach Storage-Engine gelistet 31 globale Lese-Locks 710
Index | 737
globale Locks 707 Granularität 5 implizite 13 Indizes beeinflussen 138–140 InnoDB 21, 712–714 Leistung der 5 Lese-Locks 4 Memory-Engine 22 MyISAM-Engine 19 nach Storage-Engine Funktionen gelistet 31 Name-Locks 707, 710 Row-Locks 6, 15, 161 Schreib-Locks 4 Statusvariablen für 614 String-Locks 707 Tabellen-Locks 6, 160, 707–710 Concurrency-Ebene der 15 als potenzielle Engstelle 160 Statusvariablen für 614 SphinxSE-Storage-Engine 695–697 Sphinx-Werkzeug 677, 692 Attributeunterstützung 694 Beispiele für die Benutzung 678–681, 698–705 Bereichsabfragen 697 für Daten-Sharding 470 Filterung 695 GROUP BY-Abfragen optimieren 685, 703 Gründe für die Benutzung 681 indexer-Programm 690 installieren 691 Leistungssteuerung mit 697 oberste Ergebnisse suchen 683 parallele Ergebnismengen mittels 686 Partitionierung 692 Phrasennähe-Ranking 693 searchd-Programm in 690 Skalierbarkeit 687 unterstützte MVAs 694 verteilte Daten sammeln 689, 704 Volltextsuche mit 681, 698–701 WHERE-Klausel, Effizienz verbessern 682 Spindeldrehgeschwindigkeit der Festplatte 344 SQL SECURITY DEFINER-Eigenschaft 580 SQL SECURITY INVOKER-Eigenschaft 580 SQL_BIG_RESULT-Option 211 SQL_BUFFER_RESULT-Option 211 SQL_CACHE-Option 212, 233 SQL_CALC_FOUND_ROWS-Option 208, 212
738 | Index
SQL_NO_CACHE-Option 77, 212, 233 SQL_SMALL_RESULT-Option 211 sql-bench (MySQL Benchmark Suite) 47, 58 SQL-Dumps 534 SQL-Slave-Thread 377 SQLyog-Werkzeug 637 SSH-Tunnelung 599 ssh-Werkzeug 659 SSL (Secure Sockets Layer) 597, 614 Ssl_*-Statusvariablen 614 Standardroute, keine, für Firewall 595 Starkey, Jim (Entwickler von Falcon) 25 START SLAVE-Befehl 381 START TRANSACTION-Befehl 7 statische Optimierungen für Abfragen 178 Statistics-Zustand der Abfrage 176 Status des MySQL-Servers Binärlogs 633 Ermitteln, Methoden zum 608 INFORMATION_SCHEMA-Sichten für 634 InnoDB-Status 616–630 adaptiver Hash-Index 627 aktuelle Wartezustände 617 Deadlocks 621–623 Eingabepuffer 627 Event-Zähler 617 Fremdschlüsselfehler 619–621 Helper-Threads 626 Leistungszähler 626 Mutexe 631 Pufferpool 628 Transaktionen 623–626 Transaktions-Logs 628 Zeilenoperationen 629 Replikation 633 Statusvariablen 326–330, 609 Abfrage-Cache 612 Abfrageplankosten 615 Befehlszähler 611 Binär-Logging 611 Dateideskriptoren 612 Handler-Operationen 612 InnoDB 615 INSERT DELAYED-Abfragen 615 MyISAM-Schlüsselpuffer 612 NDB Cluster-Konfiguration 615 Plugin-spezifische 615 SELECT-Abfragen 613 Sortierung 614
SSL 614 Tabellen-Locking 614 temporären Dateien und Tabellen 612 Threads 610 Verbindungen 610 verteilte (XA) Transaktionen 616 Systemvariablen für 608 Verbindungen, Liste der 631 Status von Betriebssystem, überwachen 366–372 Stoppwörter 263, 271 Storage Area Network (SAN) 354 storage_engine-Variable 609 Storage-Engine-API 2 Storage-Engines 2, 11, 31 erzeugen 513 für eine Tabelle ermitteln 16 Konsistenz von Backups mit 528–530 Liste von, einschließlich Funktionen 31 mischen in Transaktionen 12 Umwandeln von Tabellen zwischen 33 von Drittherstellern 26 wählen für eine Anwendung 27–31 Wartezustände in 712–715 (siehe auch spezielle Storage-Engines) strace-Werkzeug 82, 84 STRAIGHT_JOIN-Option 211 String-Locks 707 Strings für Bezeichnerspalten 101 Datentypen für 90–96 Stunnel-Werkzeug 599 Suche, Volltext (siehe Volltextsuche) Summary-Tabellen 152–156 Super Smack 48 SUPER-Berechtigung für Operations- und Überwachungs-Accounts 578 und read_only-Option 385 für Trigger 580 wann gewähren? 586 Surrogatschlüssel 125 Swapping 364, 372 sync_binlog-Variable 319, 357, 384 synchrone Replikation für Hochverfügbarkeit 492 Synchronisation von Daten (siehe Replikation) sysbench-Werkzeug 47, 50–55 Systemadministrator-Account 576 Systemsicherheit 592
Systemvariablen beeinflussen Abfrageoptimierer 213 präsentieren 608
T Tabellen benutzte Storage-Engine ermitteln 16 Beschädigung 146 Dateiname 15 Informationen anzeigen 16 Prüfsummen 655 synchronisieren 655 Tabellen-Cache 290, 301 Tabellendefinitions-Cache (siehe Data-Dictionary) Tabellen-Locks 6, 160, 707–710 Concurrency-Ebene der 15 als potenzielle Engstelle 160 Statusvariablen für 614 Tabellenstatistiken 183 Tabellenumwandlungen, zwischen StorageEngines 33 table_cache_size-Variable 290 table_cache-Variable 289 table_definition_cache-Variable 302 Table_locks_immediate-Statusvariable 614 Table_locks_waited-Statusvariable 329, 614 table_open_cache-Variable 302 tables_priv-Tabelle 573 Tablespace, InnoDB 21, 314–317 Tagged-Cache 511 tar-Befehl 658 T-Baum-Indizes 104 Tc_log_*-Statusvariablen 616 tcpdump-Werkzeug 82 TCP-Wrapper 599 temporäre Dateien, Statusvariablen für 612 temporäre Tabellen Berechtigungen 584 Leistung verbessern 324 Statusvariablen 612 verglichen mit Memory-Tabellen 22 vermeiden 94 TEMPTABLE-Algorithmus für Sichten 251 TEXT-Typen 93, 323–326 thread_cache_size-Variable 290, 301 Thread-Bibliotheken 363 Thread-Cache 290, 301
Index | 739
Thread-fähige Diskussionsforen, geeignete Storage-Engines 30 Threads (siehe Verbindungen to MySQL) Threads_connected-Statusvariable 301, 609–610 Threads_created-Statusvariable 301, 330, 611 threads-Benchmark, sysbench-Werkzeug 54 Time to Live (TTL), Cache-Kontrollstrategie 509 TIMESTAMP-Typ 97 hohe Auflösung 98 verglichen mit DATETIME-Typ 88 TINYBLOB-Typ 93 TINYINT-Typ 88 TINYTEXT-Typ 93 TPC-C-Test 38, 55 TRANSACTIONS-Abschnitt, SHOW INNODB STATUS-Ausgabe 623–626 Transaktionen 6–13 Abfrage-Cache beeinflusst durch 222, 231 ACID-Test für 7 AUTOCOMMIT-Modus für 11 DDL-Befehle bestätigen automatisch in 12 Deadlocks von 10, 621–623, 652 Funktionen gelistet nach Storage-Engine 31 Isolationsebenen für 8, 12, 15, 21 Mischen der Storage-Engines in 12 Status von 623–626 Überwachung 645 Wahl der Storage-Engine basierend auf 27 Transaktionen pro Zeiteinheit (Durchsatz) 38 Transaktions-Logs 11, 307–308, 628 Trigger 238–240 Berechtigungen benutzt mit 580 (siehe auch gespeicherter Code) TRIGGER-Berechtigung 581 TTL (Time to Live), Cache-Kontrollstrategie 509 Tunnelung 596, 599 Two-Pass-Sortieralgorithmus 190
U Überprüfung, Backups zur 520 Übertragungsgeschwindigkeit der Festplatte 343 Überwachungs-Accounts 578 Überwachungswerkzeuge 638–649 interaktive Werkzeuge 645–649 noninteraktive Werkzeuge 639–644 (siehe auch Status des MySQL-Servers) UDFs (siehe benutzerdefinierte Funktionen)
740 | Index
UFS2-Dateisystem 363 UFS-Dateisystem 363 Uhrzeit-Datentypen (siehe Datums- und UhrzeitDatentypen) UNION-Klausel 197, 209, 274 UNLOCK TABLES-Befehl 13, 708 unsichtbare Berechtigungen 588–591 UNSIGNED-Attribut 88 Unterabfragen korrelierte, Optimierung 193–197 Optimierungen 181, 205 UPDATE-Befehl EXPLAIN-Befehl mit 663 mit SELECT, nicht unterstützt 202 update-fähige Sichten 252 Upgrades Änderungen an Optimierer in 218 testen mit Replikation 375 Uptime-Statusvariable 616 USAGE-Berechtigung 588 USE INDEX-Option 212 user-Tabelle 573 UUID-Werte einfügen 125–127 generieren 434 speichern 102
V VARCHAR-Typ 90–93, 96 Variablen, benutzerdefinierte 213–218 Variablen, Server (siehe Servervariablen) Variablen, Status (siehe Statusvariablen) Variablen, System (siehe Systemvariablen) variables-Befehl, mysqladmin 608 veraltete Berechtigungen 591 Verbindungen 3 auf Localhost beschränkte 594 Authentifzierung 3, 570 erforderlicher Speicher 294 Fehler 583 Fehlerbehebung 83–85, 583 Liste der 631 Localhost, Unix-Socket benutzt für 583 nach dem Entfernen aller Berechtigungen 587 Statusvariablen 610 verbindungsweise Einstellungen anpassen 330 Verschlüsselung 596–599
Verbindungs-Pooling, verglichen mit persistenten Verbindungen 501 verborgene Berechtigungen 588–591 Verbreitung von Gleichheit 182 Verfügbarkeit, hohe 445–446, 487–497 Architekturen mit gemeinsam genutztem Speicher 490 Failover und Failback für 493–497 IP-Adressen verschieben 494 IP-Übernahme für 494 Master, Wechsel der Rollen des 494 Middleman-Lösungen für 496 MySQL Master-Master Replication ManagerWerkzeug für 495 Planen für 488 Redundanz hinzufügen 489 Replikation für 375 replizierte-Festplatten-Architekturen für 490 Slave befördern 494 synchrone Replikation für 492 Verschlüsselung Anwendungsebene 603–605 der Dateisysteme 602 von Daten 601–605 Hashing von Passwörtern 578, 601 in MySQL 605 von Verbindungen 596–599 versionsbasierte Aufteilung 479 verteilte Speicher-Caches 508 verteilte (XA) Transaktionen 283, 616 Verteilung der Daten, Replikation für 375 verzögerte Replikation, für Wiederherstellung 553 verzögertes Schlüsselschreiben, MyISAM 19 Views siehe Sichten Virtual Private Network (VPN) 597 vmstat-Werkzeug 365, 367 Volltextsuche 263–267 Änderungen in Version 5.1 268 Beschränkungen der 268–270 Indizes für 114 natürlichsprachig 264–266 Parser-Plugins für 513 Sammlung für 263 mit Sphinx 681, 698–701 Verfeinerung und Optimierung 270–272 Volltextsuchen Boolesche 266 Replikations-Slaves benutzt für 405
vorbereitete Anweisungen 243 Einschränkungen von 247 optimieren 244 SQL-Schnittstelle für 245 VPN (Virtual Private Network) 597
W Wackamole 477 »warme« Backups 516 Wartezustände Serverebene 706–712 in Storage-Engine 712–715 Wartezustände, Betriebssystem 617 Wartezustände, Lock (siehe Lock-Wartezustände) Webserver-Logs für Profiling 82 Webserverprobleme 502–505 Websiteressourcen Absturztestskript 353 ab-Werkzeug 46 benutzerdefinierte Funktionen, Beispiele 249 Bibliothek gespeicherter Routinen 234 Cacti-Werkzeug 358, 644 Cricket 644 Database Test Suite 47 Dormando’s Proxy for MySQL 653 DRBD 490 ESI (Edge Side Includes) 503 Funktionen für memcached 248 Groundwork Open Source 642 HackMySQL-Werkzeuge 650 Hibernate Shards 470 High Availability Linux-Projekt 493 HiveDB 470 http_load-Werkzeug 46 Hyperic HQ 641 InnoDB Recovery Toolkit 557 innotop 645, 649 JMeter-Werkzeug 46 LVS (Linux Virtual Server) 477 Maatkit-Werkzeuge 651, 655 MONyog-Werkzeug 643 MRTG (Multi Router Traffic Grapher) 358, 643 mtop 645 Munin 643 mylvmbackup-Werkzeug 563 MySQL Benchmark Suite (sql-bench) 48
Index | 741
MySQL Forge-Community-Site 635, 655 MySQL Master-Master Replication ManagerWerkzeug 495 MySQL Monitoring and Advisory Service 643 MySQL Proxy 652 MySQL Statement Log Analyzer (mysqlsla) 75 MySQL Visual Tools 637 mysql_slow_log_filter-Werkzeug 75 mysql_slow_log_parser-Werkzeug 75 MySQL-Dokumentation 610 MySQL-Entwickler 513 mysqlpdump-Werkzeug 537 mysqlslap-Werkzeug 47 mysqlsniffer-Werkzeug 82 mytop 645 Nagios 640 NDB-API 514 NDB-Modul für Apache 514 OpenNMS 642 OProfile-Werkzeug 85 Paket-Sniffer 82 Patch für langsame Abfragezeiten 71 Patch zum Entfernen ausführlicher DatensatzDumps 714 phpMyAdmin 638 Planet MySQL-Blog-Aggregator 655 Putty-Werkzeug 599 R1Soft 565 RRDTool 643 Smokeping-Werkzeug 358 SNAP Innovation GmbH 26 Solid Information Technology 25 Sphinx 677, 705 SQLyog 638 SSH-Tunnel, verbinden mit MySQL, Anleitung 599 Super Smack 48 sysbench-Werkzeug 47 tcpdump-Werkzeug 82 TPC-C-Test 38 Wackamole 477 Zabbix 642 Zenoss 641 ZRM (Zmanda Recovery Manager) 563–565 wenigste Verbindungen, Lastausgleichsalgorithmus 484
742 | Index
WHERE-Klausel Sphinx verbessert Effizienz von 682 Verbreitung von 182 Widenius, Michael (Entwickler der MariaEngine) 26 Wiederherstellung 516–518, 546–557 Beschränken des MySQL-Zugriffs während 546 Geschwindigkeit von 558 mit InnoDB 554–557 logische Backups reparieren 548–551 Log-Server für 553 nach Katastrophe 520 punktgenaue Wiederherstellung 551 rohe Dateien reparieren 547 verzögerte Replikation für 553 Wiederherstellung nach Absturz, Storage-Engine wählen 28 Wiederverwendung von Code 234 Windows-Betriebssystem 360 WITH ROLLUP-Klausel, Optimierungen mittels 207 with-libwrap-Variable 600 Write-Ahead-Logging 339
X XA-Transaktionen (siehe verteilte (XA) Transaktionen) XFS-Dateisystem 361, 363
Y yaSSL-Bibliothek 597
Z Zabbix-Werkzeug 642 Zählertabellen 155 Zeichensätze 255–258 Client/Server-Kommunikation 256 Escape-Folgen, Behandlung von 258 festlegen in Anweisungen 258, 260 Indexbeschränkungen beeinflusst durch 261 unterstützte, ermitteln 259 Vergleich von Werten zwischen 257 vorgegebene 256 wählen 259, 262
Wirkungen auf Abfragen 260–262 Zeichenlänge beeinflusst durch 261 zeilenbasierte Replikation 373, 387 Zeilenfragmentierung 148 Zeilenoperationen, Status von 629 zeitbasierte Datenpartitionierung 473 Zenoss-Werkzeug 641 Zertifikate, SSL (siehe SSL)
ZFS-Dateisystem 361, 363 Zmanda Recovery Manager (ZRM) 563–565 ZRM (Zmanda Recovery Manager) 563–565 zufällig, Lastausgleichsalgorithmus 484 zufällige Ein/Ausgabe 337 Zugriffssteuerng 571 Zugriffszeit der Festplatte 343–344
Index | 743
Über die Autoren Baron Schwartz ist ein Software-Entwickler aus Charlottesville, Virginia, der online mit dem Namen »Xaprb« (das ist sein Vorname, getippt in QWERTZ auf einer Dvorak-Tastatur) unterwegs ist. Wenn er nicht gerade ein lustiges Programmierproblem löst, entspannt sich Baron mit seiner Frau Lynn und seinem Hund Carbon. Sein Blog zu Fragen der Software-Entwicklung ist unter http://www.xaprb.com/blog zu finden. Peter Zaitsev, früherer Manager der High Performances Group bei MySQL AB, betreibt jetzt die Site mysqlperformanceblog.com. Seine Spezialität ist es, Administratoren dabei zu helfen, Probleme mit Websites zu lösen, die mit Millionen von Besuchern pro Tag zurechtkommen und Terabytes an Daten auf Hunderten von Servern jonglieren müssen. Er ist daran gewöhnt, sowohl Hardware als auch Software zu ändern, um Lösungen zu finden. Peter spricht außerdem häufig auf Konferenzen. Vadim Tkachenko ist Miteigentümer von Percona Inc., einem Unternehmen, das sich auf die Beratung im MySQL-Performance-Bereich spezialisiert hat. Er war Performance-Ingenieur bei MySQL AB. Als Experte in der Multithread-Programmierung und Synchronisation bestanden seine hauptsächlichen Aufgaben in BenchmarkTests, Profiling und dem Finden von Flaschenhälsen. Er hat darüber hinaus an einer Reihe von Funktionen für die Leistungsüberwachung und -verbesserung sowie zur Skalierung von MySQL auf mehreren CPUs mitgearbeitet. Jeremy D. Zawodny und seine beiden Katzen sind Ende 1999 aus dem Nordwesten von Ohio in das Silicon Valley umgezogen, um für Yahoo! zu arbeiten – gerade noch rechtzeitig, um das Platzen der Dotcom-Blase aus nächster Nähe mitzuerleben. Er verbrachte 8 1/2 Jahre bei Yahoo!, wo er dabei half, MySQL und andere Open SourceTechniken auf lustige, interessante Weise und in oft sehr großem Maßstab in Betrieb zu nehmen. In jüngster Zeit hat er seine Liebe zum Fliegen wiederentdeckt und Anfang 2003 seinen privaten Segelflugschein gemacht sowie 2005 eine Berufspilotenlizenz erworben. Seither verbringt er viel zuviel Zeit damit, mit dem Segelflugzeug über Hollister, Kalifornien, und dem Bereich am Lake Tahoe herumzufliegen. Er fliegt außerdem gelegentlich kleine, einmotorige Flugzeuge, da er Mitbesitzer einer Citabria 7KCAB und einer Cessna 182 ist. Gelegentliche Beratertätigkeiten helfen ihm dabei, für sein Fliegerhobby aufzukommen. Jeremy lebt mit seiner wunderbaren Frau und ihren vier Katzen in der San Francisco Bay Area in Kalifornien. Sein Blog ist unter jeremy.zawodny.com/blog zu finden. Arjen Lentz wurde in Amsterdam geboren, lebt aber seit der Jahrtausendwende in Queensland, Australien, wo er sein Leben zur Zeit mit seiner wunderbaren Tochter Phoebe und dem schwarzen Kater Figaro teilt. Arjen, ursprünglich ein C-Programmierer, war Angestellter Nummer 25 bei MySQL AB (2001–2007). Nach einer
kurzen Pause im Jahr 2007 gründete Arjen Open Query (http://openquery.com.au), ein Unternehmen, das eigene Datenmanagement-Trainings- und Beratungsdienstleistungen im asiatisch-pazifischen Raum und anderswo anbietet. Arjen spricht außerdem häufig auf Konferenzen und bei User Groups. In seiner überreichlichen Freizeit frönt er dem Kochen, Gärtnern, Lesen, Camping und Erkunden von RepRap. Besuchen Sie sein Weblog unter http://arjen-lentz.livejournal.com. Derek J. Balling ist seit 1996 Linux-Systemadministrator. Er hat daran mitgearbeitet, die Server-Infrastrukturen für Unternehmen wie Yahoo! und Einrichtungen wie das Vassar College aufzubauen und zu warten. Außerdem hat er Artikel für The Perl Journal und eine Reihe von Online-Magazinen geschrieben und war im Programmkomitee für die LISA-Konferenz (Large Installation System Administration). Er ist als Rechenzentrumsmanager bei Answers.com angestellt. Seine Freizeit verbringt Derek gern abseits der Computer mit seiner Frau Debbie und ihren Tieren (vier Katzen und ein Hund). Seine Ansichten über aktuelle Ereignisse und alles, was ihn so aufregt, tut er in seinem Blog unter http://blog.megacity.org kund.
Über die Übersetzerin Kathrin Lichtenberg studierte Informatik an der Technischen Universität Ilmenau und ist seit mehr als 10 Jahren als freie Übersetzerin tätig. Für O’Reilly hat sie bereits eine ganze Reihe von Büchern übersetzt. Auch ihre Freizeit würde sie am liebsten lesend verbringen, und wie ihre Töchter zeigen, hat sich diese Leidenschaft offensichtlich weitervererbt.
Kolophon Das auf dem Cover von High Performance MySQL dargestellte Tier ist ein Sperber (Accipiter nisus). Dieser in Wäldern lebende kleine Greifvogel der Familie der Habichte ist in Eurasien und Nordafrika beheimatet. Sperber haben einen langen Schwanz und kurze, gerundete Flügel. Das Männchen ist oberseits bläulich-grau gefärbt mit rostfarbener Unterseite, während das Weibchen eine mehr bräunlichgraue Färbung und eine fast weiße Unterseite aufweist. Die Männchen sind mit ca. 28 cm normalerweise kleiner als die ca. 38 cm großen Weibchen. Sperber leben überwiegend in Nadelwäldern und ernähren sich von kleinen Säugetieren, Insekten und Vögeln. Sie brüten in den Wipfeln hoher Nadelbäume, vereinzelt aber auch auf Felssimsen. Im Frühsommer legt das Weibchen 4 bis 6 weiße, rot-braun gefleckte Eier. Während der Brutzeit versorgt das Männchen das Weibchen und die Jungen. Wie alle Habichte schlägt der Sperber seine Beute im Sturzflug. Beim Jagen zeigt er seinen charakteristischen Gleitflug, der von flatternden Flügelschlägen unterbrochen wird. Der lange Schwanz ermöglicht es ihm dabei, sich mühelos zu drehen und blitzschnell die Richtung zu wechseln und so die Beute im Überraschungsangriff zu schlagen. Der seit einiger Zeit wieder häufiger zu beobachtende Sperber kommt zur Nahrungssuche gelegentlich in Gärten und Ortschaften, wo das aufgeregte Gezeter der Singvögel seine Anwesenheit verrät. Dem Cover liegt ein Stich des Dover Pictorial Archive aus dem 19. Jahrhundert zugrunde, die dort verwendete Schrift ist die Adobe ITC Garamond. Als Textschrift verwenden wir die Linotype Birka, die Überschriftenschrift ist die Adobe Myriad Condensed und die Nichtproportionalschrift für Codes ist LucasFont’s TheSansMonoCondensed.