PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


PHP beschleunigen mittels Caching: Zend_Cache

with 6 comments

Der Titel ist vielleicht nicht ganz korrekt: PHP selbst kann man durch Caching nicht direkt beschleunigen, aber PHP-Applikationen.

Sobald man mit Datenbanken und Objekten arbeitet, fällt einem schnell auf, dass man gern für vieles eigene Objekte bastelt. Häufig ist es so, dass es für fast jede Tabelle eine Klasse gibt, und jede Zeile einer Tabelle einem Objekt entspricht. Das artet recht schnell aus, sodass man sehr viele Objekte hat, die auch hier und dort mehrfach erstellt werden. Das kostet vor allem Rechenkapazität.

Hier mal einige Klassen, mit denen wir weiter unten arbeiten werden:

class App_User
{
	private $username;
	private $newsletter;
	
	public function __construct($id) {
		$db = Zend_Registry::get('Zend_Db');
		$data = $db->fetchAll('SELECT Username, Newsletter FROM User WHERE UserID='.$id);
		$this->setUsername($data['Username']);
		$this->setNewsletter($data['Newsletter']);
	}
	
	public function getUsername() {
		return $this->username;
	}
	
	public function setUsername($username) {
		$this->username = $username;
	}
	
	public function getNewsletter() {
		return $this->newsletter;
	}
	
	public function setNewsletter($newsletter) {
		$this->newsletter = $newsletter;
	}
	
	public static function getAllUsers() {
		$db = Zend_Registry::get('Zend_Db');
		$allIds = $db->fetchCol('SELECT UserID FROM User');
		
		$users = array();
		foreach ($allIds as $id) {
			$users[] = new App_User($id);
		}
		return $users;
	}
}

Wenn man nun zum Beispiel alle User der Webseite überprüfen will, ob sie den Newsletter empfangen wollen, tut man dies objektorientiert dann so:

$newsletterCounter = 0;
$allUsers = App_User::getAllUsers();
foreach ($allUsers as $user) {
	if ($user->getNewsletter()) {
		$newsletterCounter++;
	}
}

An anderer Stelle irgendwo anders im Code (möglicherweise tief in anderen Klassen versteckt) möchte man dann vielleicht noch alle User durchgehen und ihre Usernamen ausgeben:

$allUsers = App_User::getAllUsers();
foreach ($allUsers as $user) {
	echo $user->getUsername().'<br>';
}

Nehmen wir weiter an, wir haben 5000 User in unserer Datenbank. Was passiert nun? Richtig, es werden in beiden Fällen jeweils 5000 User-Objekte erzeugt, ein Attribut abgefragt, und dann braucht man sie nicht mehr. 10000 Datenbankabfragen + 10000 Objektinstanziierungen.

Was können wir dagegen tun? Es gibt mehrere Möglichkeiten. Wir können zum Beispiel nach dem ersten Aufruf der getAllUsers()-Funktion das Ergebnis in einer globalen Variablen speichern:

$allUsers = App_User::getAllUsers();
$GLOBALS['allUsers'] = $allUsers;

Das ist vergleichbar mit der Zend_Registry, es funktioniert intern ähnlich, ist aber weit schöner und ein Zugriff „aus Versehen“ wird vermieden:

$allUsers = App_User::getAllUsers();
Zend_Registry::set('allUsers', $allUsers);

Der Zugriff würde dann so aussehen:

Zend_Registry::get('allUsers');

Das würde zwar funktionieren, ist aber ziemlich unpraktisch, da man nie weiß, wo genau der erste Zugriff ist, man also nicht genau weiß, ob die Informationen bereits in der Zend_Registry sind oder nicht. Das bedeutet viele if-Abfragen und ist unhandlich. Also verschieben wir den „Cache“ etwas weiter nach „innen“, wir verändern die getAllUsers()-Funktion wie folgt:

public static function getAllUsers() {
	if (!Zend_Registry::isRegistered('allUsers')) {
		$db = Zend_Registry::get('Zend_Db');
		$allIds = $db->fetchCol('SELECT UserID FROM User');
		
		$users = array();
		foreach ($allIds as $id) {
			$users[] = new App_User($id);
		}
		Zend_Registry::set('allUsers', $users);
	}
	
	return Zend_Registry::get('allUsers');
}

Nun wird also beim Aufruf von getAllUsers() beim ersten Mal die Datenbank abgefragt, und das Ergebnis in der Zend_Registry gespeichert. Beim zweiten Aufruf wird nun das bereits gespeicherte Ergebnis genommen. Wir sparen uns also viele Datenbankabfragen und Objekterstellungen. Von „außen“ kann man die Funktion ganz normal verwenden, man merkt nicht, dass intern gecacht wird.

Zwischenstand: Wir können innerhalb eines Scriptes viele Abfragen sparen, indem wir Ergebnisse und Objekte in der Zend_Registry speichern und diese bei Bedarf wiederverwenden.

Doch wir können noch mehr an Performance gewinnen, indem wir prozessübergreifend cachen. Wenn also 10 Besucher innerhalb von 5 Sekunden auf unserer Webseite unterwegs sind, sollen diese wenn möglich die selben Daten teilen, sodass für diese 10 Besucher nur einmal die 5000 Datensätze abgefragt und die entsprechenden Objekte erstellt werden müssen. Das geht nun nicht mehr mit globalen Variablen bzw. Zend_Registry, man muß mittels gemeinsamen Speichers (zB Memcached-Server, Festplatte, Netzspeicher) diese Daten austauschen. Diese gemeinsamen Daten sollen allerdings nach einer gewissen Zeit „ungültig“ werden, sodass regelmäßig frische und aktuelle Daten aus der Datenbank geholt werden. Genau das alles kann Zend_Cache.

Zend_Cache besteht grundlegend aus zwei Schichten: Dem Frontend und dem Backend. Das Backend definiert man nur einmal am Anfang, indem man den gewünschten Storage wählt und spezifiziert. Zur Auswahl stehen derzeit: File, Sqlite, Memcached, Apc, Xcache, ZendPlatform, TwoLevels, ZendServer_Disk
Die gebräuchlichsten dürften die ersten vier sein.

Wenn wir nun beispielsweise Zend_Cache_Backend_File wählen, müssen wir nur den Dateipfad angeben, die anderen Einstellungen können wir vorerst vernachlässigen.

Das Frontend ist die Schicht, über die wir den eigentlichen Cache ansprechen. Hier stehen uns mehrere Möglichkeiten zur Verfügung: Wir können beispielsweise einfache Variablen cachen, aber auch ganze Funktionen, Klassen, Dateien oder Seiten. Wir wollen uns hier erstmal nur um Variablen kümmern, die anderen Dinge könnt ihr euch ja im ZF-Manual nachlesen.

Nun aber Butter bei die Fische:

$frontendOptions = array(
   'lifetime' => 60, // cache lifetime of 1 minute
   'automatic_serialization' => true
);

$backendOptions = array(
    'cache_dir' => './tmp/' // Directory where to put the cache files
);

// getting a Zend_Cache_Core object
$cache = Zend_Cache::factory('Core',
                             'File',
                             $frontendOptions,
                             $backendOptions);
Zend_Registry::set('Zend_Cache', $cache);

Hier haben wir nun den Cache erstellt. Hier sieht man auch, dass man eine Lifetime definieren kann. Liegt ein Element länger als eine Minute im Cache, wird es gelöscht und muß dementsprechend neu aus der Datenbank geholt werden.

public static function getAllUsers() {
$cache = Zend_Registry::get(‚Zend_Cache‘);
// see if a cache already exists:
if(!$allUsers = $cache->load(‚allUsers‘)) {
// cache miss; connect to the database
$db = Zend_Registry::get(‚Zend_Db‘);
$allIds = $db->fetchCol(‚SELECT UserID FROM User‘);

$allUsers = array();
foreach ($allIds as $id) {
$allUsers[] = new App_User($id);
}
$cache->save($allUsers, ‚allUsers‘);
}

return $allUsers;
}
Wie man sieht, es ist der Zend_Registry Lösung sehr ähnlich. Es braucht nicht mehr als 20 Zeilen, um Caching zu aktivieren und zu nutzen. Nun haben wir die Freiheit, das Backend zu wählen, die Lebensdauer der Elemente zu spezifizieren, und bei Bedarf kann man auch Tags setzen. Tags sind vor allem dazu da, alle Elemente mit einem bestimmten Tag gleichzeitig zu löschen. Näheres dazu auch im ZF-Manual.

Ich hoffe man sieht, dass man durch Caching ordentlich Performance gewinnen kann, und sowohl die Besucher als auch die Hardware schonen kann. Natürlich macht Caching nicht überall Sinn (bei einem Ajax-Chat wäre es wohl eher hinderlich), aber die meisten Inhalte ändern sich nicht sekündlich, sondern eher in größeren Zeitabständen, und wenn ein Besucher einige Minuten „veraltete“ Inhalte zu sehen bekommt, ist das nicht unbedingt schlimm.

Morgen poste ich einen Artikel, der auch zu diesem Themenkomplex passt, aber das ganze von der anderen Seite betrachtet.

Written by Michael Kliewe

Juli 9th, 2009 at 6:50 pm

Posted in PHP

Tagged with , ,

6 Responses to 'PHP beschleunigen mittels Caching: Zend_Cache'

Subscribe to comments with RSS or TrackBack to 'PHP beschleunigen mittels Caching: Zend_Cache'.

  1. […] nun will ich das Thema nachholen, was ich am letzten Donnerstag hab anklingen lassen. Am Wochenende ist dann jedoch das Gewinnspiel dazwischengekommen, sodass ich nun etwas zum Thema […]

  2. Ich würde gerne ein paar Datenbankabfragen cachen und habe nun die Performance getestet. Das Ergebnis (ich habe Dauer in Sekunden und Memory Usage verglichen) ist für mich ernüchternd:

    * Wenn man einen Datensatz 1000mal aus einer Tabelle mit 50 Einträgen ausliest:
    -> query: 0,7 sec / 7,25M
    -> cached: 0,5 sec / 4,5M
    * Wenn man einen Datensatz 1000mal aus einer Tabelle mit 7000 Einträgen ausliest:
    -> query: 0,8 sec / 7,25M
    -> cached: 0,5 sec / 4,5M
    * Wenn man alle Datensätze 1000mal aus einer Tabelle mit 50 Einträgen ausliest:
    -> query: 2,0 sec / 7,5M
    -> cached: 1,2 sec / 4,75M
    * Wenn man alle Datensätze 10mal aus einer Tabelle mit 7000 Einträgen ausliest:
    -> query: 1,6 sec / 10,5M
    -> cached: 2,7 sec / 7,5M
    * Wenn man alle Datensätze 1000mal (komplexe SQL-Query mit 6 joins) aus einer Tabelle mit 200 Einträgen ausliest:
    -> query: 2,7 sec / 16,25M
    -> cached: 0,5 sec / 4,5M

    Bei den ersten 3 Fällen ist der Performancegewinn ja in Ordnung, aber bei einer Tabelle mit vielen Einträgen habe ich mit Zend_Cache ordentliche Performanceeinbrüche.

    Die ersten 4 SQL Anfragen sind einfacherer Natur, also keine joins und max. eine where-Bedingung.

    Ist die Schlussfolgerung nun, dass man Zend_Cache am besten nur bei komplexen Datenbankabfragen nutzt, die als Ergebnis möglichst wenige Datensätze haben?

    Sven

    10 Okt 09 at 12:03

  3. Kannst du das Testscript irgendwo hochladen? Dann kann man da evtl. mehr zu sagen. Welches Backend hast du verwendet?
    Der 4. Test ist ja der einzige, den ich nicht ganz verstehe, wobei ich gedacht hätte dass auch bei den anderen Tests mehr Vorteile hätten sichtbar sein sollen. Test 2 und 4 sind gleich, außer einmal 1000 und einmal 10 mal? Warum ist Test 4 langsamer (auch nicht-cached)?

    Falls du das File-backend verwendet hast liegt es aber auch vielleicht an der langsamen Festplatte, Arbeitsspeicher wäre natürlich besser.
    Welche Datenbank hast du verwendet, mysql? Hast du dort den Query-Cache aktiviert oder deaktiviert?

    Michael Kliewe

    11 Okt 09 at 00:19

  4. Die Tests habe ich innerhalb meines Projektes angelegt, diese habe ich auch mittlerweile entfernt, aber ich habe im obigen Post ja beschrieben, wie die SQL-Anweisungen aussahen.

    Als Backend habe ich „File“ verwendet, die Datenbank ist MySQL. Der Query-Cache ist nicht aktiviert.

    > Test 2 und 4 sind gleich, außer einmal 1000 und einmal 10 mal?

    Nein, bei Test 2 und 3 lese ich ALLE Datensätze aus der Tabelle aus und cache diese (ist also eine größere Datenmenge bei ca 7000 Datensätzen mit ca 30 Spalten).

    > Falls du das File-backend verwendet hast liegt es aber auch vielleicht an der langsamen Festplatte, Arbeitsspeicher wäre natürlich besser.

    Dafür benötige ich ja eine PHP Erweiterung, ich glaube nicht, dass diese in meinem Shared Hosting mit dabei ist. Außerdem wird bei einer SQL-Anfrage an die Datenbank ja auch auf die Festplatte zugegriffen, oder sehe ich das falsch? Wenn ich eine langsame HDD habe, ist also Zend_Cache und die SQL-Query im gleichen Maße betroffen.

    Sven

    11 Okt 09 at 22:09

  5. Bitte nicht zu harsh verstehen, aber das Codebeispiel ist ja gerade so gewählt, dass es nicht gerade Sinnvoll ist. So ein Design ist der pure Overkill in jeder hinsicht, weil man pro User eine SQL-Query absetzt. Die Abfrage „Alle User“ ist dann dementsprechend mit einem Overhead belegt.

    Desweiteren macht Caching dann Sinn, wenn ich mir zB. den kompletten DB Layer von zend sparen kann, d.h. die ganze alte behäbige Dame erst gar nicht in den Speicher laden und initialisieren brauche. Deswegen sind die Ergebnisse vielleicht auch so ernüchternd.

    Soll nicht heissen, das Cachen schlecht sei, im Gegenteil cachen ist absolut legitim, man sollte sich halt überlegen an welcher Stelle.

    Die Notwendigkeit eines sinnvollen (Daten-) Designs kann man nicht durch Caching aufheben.

    Malte

    12 Okt 09 at 10:01

  6. […] Diese kann man entweder in der Session speichern, oder in memcached-Instanzen, oder oder. Über Zend_Cache und die verschiedenen Backends hatte ich ja auch bereits […]

Leave a Reply

You can add images to your comment by clicking here.