PHP und Apache Rewrite von Web-Requests – Ausschluss von Dateien des Typs CSS, JPG, etc. ?

Gestern bin ich in eine klassische Falle im Zusammenhang mit Apache Rewrites gestolpert.

Für ein CMS-Projekt hatte ich in einer “.htacces”-Datei eines Apache-Servers Rewrite-Direktiven für externe HTTP-Requests nach HTML-Dateien hinterlegt. Das CMS arbeitet intern ausschließlich mit PHP-Dateien und Parametern zur Erzeugung von Webseiten. Nach außen hin werden aber reguläre Adressen von HTML-Dateien angeboten. Angeforderte HTML-Seiten müssen daher auf dem Server auf bestimmte Generatorprogramme und zugehörige GET/POST-Parameter abgebildet werden.

Rewriting ist für solche Anforderungen eine Standardlösung (siehe etwa auch das Vorgehen von WordPress):

Der Request wird an eine zentrale PHP-Datei weitergereicht. Diese zerlegt den URL-String der angeforderten HTML-Datei; über Datenbank-Informationen werden dann Parameter für Webseitengeneratoren (PHP-Programme) ermittelt. Die zentrale Datei gibt danach die Kontrolle an die Generatoren ab. Die notwendige Datenbankinformation wird vom CMS bereits während der Anlage und Konfiguration der Webseiten durch den User erzeugt.

Im meinem Fall war ich bzgl. der Rewrite-Anweisung allerdings ein wenig bequem:

Alle (!) Abfragen zu nicht existierenden Dateien wurden zur Behandlung an eine zentrale PHP-Datei “pager.php5” meines CMS verwiesen.

Das funktionierte auch wunderbar – solange nur HTML-Dateien abgefragt wurden, zu denen die Website Links anbot und die im CMS auch mal angelegt worden waren. Traten bzgl. solcher Anfragen Fehler auf oder lies sich aus der Datenbank keine adäquate Info zur angeforderten HTML-Seite ermitteln, wich das PHP-Programm “pager.php5” kontrolliert auf Fehlerroutinen aus.

Nun sah ich bei der Überprüfung des Netzwerkverkehrs bei bestimmten Seiten allerdings, dass es gleich zig-fach zu einem wiederholten Abrufversuch für eine Datei “err_page.php5” in einem bestimmten Bild-Verzeichnis kam; diese PHP-Fehler-Datei existierte dort jedoch gar nicht und war dort auch nie vorgesehen.

Ursachenanalyse

Tatsächlich rufe ich solche PHP-Files zur Behandlung bestimmter Fehler auf, die im CMS im Zuge der Seitengenerierung entstehen können. Allerdings nicht in einem Bildverzeichnis ….

Nach einer Weile fand ich heraus, dass das Problem dennoch durch eine angeforderte, aber auf dem Test-Server nicht vorhandene Bilddatei ausgelöst wurde.

Das war keineswegs so einfach zu erkennen, wie man vielleicht meinen möchte – bei nicht vorhandener Datei übernimmt ja ordnungsgemäß “pager.php5” die Kontrolle – und somit erscheint im Browser nicht zwingend eine Warnung. Eine Warnung auf HTTP-Ebene würde im Einzelfall ja das gezielte Absetzen einer HTTP-Protokoll-Meldung im Verlauf der Situationsbehandlung erfordern. So schlau war ich bei der Konzeption aber nicht gewesen.

Ich dachte deshalb zunächst an einen Fehler in einer PHP-Routine zur automatischen Bildskalierung auf vom CMS-User vorgegebene Größen. Ein Fehler bzw. eine Fehlerbehandlung für nicht existierende Bilddateien in der festgestellten Form lag dort aber nicht vor.

Weitere Tests und ein genauerer Blick in den HTTP-Verkehr zeigten schließlich, dass der “Referrer” der fehlerhaften Datei-Anforderung eine CSS-Datei war! Selbige CSS-Datei existierte und wurde auch ordnungsgemäß gefunden.

Was war das eigentliche Problem?

In der CSS-Datei gab es eine Anweisung der Art

background-image:url(Pfad-zum-(fehlenden)-Bild);

für ein Hintergrundsbild – leider für eines, das auf dem Server nicht existierte.

Der entsprechende Abruf führte dann in Kombination mit der Rewrite-Anweisung zu einer Reaktion nach dem Muster

  • Abruf nicht existierende Datei aus CSS-Anweisung
  • => pager.php5
  • => Auslösen
    einer “Fehlerbehandlung” durch eine err_page.php5, die aus Gründen mangelnder Voraussicht im Bildverzeichnis erwartet wurde, dort aber nicht existierte
  • => Abruf einer nicht existierenden PHP-Datei
  • => pager.php5 =&gt. Erneuter Verweis auf Fehlerbehandlung durch eine nicht existierende “err_page.php5”
  • => Abruf einer nicht existierenden PHP-Datei
  • etc., etc.

Apache versucht es dann mehrfach und bricht schließlich ab.

Lösungsansatz 1: Klammere Dateien bestimmter Typen aus der Rewrite-Anweisung aus

Das Erlebnis brachte mich dazu, genauer darüber nachzudenken, wie ich eigentlich mit Rewrites normaler Dateien der Typen “.jpg, .gif, .png, .swf, .css, .js” etc. umgehen sollte, für die eine Ersetzung durch PHP-Programme gar nicht vorgesehen ist.

Eine Lösungsvariante ist das Ausklammern dieser Dateitypen von der Rewrite-Anweisung in der “.htaccess”-Datei. Das sieht im einfachsten Fall etwa so aus:

Options +FollowSymLinks
RewriteEngine On
RewriteBase /
RewriteRule ^php/hmenu/pager.php5(.*)$ - [L] 

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule \.(js|css|ico|gif|jpg|png|swf|ttf|eot)$ - [NC,L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ my_Rw_Php_Path/pager.php5?adr=$1 [PT]   

 
Hier werden zwei “Condition/Rewrite”-Sequenzen eingesetzt, da ohne besondere Tricks (Skip-Direktiven) zu einem Block aus Condition-Anweisungen nur genau eine Rewrite-Anweisung gehören sollte. “NC” sorgt für eine Nichtbeachtung von Groß-/Klein-Schreibung. “L” beendet die Rewrite-Analyse. “my_Rw_Php_Path” steht für einen Pfad zu einem Serververzeichnis, das die zentralen Programme zur Rewrite-Behandlung beherbergt.

Wird nun eine nicht vorhandene Datei der genannten Typen von einem Web-Client angefordert, wird diese Anforderung durchgereicht und vom Apache-Server mit HTTP-Fehlern der Art “404 Not Found” quittiert. Das reicht in Testphasen zur Prüfung der Lauffähigkeit einer CMS-basierten Website normalerweise aus.

Lösung 2: Behandle fehlende Dateien bestimmter Typen als Sonderfälle in einer zentralen PHP-Datei

Eine kontrollierte Reaktion des Systems auf nicht vorhandene Dateien bestimmter Typen jenseits von HTML-Dateien lässt sich natürlich auch in einer weiteren zentralen PHP-Datei (etwa “missing.php5”) vorsehen, auf die eine gesonderte Rewrite-Anweisung verweist. Beispielsweise könnte man den mittleren Teil der obigen “.htaccess” in diesem Sinne ersetzen durch:

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)\.(js|css|ico|gif|jpg|png|swf|ttf|eot)$ my_Rw_Php_Path/missing.php5?missadr=$1\.$2 [NC,L]

Bzgl. der Problembehandlung in der “missing.php5” muss man sich aber genau überlegen, für welche Dateien man tatsächlich eine offene und für den User auch erkennbare Fehlermeldung vorsehen will. Ein fehlendes Bild z.B. ist meist nicht überlebenskritisch.

Ich tendiere im Moment dazu, gezielt Meldungen in eine eigene Log-Datei auf dem Server zu schreiben, die man sowohl im Test- als auch Produktivbetrieb regelmäßig auswertet. Ein Minimal-PHP-Skript “missing.php5” könnte für diesen Zweck dann in etwa so aussehen:

<?php
$missadr = 'unknown'; 
if (isset($_GET['missadr']) ) {
	$missadr = $_GET['missadr']; 
}

$fh = fopen("missing.log", 'a+'); 
$out_str = "\r\n" . date('d.m.Y :: H.I.s') . " :: A requested file (" .$missadr . ") is missing"; 
fputs($fh, $out_str)
; 
fclose($fh); 

header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
exit;
?> 

 
Natürlich wäre das in dieser Einfachheit fahrlässig; der Inhalt von $_GET[‘missadr’] ist im produktiven Einsatz zu prüfen und ggf. zu bereinigen, um den Inhalt als Teil eines Angriffsvektors auszuschalten. In diesem Artikel geht es aber nur um einen ersten Ansatz.

Der Header-Output ist wichtig; durch ihn kann man z.B. auch in Browser-Tools (bei FF etwa in der Web-Konsole) erkennen, dass ein Fehler vorliegt und eine Datei tatsächlich nicht vorhanden ist.

Ein typischer Output in der Datei “missing.log” hat nach zwei Aufrufen bestimmter Webseite, für die indirekt eine Bilddatei “hg_dxm_7.jpg” angefordert wird, dann ggf. folgenden Inhalt:

27.06.2017 :: 12:0:38 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:39 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:02 :: A requested file (image/hg_dxm_7.jpg) is missing
27.06.2017 :: 12:0:02 :: A requested file (image/hg_dxm_7.jpg) is missing

Man erkennt hier an der Zeitangabe, dass die fehlende Datei pro Seitenaufruf gleich zweimal angefordert wird; in meinem Fall aus einer CSS-Datei heraus, aber auch direkt über ein HTML-Tag.

Fazit

Nicht nur in einem CMS will man ggf. Requests nach HTML-Dateien durch den gezielten Einsatz von PHP-Webgeneratoren beantworten. Die Nutzer (und auch Suchmaschinen) glauben, reguläre HTML-Dateien abzurufen. In Wirklichkeit sind die Dateien nicht vorhanden; Apache Rewrites sorgen vielmehr für die Erzeugung von HTML-Seiten durch PHP-Programme.

Zu einfach gehaltene Rewrite-Anweisungen für nicht vorhandene Dateien können dabei allerdings schnell zu schwer zu durchschauenden bis rekursiven Fehlern führen. Fordern HTTP-Requests evtl. nicht vorhandene Dateien eines bestimmten Typs an, für die eine gezielte Ersetzung gar nicht vorgesehen ist, so hängt es allein von der Voraussicht der Entwickler ab, was im Detail über Ersetzungen passiert. Es empfiehlt sich deshalb, solche Datei-Anforderungen

  • entweder von vornherein aus der Rewrite-Behandlung auszuschließen
  • oder sie aber einer gezielten Sonderbehandlung durch eine eigene PHP-Datei zuzuführen. Dabei sollten angemessene HTTP-Antwortcodes erzeugt werden.

WordPress-Blog mit Themes in eine neue Domäne auf einem anderen Apache-Server umziehen

Gestern hat mich ein Problem meiner Frau etwas Nerven gekostet:
Sie erstellt gerade eine Webseite für eine Firma auf Basis von WordPress [WP] unter Einschluss des Pinnacle Themes. Ich selbst bin ehrlich gesagt kein Freund solcher viel zu komplexen und deshalb wenig performanten Themes. Aber ich hatte da nichts zu entscheiden.

Jedenfalls stand am gestrigen Morgen der Transfer der WP-Installation von einer gehosteten Testdomäne bei “1&1” zur endgültigen Zieldomäne des Auftraggebers an. Letztere wird auch gehostet – allerdings beim Provider “Strato”. Unter “Hosting” verstehe ich dabei klassisches Web-Hosting: Man erhält auf einem Server einen per “chroot” abgesicherten Web-Space. Man teilt sich aber die Ressourcen des Webservers mit anderen Usern und hat keinerlei Kontrolle über die Serverkonfiguration selbst.

Schritte für einen WP-Umzug zu einer anderen Domäne und auf einen anderen Webserver

Zu bewältigen war also ein WordPress-Umzug zwischen zwei Domänen bei zwei klassischen deutschen Web-Hosting-Providern. Beide Provider nutzen Apache- und MySQL-Server. Man stellt sich den Umzugsprozess deshalb als gut beherrschbar vor. Als notwendige Teilschritte fallen einem sofort die folgenden ein:

  • Datenbank-Export vom Datenbankserver des alten Hosters (hier 1&1) auf ein eigenes Linux-System; den Export kann man mit PhpMyAdmin vornehmen. Wir erhalten dadurch ein Text-File mit den erforderlichen SQL-Statements für einen späteren Import und natürlich mit den Inhalten der Datenbankfelder selbst.
  • Änderung der Domain-Adresse [URL] und ggf. auch absoluter Server-Pfade in der SQL-Datei. Diesen Schritt kann man z.B. mittels eines schrittweisen “Search und Replace” unter “kate” oder einem anderen Linux-Editor vornehmen.
  • Datenbankimport des modifizierten SQL-Files in die Zieldatenbank eines MySQL-Servers beim neuen Provider; wieder mit PhpMyAdmin.
  • Kopieren der WordPress-Verzeichnisse und WP-Dateien per SFTP vom alten Web-Server (Webspace) auf einen Linux-Desktop und von dort weiter auf den Ziel-Webserver (Strato). Hierzu verwendet man ein FTP-Tool wie etwa Filezilla.
  • Änderung der Einträge in der “wp-config.php” (u.a. der Datenbank Access Daten) und Transfer der modifizierten Datei auf den Zielserver.

Leider ist ein WP-Umzug manchmal nicht ganz so einfach. Gestern stolperte ich in 2 typische Fallen. Die zweite davon war mir schon mal begegnet; ich hatte sie aber schlicht vergessen.

Problem 1: Erfordert der Zielserver bestimmte Gruppenrechte für den Zugriff auf Dateien?

Der Standard-Rechtekamm auf Web-Servern, die per FTP erreichbar sind, kann sich durchaus unterscheiden. Und manchmal sind gezielte Rechtesetzungen erforderlich, damit eine Web-Anwendung anstandslos läuft.

Interessant ist in diesem Zusammenhang u.a. eine Unterscheidung des Owners der zugänglichen Web-Verzeichnisse/Dateien von demjenigen User unter dem die Apache-Prozesse laufen. Von Bedeutung ist etwa die Gruppenzugehörigkeit des letzteren Users. Ich selbst lege eigene Apache2-Server oft so an, dass die Apache-Prozesse unter einem User laufen, der Mitglied einer Standard-Gruppe ist, welche wiederum den Webserver-Verzeichnissen zugeordnet wird. Für neu anzulegende Verzeichnisse/Dateien unterhalb des chroot-Pfades werden die Rechte dann über SGID und/oder ACLs gesteuert.

Nun gibt es aber leider keine allgemeingültigen Regeln, was die erforderlichen Zugriffsrechte auf verschiedene Dateitypen anbelangt, damit komplexe Web-Anwendungen funktionstüchtig laufen.

So kann man etwa die PHP-Ausführung sowohl in FastCGI- als Apache-Modul-Installationen so einrichten, dass der Apache-Prozess dabei die Rechte des PHP-File-Owners annimmt und mit diesen Rechten auf weiteren Dateien operiert (
“suexec”-Varianten). Anders mag es aber aussehen, wenn ein Webserver oder andere Prozesse unabhängig von PHP direkt auf Dateien zugreifen müssen. Beispiel: PHP erstellt auf Anforderung eines Browsers auf dem Server eine HTML-Datei; diese lädt im Browser JS-Programme nach und letztere wiederum fordern Bilder zur Darstellung im Browser an. Nehmen wir an, der Apache-Prozess gehöre zu der Gruppe von Nutzern, die auf die hochgeladenen Dateien des Webservers zugreifen darf. Dann muss die Gruppe ggf. zwar nicht unbedingt ein “read”-Recht haben, um PHP-Dateien ausführen zu können, wenn das über suexec-Mechanismen geregelt wird. Aber wenn ein nachgelagertes Javascript-Programm eine Bilddatei lesen muss, gibt es womöglich doch ein Problem, falls diese Bild-Datei auf dem Webserver nicht mit dem erforderlichen Gruppenrecht versehen wurde.

Warum werden Rechte im Kontext eines WP-Umzugs u.U. wichtig?
Installiert man WP, so analysiert WP selbst, unter welchen Rechten (bzw. welcher UID) ein PHP-Programm läuft – unter denen des Apache-Prozesses oder denen des File-Owners? Daran richtet sich dann die Installation aus. Was aber, wenn das Analyseergebnis auf einer Test-Umgebung nicht zur Ziel-Umgebung des späteren Produktivservers passt?

Genau eine solche Situation trat gestern in unserem Fall auf und führte gleich zwei Teil-Probleme mit sich.

Problem 1.1: Kann die “.htaccess”-Datei von relevanten Prozessen gelesen werden?
Ich beschreibe mal, was mir im Zuge unseres geplanten Umzugs passierte:
Auf dem Zielsystem bei Strato schien die von WP ursprünglich auf dem 1&1-Server angelegte und von uns kopierte “.htaccess”-Datei nicht zu wirken. Die “.htaccess” ist aber für den gesamten WordPress-Prozess entscheidend: Sie aktiviert die “Rewrite Engine” des Apache-Servers und verweist alle URL-Anfragen zu nicht existierenden Dateien und Verzeichnissen zur Auflösung an die zentrale “index.php” der WP-Installation.

Beim Aufruf bestimmter Dateien im Browser kam auf unserem Zielsystem laufend die Meldung, dass ich auf diese Dateien “nicht zugreifen dürfe”. Das geschah etwa, wenn ich den Zugriff auf HTML-Dateien versuchte. Löschte ich dagegen die “.htaccess” auf dem Zielserver und transferierte eine eigens neu angelegte HTML-Datei in das Domain-Verzeichnis auf dem Zielserver, so war alles OK. Die HTML-Datei wurde auf Anforderung anstandslos im Browser dargestellt.

Zunächst dachte ich an einen seltsamen Fehler in der “.htaccess”-Datei. Der Grund war aber ein anderer:

Mein FTP-Programm hatte die auf dem 1&1-Server geltenden Berechtigungen 1:1 auf mein lokales Linux-System abgebildet und diese Berechtigungen danach auch auf den Zielserver transportiert. (Die meisten FTP-Programme haben heute Parameter, die das Erzeugung des Rechtekamms auf dem Zielsystem steuern, soweit dieses die Rechtevergabe gem. eigener Regeln/ACLs überhaupt zulässt). Der Rechtekamm für die “.htaccess”-Datei war auf dem 1&1-Server ursprünglich aber “604” gewesen.
Auf dem Zielsystem bei Strato hing die Lese- (und auch die Schreib-) Berechtigung bestimmter Webserver-Prozesse dagegen offenbar an den Gruppen-Rechten! Mindestens mal für das Auslesen der “.htaccess”-Datei!

[In diesem Zusammenhang ist es übrigens bemerkenswert, dass bei Strato schon das Anlegen eines Dateischutzes per “.htaccess” irgendwelche besonderen Programme zu involvieren scheint …]

Bei 1&1 hingegen musste die Gruppe hingegen keine Zugriffsrechte auf die “.htaccess”-Datei haben; dort genügte vielmehr das Leserecht von “Others”. Offenbar weichen die Server-Installationen der beiden Provider für Web-Hosting deutlich voneinander ab!

Lesson learned:

Das Leserecht für die Gruppe ist auf manchen Serverinstallationen zwingend erforderlich, damit eine “.htaccess”-Datei ordnungsgemäß ausgewertet werden kann.

Ich kam in unserem Fall bei Strato zufällig drauf, als ich das “.
htaccess”-File auf meinem Desktop händisch neu anlegte, mit wachsendem Inhalt versah und immer wieder auf den Zielserver hochlud. Das führte erst zur Vergabe von Standardrechten für den Gruppenzugriff auf meinem Linux-Desktop (644); diese Rechte wurden nach dem SFTP-Transfer auch auf dem Server wirksam. Mit positiven Folgen! Die “.htaccess” griff nun endlich.

Problem 1.2: Können auch andere Dateien ohne Gruppenrechte gelesen werden?
Tja, meine einleitenden Sätze deuteten ja schon an, dass die Politik von Providern hier nicht zwingend einheitlich aussehen muss. Der ursprüngliche, von 1&1 vielfach kopierte Rechtekamm “604” erwies sich auf dem Zielserver jedenfalls auch bzgl. anderer Dateien als der “.htaccess” als problematisch: U.a. konnten vorhandene Bild- und Medien-Dateien nicht ausgelesen werden.

Das erklärt sich etwa wie folgt: Laufen bestimmte Server-Prozesse über User, die Mitglied der Gruppe, aber nicht der Owner der zu verarbeitenden Datei sind, so können diese Prozesse (ohne weitere besondere Massnahmen wie suexec) im Falle eines Rechtekamms “604” die Datei trotz des Leserechts von “Others” in keinem Fall lesen! [Off Topic: Das ist unter Linux nützlich, um bestimmte Gruppen von Usern definitiv vom Zugang zu bestimmten Dateien auszusperren, zu denen aber alle anderen Nutzer ungehinderten Lese-Zugang haben sollen.]

Selbst als die “.htaccess” wieder griff, kam es deshalb nach unserem WP-Umzug zu weiteren Fehlern – nämlich immer dann, wenn der Webserver Dateien lesen und bereitstellen sollte, für die die Gruppenrechte nicht hinreichend waren.

Ein pauschales Setzen der “644”-Berechtigung für relevante Zieldateien (bzw. “755” für Verzeichnisse) war per FTP-Programm dann aber schnell durchgeführt. Danach konnten auf dem Zielserver alle Webseiten angesprochen werden.

Wichtiger Hinweis: Die wp-config.php sollte natürlich nie für “Others” lesbar sein – also für diese Datei bitte einen Rechtekamm “600” oder “640” wählen!

Problem 1.3: Laufen PHP-Dateien evtl. auch ohne Gruppenrechte?
Der obige Befund deutet zunächst mal an, dass der Apache-Web-Prozess unter einem User läuft, der Mitglied der Gruppe ist, die beim Hoster Strato den Webspace-Dateien zugeordnet werden. Weitere Tests ergaben aber, dass der Webserver sehr wohl auf PHP-Dateien zugreifen und die ohne Gruppenrechte ausführen konnte. (Strato bietet die PHP-Ausführung im rahmen aktuellen Web-Hostings übrigens über eine FastCGI-Implementierung an.)

Ich zeige mal den Rechtekamm für verschiedene Dateien:

Man erkennt, dass die “.htaccess”-Datei nun das Gruppenrecht zum Lesen aufweist. Hingegen sind der Gruppe alle Zugriffsrechte auf die PHP-Dateien entzogen. Dennoch läuft WordPress inkl. Pinnacle Theme einwandfrei; auch andere, selbst angelegte PHP-Dateien lassen sich mit dem Rechtekamm “600” anstandslos ausführen. Probleme gab und gibt es jedoch bzgl. des Lesens von Dateien mit Rechten wie der “readme.html” auf dem Bild; der Abruf dieser Datei über einen Browser führt zu einer Fehlermeldung

Forbidden
You don’t have permission to access /readme.html on this server.

Also sind – wie oben angedeutet – spezielle “suexec”-Mechanismen für die Ausführung von PHP-Dateien mit den Rechten des Owners zu vermuten. Das lässt sich mit bestimmten Apache-Modulen bewerkstelligen.

Bei 1&1 sah ein funktionierender Kamm dagegen wie folgt aus:

Hier erfordert das Lesen und Bereitstellen von anderen als PHP-Dateien offenbar das “read”-Recht von “others”. Das mag nun jeder selbst bewerten.

Fazit:

Auf unserem gehosteten Strato-Webspace benötigt man für die Ausführung und das Lesen von PHP-Dateien nur einen Rechtekamm der Form “600”. Der Abruf und die Bereitstellung von anderen Dateien durch den Webserver verlangen aber mindestens einen Kamm der Form “640”.

Was bedeutet das für Leute, die eine WP-Installation nach einem Umzug besonders härten wollen und sich nicht mit pauschalen Rechte-Kämmen zufrieden geben wollen? Leider muss man sich auf dem gehosteten Web-Space mancher Provider – wie etwa Strato – selbst um eine unterschiedliche Rechtevergabe für PHP-Dateien im Gegensatz zu anderen Dateien kümmern. (Wohl dem, der dafür einen SSH-Zugang hat und zur Abänderung der Rechte mit Linux-Shell-Kommandos arbeiten kann!)

Lesson learned:

Web-Server-Konfigurationen sehen bei verschiedenen Web-Hosting-Providern unterschiedlich aus. Man sollte schon vor einem WP-Umzug von einem Provider zu einem anderen genauer studieren, welche Zugriffsrechte für eine volle Funktionalität erforderlich sind. Dabei sind vor allem Gruppenrechte interessant. PHP-Dateien können ggf. allein mit den Rechten des File-Owners ausgeführt werden. Der Zugriff auf andere Dateien kann dagegen aber zwingend Gruppenrechte erfordern. Ggf. müssen die Datei-Rechte beim Umzug also an die Erfordernisse auf dem Zielserver angepasst werden. Das Leserecht für die Gruppe kann speziell für den Zugriff auf die “.htaccess”-Datei relevant sein.

Nach dem Umzug und einer Vergabe der minimal erforderlichen Rechtekämme für die unterschiedlichen Dateitypen in den WP-Verzeichnissen sollte man übrigens unbedingt auch mal testen, ob sich WP selbst und auch alle Plugins noch über entsprechende WP-Funktionalitäten upgraden lassen!


Problem 2: Serialisierte Information in den Datenbanktabellen verträgt eine Änderung der Länge des Domain-Strings nicht!

Nach der Bereinigung der Rechte-Probleme funktionierte in unserem Fall zwar das Apache-Rewriting wieder – und auch alle erstellten WP-Seiten wurden im Browser wieder samt eingebundenen Bilddateien angezeigt. Aber leider nicht im gewünschten Layout!

Sämtliche im Usprungssystem vorgenommenen Theme-Einstellungen und eigene CSS-Einstellungen (und das waren viele!) funktionierten nicht mehr! Es schien zunächst so, als ob sämtliche Theme- und CSS-Einstellungen beim Umzug des WP-Blogs verloren gegangen wären! Es gab also noch ein Problem, das zu lösen war.

Es ist ein wenig unübersichtlich, wie WP-Themes modifizierte CSS-Anweisungen behandeln und wo sie sie hinterlegen. Ein Teil fließt in reguläre CSS-Dateien ein. Ein anderer Teil aber – wie z.B. CSS-Einstellungen durch den User – werden (je nach Theme) auch in Datenbanktabellen hinterlegt. Das ist etwa beim Pinnacle-Theme der Fall. Ich begab mich bei der Analyse des Problems also zunächst einmal in die Niederungen der WP-Datenbank-Einträge.

Betrachtet man das exportierte SQL-File einer WP/Pinnacle-Installation und sucht dort ein wenig, so findet man tatsächlich die spezifischen CSS-Vorgaben, die man als User/Admin vorgenommen hat, als Datenbankeintrag der “Options”-Tabelle wieder. Warum also zogen z.B. diese Anweisungen auf dem Zielsystem nicht?

Beim genaueren Hinsehen entdeckt man weiter, dass der betreffende String-Eintrag wie auch viele andere Feldeinträge in den WP-Tabellen die Form sog. serialisierter Arrays haben. Unter der Serialisierung eines Arrays einer Programmmiersprache (hier eines PHP-Arrays) versteht man die Transformation der (PHP-) Array-Information (indizierte Elemente und deren Inhalte) in einen nach vorgegebenen Regeln codierten String (mit definierten Trennzeichen und Hinweisen zum Array-Aufbau). Im Rahmen von WP und
WP-Themes erfolgt eine Array-Serialisierung i.d.R. durch die PHP-Funktion “serialize()”. (Oder für die Deserialisierung später durch das Pendant “unserialize()”).

Das Entscheidende dabei ist, dass die Länge der Sub-String-Information zu einem bestimmten Array-Element im serialisierten String über eine vorgestellte Zahl des entsprechenden String-Abschnitts kodiert wird. Bsp: s:5″Ralph”. Gemeint ist hier, dass die Information “Ralph” 5 Zeichen lang ist.

Siehe hierzu etwa https://stackoverflow.com/questions/8641889/how-to-use-php-serialize-and-unserialize oder die PHP-Doku!

Array-Serialisierung vereinfacht die persistente Hinterlegung komplex strukturierter Information in Datenbank-Feldern (z.T. zu Lasten der Performance). Leider machen sich viele der Entwickler dabei das Leben leicht: Unter WP bzw. WP-Themes taucht in etlichen Datenbankeinträgen innerhalb serialisierter Arrays leider auch die URL für die Blog-Domäne auf.

Ändert man nun, unbedarft wie ich, pauschal die Domänen-URL im SQL-File auf die neue Zieldomänen-URL durch “Search und Replace”-Funktionalität ab, so wird das nicht in allen Fällen ohne Nebenwirkungen bleiben. In allen Fällen, in denen sich die Domänen-Bezeichnung innerhalb eines Serialisierungs-Strings wiederfindet, wird es i.d.R. ein Problem geben:

Die Länge des URL-Strings zu einer Testdomäne wird ja nur sehr selten mit der Länge der URL der Zieldomäne übereinstimmen! Versuchen PHP-Programme von WP oder WP-Themes nach dem Umzug, die serialisierte Information wieder in PHP-Arrays umzuwandeln, geht das natürlich schief – mit katastrophalen Folgen. Da Grenzen überschritten werden, lässt sich die Information nicht korrekt aus dem String extrahieren. Dies führt dann u.U. dazu, dass unserialize() leere oder fehlerhafte Strings in den aufzubauenden Arrays zurückliefert. Ein Ergebnis: Wichtige Formatierungsinformationen von Themes stehen gar nicht oder nur fehlerhaft zur Verfügung.

Lesson learned:

Man muss die Ersetzung von Domänen-Strings in der SQL-Datei mit Bedacht vornehmen – und eben, wo nötig, auch die vorgeschaltete Längeninformation in Strings mit serialisierter Information abändern!

Nutze für den Datenbank-Export ein WP-Plugin!

Die Modifikation der Domän-URL mit gleichzeitiger Anpassung der Serialisierungs-Information kann man zwar händisch oder unter Linux auch mit Hilfe von kleinen Scripts (awk, sed mit Regex) durchführen. Aber netterweise gibt es auch ein WordPress-Plugin, das einem zur Seite steht – nämlich “WP Migrate DB” (von Delicious Brains):

Das Plugin erzeugt einem bei Bedarf ein Datenbank-Export-File, das die notwendigen Korrekturen der Serialisierungsstrings bereits beinhaltet!

Als Input muss man dem Plugin die zu ändernde Domänen-URL sowie den absoluten (!) Serverpfad des Blogs auf dem Ziel-Webserver mitgeben. Ja, auch letztere Information taucht tatsächlich, je nach WP-Theme, in einigen wenigen Datenbankeinträgen auf! Wie kommt man nun an die Info zum absoluten Serverpath? Am einfachsten über ein PHP-File mit dem Inhalt:

<?php
  echo getcwd();
?>

Das transferiert man per SFTP in sein Domän-Verzeichnis auf den Web-Server, ruft es anschließend im Browser auf und kopiert dann die ausgegebene Info in den Plugin-Dialog. Das SQL-Exportfile das Plugins (im UTF-8-Format) kann man schließlich wie gewohnt per PhpMyAdmin in die MySQL-Datenbank auf dem Zielserver laden.

Funktionierte bei mir anstandslos! Danach waren standen sämtliche Theme-Einstellungen wieder zur Verfügung und auch die privaten CSS-Anpassungen meiner Frau wurden korrekt gelesen und im Layout der Webseiten erwartungsgemäß umgesetzt.

Nebeneffekt: Wenigstens einmal konnte ich meine Frau mit Linux und ein wenig PHP-
Kenntnissen glücklich machen!

Performance of Linux md raid-10 arrays – negative impact of the intel_pstate CPU governor for HWP?

Last November I performed some tests with “fio” on Raid arrays with SSDs (4 Samsung EVO 850). The test system ran on Opensuse Leap 42.1 with a Linux kernel of version 4.1. It had an onboard Intel Sunrise Point controller and an i7 6700k CPU.

For md-raid arrays of type Raid10, i.e. for Linux SW Raid10 arrays created via the mdadm command, I was quite pleased with the results. Both for N2 and F2 layouts and especially for situations where the Read/Write load was created by several jobs running in parallel. Depending on the size of the data packets you may, e.g., well reach a Random Read [RR] performance between 1.0 Gbyte/sec and 1.5 GByte/sec for packet sizes ≥ 512 KB and %gt; 1024k, respectively. Even for one job only the RR-performance for such packet sizes lay between 790 MByte/sec and 950 Mbyte/sec – i.e. well beyond the performance of a single SSD.

Significant drop in md-raid performance on Opensuse Leap 42.2 with kernel 4.4

Then I upgraded to Opensuse Leap 42.2 with kernel 4.4. Same system, same HW, same controller, same CPU, same SSDs and raid-10 setup.
I repeated some of my raid-array tests. Unfortunately, I then experienced a significant drop in performance – up to 25% depending on the packet size. With absolute differences in the range of 60 MByte/sec to over 200 Mbyte/sec, again depending on the chosen data packet sizes.

In addition I saw irregular ups and downs in the performance (large spread) for repeated tests with the same fio parameters. Up to some days ago I never found a convincing reason for this strange performance variation behavior of the md-Raid arrays for different kernels and OS versions.

Impact of the CPU governor ?!

Three days ago I read several articles about CPU governors. On my system the “intel_pstate” driver is relevant for CPU power saving. It offers exactly two active governor modes: “powersave” and “performance” (see e.g.: https://www.kernel.org/doc/html/latest/admin-guide/pm/intel_pstate.html).

The chosen CPU governor standard for Opensuse Leap 42.2 is “powersave”.

Just for fun I repeated some simple tests for the raid array again – one time with “powersave” active on all CPU cores/threads and a second time with “performance” active on all cores/threads. The discrepancy was striking:


Test setup

HW: CPU i7 6700K, Asus Z170 Extreme 7 with Intel Sunrise Point-H Sata3 controller

Raid-Array: md-raid-10, Layout: N2, Chunk Size: 32k, bitmap: none

SSDs: 4 Samsung Evo 850

Fio parameters:

size=500m
directory=/mnt2
direct=1
ioengine=sync
bs=512k
; bs=1024k
iodepth=1
numjobs=1
[read]
rw=randread
[write]
stonewall
rw=randwrite


Test results:

Fio test case 1bs=512k, Random Read [RR] / Random Write [RW]:

Leap 42.1, Kernel 4.1:
RR: 780 MByte/sec – RW: 754 MByte/sec,
RR Spread around 35 Mbyte/sec

Leap 42.2, Kernel 4.4, CPU governor powersave :
RR: 669 MByte/sec – RW: 605 MByte/sec,
RR Spread around 50 Mbyte/sec

Leap 42.2, Kernel 4.4, CPU governor: performance :
RR: 780 MByte/sec – RW: 750 MByte/sec,

RR Spread around 35 Mbyte/sec


Fio test case 2bs=1024k, Random Read [RR] / Random Write [RW]:

Leap 42.1, Kernel 4.1:
RR: 860 MByte/sec – RW: 800 MByte/sec
RR Spread around 30 Mbyte/sec

Leap 42.2, Kernel 4.4, CPU governor: powersave :
RR: 735 MByte/sec – RW: 660 MByte/sec
RR Spread > 50 Mbyte/sec

Leap 42.2, Kernel 4.4, CPU governor: performance :
RR: 877 MByte/sec – RW: 792 MByte/sec
RR Spread around 25 Mbyte/sec

Interpretation

The differences are so significant that one begins to worry. The data seem to indicate two points:

  • It seems that a high CPU frequency is required for optimum performance of md-raid arrays with SSDs.
  • It seems that the Leap 42.1 with kernel 4.1 reacts differently to load requests from fio test runs – with resulting CPU frequencies closer to the maximum – than Leap 42.2 with kernel 4.4 in powersave mode. This could be a matter of different CPU governors or major changes in drivers …

With md-raid arrays active, the CPU governor should react very directly to a high I/O load and a related short increase of CPU consumption. However, the md-raid-modules seem to be so well programmed that the rise in CPU load is on average below any thresholds for the “powersave”-governor to react adequately. At least on Leap 42.2 with kernel 4.4 – for whatever reasons. Maybe the time structure of I/O and CPU load is not analyzed precisely enough or there is in general no reaction to I/O. Or there is a bug in the related version of intel_pstate …

Anyway, I never saw a rise of the CPU frequency above 2300 Mhz during the tests on Leap 42.2 – but short spikes to half of the maximum frequency are quite normal on a desktop system with some applications active. Never, however, was the top level of 4200 Mhz reached. I tested also with 2000 MB to be read/written in total – then we talk already of time intervals around 3 to 4 secs for the I/O load to occur.

Questions

When reading a bit more, I got the impression, that the admins possibilities to influence the behavior of the “intel_pstate” governors are very limited. So, some questions arise directly:

  1. What is the major difference between OS Leap 42.1 with kernel 4.1 compared to Opensuse 42.2 with kernel 4.4? Does Leap 41.1 use the same CPU governor as Leap 42.2?
  2. Is the use of Intel’s standard governor mode “powersave” in general a bad choice on systems with a md-raid for SSDs?
  3. Are there any kernel or intel-pstate parameters that would change the md-raid-performance to the better again?

If somebody knows the answers, please contact me. The whole topic is also discussed here: https://bugzilla.kernel.org/show_bug.cgi?id=191881. At least I hope so …

Addendum, 19.06.2017:
Answer to question 1 – I checked which CPU governor runs on Opensuse Leap 42.1:
It is indeed the good old (ACPI-based) “ondemand” governor – and not the “new” intel_pstate based “powersave” governor for Intel’s HWP. So, the findings
described above raise some questions regarding the behavior of the “intel_pstate based “powersave” governor vs. comparable older CPU_FREQ solutions like the “ondemand” governor: The intel_pstate “powersave” governor does not seem to support md-raid as well as the old “ondemand” governor did!

I do not want to speculate too much. It is hard to measure details of the CPU frequency changes on small timescales, and it is difficult to separate the cpu consumption of fio and the md-modules (which probably work multithreaded). But it might be that the “ondemand” governor provides on average higher CPU frequencies to the involved processes over the timescale of a fio run.

Anyway, I would recommend all system admins who use SSD-Raid-arrays under the control of mdadm to check and test for a possible performance dependency of their Raid installation on CPU governors!