Artikel mit ‘php’ getagged

Schreibmaschine mit memory_limit

Donnerstag, 26. April 2012

Normalerweise sitze ich nicht um 1:20 Uhr noch am Rechner, um PHP-Probleme zu debuggen. Insbesondere nicht, wenn’s keine bezahlte Auftragsarbeit ist, sondern nacktes „Ich! Will! Das! Verstehen!“. Aber genau sowas hatte ich heute. Nachdem ich vor einigen Tagen schon mal ergebnislos an dem folgenden Problem herumgedoktert hatte, habe ich mich heute nochmal darin verbissen.

Ein User betreibt einen Magento-Shop auf unserer Hosting-Plattform Uberspace.de. Dazu gehören auch einige Hintergrundjobs, und zwar unter anderem dieser hier, der ganz offenkundig am memory_limit scheitert:

[demosite@krypton ~]$ php -c ~/cli /home/demosite/html/www.demosite.tld/shell/indexer.php status 
PHP Fatal error:  Allowed memory size of 262144 bytes exhausted (tried to allocate 232 bytes) in /var/www/virtual/demosite/www.demosite.tld/app/code/core/Mage/Index/Model/Mysql4/Process.php on line 110

Moment mal – ein memory_limit von gerade mal 256K? Das kann ja eigentlich nicht sein. In der php.ini steht nämlich 512M, und das wertet PHP an sich auch korrekt aus:

[demosite@krypton ~]$ php -c ~/cli -i | grep memory_limit
memory_limit => 512M => 512M

Nun gibt’s aber ja auch die Möglichkeit, in PHP-Scripts zur Laufzeit mit ini_set() einzelne Einstellungen dynamisch zu ändern, also auch das memory_limit. Schauen wir also mal, wo das möglicherweise noch so passiert:

[demosite@krypton ~]$ grep -r memory_limit /var/www/virtual/demosite/www.demosite.tld | grep -v cache
/var/www/virtual/demosite/www.demosite.tld/lib/Varien/.svn/text-base/Pear.php.svn-base:        @ini_set('memory_limit', '256M');
/var/www/virtual/demosite/www.demosite.tld/lib/Varien/Pear.php:        @ini_set('memory_limit', '256M');
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/.svn/text-base/Manager.php.svn-base:     * Default value is 2/3 of memory_limit php.ini variable
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/.svn/text-base/Manager.php.svn-base:        $memoryLimitStr = trim(ini_get('memory_limit'));
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/Manager.php:     * Default value is 2/3 of memory_limit php.ini variable
/var/www/virtual/demosite/www.demosite.tld/lib/Zend/Memory/Manager.php:        $memoryLimitStr = trim(ini_get('memory_limit'));
/var/www/virtual/demosite/www.demosite.tld/magiczoomplus/module/magictoolbox/core/.svn/text-base/magictoolbox.params.class.php.svn-base:@ini_set('memory_limit', '512M');
/var/www/virtual/demosite/www.demosite.tld/magiczoomplus/module/magictoolbox/core/magictoolbox.params.class.php:@ini_set('memory_limit', '512M');
/var/www/virtual/demosite/www.demosite.tld/.svn/text-base/payment_check.php.svn-base:ini_set('memory_limit', '1024M');
/var/www/virtual/demosite/www.demosite.tld/.svn/text-base/.htaccess.svn-base:  php_value memory_limit 128M
/var/www/virtual/demosite/www.demosite.tld/.htaccess:  php_value memory_limit 128M

Okay, kommt einige Male vor, durchaus auch mit wechselnden Werten, aber nirgendwo etwas mit 256K. Was also ist hier los?

XDebug muss her. Flugs kompiliert und die xdebug.so via zend_extension in die php.ini eingebunden und xdebug.auto_trace=1 gesetzt. Dann nochmal ausgeführt und geschaut, und siehe da – dreimal wird ini_set() aufgerufen:

[demosite@krypton ~]$ grep ini_set traces/trace.3557638816.xt 
    0.1443    8707248         -> ini_set() /var/www/virtual/demosite/www.demosite.tld/shell/abstract.php:117
    0.1443    8707384         -> ini_set() /var/www/virtual/demosite/www.demosite.tld/shell/abstract.php:117
    0.1444    8706736         -> ini_set() /var/www/virtual/demosite/www.demosite.tld/shell/abstract.php:123

Also mal geschaut, was an dieser Stelle so für Code steht – und das hat mich wirklich überrascht:

    /**
     * Parse .htaccess file and apply php settings to shell script
     *
     */
    protected function _applyPhpVariables()
    {
        $htaccess = $this->_getRootPath() . '.htaccess';
        if (file_exists($htaccess)) {
            // parse htaccess file
            $data = file_get_contents($htaccess);
            $matches = array();
            preg_match_all('#^\s+?php_value\s+([a-z_]+)\s+(.+)$#siUm', $data, $matches, PREG_SET_ORDER);
            if ($matches) {
                foreach ($matches as $match) {
                    @ini_set($match[1], $match[2]);
               	}
            }
            preg_match_all('#^\s+?php_flag\s+([a-z_]+)\s+(.+)$#siUm', $data, $matches, PREG_SET_ORDER);
            if ($matches) {
                foreach ($matches as $match) {
                    @ini_set($match[1], $match[2]);
                }
            }
        }
    }

Es kommt hier also Code zum Tragen, der bei der Ausführung eines PHP-Scripts auf der Kommandozeile eine .htaccess-Datei auswertet, die in diesem Kontext nun eigentlich überhaupt keine Rolle spielt – handelt es sich dabei doch um eine webserver-spezifische Konfigurationsdatei. Also flugs dort nochmal reingeschaut:

[demosite@krypton ~]$ grep memory_limit /var/www/virtual/demosite/www.demosite.tld/.htaccess
  php_value memory_limit 128M

Hm. Da steht aber auch nichts von 256K. Wo zum Kuckuck kommt das her? Also auf die Schnelle – es ist spät – schnell ein wenig Debugging-Code an die entsprechende Stelle gesetzt, um einfach mal auszugeben, was genau dort geschrieben wird (jaja, das ist nicht elegant, aber man muss auch mal pragmatisch sein dürfen):

               	foreach ($matches as $match) {
                    echo "setting '" . $match[1] . "' to '" . $match[2] . "'\n";
                    @ini_set($match[1], $match[2]);
                }

Die Ausgabe hat dann doch reichlich überrascht:

[demosite@krypton ~]$ php -c ~/cli /home/demosite/html/www.demosite.tld/shell/indexer.php status 
'etting 'memory_limit' to '128M
'etting 'max_execution_time' to '18000
PHP Fatal error:  Allowed memory size of 262144 bytes exhausted (tried to allocate 92 bytes) in /var/www/virtual/demosite/www.demosite.tld/app/code/core/Mage/Index/Model/Mysql4/Process.php on line 110

Auch dem ungeübten Betrachter wird auffallen, dass die Ausgabe nicht mit setting, sondern mit 'etting beginnt, und dafür nicht mit '128M' endet, sondern mit '128M. Was zum..? Es wird doch nicht..?

Ein kleiner Exkurs: Unix-Systeme benutzen andere Zeilenumbrüche als Windows-Systeme. Während unter Unix Zeilenumbrüche mit einem einzelnen line feed ("\n") separiert werden, werden unter Windows Zeilenumbrüche mit einem carriage return, line feed ("\r\n") separiert: Carriage return, der gute alte Wagenrücklauf wie bei der Schreibmaschine – am Hebel ziehen, um den Druckkopf wieder am Zeilenanfang zu positionieren; Druckwalze eine Zeile vorrücken lassen.

Und genau so sah das hier aus: Das zweite ' von '128M' ist nach vorne gerutscht; ein carriage return hat stattgefunden. Und das wiederum heißt, dass … Moment, kurz nachgucken …

[demosite@krypton ~]$ file /var/www/virtual/demosite/www.demosite.tld/.htaccess
/var/www/virtual/demosite/www.demosite.tld/.htaccess: ASCII English text, with CRLF line terminators

… richtig: Die Inhalte der .htaccess-Datei sind mit Windows-Zeilenumbrüchen gespeichert. Werden jene nun auf einem Unix-System eingelesen, was die Zeilen am „\n“ auftrennt, dann hat memory_limit hier faktisch nicht den Wert „128M„, sondern den Wert „128M\r“ – Wagenrücklauf inklusive. Und genau das ist es, was PHP nun so gar nicht schmeckt. Verdammte Axt!

Also los:

[demosite@krypton ~]$ dos2unix /var/www/virtual/demosite/www.demosite.tld/.htaccess
dos2unix: converting file /var/www/virtual/demosite/www.demosite.tld/.htaccess to UNIX format ...

Getestet:

[demosite@krypton ~]$ php -c ~/cli /home/demosite/html/www.demosite.tld/shell/indexer.php status 
setting 'memory_limit' to '128M'
setting 'max_execution_time' to '18000'
Product Attributes:            Pending
Product Prices:                Pending
Catalog Url Rewrites:          Pending
Product Flat Data:             Pending
Category Flat Data:            Pending
Category Products:             Pending
Catalog Search Index:          Running
Stock status:                  Pending

Problem gelöst. Script läuft. Nur noch die Debug-Zeile wieder rausnehmen. So, erledigt.

Und jetzt ist wirklich Zeit für’s Bett.

Mal eben schnell MySQL-Zeichensatzprobleme fixen

Montag, 13. Dezember 2010

Heute lag eine Anfrage einer Kundin auf meinem Tisch, die eine Datenbank voller MySQL-Tabellen hatte. Der Zeichensatz der Datenbank und aller Tabellen und Spalten war latin1_german_ci. Dumm nur, dass die Applikation überall UTF8-kodierte Werte reingeschrieben hat – was hier und da so ein bisschen funktionierte, an anderen Stellen aber so gar nicht. In phpMyAdmin wurden alle Umlaute aber fehlerhaft dargestellt, und damit stand dann auch fest, dass da einfach Murks passiert war (denn wir wissen ja, phpMyAdmin hat immer recht, siehe hier).

Nun ist aber das Problem, dass die nachträgliche Änderungen der Spalten in phpMyAdmin erstens mühselig ist und zweitens auch gar nichts bringt, weil MySQL dabei die Inhalte der Spalten auch gleich (in seine Sinne) korrekt konvertiert. Die einzige Möglichkeit ist, die Spalte auf eine binäre Spalte vom Typ BLOB zu ändern, weil hierbei die Zeichensatzangabe verlorengeht, was wir hier auch explizit wollen – sie ist ja falsch. Das ist aber noch viel unglaublich aufwendiger.

Auf der Shell lässt sich das Problem aber schließlich trivial lösen. Zunächst dumpen wir die gesamte Datenbank, und dabei geben wir an, dass latin1 als Zeichensatz verwendet werden soll – auf diese Weise dumpt MySQL die Daten so (falsch), wie sie in den Spalten stehen; da sind sie ja als latin1 kodiert. Dann ändern wir alle Kollations- und alle Zeichensatzangaben im SQL-Dump von latin1 auf utf8 – wobei die Daten, genauer gesagt die Bytes aber ja unverändert bleiben. Anschließend wird alles wieder in MySQL gefüttert.

Und so sieht’s aus (es wird eine .my.cnf mit hinterlegten Zugangsdaten vorausgesetzt, damit keine Passworteingabe erfolgen muss):

mysqldump --default-character-set=latin1 mydatabase mydatabase.sql

cat mydatabase.sql |
  sed 's/latin1_german_ci/utf8_general_ci/g;' |
  sed 's/latin1/utf8/g;' |
  mysql mydatabase

Fertig! In phpMyAdmin sicherheitshalber kontrollieren, dass alle Spalten als utf8 kodiert sind und gleichzeitig auch die Umlaute korrekt angezeigt werden. Und da wir die Zeichensatzänderung nicht bei Dumpen, sondern beim Einspielen des Dumps „on the fly“ machen, liegt mit mydatabase.sql dann auch gleich noch ein Backup des Originalzustands vor, falls alles schiefgehen sollte.

Problematisch wird die Sache nur, wenn nur einige Tabellen oder – noch schlimmer – nur einige Spalten einiger Tabellen falsch kodiert sind. In diesem Fall wäre dann doch eher professionelle Hilfe gefragt – oder viel Handarbeit. ;-)

Endlich verstehen: Unicode mit PHP5 und MySQL

Freitag, 06. August 2010

Wenn es um PHP und Unicode geht, am Besten auch noch in Zusammenhang mit MySQL, gibt es eine Vielzahl an Tipps und Howtos. Manche davon sind falsch, die meisten zum Glück zwar richtig, doch selbst die, die Richtiges vermitteln, tun das manchmal nur durch Zufall, und oftmals fehlt das, was für eine verlässliche Handhabung am wichtigsten ist: Eine Erklärung, warum man dieses und jenes so und nicht anders machen soll.

Dieser Artikel hat den hehren Anspruch, Erklärungen zu liefern, Verständnis für Zusammenhänge zu wecken und schließlich dabei unterstützen, Unicode mit PHP richtig zu machen – ohne dass Sie dafür ein dickes Handbuch lesen müssen. Natürlich bleiben dabei viele Details auf der Strecke, deshalb sehen Sie diesen Artikel pragmatisch: Er sollte für den gewöhnlichen Alltag reichen und Sie dabei nicht ganz dumm sterben lassen. Für tiefergehende Informationen – lesen Sie ein Buch.

Zunächst einmal gilt es eine Sache zu verstehen, nämlich:

„Zeichen“ und „Bytes“ sind etwas völlig Verschiedenes.

Das ist vor allem wichtig für diejenigen, die bei diesem Thema erstmal gleich die gute alte Zeichensatztabelle im Kopf haben. Da haben wir nämlich gelernt: Ein Zeichen ist ein Byte; mit einem Byte kann man 256 verschiedene Zeichen darstellen. Wenn man gut aufgepasst hat, weiß man vielleicht auch noch, dass die ersten 128 Zeichen immer gleich waren („US-ASCII“) und die nächsten 128 Zeichen darüber zeichensatzspezifisch waren – dass das Zeichen mit der Nummer 188 z.B. in ISO-8859-1 „¼“ bedeutet, während es in ISO-8859-2 „ź“ bedeutet.

Lektion Nummer 1: Diese Zeichensatztabellen bitte alle sofort vergessen. Zu einem späteren Zeitpunkt, wenn das mit Unicode klar geworden ist, kann man sich gerne wieder daran erinnern und sie sauber in einen technischen Zusammenhang bringen, aber für den Moment: Weg damit. Und damit auch gleich weg mit der Vorstellung, dass „1 Byte“ in irgendeiner Art und Weise „1 Zeichen“ bedeuten würde. In der gebräuchlichen UTF-8-Kodierung kann ein Zeichen nämlich durchaus bis zu vier Bytes benötigen.

Hier nun daher der große, grundsätzliche Pferdefuß von PHP in allen Versionen < 6, direkt aus der offiziellen Quelle:

A string is series of characters, therefore, a character is the same as a byte. That is, there are exactly 256 different characters possible. This also implies that PHP has no native support of Unicode.

Bitte hier kurz innehalten, durchatmen und sich noch einmal ganz deutlich vor Augen führen: PHP hat keine Ahnung von Unicode.

Um das ad hoc etwas greifbarer zu machen, hier ein konkretes Beispiel für ein PHP-Script, das ich mit einem Texteditor erstellt habe, der die Datei UTF-8-kodiert abspeichert (was heutzutage eigentlich alle tun):

$ cat test-strlen.php
<?php echo strlen("ä"); ?>

$ php test-strlen.php
2

Überrascht? Wichtig ist das vor allem für Leute, die sonst in anderen Programmiersprachen entwickeln, die von Haus aus UTF-8-tauglich sind. Nehmen wir zum Beispiel Perl: Dort werden intern Strings als Zeichen verarbeitet; ein interner UTF-8-Marker hilft Perl dabei, zu verstehen, ob die im RAM liegenden Bytes einer Zeichenkette als UTF-8 interpretiert werden müssen, um sinnvolle Zeichen zu ergeben, oder nicht. Mit PHP muss in diesem Punkt grundsätzlich anders gearbeitet werden.

Eines ist aber so oder so wichtig zu verstehen: Erst die Angabe eines Zeichensatzes kann eine Folge von Bytes sinnvoll in eine Folge von Zeichen überführen, oder griffiger für einen Klebezettel an der Kühlschranktür ausgedrückt:

1-4 Bytes + Zeichensatzangabe = Zeichen

Wann immer also auf einer Website oder in einer E-Mail „kaputte“ Umlaute zu beobachten sind, so liegt das immer daran, dass entweder eine Zeichensatzangabe vorhanden ist, die aber nicht stimmt, oder dass gar keine vorhanden ist, der Client den Zeichensatz rät und dabei den falschen rät. Das kann man ihm aber kaum zum Vorwurf machen – es ist Aufgabe des Erstellers des Inhalts, sich um die korrekte Zeichensatzangabe zu kümmern.

Nun findet auf Websites aber jene Menge Interaktion statt, und so gibt es gleich eine ganze Reihe von Bereichen, in denen Unicode eine Rolle spielt:

  • ein PHP-Script sendet Daten an den Browser
  • ein Browser sendet Daten an ein PHP-Script
  • ein PHP-Script liest Daten aus einer Datenbank
  • ein PHP-Script schreibt Daten in eine Datenbank

Jegliche Art von Kommunikation basiert aber auf Bytes und nicht auf Zeichen, was in der Konsequenz bedeutet: Bei jeglicher Art von Kommunikation muss eine Zeichensatzangabe mitgegeben werden, damit nichts schiefgeht. Das klingt auf den ersten Blick aufwendig, aber um es ganz deutlich zu sagen: Daran führt kein Weg vorbei.

Nehmen wir den einfachsten Fall: Ein PHP-Script sendet Daten an den Browser. Ein PHP-Script ist eine Textdatei, insofern wird der Zeichensatz dieser Textdatei durch den Editor festgelegt, der sie speichert. Dann ist wichtig, dass der Browser über den Zeichensatz informiert wird. Da gibt es viele Möglichkeiten; die wichtigsten wären:

  • das PHP-Script führt header("Content-type: text/html; charset=utf-8"); aus
  • im HTML-Header wird <meta http-equiv="Content-type" content="text/html; charset=utf-8"> angegeben
  • in der php.ini wird default_charset = utf-8 angegeben
  • in der Apache-Konfiguration wird AddDefaultCharset UTF-8 angegeben

Der umgekehrte Weg, dass ein Browser Daten an ein Script sendet, ist überraschend unklar: HTTP-Requests sehen nämlich keinen Zeichensatz-Header vor. Dabei wäre es doch schon durchaus wichtig zu wissen, ob ein vom Website-Besucher in einem Formular angegebenes „ä“ nun als ein oder als zwei Bytes übermittelt wird! In der Praxis wird ein Browser die Formulardaten im gleichen Zeichensatz kodieren wie der der Seite, die er abgerufen hatte, mit einem Fallback auf ISO-8859-1. Für den Alltag reicht das aus. Entwickler von HTTP-basierenden Schnittstellen, bei denen ein Client initiativ Formulardaten an den Webserver schickt, sind aber gut beraten, in der Schnittstellendefinition kurzerhand ausdrücklich festzulegen, was erwartet wird.

Nun haben wir also Formulardaten erhalten, und diese Formulardaten haben irgendeinen Zeichensatz, idealerweise UTF-8, wenn wir dem Browser schon das Formular als UTF-8 präsentiert haben. So weit, so gut.

Nun müssen die Daten in eine Datenbank, wobei ich hier als Beispiel einfach mal MySQL verwende. Und auch hier gibt es eine „Grundwahrheit“, die man sich am Besten gleich neben den Klebezettel von vorhin an den Kühlschrank heften sollte:

Bei MySQL sind Zeichensatzangaben für zwei Dinge wichtig:

  1. für die Information, in welchem Zeichensatz Daten gespeichert werden sollen;
  2. für die Information, in welchem Zeichensatz Daten übermittelt werden sollen.

(Ja, das ist schnöde vereinfacht, denn schließlich gibt es in MySQL ein client character set, ein connection character set, ein results character set, ein system character set, ein database character set und ein server character set. Wir wollen es aber für den Moment nicht zu kompliziert machen und uns auf die Dinge konzentrieren, bei denen es normalerweise Probleme gibt. Ganz pragmatisch, wissenschon.)

Über den ersten Punkt ist sicher fast jeder schon mal gestolpert, der mit einer halbwegs aktuellen phpMyAdmin-Version hantiert hat: Für jede Spalte, in die textliche Angaben hineinkommen (CHAR, VARCHAR, TEXT, …), muss ein Zeichensatz angegeben werden, wobei der Defaultwert von der Tabelle und der wiederum von der Datenbank und der wiederum vom Server geerbt wird. Und eines sei an dieser Stelle direkt gesagt: Man mag von phpMyAdmin halten, was man will, aber das mit den Zeichensätzen macht es richtig. Wenn Sie also durch die Daten blättern und über Umlaute stolpern, die kaputt sind: Suchen Sie den Fehler bei sich. phpMyAdmin hat immer recht. Wiederholen Sie das.

Bevor es weitergeht, wenden wir uns dem Herren weiter hinten im Publikum zu, der schon seit einer Minute mit dem Finger schnippst und behauptet, das könne nicht stimmen, denn seine Applikation würde prima funktionieren, aber phpMyAdmin würde die Umlaute falsch anzeigen. Nun: Lassen Sie sich nicht foppen. Suchen Sie den Fehler bei sich, und erinnern Sie sich an das Mantra: phpMyAdmin hat immer recht. Es wird reiner Zufall, um nicht zu sagen: schieres Glück sein, dass es trotzdem funktioniert. Typischerweise passiert das dann, wenn Sie UTF-8-kodierte Daten in einer als Latin1 markierten Spalte ablegen. Hier werden die Zeichen dann zweimal falsch verarbeitet: Wenn MySQL die Bytes aus dem Datensatz entsprechend dem Zeichensatz der Spalte interpretiert, und wenn es dann dieses fehlerhafte Ergebnis in den gewünschten Zeichensatz der Verbindung konvertiert, der vermutlich Latin1 und nicht UTF-8 ist und somit die erhaltenen Bytes weitestgehend unbehelligt lässt. Und zweimal falsch ergibt in diesem speziellen Zusammenhang ausnahmsweise sogar mal richtig: Sie geben faktisch eine Reihe von Bytes aus, die in Latin1 ein krummes „Büttelborn“ ergeben und schreiben dann für den Browser eiskalt drüber: So, das ist jetzt aber UTF-8. Und erst damit kommt das Zeichensatzproblem wie auf magische Weise in Ordnung, und der Browser rendert „Büttelborn“ aus den schnöden Bytes.

Wenn Sie dann aber Ihre selbstgeschriebene Applikation später mal auf einen anderen Server übertragen, oder wenn Sie MySQL updaten, oder wenn Sie PHP updaten, … und plötzlich die Umlaute nicht mehr funktionieren, dann wissen Sie, dass Sie etwas falsch gemacht haben, und zwar nicht mit dem Update, sondern mit Ihrem Code, schon vor langer Zeit. Sagen Sie nicht, ich hätte Sie nicht gewarnt.

Es wird somit Zeit für den nächsten Kühlschrankzettel:

SET NAMES utf8;

Damit stellen Sie ein, in welchem Zeichensatz Sie die Daten zwischen PHP und MySQL übermitteln. Das betrifft sowohl die Daten, die Sie in die Datenbank schreiben, als auch die, die Sie aus der Datenbank beziehen. Und jetzt der Clou: Die Information, in welchem Zeichensatz MySQL die Daten speichert, hat damit absolut nichts zu tun. Mit SET NAMES utf8 bekommen Sie immer UTF-8 raus, egal welchen Zeichensatz die Strings in der Tabelle faktisch haben. Und auch umgekehrt schicken Sie einfach Bytes in dem Zeichensatz rüber, den Sie bei SET NAMES angegeben haben, und MySQL kümmert sich darum, diese Folge von Bytes anhand des angegebenen Zeichensatzes als Zeichen zu interpretieren und dann diese Zeichen anhand des Zeichensatzes der Spaltendefinition in eine Folge von Bytes zu konvertieren, dies es dann fröhlich auf die Festplatte schreiben kann. Um so besser, wenn die beiden Zeichensätze identisch sind: Dann muss nämlich überhaupt nichts konvertiert werden.

Das Mantra für PHP-Entwickler muss also lauten:

Es spielt für Ihr Script überhaupt keine Rolle, welchen Zeichensatz die Spalten in der Datenbank haben.

Es spielt ausschließlich eine Rolle, in welchem Zeichensatz Sie mit MySQL kommunizieren.

(Na gut, erwischt. Das ist natürlich nicht die volle Wahrheit. Es spielt insofern doch eine Rolle, als dass es nicht jedes Zeichen in jedem Zeichensatz gibt. Übermitteln Sie also das Zeichen „✌“ UTF-8-kodiert an MySQL und verlangen vom Datenbankserver, dass er dieses Zeichen in eine als Latin1 formatierte Spalte schreibt, obwohl Latin1 das Zeichen gar nicht kennt, dann haben Sie schlechte Karten. Wunder kann MySQL nicht vollbringen. Insofern: Formatieren Sie Ihre Spalten lieber gleich als UTF-8.)

Gewöhnen Sie sich also einfach an, nach jedem mysql_connect(...) direkt ein mysql_query("SET NAMES utf8"); auszuführen. Wenn Sie zu den verantwortungsvollen Entwicklern gehören, die den Aufbau der Datenbankverbindung an einer zentralen Stelle untergebracht haben, sollte das ein Klacks sein.

Nun sind sie schon mal ziemlich weit: Sie kriegen die Daten aus Ihrer Datenbank als Bytes, die – wenn man UTF-8-Kodierung auf sie anwendet – hübsch korrekte Zeichen ergeben. Wenn Sie diese Zeichen mit echo ausgeben und dazu einen Content-type-Header mit „charset=utf-8“ (siehe oben), wird ein Browser die Zeichen richtig darstellen. Sie bekommen bei einem abgeschickten Formular Bytes rein, und wenn Sie diese Bytes an MySQL schicken, nachdem Sie ihm mit SET NAMES utf8; mitgeteilt haben, dass diese Bytes, als UTF-8 interpretiert, hübsche Zeichen ergeben, wird MySQL das auch verstehen und dann schließlich ganz ohne Ihr Zutun die Zeichenkette auf die Festplatte schreiben, in dem Zeichensatz, der in der Spaltendefinition steht.

Was noch fehlt, ist PHP selbst. Denken Sie an das obige Beispiel mit dem strlen("ä"), das 2 als – nun gar nicht mehr so – überraschendes Ergebnis liefert, weil ein „ä“ in UTF-8 eben zwei Bytes entspricht. Denken Sie an die unzähligen Vorkommen von strlen, strpos oder auch strtoupper: Letztere Funktion kann natürlich aus einem „u“ ein „U“ machen. Aber kann sie auch aus einem „ü“ ein „Ü“ machen? Wenn Sie bedenken, dass PHP einen String als eine Folge von Bytes ansieht, dann sollten Sie sich vor Augen halten, dass die zwei Bytes, die in der UTF-8-Kodierung ein „ü“ ergeben, im Latin1-Zeichensatz für zwei einzelne Zeichen stehen, nämlich für ein „Ó und ein „¼“. Diese Zeichen in Großbuchstaben umzuwandeln, dürfte schwierig sein. Das findet dann auch PHP:

$ cat test-strtoupper.php
<?php echo strtoupper("über"); ?>

$ php test-strtoupper.php
üBER

Ihnen wird schon ganz übel? Gut so, das ist der erste Schritt zur Heilung.

Hier kommt die mbstring-Erweiterung ins Spiel. Wenn Sie PHP nicht gerade selbst kompiliert haben (wo mbstring gerne vergessen wird), ist es typischerweise bei allen Distributionen mit einkompiliert. Schon mit phpMyAdmin werden Sie zeichensatzmäßig keine große Freude haben, wenn mbstring fehlt – so wenig, dass schon auf der Startseite deutlich gewarnt wird, dass Sie sich gar nicht einbilden müssen, irgendwie korrekt dargestellte Zeichen zu erwarten.

Mit der mbstring-Erweiterung bekommen Sie für nahezu alle stringverarbeitenden Funktionen Pendants mit dem Präfix mb_, der genau das gleiche tut, aber zeichensatzsensibel ist:

$ cat test-mb_strlen.php
<?php
echo strlen("ä") . "\n";
echo mb_strlen("ä", "utf8") . "\n";
?>

$ php test-mb_strlen.php
2
1

Ja, genau: Sie müssen den mb_-Funktionen den Zeichensatz mitteilen, in dem es den String aus Bytes interpretieren soll. Das sollte Sie inzwischen aber nicht mehr wundern. Sie können alternativ in der php.ini (oder mit ini_set()) auch mbstring.internal_encoding auf den gewünschten Zeichensatz setzen und sich die Angabe sparen.

Bleibt aber noch, überhaupt erstmal überall den mb_-Präfix einzustreuen.

Möglicherweise stöhnen Sie jetzt und lassen vor dem inneren Auge Tausende von Codezeilen vorbeiwandern, die anzupassen sind. Dafür hat sich das PHP-Team mbstring.func_overload ausgedacht. Das bedeutet nichts anderes, als dass die wichtigsten nicht multibytefähigen Stringfunktionen kurzerhand auf ihre multibytefähigen Pendants „umgebogen“ werden. Das kann allerdings nur in der php.ini, einer .htaccess-Datei oder in der httpd.conf gesetzt werden – nicht mit ini_set() im PHP-Script. Zu den nur verzeichnisspezifischen Einstellungen bemerkt die Dokumentation dann aber auch gleich:

It is not recommended to use the function overloading option in the per-directory context, because it’s not confirmed yet to be stable enough in a production environment and may lead to undefined behaviour.

Es bleibt also eigentlich nur der Einsatz in der php.ini – und genau dort ist er prädestiniert für „fix one, break another“: Vielleicht funktioniert Ihre Applikation danach problemlos; eine andere geht dafür kaputt. Sofern Sie den gesamten Server mit allen PHP-Scripts selbst kontrollieren, kann function overloading eine Option für Sie sein, unter Vorbehalt. Aber spätestens dann, wenn Sie Ihre Scripts z.B. auf eine andere Maschine portieren möchten oder gar auf einen Webspace, bei dem Sie gar keinen Zugriff auf die php.ini haben, stehen Sie dumm da und wünschen sich, doch lieber überall die Funktionen mit dem mb_-Präfix verwendet zu haben.

Bei PHP 6 wird dann übrigens alles anders. Dort wird es dann für Unicode-Strings und für Byte-Strings zwei verschiedene Typen geben, und die ganzen stringverarbeitenden Funktionen werden sich automatisch entsprechend des Typs des Strings korrekt verhalten. Soll heißen: Was in der guten alten Zeiten von Latin1 noch „einfach so“ funktionierte, wird mit PHP 6 dann vermutlich auch „einfach so“ funktionieren, mit schickem Unicode-Support.

Nur heute – heute ist es ein K(r)ampf. Nehmen Sie lieber Perl.

Update: Christian hat die Aufforderungen des Artikels erfreulich ernst genommen, bemerkt aber auch zerknirscht, dass seine Frau die Zettel wieder vom Kühlschrank weg haben möchte. Aber der Gedanke zählt!

I/O-Lastprobleme durch Logging

Montag, 05. Oktober 2009

Ein Kundenserver machte Ärger. Dass bei einem iowait-Wert von konstant über 90% nicht mehr viel zu wollen ist, dürfte klar sein. Die Maschine ist aber nicht grundsätzlich überlastet: Die Tage davor schnurrte sie noch wie ein Kätzchen.

Mittels iostat war schnell herausgefunden, dass die I/O-Last nicht auf einen Plattendefekt o.ä. zurückzuführen war, sondern wirklich tonnenweise Daten auf die Platten geschrieben wurden.

Leider sind solche Probleme oft schwierig zu analysieren, weil es für I/O-Last im Standard-Linux-Kernel kein Accounting gibt – sprich, man kann nur sehen, auf welchem Blockdevice die Last stattfindet, aber nicht, wer sie verursacht.

Einigermaßen zügig konnten wir „irgendwas mit dem Webserver“ ausmachen, denn wenn der gestoppt war, lief die Maschine wieder ruhig. Also haben wir kurzerhand den server-status-Handler im Apache aktiviert und dazu ExtendedStatus eingeschaltet. Auf diese Weise bekommt man eine schöne Prozessliste der laufenden Requests zu sehen. Und wir hatten Glück: Ziemlich schnell ließ sich eine Site ausmachen, bei der Andreas ein error_log vorfand, das inzwischen rund 42 GB groß war – angesichts der bereits rotierten error_logs, die nur weniger MB groß waren, lag auf der Hand, dass es hier Probleme gibt. Über das error_log fanden wir folgende Zeilen PHP-Code:

...
$handle=fopen($filename,"r");
while(!feof($handle))
...

Das geübte Auge erkennt sofort: Es wird nicht geprüft, ob $filename überhaupt zum Lesen geöffnet werden konnte. Konnte es nämlich nicht, weil die fragliche Datei gar nicht existierte. Wenn es nun aber kein $handle gibt, kann feof() ergo auch niemals am Ende von $handle ankommen – die Schleife läuft somit endlos. Lustigerweise entspricht dieser vorgefundene Code-Abschnitt ziemlich genau dem Negativbeispiel aus der PHP-Doku, und die Kommentare der Seite sind voller Lösungen, wie man sowas richtig programmiert.

Zurück zum Lastproblem. Das entsteht natürlich aus dem Logging, denn für jeden Schleifendurchlauf meckert PHP an, dass man feof() auf etwas ausführt, das kein geöffnetes Filehandle ist. grep sagt uns, dass das etwa 40.000 Mal im error_log steht. Pro Sekunde, wohlgemerkt – kein Wunder, dass das error_log nicht mal den Vormittag über brauchte, um langsam aber sicher die Platte zu füllen und gleichzeitig den kompletten Server auszubremsen. Man kann ja vieles accounten: CPU-Zeit, RAM, Prozessanzahl … aber dummerweise nicht ohne weiteres, wieviel Log-Output ein Script so generieren darf.

SCRIPT_NAME unter PHP+FastCGI

Mittwoch, 26. März 2008

Auf mehreren Kundenservern setzen wir aus Sicherheitsgründen PHP unter FastCGI in Kombination mit suEXEC ein, in Anlehnung an diese Doku. Das läuft auch weitestgehend unproblematisch, allerdings stehen SCRIPT_NAME und SCRIPT_FILENAME in diesem Fall nicht auf dem Namen des PHP-Scripts, sondern auf dem des PHP-Interpreters, was einige PHP-Scripts etwas durcheinanderbringt.

Für diejenigen, die das für Bug #19656 halten: Nö. Viel simpler ist es mit dem Aktivieren von cgi.fix_pathinfo in der php.ini gemacht.

vorher:

_SERVER["SCRIPT_FILENAME"] = /var/www/virtual/site174/fcgi-bin/php4-fcgi-starter
_SERVER["SCRIPT_NAME"] = /fcgi-bin/php4-fcgi-starter

nachher:

_SERVER["SCRIPT_FILENAME"] = /var/www/virtual/site174/html/shop/info.php
_SERVER["SCRIPT_NAME"] = /shop/info.php


Impressum