Du bist nicht angemeldet
Facebook Login
Hallo! Du bist nicht angemeldet. Wenn Du Dich anmeldest, dann kannst Du hier Kommentare verfassen und Dich mit anderen Nutzern austauschen. Es werden keine Daten von Dir ausspioniert und nach einer Stunde wirst Du hier automatisch wieder abgemeldet.
Weiterempfehlen

« PHP Performance Tuning »

(26.05.2011 18:40)

Erst kürzlich habe ich meinen Blog von WordPress befreit und die Software komplett neu entwickelt. Der Hauptgrund war, dass WordPress an sich - also dessen PHP Quellcode - sowie die MySQL Datenbank von WordPress alles andere als Performance optimiert sind. Ich wollte, dass mein Blog insbesondere hinsichtlich Suchmaschinenoptimierung die schnellstmögliche Ladezeit bietet und nicht wesentlich länger braucht als statische HTML Seiten. Soweit ist es mir, denke ich, auch sehr gut gelungen. In diesem Artikel beschreibe ich, was man beim Applikations- und Datenbankdesign mit PHP und MySQL beachten sollte, um eine schnelle Verarbeitung und sehr gute Performance garantieren zu können.

Effektives Datenbankdesign mit Views und Stored Procedures

Einige Leser werden mir vielleicht widersprechen, aber Datenbankgurus mit denen ich sprach stimmten mir zu: komplexe SQL Statements haben in PHP Applikationen nichts zu suchen. Zum Einen sind diese anfällig gegen SQL Injection und zum anderen rauben Sie der Datenbank die Möglichkeit die Performance zu optimieren.

Das Buch "High Performance MySQL", welches ich nur empfehlen kann, fordert dazu auf komplexe Abfragen in Views zu verpacken, sodass darauf einfach SELECT Statements durchgeführt werden. MySQL hat dann die Möglichkeit entsprechend seinen Cache aufzubauen und zu optimieren. Allein diese Tatsache beschleunigt den Vorgang ungemein, da die PHP Anwendung keine JOINs mehr durchführt, sondern diese direkt auf der Datenbank geschehen und MySQL dann Caching und Index-Optimierung durchführen kann. Die Architektur meines Blogs sieht demnach wie folgt aus.

Kammerath Network Datenbankdesign

Wie hier klar zu sehen ist gibt es für jede Frontend-Ansicht, also jedes Statement, welches die PHP Anwendung durchführen wird eine View, welche die Daten bereits aufbereitet. Alle INSERT, UPDATE oder DELETE Operationen werden durch Stored Procedures abgebildet. Die Procedures, welche "ADD" vorangestellt haben, sind für INSERT und UPDATE zuständig. Wird zum Beispiel ein Artikel per "ADD" eingefügt, den es bereits gibt, so macht das Procedure natürlich ein UPDATE. Der Vorteil dieser Implementierung ist neben des verbesserten Schutzes gegen SQL Injection auch die verbesserte Wartbarkeit der Anwendung durch die Isolierung der Datenbanktabellen von der Applikation. Wird die Datenbank erweitert oder umgestellt, muss die PHP Anwendung nicht zwangsläufig angepasst werden, sondern lediglich die Stored Procedures und die Views.

Aus Sicht der Datenbank oder eines Datenbankentwicklers ist das eine sehr gute Angelegenheit, denn man möchte nicht, dass eine Webanwendung sich um die Datenintegrität oder Performance des Datenbankmanagementsystems kümmert. Vom Standpunkt des PHP Entwicklers aus, ist dies zudem noch angenehmer, da nicht tonnenweise SQL Code mit PHP Code vermischt wird, sondern lediglich Stored Procedures und Views aufgerufen bzw. abgefragt werden. Ein anschauliches Beispiel bietet der folgende Code.

        /* returns a list of all articles in the database
	 * without the content in the articles */
	public function getArticleList(){
		$result = array();
		$sql = "SELECT * FROM article_list";
		$article_result = mysqli_query($this->link,$sql);
		$i = 0;
		while($row = mysqli_fetch_assoc($article_result)){
			$result[$i] = $row;
			// there is no timestamp when it comes out of the db
			// as the date is represented by a string and therefore
			// we do parse the proper timestamp here.
			$result[$i]["date_ts"] = strtotime($result[$i]["date"]);
			$i++;
		}

		mysqli_free_result($article_result);
		return $result;
	}

Hierbei sieht man, dass lediglich ein sehr simples SELECT statement auf die View "article_list" durchgeführt wird, was die Methode hier in der Klasse "database" sehr schlank macht. Schauen wir uns nun einmal das dahinterliegende SQL der View an.

DROP VIEW IF EXISTS kammerath.article_list;
CREATE VIEW kammerath.article_list AS
	SELECT 	kammerath.article.article_title AS title, 
			kammerath.article.article_url AS url,
			kammerath.article.article_body AS body,

			kammerath.article.article_keyword AS keyword,
			kammerath.article.article_published AS published,
			kammerath.article.article_date AS date,
			kammerath.user.user_firstname AS author_firstname,
			kammerath.user.user_lastname AS author_lastname
	FROM kammerath.article, kammerath.user
	WHERE kammerath.user.user_id = kammerath.article.article_author_id
	ORDER BY kammerath.article.article_date DESC;

Hier sehen wir, dass ein JOIN der Tabelle "article" und "user" durchgeführt wird. Dabei muss man jetzt im Hinterkopf behalten, dass JOIN Operation das wohl intensivste Verfahren auf einer Datenbank ist. Wir haben hier im Quelltext nicht nur unseren PHP Code verschlankt sondern geben MySQL auch die Chance die Daten im Cache zu halten und nicht jedes Mal auf ein Neues einen JOIN durchführen zu müssen. Der Performance-Zuwachs durch solche einfachen Dinge ist enorm.

Selbiges gilt natürlich dann insbesondere für komplexere Abfragen wie z.B. Suchalghoritmen, mit welchen man in der Datenbank z.B. Textsuchen durchführen. Dies ist im Falle meines Blogs die Suchfunktion, mit welcher der Benutzer Artikel suchen kann. Da möchte man natürlich nicht nur eine lapidare WHERE Kondition verwenden, sondern natürlich auch die Häufigkeit des Vorkommens berücksichtigen. Das Stored Procedure sieht entsprechend wie folgt aus.

DROP PROCEDURE IF EXISTS kammerath.search_article;
DELIMITER $$
CREATE PROCEDURE kammerath.search_article (query VARCHAR(100))
BEGIN
	-- declare the query condition and set it
	DECLARE query_cond VARCHAR(100) DEFAULT "";
	SELECT CONCAT('%',query,'%') INTO query_cond;
	SELECT REPLACE(query_cond,' ','%') INTO query_cond;


	SELECT	kammerath.article_list.title,
			kammerath.article_list.url,
			kammerath.article_list.body,
			kammerath.article_list.keyword,
			kammerath.article_list.date,
			CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname) AS author,

			-- calculates the occurences of the query word in the corresponding fields
			((LENGTH(kammerath.article_list.body) - LENGTH(REPLACE(LOWER(kammerath.article_list.body), LOWER(query), ''))) / LENGTH(query)) AS body_index_value,
			((LENGTH(kammerath.article_list.title) - LENGTH(REPLACE(LOWER(kammerath.article_list.title), LOWER(query), ''))) / LENGTH(query)) AS title_index_value,
			((LENGTH(kammerath.article_list.url) - LENGTH(REPLACE(LOWER(kammerath.article_list.url), LOWER(query), ''))) / LENGTH(query)) AS url_index_value,
			((LENGTH(kammerath.article_list.keyword) - LENGTH(REPLACE(LOWER(kammerath.article_list.keyword), LOWER(query), ''))) / LENGTH(query)) AS keyword_index_value,
			
			-- calculates the occurences in the author name
			((LENGTH(CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname)) 
			- LENGTH(REPLACE(LOWER(CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname)), LOWER(query), ''))) 
			/ LENGTH(query)) AS author_index_value,

			-- calculates the total index value with weight
			(((LENGTH(kammerath.article_list.title) - LENGTH(REPLACE(LOWER(kammerath.article_list.title), LOWER(query), ''))) / LENGTH(query))*4)
			+ (((LENGTH(kammerath.article_list.url) - LENGTH(REPLACE(LOWER(kammerath.article_list.url), LOWER(query), ''))) / LENGTH(query))*3)
			+ (((LENGTH(kammerath.article_list.body) - LENGTH(REPLACE(LOWER(kammerath.article_list.body), LOWER(query), ''))) / LENGTH(query))*2)
			+ ((LENGTH(kammerath.article_list.keyword) - LENGTH(REPLACE(LOWER(kammerath.article_list.keyword), LOWER(query), ''))) / LENGTH(query))
			+ ((LENGTH(CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname)) 
				- LENGTH(REPLACE(LOWER(CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname)), LOWER(query), ''))) 
				/ LENGTH(query))

			AS total_index_value
		
		FROM kammerath.article_list 
		WHERE kammerath.article_list.published = 1
		AND (kammerath.article_list.title LIKE query_cond
		OR kammerath.article_list.url LIKE query_cond
		OR kammerath.article_list.body LIKE query_cond
		OR kammerath.article_list.keyword LIKE query_cond
		OR kammerath.article_list.author_firstname LIKE query_cond
		OR kammerath.article_list.author_lastname LIKE query_cond)

		-- order the search results by total index value descending
		ORDER BY (((LENGTH(kammerath.article_list.title) - LENGTH(REPLACE(LOWER(kammerath.article_list.title), LOWER(query), ''))) / LENGTH(query))*4)
			+ (((LENGTH(kammerath.article_list.url) - LENGTH(REPLACE(LOWER(kammerath.article_list.url), LOWER(query), ''))) / LENGTH(query))*3)
			+ (((LENGTH(kammerath.article_list.body) - LENGTH(REPLACE(LOWER(kammerath.article_list.body), LOWER(query), ''))) / LENGTH(query))*2)
			+ ((LENGTH(kammerath.article_list.keyword) - LENGTH(REPLACE(LOWER(kammerath.article_list.keyword), LOWER(query), ''))) / LENGTH(query))
			+ ((LENGTH(CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname)) 
				- LENGTH(REPLACE(LOWER(CONCAT(kammerath.article_list.author_firstname," ",kammerath.article_list.author_lastname)), LOWER(query), ''))) 
				/ LENGTH(query)) DESC;
END$$

In diesem Beispiel sehen wir ein ziemliches Abfrage-Monster. Niemand möchte eine solche Monster-Funktion in seienm PHP Quelltext sehen. Der entsprechende PHP Quelltext hat hier auch nur eine Zeile, nämlich den Aufruf der Stored Procedure. Der Ansatz bei diesem Procedure war, dass die PHP Anwendung einfach nur den Suchbegriff angibt und die Datenbank selbstständig sucht und dann das Ergebnis zurückliefert. Das ist meines Erachtens nach eine der sinnvollsten Implementierungen von Stored Procedures. Wer sich das Procedure jetzt noch etwas genauer ansieht und diesen Artikel richtig gelesen hat, wird auch feststellen, dass die Abfrage der Suchfunktion ebenfalls auf einer View durchgeführt wird. Die tatsächliche Abfrage ohne Procedures und Views wäre also gut und gerne zwei A4 Seiten lang. Was das zusätzlich auf der Netzwerkschnittstelle bedeutet, wird wohl jedem klar. Statt hier jedes Mal unnötige Bytes hin- und her zu schieben belassen wir es bei dem Suchbegriff und der Rest ist auf der Datenbank vorbereitet, MySQL kann die Abfragen zudem selbst optimieren und weiß im Voraus, welche Abfragen Ihn erwarten.

PHP Performance Tuning durch Caching

Eine weitere sehr gute Maßnahme, um die Performance zu steigern, ist der Einsatz von Caching - also dem Zwischenspeichern von Information, sodass diese nicht jedes Mal von einer langsameren Quelle abgeholt werden müssen. Ein gutes Beispiel hier in meinem Blog sind die alternativen Artikelvorschläge (siehe dazu Google Analytics API für Amazon-ähnliche Vorschläge). Hierbei wird über die Google Analytics API abgefragt, welche Artikel andere Nutzer sonst noch gelesen haben, die sich für den Artikel interessieren, den der Nutzer sich gerade ansieht. Solche Berichte dauern recht lange und würden die Ladezeit unnötig verlangsamen. Zeitgleich ist es nicht notwendig die Daten mehr als 1x pro Tag von Google Analytics abzufragen, da diese dort auch nicht häufiger aktualisiert werden. Es macht also Sinn die Informationen zu cachen, was nachfolgende Klasse anbietet.

/* this class caches data on the filesystem to improve
 * performance of the system by using the cached data 
 * instead of doing queries or calls to apis 
 * 
 * the cache file format is:
 * [key]_[expiry].cache
 * 
 * */
class cache{
	// path where the cache files are in
	private $cache_path = CONFIG_CACHE_PATH;
		
	/* returns the key with underscores replaced 
	 * as they are used exclusively for the expiry mark */
	private function getKey($key){
		return str_replace("_","-",$key);
	}
	
	/* creates a hash value for a message */
	public function hash($message){
		return hash("sha512",$message);
	}
	
	/* returns bool when a certain key exists */
	public function has($key){
		$result = false;
		
		/* check if the key exists in the file list */
		if(array_key_exists($this->getKey($key), 
						$this->getFileList())){
			// set the result to true
			$result = true;
		}
		
		return $result;
	}
	
	/* puts data into the cache */
	public function put($key, $value, $expiry){
		// generate the cache key
		$c_key = $this->getKey($key)."_".$expiry;
		// write the data to the cache
		file_put_contents($this->cache_path."/".$c_key
						.".cache", serialize($value));
	}
	
	/* returns data from the cache */
	public function get($key){
		$result = "";
		$list = $this->getFileList();
		$c_key = $this->getKey($key);
		if(array_key_exists($c_key, $list)){
			// read the cache file, deserialize it, return it
			$result = unserialize(file_get_contents($this->cache_path."/"
							.$c_key."_".$list[$c_key].".cache"));
		}
		return $result;
	}
	
	/* clears the entire cache */
	public function clear($prefix){
		// dnra
	}
	
	/* returns a list of all cached files: the result
	 * is an array that contains the cache key as key
	 * and the expiry as value where neither the cache key
	 * nor the expiry includes the underscore prefix  
	 * 
	 * e.g.: pageH23489ABM23443-1345000.cache
	 * 		key: 	pageH23489ABM23443
	 * 		value:	1345000
	 * */
	private function getFileList(){
		$result = array();
		
		if ($handle = opendir($this->cache_path)) {		
		    /* walk through all files in the dir */
		    while (false !== ($file = readdir($handle))) {
		    	// don't use the dir links
		    	if($file != "." && $file != ".."){
		        	$full_filename = $this->cache_path."/".$file."n";
		        	$filetitle = explode(".",$full_filename);
		        	$filetitle = $filetitle[0];
		        	$cf_data = explode("_",$filetitle);
		        	
		        	// extract the cache key from the filetitle
		        	$cache_key = substr($cf_data[0],strrpos($cf_data[0], "/")+1);
		        	// extract the expiry
		        	$expiry = $cf_data[1];
		        	
		        	/* do not add, but delete the file when expired */
		        	if($expiry < time()){
		        		// delete the file
		        		unlink($this->cache_path."/".$file);
		        	}else{
		        		// just add it to the result when not expired
		        		$result[$cache_key] = $cf_data[1];
		        	}
		    	}
		    }
		
		    closedir($handle);
		}		
		
		return $result;
	}
}

In diesem Fall verwendet die Klasse die Festplatte als Cache, da dies hier die beste Möglichkeit war. Ein Memcached-Server steht nicht zur Verfügung und wir möchten wir hier die Datenbank nicht weiter durch etwaige Memory-Tables belasten, zumal wir dann auch über die Netzwerkschnittstelle gehen müssen. In virtualisierten Umgebungen ist die Festplatte dann teilweise sowieso im RAM und damit hätten wir dann auch RAM-Caching, was PHP im Vergleich zu ASP.NET auf dem Microsoft Internet Information Server nicht direkt mitliefert. Die entsprechende Methode "getRecommendations" in einer anderen Klasse lädt die Berichtsdaten von Google Analytics und darum haben wir nun eine Methode "getCachedRecommendations" gebaut, welche den Cache verwendet und wie folgt aussieht.

	// does the same as 'get_ga_also_interested_in',
	// but caches the data for 24hrs on the disk which
	// provides way, way better performance
	public function getCachedRecommendations($page,$refresh = false){
		// result will hold the recommendations
		$result = array();

		// generate the cache key for the recommendations
		$cache_key = $this->cache->hash("recommendations".$page);
		
		// check whether the recommendations exist in cache
		if($this->cache->has($cache_key)==true && $refresh==false){
			$result = $this->cache->get($cache_key);
		}else{
			// query the recommendations from google
			$result = $this->getRecommendations($page);
			
			// put the recommendations into the cache for 1 day
			$this->cache->put($cache_key, $result, time()+86400);
		}

		// finally return the recommendations
		return $result;
	}

Anzumerken sei hier der zweite Parameter der Methode, welcher es erlaubt einen vorzeitigen Refresh der Datei im Cache durchzuführen. Dieser Parameter wird von einem Script verwendet, welches als Cronjob läuft und zweimal täglich im Hintergrund den Cache erneuert, sodass wir verhindern, dass ein Besucher der gerade nach dem Auslaufen des Caches eine Seite aufruft eine langsamere Ladezeit erfährt. Dieses Verfahren ist auch häufig als sog. "Warmup" bekannt - also das Aufwärmen der Anwendung.

Client-seitige Performance-Optimierung

Es hilft nichts, wenn man ein enorm schnelles Backend hat und das Frontend durch diverse Unsinnigkeiten extrem langsam wird. Insbesondere Werbebanner sind gerne ein Kandidat dafür; in meinem Blog hier funktioniert das z.B. so, dass die Banner am Ende der Seite geladen werden und dann als absolutes DIV auf die gewünschte Position gebracht werden und das schlägt bei den Ladezeiten richtig gut zu Buche. Die wichtigsten Tipps zur Ladezeit-Optimierung am Frontend hat Yahoo zusammengestellt: Best Practices for Speeding Up Your Website.

Ganz zum Schluss möchte ich noch erwähnen, dass der gezeigte Code unter der MPL 1.1 veröffentlicht wurde und der vollständige Code deshalb Open Source und als Kammerath Network Website System bei Google Project Hosting zur Verfügung steht. Des Weiteren freue ich mich immer sehr über Kritik, Fragen und Anregungen die gerne als E-Mail oder Kommentar kommen dürfen!

Bücher zum Thema „php“

Die nachfolgenden Bücher behandeln das Thema "php" und werden von Amazon empfohlen. Viele dieser Bücher habe ich selbst gelesen und auch zur Recherche für diesen Artikel genutzt. Wer sich, insbesondere bei technischen Themen, unsicher ist, ob er die gewünschte Aufgabe bewältigen kann, sollte zu einem dieser Bücher greifen. Die Bücher habe ich zum großen Teil selbst auch auf ihre Qualität geprüft.
Einstieg in PHP 5.5 und MySQL 5.6: Für Programmieranfänger geeignet (Galileo Computing)
Einstieg in PHP 5.5 und ...
Autor: Thomas Theis
Verlag: Galileo Computing
Preis: 19,90 €
PHP 5.4 und MySQL 5.5: Grundlagen, Anwendung, Praxiswissen, Objektorientierung, MVC, Sichere Webanwendungen, PHP-Frameworks, Performancesteigerungen, CakePHP (Galileo Computing)
PHP 5.4 und MySQL 5.5: G...
Autor: Stefan Reimers
Verlag: Galileo Computing
Preis: 39,90 €
PHP Einsteigerkurs: Grundlagen der PHP/MySQL Programmierung in 5 Tagen verstehen
PHP Einsteigerkurs: Grun...
Autor: Klaus Thenmayer
Verlag: CreateSpace Indep...
Preis: 2,80 €
PHP 5.4 & MySQL 5.5: Der Einstieg in die Programmierung dynamischer Websites (Open Source Library)
PHP 5.4 & MySQL 5.5: Der...
Autor: Florence Maurice
Verlag: Addison-Wesley Ve...
Preis: 14,95 €
Gefällt dir der Artikel oder has Du Fragen dazu? Neben anderen Interessierten warten dort auch exklusive Videos und Tutorials auf Dich. Werde jetzt Fan vom Kammerath Network auf Facebook und entdecke die neuesten Artikel noch bevor Sie fertig sind: facebook.com/kammerath.net

Besucher, die diesen Beitrag gelesen haben, interessieren sich auch für...

16 Besucher haben auch das gelesen
12 Besucher haben auch das gelesen
9 Besucher haben auch das gelesen
7 Besucher haben auch das gelesen
5 Besucher haben auch das gelesen
2 Besucher haben auch das gelesen

Du musst Dich lediglich mit deinem Benutzer über Facebook anmelden, um hier Kommentare schreiben zu können. » Mit Facebook anmelden.
© 2014 Jan Kammerath
Telefon +49 2241 955 98 60 oder E-Mail Kontakt.

IPv6 Ready

Das Kammerath Network Website System ist unter der Mozilla Public License 1.1 veröffentlicht.



Seite durchsuchen
Newsletter
Auf dem Laufenden bleiben und den Newsletter abonnieren. Bitte E-Mail Adresse eingeben:
» Jetzt abonnieren
Angebote
Laden...