![]() ![]() 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. Jetzt für den Kammerath Network Newsletter anmelden und mit einem bisschen Glück einen 20,- € Amazon-Gutschein gewinnen! Internet (43) Technik (41) Linux (18) Programmierung (17) OpenWrt (6) Sonstiges (6) WLAN (6) Web Analytics (5) Asterisk (3) Raspberry Pi (2) 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 ProceduresEinige 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. 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 CachingEine 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-OptimierungEs 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“ Es sind eine Reihe interessanter php-Bücher und Veröffentlichungen rund um das Thema „php“ erschienen, wozu unter
Anderem „PHP 5.4 & MySQL 5.5: Der Einstieg in die Programmierung dynamischer Websites (Open Source Library)“ von Florence Maurice, erschienen bei
Addison-Wesley Verlag für 19,80 €, zählt. Gut und empfehlenswert ist auch das 1085-Seiten starke Buch „PHP 5.4 und MySQL 5.5: Grundlagen, Anwendung, Praxiswissen, Objektorientierung, MVC, Sichere Webanwendungen, PHP-Frameworks, Performancesteigerungen, CakePHP (Galileo Computing)“ von Stefan Reimers (erschienen bei Galileo Computing). Der Autor Stefan Reimers bietet ein paar sehr hilfreiche Details zum Thema php in seinem Buch, welches mit einem Preis von 33,98 € auch noch recht erschwinglich für ein php-Buch ist. Wenn es um php-Wissen geht sollte das Buch
„Einstieg in PHP 5.4 und MySQL 5.5: Für Programmieranfänger geeignet (Galileo Computing)“ ebenfalls nicht ungenannt bleiben. Thomas Theis hat hier viele gute Informationen
über php festgehalten. „Einstieg in PHP 5.4 und MySQL 5.5: Für Programmieranfänger geeignet (Galileo Computing)“ wird
durch Galileo Computing herausgegeben, kostet
19,90 € und umfasst insgesamt 594
äußerst informative Seiten. Gleichwohl hier im Artikel PHP Performance Tuning und auch in anderen Artikeln zahlreiche Informationen über php zu finden sind, empfiehlt sich immer ein Blick auf aktuelle Buchveröffentlichungen, da natürlich auch im Bereich php sich die Dinge mit der Zeit ändern. Besonders empfehlen kann ich das Lesen auf dem Amazon Kindle, welches eine besonders angenehme Möglichkeit ist, php-Bücher unterwegs zu lesen. 9 Besucher haben auch das gelesen 1 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. Telefon +49 2241 955 98 60 oder E-Mail Kontakt. Das Kammerath Network Website System ist unter der Mozilla Public License 1.1 veröffentlicht. Diese Website wurde von Jan Kammerath entwickelt und ist
in Ihren Bestandteilen somit teilweise Eigentum von Jan
Kammerath. Sie besteht jedoch auch aus Teilen, die unter der
Open Source Lizenzen veröffentlicht wurden. Die Familie Kammerath besitzt
zudem E-Mail Adressen unter dieser Domain und wenn Du auch ein Kammerath bist, dann kannst Du Dich gerne melden und vielleicht bekommst Du
dann auch eine E-Mail Adresse bei Kammerath.net oder Kammerath.com.
Wenn Du Dich fragst, woher der Name Kammerath kommt, dann
können selbst die besten Ahnenforscher Dir dies nicht so richtig
beantworten, denn Kammerath ist schon ein sehr spezieller
Nachname. |