Percona-Toolkit… das ist doch nur für Admins? Mag man denken, aber es gibt in dieser großen Sammlung von Werkzeugen (der Nachfolger von Maatkit) auch Dinge, die ein reiner Developer nutzen kann und sollte. Und ich meine da nicht nur DevOps. Im Artikel wird genau erklärt, welche Tools das sind und wie ihr sie insbesondere als reiner Developer sofort nutzen könnt.
Vielleicht als erstes auch den Tipp, sich die aktuelle Version der Percona-Tools zu installieren. Das wäre aktuell die 2.1.7 vom 19. November. Für die unter uns, die sich jetzt sagen “ochnööö, schon wieder was installieren”: Ihr könnt die Tools auch einzeln (mit wget) herunterladen und ausführen – vorausgesetzt Perl ist installiert:
|
1 2 |
# wget percona.com/get/TOOL # perl TOOL |
wobei ihr natürlich TOOL durch den Namen des jeweiligen Tools ersetzt.
So, und bevor ich anfange den knapp bemessenen Platz zu verschwenden (man sagte mir maximal zwei Bildschirmseiten) fange ich einfach mal an.
Indexe aufräumen mit pt-duplicate-key-checker
Der Key-checker ist das Gegenstück zu MySQLs Procedure Analyse(), aber halt für Indexe. Das Programm prüft Notwendigkeit und Redundanz von Indexen und Foreign-Keys und macht Vorschläge zur “Verbesserung”. Sieht so aus:
|
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 |
[TESTVM]# pt-duplicate-key-checker D=TESTDB,u=test,p=supergeheim # ######################################################################## # TESTDB.Boxes # ######################################################################## # BoxID is a duplicate of PRIMARY # Key definitions: # UNIQUE KEY `BoxID` (`BoxID`), # PRIMARY KEY (`BoxID`), # Column types: # `boxid` bigint(20) not null default '0' # To remove this duplicate index, execute: ALTER TABLE `TESTDB`.`Boxes` DROP INDEX `BoxID`; # ######################################################################## # TESTDB.groups # ######################################################################## # Key group_name ends with a prefix of the clustered index # Key definitions: # UNIQUE KEY `group_name` (`group_name`,`group_id`) # PRIMARY KEY (`group_id`), # Column types: # `group_name` varchar(255) not null default '' # `group_id` int(10) unsigned not null default '0' # To shorten this duplicate clustered index, execute: ALTER TABLE `TESTDB`.`groups` DROP INDEX `group_name`, ADD INDEX `group_name` (`group_name`); ... |
Und so weiter. Ihr könnt die Ausgabe direkt als SQL-Script ausführen, aber erst nachdem ihr alles angeschaut habt. Denn zum Beispiel macht der Index `group_name´ oben unter Umständen viel Sinn! Das ist aber ein ganz anderes Thema.
Etwas grundsätzliches
Die meisten (!) Percona-Tools verlangen als Verbindungsparameter zur Datenbank ein “DSN”. Wer jetzt an die DSN bei PDO denkt, liegt falsch. Percona benutzt hier ein Format, welches mich stark an LDAP erinnert. Das mag verwirren – wenn ihr euch aber näher damit beschäftigt, merkt ihr, dass es durchaus durchdacht ist. “D=TESTDB” bedeutet nichts anderes als verbinde zu Datenbank “TESTDB”, “u=test” ist der Username und “p=supergeheim” das Passwort. “h” wäre – wer hätte es geahnt? – der Host. Und so weiter, alle Parameter sind in jedem Befehl in der Doku erklärt und sie werden jeweils auch erweitert, also beachten. Die Parameter könnt ihr in beliebiger Reihenfolge mit Komma getrennt aneinander hängen.
Sich einen Überblick verschaffen mit pt-mysql-summary und pt-summary
Es kommt vor, dass ihr in einer völlig fremden Umgebung arbeiten müsst. Eine neue VM, ein anderes Betriebssystem, ein neues Projekt… Es ist dann eine gute Idee, sich erst mal einen Überblick zu verschaffen. Ausdrücklich erwähne ich hier auch noch mysqltuner.pl, was für diesen Zweck ebenfalls gut zu gebrauchen ist (und dazu noch Tipps gibt; zu Tipps von Tools befolgen aber später noch mehr).
Es kommt ja auch mal vor, dass ihr vor lauter Kopf die Bretter nicht mehr seht – auch dann ist es manchmal eine gute Idee, diese Tools einfach mal laufen zu lassen, denn mit ihnen könnt ihr systematische Fehler gut finden.
Ausdrücklich sei hiermit davor gewarnt diese Tools “einfach mal so” auf einem Produktiven System laufen zu lassen! Dazu bitte vorher mal die Dokumentation genau durchlesen.
pt-mysql-summary
Dieses Tool benutzt für die Datenbankverbindung leider wieder die MySQL-Syntax, also kein DSN. Zwei Bindestriche tippen und alles danach kennt ihr vom MySQL-Kommandozeilen-Tool.
|
1 |
# pt-mysql-summary -- -u root -psupergeheim |
Erzeugt viel Ausgabe…
|
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 |
# Percona Toolkit MySQL Summary Report ####################### System time | 2012-12-07 21:00:15 UTC (local TZ: CET +0100) # Instances ################################################## Port Data Directory Nice OOM Socket ===== ========================== ==== === ====== 3306 /var/lib/mysql 0 0 /var/run/mysqld/mysqld.sock # MySQL Executable ########################################### Path to executable | /usr/sbin/mysqld Has symbols | No # Report On Port 3306 ######################################## User | root@localhost Time | 2012-12-07 22:00:15 (CET) Hostname | vaillant-debian Version | 5.5.20-1~dotdeb.0 (Debian) Built On | debian-linux-gnu x86_64 Started | 2012-12-07 11:35 (up 0+10:24:32) Databases | 6 Datadir | /var/lib/mysql/ Processes | 1 connected, 1 running Replication | Is not a slave, has 0 slaves connected Pidfile | /var/run/mysqld/mysqld.pid (exists) # Processlist ################################################ Command COUNT(*) Working SUM(Time) MAX(Time) ------------------------------ -------- ------- --------- --------- Query 1 1 0 0 ... |
Das schaut ihr – mit der geöffneten Dokumentation daneben – durch und versucht zu verstehen, was da angezeigt wird. Dann speichert es einfach ab, mit Datum und Uhrzeit. Wenn dann mal irgendwann irgendwas nicht mehr läuft, vergleicht ihr diese Datei mit der aktuellen Ausgabe. Hilft in 50 Prozent aller Fälle weiter! Ganz ehrlich: Ich überspringe das durchlesen meist, außer wenn ich weiß, was ich suche. Aber das zu speichern/archivieren solltet ihr euch angewöhnen.
Beispiel für pt-summary
Funktioniert im Prinzip genauso wie pt-mysql-summary, nur liefert es Daten über die gesamte Maschine. Es ist also stark von Vorteil, wenn ihr dafür root-Rechte habt. Der Aufruf braucht prinzipiell keine Parameter.
|
1 |
# pt-summary |
Ich zeige keine Ausgabe, die Dokumentation dazu ist viel ausführlicher, als ich das hier darstellen könnte. Auch hier gilt: Ausgabe abspeichern mit Datum im Dateinamen und diffen, wenn etwas nicht mehr funktioniert.
Besser als nix: pt-query-advisor und pt-variable-advisor
Bei diesen beiden Tools bin ich zwiegespalten. Immer wenn eine Maschine einem Menschen Vorschläge macht, dies oder jenes zu “verbessern”, solltet ihr sicher sein, daß die vorgeschlagenen Änderungen wirklich etwas verbessern, ansonsten bleiben lassen. Ihr könnt diese Werkzeuge also entweder als DAU oder als schlauer Developer nutzen, daher sind sie in meiner Hitliste dabei.
pt-query-advisor
Ihr wollt sehen, welche Queries von einem Request ausgelöst werden. Die Daten zu bekommen ist seit MySQL 5.1.21 recht einfach: Slow-Query-Log anschalten, Wartezeit auf 0 Sekunden setzen.
|
1 2 |
mysql> SET GLOBAL slow_query_log ='ON'; mysql> SET GLOBAL long_query_time=0.0; # auf millisekunden genau |
Anfänger, Vorsicht: Damit werden alle Queries in die Datei geschrieben (welche Datei seht ihr mit “SHOW VARIABLES LIKE ‘%slow%’”), wenn ihr also öfter mal ein Backup einspielt (was auf einem Entwicklungssystem ja Sinn machen kann), dann ist da schnell die Platte voll. Also bevor ihr euch wundert, warum plötzlich gar nix mehr funktioniert, hoffe ich, ihr erinnert euch an pt-summary?
Den Dump in den pt-query-advisor pumpen:
|
1 |
# tail -f /var/lib/mysql/slow-query.log | pt-query-advisor --group-by none --report-format full -v |
Und schon erzeugt das für alle Queries eine Analyse und gibt aus, was ihm nicht passt.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Query ID 0x4B1E7805DC277905 at byte 1401856 # NOTE ALI.001 Aliasing without the AS keyword. Explicitly using the AS keyword in column or table aliases, such as "tbl AS alias," is more readable than implicit aliases such as "tbl alias". # NOTE CLA.003 LIMIT with OFFSET. Paginating a result set with LIMIT and OFFSET is O(n^2) complexity, and will cause performance problems as the data grows larger. # NOTE COL.001 SELECT *. Selecting all columns with the * wildcard will cause the query's meaning and behavior to change if the table's schema changes, and might cause the query to retrieve too much data. # NOTE KWR.001 SQL_CALC_FOUND_ROWS is inefficient. SQL_CALC_FOUND_ROWS can cause performance problems because it does not scale well; use alternative strategies to build functionality such as paginated result screens. # CRIT JOI.001 Mixing comma and ANSI joins. Mixing comma joins and ANSI joins is confusing to humans, and the behavior and precedence differs between some MySQL versions, which can introduce bugs. # WARN RES.001 Non-deterministic GROUP BY. The SQL retrieves columns that are neither in an aggregate function nor the GROUP BY expression, so these values will be non-deterministic in the result. # NOTE STA.001 The != operator is non-standard. Use the <> operator to test for inequality instead. SELECT SQL_CALC_FOUND_ROWS err.*, kb.*, ... |
Wenn bestimmte Regeln nicht gefallen, könnt ihr diese excluden: “–ignore-rules ALI.001″ und schon meckert es nicht mehr das fehlende “AS” an, welches für dieses Projekt in fast jeder Query benutzt wurde und nachträglich kaum änderbar ist.
Fazit: Super bei einem Code-Review. Einfach den Analyzer mal draufschalten und ich garantiere interessante Diskussionen! :) Ihr könnt es vielleicht auch dazu verwenden, um in einem Projekt bestimmte Antipatterns zu verbieten. Keine Empfehlung dies zu automatisieren, denn mit manchen Queries kommt es nicht zurecht, beispielsweise moserte es mir bei einem verschachteltem JOIN mit Subselects an, siehe Zeile 6. Die Query ist aber diesbezüglich einwandfrei. Da scheinen also noch Bugs drin zu sein.
pt-variable-advisor
Oder auch das mysqltuner.pl (siehe oben) für Arme. Naja, ich übertreibe :) . pt-variable-advisor macht sehr zuverlässig genau was es soll. Aber macht es Sinn? Nun, ich nehme es deswegen hier mit rein, so manchen dummen Fehler findet ihr damit tatsächlich.
Hier die Ausgabe von meinem Testsystem:
|
1 2 3 4 5 |
[TESTVM]# pt-variable-advisor u=root,p=supergeheim | grep -v 'NOTE\|^$' # WARN delay_key_write: MyISAM index blocks are never flushed until necessary. # WARN innodb_log_file_size: The InnoDB log file size is set to its default value, which is not usable on production systems. # WARN slave_net_timeout: This variable is set too high. # WARN log_bin: Binary logging is disabled, so point-in-time recovery and replication are not possible. |
Ich hab mir hier die NOTE’s und leere Zeilen schon rausgefiltert. Sicher: alles richtig, aber ist es auf einem Entwicklungssystem auch wichtig? Kann ich so nicht beantworten.
Zum Schluss pt-visual-explain
Du verstehst deine eigene Query nicht mehr? Deine Query versteht dich nicht? Dann ist pt-visual-explain vielleicht das richtige für Dich!
Über dieses Tool wurde schon in einigen anderen Blog-Artikeln gepostet und ich mache das jetzt auch. Denn es stellt den zeilenorientieren EXPLAIN einer Query als Baumstruktur dar, was allein schon revolutionär ist.
Meiner Meinung nach hilft es, das zeilenorientierte Explain besser zu verstehen. Wer aber erwartet, dass dadurch alles einfacher wird, den muss ich enttäuschen; es sieht zwar so aus, als wäre es viel logischer, aber eigentlich wird die Information nur anders verteilt. Das seht ihr gut bei richtig großen Queries:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# mysql -uroot -psupergeheim TESTDB <moerderqueryexplain.sql id select_type table type possible_keys key key_len ref rows Extra 1 PRIMARY err range msg_status,errorcode_deviceid_idx,sernum sernum 66 NULL 4 Using where; Using temporary; Using filesort 1 PRIMARY ger eq_ref PRIMARY PRIMARY 18 TESTDB.err.device_id 1 1 PRIMARY err_ref ref PRIMARY PRIMARY 257 TESTDB.err.errorcode 1 Using where; Using index 1 PRIMARY kb eq_ref PRIMARY PRIMARY 66 TESTDB.err.sernum 1 1 PRIMARY uma eq_ref PRIMARY PRIMARY 8 TESTDB.kb.boxowner_umaid 1 1 PRIMARY cB eq_ref PRIMARY,p_user_id PRIMARY 34 TESTDB.uma.user_id 1 Using index 5 DEPENDENT SUBQUERY mand const PRIMARY PRIMARY 2 const 1 Using index 5 DEPENDENT SUBQUERY eref eq_ref PRIMARY PRIMARY 299 func,func 1 Using where 5 DEPENDENT SUBQUERY e_mand eq_ref device_id,errorcode_deviceid_idx device_id 301 TESTDB.eref.device_id,TESTDB.eref.errorcode,const 1 5 DEPENDENT SUBQUERY kb eq_ref PRIMARY PRIMARY 66 func 1 Using where; Using index 5 DEPENDENT SUBQUERY e_obj eq_ref errorcode errorcode 365 TESTDB.e_mand.errorcode,TESTDB.e_mand.device_id,TESTDB.kb.sernum 1 Using where 4 DEPENDENT SUBQUERY mand const PRIMARY PRIMARY 2 const 1 Using index 4 DEPENDENT SUBQUERY eref eq_ref PRIMARY PRIMARY 299 func,func 1 Using where 4 DEPENDENT SUBQUERY e_mand eq_ref device_id,errorcode_deviceid_idx device_id 301 TESTDB.eref.device_id,TESTDB.eref.errorcode,const 1 4 DEPENDENT SUBQUERY kb eq_ref PRIMARY PRIMARY 66 func 1 Using where; Using index 4 DEPENDENT SUBQUERY e_obj eq_ref errorcode errorcode 365 TESTDB.e_mand.errorcode,TESTDB.e_mand.device_id,TESTDB.kb.sernum 1 Using where 3 DEPENDENT SUBQUERY eref ref PRIMARY PRIMARY 257 func 1 Using where; Using filesort 2 DEPENDENT SUBQUERY vw_error_reference ref PRIMARY PRIMARY 257 func 1 Using where; Using index; Using filesort |
Ufff! Das ist schon ziemlich viel. Durch pt-visual-explain gejagt:
|
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# mysql -uroot -psupergeheim TESTDB <moerderqueryexplain.sql | pt-visual-explain Filesort +- TEMPORARY table temporary(err,ger,err_ref,kb,uma,cB,mand,eref,e_mand,kb,e_obj,mand,eref,e_mand,kb,e_obj,eref,vw_error_reference) +- DEPENDENT SUBQUERY +- Filesort | +- Filter with WHERE | +- Index lookup | key vw_error_reference->PRIMARY | possible_keys PRIMARY | key_len 257 | ref func | rows 1 +- DEPENDENT SUBQUERY +- Filesort | +- Filter with WHERE | +- Bookmark lookup | +- Table | | table eref | | possible_keys PRIMARY | +- Index lookup | key eref->PRIMARY | possible_keys PRIMARY | key_len 257 | ref func | rows 1 +- DEPENDENT SUBQUERY +- JOIN | +- Filter with WHERE | | +- Bookmark lookup | | +- Table | | | table e_obj | | | possible_keys errorcode | | +- Unique index lookup | | key e_obj->errorcode | | possible_keys errorcode | | key_len 365 | | ref TESTDB.e_mand.errorcode,TESTDB.e_mand.device_id,TESTDB.kb.sernum | | rows 1 | +- JOIN | +- Filter with WHERE | | +- Unique index lookup | | key kb->PRIMARY | | possible_keys PRIMARY | | key_len 66 | | ref func | | rows 1 | +- JOIN | +- Bookmark lookup | | +- Table | | | table e_mand | | | possible_keys device_id,errorcode_deviceid_idx | | +- Unique index lookup | | key e_mand->device_id | | possible_keys device_id,errorcode_deviceid_idx | | key_len 301 | | ref TESTDB.eref.device_id,TESTDB.eref.errorcode,const | | rows 1 | +- JOIN | +- Filter with WHERE | | +- Bookmark lookup | | +- Table | | | table eref | | | possible_keys PRIMARY | | +- Unique index lookup | | key eref->PRIMARY | | possible_keys PRIMARY | | key_len 299 | | ref func,func | | rows 1 | +- Constant index lookup | key mand->PRIMARY | possible_keys PRIMARY | key_len 2 | ref const | rows 1 +- DEPENDENT SUBQUERY +- JOIN | +- Filter with WHERE | | +- Bookmark lookup | | +- Table | | | table e_obj | | | possible_keys errorcode | | +- Unique index lookup | | key e_obj->errorcode | | possible_keys errorcode | | key_len 365 | | ref TESTDB.e_mand.errorcode,TESTDB.e_mand.device_id,TESTDB.kb.sernum | | rows 1 | +- JOIN | +- Filter with WHERE | | +- Unique index lookup | | key kb->PRIMARY | | possible_keys PRIMARY | | key_len 66 | | ref func | | rows 1 | +- JOIN | +- Bookmark lookup | | +- Table | | | table e_mand | | | possible_keys device_id,errorcode_deviceid_idx | | +- Unique index lookup | | key e_mand->device_id | | possible_keys device_id,errorcode_deviceid_idx | | key_len 301 | | ref TESTDB.eref.device_id,TESTDB.eref.errorcode,const | | rows 1 | +- JOIN | +- Filter with WHERE | | +- Bookmark lookup | | +- Table | | | table eref | | | possible_keys PRIMARY | | +- Unique index lookup | | key eref->PRIMARY | | possible_keys PRIMARY | | key_len 299 | | ref func,func | | rows 1 | +- Constant index lookup | key mand->PRIMARY | possible_keys PRIMARY | key_len 2 | ref const | rows 1 +- JOIN +- Unique index lookup | key cB->PRIMARY | possible_keys PRIMARY,p_user_id | key_len 34 | ref TESTDB.uma.user_id | rows 1 +- JOIN +- Bookmark lookup | +- Table | | table uma | | possible_keys PRIMARY | +- Unique index lookup | key uma->PRIMARY | possible_keys PRIMARY | key_len 8 | ref TESTDB.kb.boxowner_umaid | rows 1 +- JOIN +- Bookmark lookup | +- Table | | table kb | | possible_keys PRIMARY | +- Unique index lookup | key kb->PRIMARY | possible_keys PRIMARY | key_len 66 | ref TESTDB.err.sernum | rows 1 +- JOIN +- Filter with WHERE | +- Index lookup | key err_ref->PRIMARY | possible_keys PRIMARY | key_len 257 | ref TESTDB.err.errorcode | rows 1 +- JOIN +- Bookmark lookup | +- Table | | table ger | | possible_keys PRIMARY | +- Unique index lookup | key ger->PRIMARY | possible_keys PRIMARY | key_len 18 | ref TESTDB.err.device_id | rows 1 +- Filter with WHERE +- Bookmark lookup +- Table | table err | possible_keys msg_status,errorcode_deviceid_idx,sernum +- Index range scan key err->sernum possible_keys msg_status,errorcode_deviceid_idx,sernum key_len 66 rows 4 |
Soooo… jetzt die Codeansicht oben behutsam zuklappen! :) Die Männer mit der weißen Jacke kommen gleich…
… nein, so schlimm ist es nicht, ehrlich! Wie gesagt, es gibt einem eine andere Sicht und verdeutlicht, wie komplex das Thema eigentlich ist. Und – ernsthaft – das ist oft sehr wichtig, zum Beispiel, um dem Kunden zu zeigen, dass seine Ideen gut, aber anstrengend sind.
Damit wären wir schon am Ende des ersten Teils und haben – wie versprochen – knapp zwei Bildschirmseiten gefüllt (wenn ihr den Monitor hochkant stellt und die Schrift ganz klein macht). Ich verspreche, im zweiten Teil dieses Artikels geht es nochmal einen Schritt tiefer in die erstaunliche Welt dieses Toolsets (Schritt tiefer, nicht Schrift kleiner). Unter anderem wird pt-query-digest und pt-diskstats behandelt.
Das wird aber erst nach Weihnachten der Fall sein. Darum wünsche ich allen Lesern, die bis hier durchgehalten haben, ein Frohes Fest!
Lesenswert: Percona-Tools für Entwickler, Teil 1 http://t.co/zXcBemFJ