Mai 2011 Archive

Ich selbst nutze unter anderem PowerDNS als DNS-Server. Im letzten Monat betrachtete ich mir dessen MySQL-Backend, da ich über zwei Kanäle darüber informiert wurde, dass PowerDNS mit dem Backend nicht skaliert.

Die DNS-Records werden in PowerDNS in zwei Tabellen abgelegt. Eine für die Domains:

create table domains (
id INT auto_increment,
name VARCHAR(255) NOT NULL,
master VARCHAR(128) DEFAULT NULL,
last_check INT DEFAULT NULL,
type VARCHAR(6) NOT NULL,
notified_serial INT DEFAULT NULL,
account VARCHAR(40) DEFAULT NULL,
primary key (id)
) Engine=InnoDB;

Und eine weitere für die Records:

CREATE TABLE records (
id int(11) NOT NULL auto_increment,
domain_id int(11) NOT NULL,
name varchar(255) NOT NULL,
type varchar(10) NOT NULL,
content varchar(255) NOT NULL,
ttl int(11) NOT NULL,
prio int(11) default NULL,
change_date int(11) default NULL,
PRIMARY KEY (id),
KEY name_index(name),
KEY nametype_index(name,type),
KEY domainid_index(domain_id)
);

Zudem existiert noch ein FK-Constraint, aber auch der tut hier nichts zur Sache.
Die Tabelle domains wurde mit 6.000.000 Datensätzen bestückt. Die Tabelle records wurde mit 46.195.356 Datensätzen bestückt. Ich denke damit wird schon ein größerer DNS-Server simuliert :D

Zwar schien es, als sollte man sich mal Gedanken über die Normalisierung ansich machen, doch das war nicht mein Skope. Es galt mit etwas Mikrotuning schon Erfolge zu erzielen. PowerDNS pdns-3.0-rc2 und folgende MySQL-Version kam zum Einsatz:

[pdns]> SELECT VERSION();
+-------------------+
| VERSION()         |
+-------------------+
| 5.2.5-MariaDB-log |
+-------------------+
1 row in set (0.00 sec)

Die Datenbank meinte die Tabellen würden folgenden Platzverbrauch haben.

> SELECT TABLE_NAME,INDEX_LENGTH,DATA_LENGTH from information_schema.TABLES where TABLE_NAME IN('records','domains');
+------------+--------------+-------------+
| TABLE_NAME | INDEX_LENGTH | DATA_LENGTH |
+------------+--------------+-------------+
| domains    |    475004928 |   431898624 |
| records    |  11372855296 |  5813305344 |
+------------+--------------+-------------+

Bei PowerDNS wurden alle Caches abgeschaltet (es galt die Datenbank zu testen!) und 494969 disjunkte DNS-Abfragen gestellt. Diese waren in 48.9 Sekunden durchgelaufen, was ca. 10114 qps entspricht. (Für jeden Test wurde die Datenbank restartet und der zweite Lauf genommen.)

Da Abfragen gegen die Tabelle records gehen,  nur diese Tabelle "optimiert"- Einige Änderungen sind analog in der Tabelle domains möglich.

Als erste Optimierung wurde ein redundanter Index entfernt.

drop  index `rec_name_index`  on records;


Hiernach wurden 10822 qps gemessen. Dies ist wohl nicht die Welt. Beim Platzverbrauch sieht es schon besser aus:


+------------+--------------+-------------+
| TABLE_NAME | INDEX_LENGTH | DATA_LENGTH |
+------------+--------------+-------------+
| domains    |    475004928 |   431898624 |
| records    |   6116343808 |  5813305344 |
+------------+--------------+-------------+

Die Spalte type speichert die Recordtypen. Aus dem VARCHAR wird im Index ein CHAR. Da die Menge der Werte begrenzt ist bietet sich hier ein ENUM an. Welches den Vorteil hat ein INT zu sein und zum anderen sicherstellt, dass nicht andere als die definierten Werte in die Tabelle kommen.

ALTER TABLE records   MODIFY  `type` enum('A','AAAA','SOA','NS','MX','CNAME','PTR','TXT');

Zugegeben, dies ist nur ein Subset der nötigen Recordtypen. Am Ergebnis wird dies nichts ändern. Nach dieser Änderung haben wir nun 10918 qps. Angenehmer ist die weitere Reduktion der Datengröße. Diesmal auch nicht nur bei den Indexdaten.


+------------+--------------+-------------+
| TABLE_NAME | INDEX_LENGTH | DATA_LENGTH |
+------------+--------------+-------------+
| domains    |    475004928 |   431898624 |
| records    |   5816451072 |  5696913408 |
+------------+--------------+-------------+

All diese Tests liefen mit  distributor-threads=32. Das ist eine Konfigurationseinstellung (im PowerDNS) für die Anzahl der Verbindungen, die PowerDNS zur Datenbank öffnet. Der Default liegt bei 3. Mit distributor-threads=3 erreichte ich lediglich 5656 qps.
Zu guter Letzt ändern wir noch den Index nametype_index. Die wenigsten FQDN nutzen die im RFC ermöglichten 255 Zeichen aus. Sprich hier lohnt sich ein prefix-Index. (Der alte wurde gedropt)

CREATE INDEX `nametype_index` on records(name(100),type);

Nun waren wir bei 10923 qps angelangt und was sagt der Platzverbrauch?

+------------+--------------+-------------+
| TABLE_NAME | INDEX_LENGTH | DATA_LENGTH |
+------------+--------------+-------------+
| domains    |    475004928 |   431898624 |
| records    |   3547332608 |  5696913408 |
+------------+--------------+-------------+


Sweet! Halten wir fest die Index_legth ist von 11372855296 Bytes auf 3547332608 Bytes reduziert worden. Damit wurden hier etwas über 7GB gespart \o/
An diesem Punkt angelangt wurden die records noch in PBXT geändert. Hierbei wurden 12375 qps erreicht:) Wobei der Platzverbrauch immens anstieg:

| records    |   5684629504 | 12380356432 |


Später wurde ich in #powerdns darauf hingewiesen, dass das verwendete Benchmarktool (dnsperf) auch mit einer längeren Queue ausgeführt werden kann. So wurden mit ./dnsperf -d /var/tmp/pdns.list -q 2000 -s localhost schnell 22994 qps erreicht.
Das ist selbstredend nur ein Anfang. Aber zeigt es doch, dass in vielen Projekten noch Steigerungspotential steckt. Von nicht skalieren kann aber nicht gesprochen werden. :)


Viel Spaß
Erkan
Mit MariaDB 5.3 wird ein neuer ColumnType Einzug halten. Das sogenannte Dynamic Column. Die Idee ist gewisse Vorgehensweisen, welche aus den DocumentStore-Lösungen (z.B. MongoDB) bekannt sind zu erfüllen. Ob man das wirklich will, liegt nicht an mir zu entscheiden. Doch sei folgender Link dem geneigten Leser nahe gelegt.
Die Idee ist denkbar einfach: In der Tabelle wird eine BLOB-Spalte verwendet um darin die dynamischen Element zu verwalten.
Es sei erwähnt, dass zum Nachspielen  https://code.launchpad.net/~maria-captains/maria/5.3-mwl34 zu verwenden ist.

MariaDB [test]> SELECT VERSION();
+---------------------+
| VERSION()           |
+---------------------+
| 5.3.0-MariaDB-alpha |
+---------------------+
1 row in set (0,00 sec)

CREATE TABLE dyni ( id INT, sonderlocke BLOB);

Hier ist noch keine Magie. Jede StorageEngine, welche mit BLOB umgehen kann erfüllt dies.

sql/lex.h verrät uns dass folgende Funktionen existieren:
COLUMN_ADD    
COLUMN_CREATE     
COLUMN_DELETE 
COLUMN_EXISTS
COLUMN_GET    
COLUMN_LIST

COLUMN_CREATE() wird zum initialisieren des BLOB-Eintrages genutzt:

INSERT INTO dyni VALUES(1,COLUMN_CREATE(1 , "hallo", 2 , 42));

COLUMN_CREATE() erhält seine Argumente in der Form key, value , key value. Perlmonger wünschen sich hier schnell ein => herbei. key => value , key => value ;)
Hier wird auch klar, dass es sich *genaugenommen* nicht um einen neuen ColumnType handelt. Dieser ist und bleibt BLOB. Mit COLUMN_CREATE() wird der BLOB Zeile für Zeile initialisiert.
Mit COLUMN_LIST() gibt die Keys der Spalte zurück und COLUMN_GET(key) den Value des Keys:

MariaDB [test]> SELECT id, COLUMN_LIST(sonderlocke) from dyni;
+------+--------------------------+
| id   | COLUMN_LIST(sonderlocke) |
+------+--------------------------+
|    1 | 1,2                      |
+------+--------------------------+
MariaDB [test]> SELECT id,COLUMN_GET(sonderlocke,1 as char) from dyni;
+------+------------------------------------+
| id   | COLUMN_GET(sonderlocke,id as char) |
+------+------------------------------------+
|    1 | hallo                              |
+------+------------------------------------+
1 row in set (0,00 sec)

COLUMN_GET() verlangt als erstes Argument den Spaltennamen, gefolgt von dem Key. Hier muss - wie bei NoSQL wohl üblich - der Anwender angeben, von welchem typ der Inhalt ist ( .. as char). Sollen weitere Einträge gemacht werden ist COLUMN_ADD() zu verwenden. COLUMN_ADD() kann auch Keys löschen indem der Value auf NULL gesetzt wird. Wem dies zu merkwürdig ist kann auch COLUMN_DELETE() verwenden.

MariaDB [test]> UPDATE dyni SET sonderlocke=COLUMN_ADD(sonderlocke,2,NULL,3,"\o/");
Query OK, 1 row affected (0,00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MariaDB [test]> SELECT id, COLUMN_LIST(sonderlocke) from dyni;
+------+--------------------------+
| id   | COLUMN_LIST(sonderlocke) |
+------+--------------------------+
|    1 | 1,3                      |
+------+--------------------------+
1 row in set (0,00 sec)

  COLUMN_EXISTS() prüft ob der angefragte Key vorhanden ist.

MariaDB [test]> SELECT COLUMN_EXISTS(sonderlocke,1) from dyni;
+------------------------------+
| COLUMN_EXISTS(sonderlocke,1) |
+------------------------------+
|                            1 |
+------------------------------+

Das schöne an dieser Lösung ist  - wie anfangs erwähnt -, dass die StorageEngine nur BLOBs unterstützen muss.

MariaDB [test]> ALTER TABLE dyni ENGINE=Aria;
Query OK, 1 row affected (0,01 sec)
Records: 1  Duplicates: 0  Warnings: 0

MariaDB [test]> SELECT id, COLUMN_LIST(sonderlocke) from dyni;
+------+--------------------------+
| id   | COLUMN_LIST(sonderlocke) |
+------+--------------------------+
|    1 | 1,3                      |
+------+--------------------------+
1 row in set (0,00 sec)

MariaDB [test]> ALTER TABLE dyni ENGINE=InnoDB;
Query OK, 1 row affected (0,04 sec)
Records: 1  Duplicates: 0  Warnings: 0

MariaDB [test]> SELECT id,COLUMN_GET(sonderlocke,1 as char) from dyni;
+------+-----------------------------------+
| id   | COLUMN_GET(sonderlocke,1 as char) |
+------+-----------------------------------+
|    1 | hallo                             |
+------+-----------------------------------+
1 row in set (0,00 sec)

MariaDB [test]> ALTER TABLE dyni ENGINE=PBXT;
Query OK, 1 row affected (0,05 sec)
Records: 1  Duplicates: 0  Warnings: 0

MariaDB [test]> UPDATE dyni SET sonderlocke=COLUMN_ADD(sonderlocke,2,"pbxt");
Query OK, 1 row affected (0,00 sec)
Rows matched: 1  Changed: 1  Warnings: 0



Damit ist der Schnelldurchlauf durch eines der kommenden Features von MariaDB 5.3 schon am Ziel angelangt. Auf der TODO-Liste des Features steht unter anderem noch die Möglichkeit die Keys (dynamic Columns) zu benamsen.
Dieses Feature ist imho als Antwort auf die DocumentStore-Fraktion von NoSQL zu verstehen. Auch wenn es nicht den Anspruch haben sollte äquivalent zu den Möglichkeiten von i.e. BSON zu sein. Aber für wen es reicht .. :)

Viel Spaß
Erkan

Bughunter der ich bin?

| 2 Kommentare | Keine TrackBacks
Ich bin der Meinung, dass es sich gehört, dass Blogs Kommentare zulassen. Ist dies nicht der Fall, wird aus einem Blog zu schnell ein authistisches Tagebuch.
In diesem Blog fragt sich der Autor, ob er einen Bug in den neueren MySQL-Versionen (>5.1.x) gefunden hat:

mysql>SELECT BENCHMARK(100, fibonacci(10000));
ERROR 1690 (22003): DOUBLE value is out of range in '(f1@1 + f2@3)'

Uups, was ist das. Die MySQL Version 5.5.9 steigt mit einem SQL-Fehler (ERROR 1690) aus. Ist dies ein Bug?

Ganz im Gegenteil die Fehlermeldung ist hier genau richtig. Denn die Fibonacci-Reihe übersteigt den Definitionsbereich von DOUBLE. Das tat es schon in der 5.1.x und früher. Nur jetzt gibt es eben einen Fehler .. was gut ist!
Warum die genutzte Funktion überhaupt mit DOUBLE arbeitet ist mir bei der Fibonacci-Reihe so oder so schleierhaft :/


BTW: Dies hat nichts mit der ursprünglichen Fragestellung des Autors über die Performance von Proceduren zu tun. Dazu will ich mich nicht äußern.