Neue Serpent-Version: 1.2.1

10 Mai 2009 In: PHP, Webworking

Eben gerade hab ich die neue Version (Version 1.2.1) der Serpent Template Engine als Download bei Google Code zur Verfügung gestellt.

Dieses ist ein Bugfix, der den Creole-Parser etwas verbessert, welcher mit der Version 1.2 eingeführt wurde. Creole ist eine Wiki-Syntax, die eingeführt wurde, um den Wildwuchs an Syntaxen etwas zu bereinigen. Creole wird mittlerweile von einer Vielzahl von Wikis verwendet und sollte beim Bau einer eigenen Wiki-Engine zumindest unterstützt werden. Der User wirds euch danken.


  1. Serpent - PHP Template Engine
    http://code.google.com/p/serpent-php-template-engine/

Verwandte Artikel:

Ja, die Headline dieses Artikels ist vielleicht etwas gewöhnungsbedürftig. Aber es geht um ein tolles Thema… fangen wir von vorn an:

Seit längerer Zeit suche ich einen guten Konsolen-Editor, mit dem meine PuTTY-Sessions nicht so nervig wie bisher sind. Mit Vi(m), Nano, Joe usw. konnte ich mich einfach nicht anfreunden, weil sie alle ihre eigenen Tastenkürzel haben und halt nicht die, die ich aus GUI-Oberflächen (egal ob Windows oder Linux) gewohnt bin. Jetzt bin ich aber gerade über die optimale Lösung gestolpert: XMing zusammen mit PuTTY.

XMing ist ein X-Server, mit dem man auf die X11-Programme eines Servers zugreifen kann. Ich kann also über PuTTY ganz normal eine Konsolen-Session führen, mit dem Unterschied, dass ich jetzt auch Dateien mit z.B. GEdit bearbeiten kann. Ich sehe dann unter Windows ein Fenster mit GEdit drin. Genial. Selbst Gimp und OpenOffice liefen problemlos.

Hier die Anleitung:

  1. XMing und XMing-portablePuTTY von der XMing Hauptseite herunterladen und installieren.
  2. Wie gewohnt eine PuTTY-Session starten mit einem kleinen Unterschied: Bei den Einstellungen unter “Connection > SSH > X11″ muss “X11 forwarding” aktiviert sein.
  3. Auf dem Server muss X11 forwarding in “/etc/ssh/sshd.config” aktiviert sein. Bei mir (Ubuntu-Server) war es das standardmäßig, musste also nichts ändern. Falls man etwas ändern muss: SSH-Daemon oder Rechner neu starten.

Schon klappt es. Auf der XMing-Seite sieht man auch ein paar Screenshots, die Lust auf mehr machen.

Ach ja: Damit eure Konsole nicht blockiert ist, hängt ein & an den Befehl heran, z.B.

gedit dummy.txt &

Und Nautilus solltet ihr so starten:

nautilus --no-desktop &

Verwandte Artikel:

Hatte gerade das Problem, dass ich für einen Kunden eine Bilddatenbank aufsetzen sollte, die mit phpThumb läuft. Der Kunde wollte jetzt auch z.B. PSD-Dateien mit einer Vorschau angezeigt bekommen und somit musste natürlich auch ImageMagick auf dem Server liegen… tat es aber nicht… und war auch nicht nachzuinstallieren, weil es um einen Managed Server ging.

Allerdings geistert im Web eine statisch kompilierte Version 4.2.9 (uralt) herum, die eigentlich für Typo3 gedacht ist. Die kann man dann per FTP einfach hochladen, muss noch die Ausführungsrechte setzen und schon gehts… nur nicht mit PHPThumb. Hier sind ein paar kleine Änderungen nötig.

Zuerst muss der Pfad zum convert-Tool in der “phpThumb.config.php” angegeben werden. Einfach folgende Zeile suchen und anpassen:

$PHPTHUMB_CONFIG['imagemagick_path'] = 'path/to/imagemagick-4.2.9_i386-static/convert';

Dann muss die “phpthumb.class.php” leicht angepasst werden. Zuerst die Funktion “ImageMagickVersion”. Da der reguläre Ausdruck in dieser Funktion nicht die Versionsnummer der statisch kompilierten IM-Version matcht, müssen wir da ein wenig nachhelfen. Am einfachsten (und schmutzigsten) ist es, direkt eine eigene Versionsnummer zurückzugeben:

// diese Zeile vor dem return am Ende hinzufügen
return '4.2.9 99/09/01';

return @$versionstring[intval($returnRAW)];

Jetzt erkennt phpThumb schon einmal, dass es eine gültige ImageMagick-Version gibt. Allerdings haben wir noch das Problem, dass es in dieser uralten Version noch nicht die “-resize” Parameter gab. Hierfür reicht es, in der Funktion “ImageMagickThumbnailToGD” die Zeile

$IMresizeParameter = 'resize';

durch

$IMresizeParameter = 'size';

zu ersetzen. Und schon läufts.

Verwandte Artikel:

InnoDB und große Tabellen

7 Apr 2009 In: PHP, Webworking

Vor kurzem hatte ich die Aufgabe, ein Gewinnspiel umzusetzen, bei dem User sich pro E-Mail-Adresse einen Gewinncode zurückgeben lassen konnten. Die 7 Millionen Gewinncodes kamen dabei vom Kunden und waren alle unique. Jeder Code durfte dabei nur einmal ausgespielt werden. Um das sicherzustellen kamen nur Transaktionen in Frage. Somit war InnoDB Pflicht.

Ok, als erstes hab ich die beiden Tabellen erstellt:

--
-- Tabellenstruktur für Tabelle `codes`
--

CREATE TABLE `codes` (
	`code` char(6) character set latin1 collate latin1_bin NOT NULL,
	`user_id` int(11) default NULL,
	PRIMARY KEY  (`code`),
	KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=FIXED;

-- --------------------------------------------------------

--
-- Tabellenstruktur für Tabelle `users`
--

CREATE TABLE `users` (
	`id` int(11) NOT NULL auto_increment,
	`salutation` set('Herr','Frau') NOT NULL,
	`firstname` char(50) NOT NULL,
	`lastname` char(50) NOT NULL,
	`street` char(100) NOT NULL,
	`hnr` char(20) NOT NULL,
	`zip` char(10) NOT NULL,
	`city` char(100) NOT NULL,
	`email` char(100) NOT NULL,
	`created` datetime NOT NULL,
	PRIMARY KEY  (`id`),
	UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED AUTO_INCREMENT=1;

Problem 1: Wie importiert man 7 Millionen Datensätze möglichst schnell?

Einzelne Queries sind langsam. Mehrere Queries per Transaktion zu bündeln ist ein wenig schneller. Das schnellste aber scheint LOAD DATA zu sein, welches auch über das Shell-Tool mysqlimport zu verwenden ist. Da man aber bei mysqlimport keinen Fortschritt ausgeworfen bekommt, hab ich fix ein Shell-Skript zusammengeschrieben, welches alle 10.000 Zeilen eine Meldung auswirft.

$data_file = '/tmp/codes.txt';
$db = 'otto_paf_mai2009';
$chunk_file = '/tmp/codes.part';

# split the file into small chunks to output a status report
$line = 0;
$buffer = '';
$handle = fopen ($data_file, 'r');
while (!feof($handle))
	{
	$line++;
	$buffer .= fgets($handle);

	if ($line%10000 === 0)
		{
		file_put_contents($chunk_file, $buffer);
		$buffer = '';

		# the data file has to have the same name as the table
		# it has to be readable by the mysql server (especially the directory)
		# mysqlimport [options] [db_name] [data_file]
		shell_exec('mysqlimport -uroot --columns=code --verbose '.$db.' '.$chunk_file);

		stdout($result);
		stdout(number_format($line, 0, ',', '.'));
		}

	}
fclose ($handle);

function stdout($out){fwrite(STDOUT, $out."\n");}
function stderr($out){fwrite(STDERR, $out."\n");}

Interessant war hierbei schon, wie lange MySQL dafür braucht und wie tödlich ein Unique-Key (in diesem Fall der PRIMARY) sein kann. Der Import in MyISAM ohne Unique-Key dauerte eine halbe Minute. Der Import in InnoDB ohne Unique-Key etwa 5 Minuten. Und der Import in InnoDB MIT dem Unique-Key dauerte letztendlich 5,5 Stunden. Autsch! Allerdings war es ganz gut, dass ich den Unique-Key gesetzt habe, denn bei den ersten Daten des Kunden waren tatsächlich Dubletten in den Codes.

Problem 2: Wie gibt man einen zufälligen Code aus?

Der offensichtlichste und verbreitetste Ansatz ist:

SELECT code
FROM codes
WHERE user_id IS NULL
ORDER BY RAND()
LIMIT 1

Dauert aber leider schon bei 600.000 Datensätzen über eine halbe Sekunde. Und das EXPLAIN macht alles klar. In der Spalte “extra” steht:

Using where; Using index; Using temporary; Using filesort

Das sieht übel aus.

Mein nächster Ansatz war, das ganze in zwei Abfragen auszulagern. Die erste sollte sich die Anzahl der gültigen Codes zurückliefern, die zweite schließlich mit einem zufälligen Offset beim LIMIT arbeiten, der halt zwischen 0 und der Anzahl der gültigen Codes liegen sollte. Das sah etwa so aus:

// get count of codes
$sql = $this->db->result("
	SELECT count(*) as count
	FROM codes
	WHERE user_id IS NULL
");

$offset = rand(0, $sql['RESULT'][0]['count']);

// get code from db
$sql = $this-db->Result("
	SELECT code
	FROM codes
	WHERE user_id IS NULL
	LIMIT $offset, 1
");
$code = $sql['RESULT'][0]['code'];

Schon die erste Abfrage dauerte im Schnitt 1,7 Sekunden. Aber warum? Die Antwort findet sich hier: http://www.mysqlperformanceblog.com/2006/12/01/count-for-innodb-tables/
Ein COUNT(*) auf viele Datensätze ist bei InnoDB einfach höllisch langsam. Die nächste Idee war hierbei, die erste Abfrage umzudrehen. Da ich nämlich weiß, wieviele Keys in der DB sind, brauche ich nur die user_ids zählen und von der Gesamtzahl abzuziehen. Das war auch tatsächlich sehr schnell. Selbst die zweite Abfrage…

ABER HALT…

… anscheinend nur bei der ersten Abfrage. Irgendwie schwanken die Zeiten zwischen 0,0003 Sekunden und 2,5 Sekunden. Nach ein bisschen Herumprobieren kam auch hier der Übertäter zum Vorschein: Das LIMIT. Ist der Offset klein, kommt das Ergebnis sehr schnell, ist er groß, dauert es ewig lang. Mist.

Mein derzeitiger Ansatz ist mehr ein Workaround. Da ich weiß, dass die Codes mit einem Klein- oder Großbuchstaben anfangen, wähle ich einfach einen aus und lasse mir nur Codes zurückgeben, die halt diesen am Anfang stehen haben:

$values = array_merge( range('a', 'z'), range('A', 'Z') );
$char = $values[ rand(0, count($values)-1 ) ];

$sql = $this-db->Result("
	SELECT code
	FROM codes
	FORCE INDEX(PRIMARY)
	WHERE code LIKE '".$char."%'
	AND user_id IS NULL
	LIMIT 1
");

Interessant war hierbei das FORCE INDEX. Ohne wollte MySQL lieber den Index für die user_id verwenden als den PRIMARY KEY. Verrückt. Allerdings ist die endgültige Lösung mit 0,007 Sekunden recht schnell. Allerdings bekomme ich nicht wirklich zufällige Ergebnisse heraus.

Wer also eine bessere Lösung kennt oder eine andere Idee hat: Immer her damit!

Verwandte Artikel:

Neue Serpent Version: 1.1 RC1

7 Mär 2009 In: PHP, Webworking

Eben gerade hab ich die neue Version (Version 1.1 RC1) der Serpent Template Engine als Download bei Google Code zur Verfügung gestellt.
Wie man an der Versionsnummer sehen kann, ist es ein Release Candidate, der also schon sehr stabil läuft.

Neue Features sind eine einfachere Initialisation, ein neues Plugin-System (welches es noch einfacher macht, die Template-Engine zu erweitern) und die Möglichkeit, auch den Compiler on the fly zu wechseln (weil dieser selbst jetzt auch als Plugin eingebunden ist). Es liegt jetzt auch ein Compiler für die Wiki-ähnliche Markdown-Syntax dabei.

Die Dokumentation ist natürlich schon auf dem neuesten Stand.


  1. Serpent - PHP Template Engine
    http://code.google.com/p/serpent-php-template-engine/

Verwandte Artikel: