Benutzer:Rolf b/PHP Performance

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

„Das schlimmste, was einer Website zustoßen kann, ist Erfolg!“

Dieser Satz klingt zynisch, und ist doch Realität. Denn eine Webseite, die bei schwacher Last problemlos läuft und flott reagiert, kann einem Benutzeransturm oft nicht standhalten. Gründe gibt es dafür viele. Nicht alle sind innerhalb Ihres PHP Scripts zu finden, und auf diese externen Gründe soll hier auch nicht weiter eingegangen werden.

Um Performance-Engpässe innerhalb Ihres Scripts zu finden, muss man vor allem eines tun: Messen. Und sollte sich dabei herausstellen, dass das Script selbst sehr schnell ist, die Seite aber trotzdem langsam reagiert, ist es an der Zeit, entweder nach Fehlern im Messverfahren zu suchen, oder nach externen Gründen Ausschau zu halten.

Dieser Artikel möchte einen Überblick über mögliche interne Performance-Engpässe geben und ein selbstgebautes Tool für Performance-Messungen vorstellen. Grundkenntnisse in objektorientierter Programmierung sind dabei hilfreich.

Hauptgründe für schlechte Performance

Eine langsam reagierende Webseite ist zumeist auf langsam reagierende externe Ressourcen zurückzuführen. Dabei handelt es sich vor allem um den Session-Speicher, und um Datenbankzugriffe.

Man muss allerdings eines dabei beachten: Wenn man Blockaden im Zugriff auf externe Ressourcen auflöst, ist die Folge, dass der Webserver mehr PHP Requests bearbeiten kann. Damit steigt die Last für Prozessor und Arbeitsspeicher. Das Beseitigen eines Engpasses kann schlimmstenfalls dazu führen, dass man lediglich den nächsten Engpass entdeckt.

Sessions

Beim Sessionspeicher muss man beachten, dass der PHP-interne, dateibasierende files Sessionhandler die Session-Datei sperrt, sobald sie beim Sessionstart gelesen wird, und sie erst wieder freigibt, wenn die Session geschlossen wird. Um feststellen zu können, ob Ihr Script beim Öffnen der Session aufgehalten wird, ist der erste Schritt, den Autostart der Session abzuschalten (session.auto_start in der php.ini) und sie mit der Funktion session_start zu Beginn Ihres Scripts selbst zu starten. Das gibt Ihnen die Gelegenheit, die Dauer des Session-Starts messen zu können.

Wenn die Session erst einmal gestartet ist, sind im files Sessionhandler keine weiteren Konflikte mehr möglich.

Konflikte beim Sessionzugriff sind vor allem dann möglich, wenn Ihre Seite im Browser mit JavaScript agiert und Daten per Ajax nachliest. Wenn mehrere dieser Ajax-Requests kurz nacheinander angestoßen werden und jeder die Session öffnen will, können sie nicht gleichzeitig laufen.

In solchen Fällen kann es sich lohnen, für jeden implementierten Zugriff genau zu überlegen, ob die Sessiondaten überhaupt benötigt werden. Wenn nicht, können Sie auf session_start ganz verzichten. Wenn Sie Sessiondaten benötigen, aber kein Update der Session erforderlich ist, können Sie der session_start-Funktion die Option 'read_and_close' mit dem Wert true mitgeben, so dass es zu keiner Sperrung kommt. Wenn zuvor ein Request gestartet wurde, der länger läuft und die Session blockiert, nützt das natürlich auch nichts.

Bei länger laufenden Requests kann es auch helfen, Änderungen an der Session möglichst früh im Script vorzunehmen und dann, wenn keine weiteren Änderungen mehr erforderlich sind, die Session mit session_write_close() zu beenden, um die Session freizugeben.

Bei anhaltenden Zugriffskonflikten auf den Sessionspeicher müssen Sie überlegen, Ihre Sessiondaten anderswo zu speichern, wo Sie Parallelzugriffe besser managen können.

Datenbankzugriffe

Datenbankzugriffe können aus verschiedenen Gründen langsam sein: ein überlasteter DB Server, Updatesperren durch andere Benutzer, und schlecht geschriebene eigene Queries. Einen heißlaufenden DB Server kann man möglicherweise durch Optimierung der eigenen Queries bereits wieder in den Griff bekommen. Irgendwann kommt natürlich der Punkt, wo Sie einen leistungsfähigeren SQL Server benötigen.

Um Queries zu finden, die Zeit vergeuden, muss man wiederum Messpunkte setzen. Aber gerade bei SQL Queries artet das sehr schnell in Arbeit aus. Deswegen sollten Sie von Anfang an überlegen, ihre SQL Zugriffe nicht blindlings im Code zu verteilen, sondern ein Datenbank-Framework zu verwenden. Damit soll kein Framework gemeint sein, das Ihnen SQL Zugriffe generiert oder ein Objektmodell auf die Datenbank abbildet. Wenn Sie so ein Framework im Einsatz haben, müssen sie sich genau anschauen, ob und wie Sie in das verwendete Framework Messpunkte für Datenbankzugriffe integrieren können.

Hier soll ein Basis-Framework angeregt werden, das die typischen Zugriffe, die ein PHP Script macht, einkapselt: SELECTs für Einzelzeilen, Mengen-SELECTs, sowie INSERTs und UPDATEs einzelner Rows. Der Gedanke dabei ist, für jeden DB-Zugriff ein Objekt zu erzeugen, das den Zugriff managed. Wenn die Erzeugung abstrakt über eine Factory erfolgt, können Sie in der Factory steuern, ob Sie für bestimmte Zugriffe ein Objekt mit oder ohne eingebaute Messtechnik bereitstellen. Die Überlegung dabei ist, dass es sehr unschön ist, wenn man für eine Performance-Messung zunächst größere Codeänderungen durchführen muss. Gerade bei SQL Zugriffen ist es wünschenswert, dafür einen Schalter zu haben, den man nur umlegen muss und der im AUS-Zustand möglichst wenig Leistung verbraucht. Die konkrete Implementierung kann hier nur angedeutet werden.



In den meisten Fällen ist die Frage einfach: In den SQL Abfragen. Irgendeine ist nicht gut optimiert, ein Index fehlt, Gründe gibt es viele. In einem größeren PHP Script mit vielen Zugriffen auf externe Ressourcen ist das Auffinden einer Performance-Engstelle aber nicht immer leicht. Deshalb soll in diesem Artikel ein Instrumentenkasten entwickelt werden, mit dem man Engpässe aufdecken kann.

Kapselung von Kommunikationskomponenten

Wenn Code, der potenziell Zeit verbraucht, durch das ganze Script verteilt ist, ist eine Zeitmessung umständlich. Besser ist es, solche Komponenten zu kapseln. Betrachten wir dafür als Beispiel die Datenbankzugriffe. Die allermeisten lesenden DB-Zugriffe folgen einem festen Pattern:

  • Optional: Prepare
  • Query
  • Fetch-Loop

Wenn man dieses Pattern auf Standardkomponenten aufbaut, kann man auch gleich eine Performancemessung integrieren. Im fachlichen PHP Code könnte dann an Stelle von

$query = $db->query("SELECT foo, bar, baz FROM sometable WHERE id=$id"); if (!$query) {

  // Log Error here
  throw new Exception("SQL Query failed");

} while (($row == $db->fetch()) {

  // process row

}

so etwas stehen wie

$query = new SqlSelect($db, "cat3", "SELECT foo, bar, baz FROM sometable WHERE id=$id"); foreach ($query as $row) {

  // process row

}

SqlSelect wäre ein Objekt, das passend zum gewählten DB-Treiber implementiert ist. Es führt die Query durch, wirft bei Fehlern eine Exception, loggt, und implentiert dann noch das Iterator Interface, um die Rows aus der Query in einer foreach-Schleife zu verarbeiten. Für sehr viele DB-Abfragen ist eine solche standardisierte Abfrage möglich. Der Konstruktor einer solchen SqlSelect Klasse könnte drei Argumente übergeben bekommen: Das Datenbankobjekt, eine Logging-Kategorie und das DB-Statement. Die Kategorie wird genutzt, um Fehlerlogs zu markieren und kann auch zum Gliedern einer Performancemessung dienen.

Beachten Sie, dass hier kein Exkurs in eine SQL Kapselschicht gemacht werden soll. Das wäre ein Thema für einen eigenen Artikel. Es geht nur um die Idee, wie man eine solche Kapsel an eine zentrale Performancemessung anbinden kann.

Solche Kapseln kann man bei Bedarf auch den Zugriff auf andere Subsysteme bereitstellen.

Instrumentierung

Es ist oft nicht sinnvoll, eine Performancemessung über den ganzen Code auszuführen. Wenn man das exzessiv tut, kann die Messung einen neuen Engpass hervorrufen. Statt dessen sollte man auf geeignete Weise