Archiv März, 2010

Einer der großen Vorteile von Piwik zu Google Analytics sind die Realtime Statistiken. Will man als Standard Einstellung nun nicht Gestern haben, sondern Heute, kann man dies in der config.ini.php einstellen. Einfach unter General das folgende eintragen:

[General]
default_day = "today"

Öffnet man nun Piwik, wird immer der aktuelle Tag angezeigt.

Ein üblicher Prozess beim Programmieren mit Relationalen Datenbanken (in diesem Fall MySQL) und besonders im Web ist das „wenn vorhanden, update, wenn nicht vorhanden insert“ Konstrukt was man in vielen Anwendungen sieht. Auch in meinen Scripten gibt es diese Abfragen und es hat mich schon immer gestört, da man recht viel Code für eine simple Sache braucht. Glücklicherweise gibt es Abhilfe zu diesem Problem, wenn man Primär oder Unique Keys benutzt einfügen/updaten will.
(mehr …)

Der MySQL INSERT Befehl kann ja unterschiedlich verwendet werden und da ich eine Anwendung habe die sehr Insertlastig ist, habe ich mir die unterschiedlichen Möglichkeiten dieses SQL Befehls näher angesehen und ein paar Benchmarks laufen lassen. Die Tests liefen auf einem unbelasteten Server über die PHP cli. Es wurden in jedem Test 1.000.000 Datensätze in eine neu erstellte Tabelle geschrieben. Hier das CREATE TABLE. Wie man sehen kann gibt es 2 Schlüssel die erstellt werden.

CREATE TABLE test 
(
    id int(10) unsigned NOT NULL auto_increment,
    other_id int(10) unsigned NOT NULL,
    `hash` char(8) NOT NULL,
    `value` text NOT NULL,
    PRIMARY KEY  (id),
    UNIQUE KEY other_id (other_id,`hash`)
) 
ENGINE=MyISAM DEFAULT CHARSET=latin1;

Inserts kann man ja als Einzelstatement per SET erstellen oder man benutzt VALUES Listen um mehrere Zeilen mir einem INSERT einzufügen. Das die zweite schneller ist war mir schon klar, aber nicht um wie viel.
Das verzögerte Schreiben der Daten per DELAYED habe ich schon immer benutzt, aber ein weiterer Ansatz war das verzögern des Schreibens des Index per DELAY_KEY_WRITE. Hierbei wird der Index nicht sofort auf die Platte geschrieben. Eigentlich wird er nie auf die Platte geschrieben, außer man gibt es durch ein FLUSH TABLE(S) explizit an. Das hat den Vorteil das man sich ein paar Schreibzugriffe auf die langsame Festplatte spart, die bei mir das Bottleneck ist. Der "Nachteil" ist das man Gefahr läuft das bei einem Absturz des Servers die Index Files unvollständig sind und repariert werden müssen. Das kann man MySQL aber automatisch prüfen und ggf. reparieren lassen.

Als erstes habe ich getestet was der DELAY_KEY_WRITE bei normalen per SET erstellten Statements bringt.

INSERT SET ohne DELAY_KEY_WRITE:
86.0647 Sekunden - inserts/s: 11619.1656
-------------------------------------
INSERT SET mit DELAY_KEY_WRITE:
73.6021 Sekunden - inserts/s: 13586.5689

Man kann schon hier eine deutliche Verbesserung feststellen und man entlastet mit DELAY_KEY_WRITE natürlich die Festplatte. Als nächstes habe ich Inserts mit VALUES Listen getestet und welche Anzahl an Zeilen pro Statement Sinn machen.

INSERT VALUE(100) ohne DELAY_KEY_WRITE:
26.0524 Sekunden - inserts/s: 38384.1796
-------------------------------------
INSERT VALUE(200) ohne DELAY_KEY_WRITE:
25.2528 Sekunden - inserts/s: 39599.5692

Das ist eine Verdreifachung und umso mehr Zeilen man ins Statement drückt umso schneller scheint es zu gehen. Jetzt ging es also nur noch darum die richtige Anzahl an Zeilen pro Statement zu finden und alles zu verknüpfen.

INSERT VALUE(10) mit DELAY_KEY_WRITE:
28.5833 Sekunden - inserts/s: 34985.4635
-------------------------------------
INSERT VALUE(100) mit DELAY_KEY_WRITE:
19.8950 Sekunden - inserts/s: 50263.8854
-------------------------------------
INSERT VALUE(200) mit DELAY_KEY_WRITE:
18.9503 Sekunden - inserts/s: 52769.6131
-------------------------------------
INSERT VALUE(500) mit DELAY_KEY_WRITE:
18.6684 Sekunden - inserts/s: 53566.4545
-------------------------------------
INSERT VALUE(1000) mit DELAY_KEY_WRITE:
18.1983 Sekunden - inserts/s: 54950.1877

Fazit

Das benutzen von VALUES Liste lohnt sich ungemein und das setzen von DELAY_KEY_WRITE bei stark INSERT lastigen Tabellen lohnt sich auch. Ich verwende aktuell irgend was zwischen 300 - 1000 Zeilen pro Statement. Ich will es auch nicht zu groß werden lassen und der Vorteil wird auch immer kleiner.

DELAY_KEY_WRITE habe ich nun auch seit ein paar Monaten im Einsatz ohne Probleme feststellen zu können. Explizite FLUSH TABLES benutze ich auch nicht, aber die nächtlichen Backups führen definitiv eins aus. Wenn der MySQL Server beendet wird, werden die Keys natürlich auch auf die Festplatte geschrieben. Man kann es auf Tabellen Basis wie folgt aktivieren.

ALTER TABLE tablename DELAY_KEY_WRITE=1;

In seiner MySQL Config sollte man noch myisam-recover=BACKUP,FORCE setzen. Damit werden beim starten des MYSQL Servers die Tabellen auf Beschädigung geprüft, gesichert und dann repariert. Zum testen habe ich ihn ein paar mal abgeschossen und es hat problemlos funktioniert.

Related Links

Die von CakePHP mitgelieferten htaccess Dateien funktionieren auf einem lighttpd Server natürlich nicht, da lighttpd keine htaccess Dateien unterstützt. Wer trotzdem die Pretty URLs von CakePHP benutzen möchte hat 2 Möglichkeiten. Die erste ist eine mod_rewrite (das von lighttpd natürlich) Lösung, die einfach und schnell eingerichtet ist, aber dafür etwas aufwändiger zu managen ist oder man benutzt ein LUA Script mit mod_magnet. Das Problem ist das lighttpd keinen Ersatz für die Apache Funktionen file_exists/dir_exists hat.

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]

Bei den RewriteCond's wird geprüft ob das angefragte Verzeichnis oder die angefragte Datei nicht vorhanden ist und wenn beides zutreffen, wird die Anfrage an CakePHP übergeben. Die mod_rewrite Lösung ist deshalb aufwändiger zu managen, da man jede Datei/jedes Verzeichnis das man in die webroot packt und das man natürlich nicht per CakePHP managen will, von Hand in die rewrite Rule eintragen muss. Wem dies nichts ausmacht, für den ist die lighttpd mod_rewrite Lösung genau die richtige. Für alle anderen wäre das LUA Script besser geeignet, da es die benötigten Funktionen file_exists/dir_exists nachbildet und man sich so nicht mehr um die neuen Dateien/Verzeichnisse kümmern muss. Ich habe mich für die schlankere mod_rewrite Lösung entschieden, da mir die LUA Lösung schon als Overkill vorkommt.

Lösung 1 lighttpd und mod_rewrite

Als erstes sollte man mod_rewrite aktivieren, wenn es nicht schon läuft. Das Modul dazu einfach in der lighttpd.conf in das server.modules Array eintragen und den lighttpd Server reloaden.

$ nano /etc/lighttpd/lighttpd.conf
$ /etc/init.d/lighttpd force-reload
server.modules = (
    "mod_access",
    "mod_alias",
    "mod_accesslog",
    "mod_compress",
    "mod_rewrite"
)

Nun muss man seine vhost Konfiguration leicht anpassen.

$HTTP["host"] =~ "vhost\.config\.de" {
    server.document-root = "/var/www/cakephp/app/webroot/"
        
    url.rewrite = (
        "(index.php|test.php|favicon.ico)" => "/$1",
        "(css|files|img|js)/(.*)" => "/$1/$2",
        "^([^\?]*)(\?(.+))?$" => "/index.php?url=$1&$3",
    )
}

Der interessante Teil beginnt nach url.rewrite. In der ersten Zeile wird gesagt das Dateien mit dem Namen index.php, test.php und favicon.ico direkt aufgerufen werden. Hier kann man alle Dateien anhängen die direkt in der webroot liegen und die man statisch ausliefern möchte. In der zweiten Zeile sind die Verzeichnisse angegeben die statische Dateien enthalten und die man natürlich nicht per CakePHP processen lassen möchte. Alle Files in diesen Verzeichnissen werden also ohne CakePHP ausgeliefert. Die dritte Zeile ist die eigentliche RewriteRule, die alles was vorher nicht abgefangen wurde, an CakePHP weitergibt. Damit sind dann auch die Pretty URL's möglich.

Lösung 2 lighttpd. mod_magnet und LUA

Diese Lösung wird im CakePHP Book gut beschrieben.

Related Links

Die Wahl des richtigen Feldtypes ist sehr wichtig bei Relationalen Datenbanken, da mit ihm der zur Verfügung stehende Zeichensatz und Länge definiert wird. Außerdem wird durch ihn die Speichergröße des Feldes und damit die Speichergröße der Zeile bestimmt. Deshalb soll man immer den kleinst möglichen Feldtype wählen der für seine Bedürfnisse ausreicht. Will man nun ein Datum speichern, gibt es mehrere Möglichkeiten dies zu tun.

Soll es ein Datum mit Uhrzeit sein bietet sich MySQL TIMESTAMP (4 Byte), DATETIME (8 Byte) und INT (4 Byte) an. Braucht man nur ein Datum, dann reicht auch DATE aus. DATETIME fällt sofort raus, da es zu viel Speicherplatz belegt. Der MySQL TIMESTAMP ist in der Speicherung und Darstellung nicht das gleiche wie ein UNIX Timestamp und da ich viel mit UNIX Timestamps arbeite, würde dies ein dauerndes umrechnen bedeuten. Bisher habe ich mich immer für INT entschieden wenn es nicht gerade um Geburtstage oder ähnliches geht die außerhalb des UNIX Timstamps liegen können, da MySQL mit Zahlen sehr gut umgehen kann und ich diese deshalb immer bevorzuge wenn möglich. Außerdem kann man damit ja Datum und Uhrzeit benutzen, was DATE nicht kann und es kostet nur 1 Byte mehr...

Um genau dieses 1 Byte geht es nun aber, den wenn man erst mal Tabellen mit mehreren Millionen Einträgen hat, zählt jedes Byte! Will man diese Spalte auch noch mit einem Index belegen, schlägt es sogar doppelt zu Buche und verbraucht nebenbei auch noch Platz im Memory (key_buffer zum Beispiel). Außerdem erhöht die größere Möglichkeit an Werten auch die Kardinalität des Index, was bei Sortierungen und Joins relevant wird.

Hier ein real life Beispiel:

Die Tabelle hat nur 2 Werte. Eine id als UNSIGNED INT und PRIMARY KEY, sowie ein Datum aktuell als INT auf dem ein Index liegt. Die Uhrzeit brauche ich aber eigentlich gar nicht. Die Tabelle hatte zum Zeitpunkt des Tests knappe 20 Millionen Zeilen (mittlerweile sind es 30 Millionen und es sind 60-120 Millionen angepeilt) und diese Stats:

Type  	Usage
Data 	189.3 	MiB
Index 	446.5 	MiB
Total 	635.9 	MiB

Für diesen Test habe ich die Tabellen Struktur kopiert und das INT Feld durch ein DATE Feld ersetzt. Dann noch die Daten kopieren und vergleichen.

INSERT INTO
	testdate
		(id, date)
    SELECT
        source.id,
        FROM_UNIXTIME(source.timeint, '%Y-%m-%d') AS date
    FROM testint AS source
ON DUPLICATE KEY UPDATE
	date = FROM_UNIXTIME(source.timeint, '%Y-%m-%d');
Type  	Usage
Data 	168.3 	MiB
Index 	420.9 	MiB
Total 	589.2 	MiB

Man kann also schon mal eine Speicherersparnis von 47 MB erkennen. Da ich in dieser Anwendung aktuell 12 dieser Tabellen habe, wären dies schon 564MB Ersparnis.

Fazit

Man sollte sich immer genau vor Auge führen was man speichern möchte, wofür man es braucht und danach das richtige Feld wählen. Auch die Ersparnis von nur 1 Byte kann sich auszahlen! Wenn man also nur ein Datum braucht, dann sollte man auch DATE wählen.

Damit ich es beim nächsten mal schneller finde, hier der Pfad unter dem man unter Eclipse .tpl Dateien als HTML Dateien öffnen kann:

window->preferences->General->
content Types->Text->HTMl-> add *.tpl