Schlagwort-Archive: PHP

PHP: Skripte mit langer Laufzeit, JQuery Ajax-Warteseite, Zwischenspeicherung der Berechnungen als JSON und Lock-Mechanismus

Das Problem

Die Überschrift hört sich etwas tricky an, aber ich denke, wenn wir das Problem auf ein praktisches Beispiel übertragen, wird es klar.

Nehmen wir an, wir haben eine Fußballstatistik-Seite, wo alle Statistiken zur 1. bis 3. Fußballbundesliga abrufbar sein sollen. Jeder Spieler hat eine eigene Seite und zu jedem Spieler müssen täglich aufwändige Statistik-Berechnungen durchgeführt werden, die mehrere Minuten dauern.

Eine Lösung wäre, dass man mit Cronjobs 1x täglich diese aufwändigen Berechnungen nachts durchführt und so die Ergebnisse bereit hält, wenn der Besucher eine Spielerseite auswählt. Das Problem an dieser Vorgehensweise ist allerdings, dass es so viele Spieler gibt, dass die Berechnungen zu lange dauern würden. Zudem werden eh nur 10% der Spielerseiten besucht und 2% der Spielerseiten haben 90% der Zugriffe. Man kann also für diese 2% (High Traffic-Seiten) Cronjobs anlegen, die restlichen Berechnungen aber, sollen quasi On-the-Fly erstellt werden. Das führt zwar zu teilweise langen Wartezeiten für die Besucher, aber die Ergebnisse dieser Berechnungen sind so großartig, dass die Besucher darauf auch bis zu mehreren Minuten warten würden.

Anforderungen an die „On the fly“-Berechnungen

  1. Die Parameter für die Berechnung ändern sich nur 1x täglich
  2. Die Ergebnisse liegen nach der Berechnung in einem Array vor und sollen als JSON zwischengespeichert werden, so dass dann die Anzeige der Ergebnisse schnell erfolgen kann
  3. Wenn der Besucher auf die Ergebnisse warten muss, soll eine Warteseite mit einem Ajax-Loader angezeigt werden. Wenn die Berechnungen beendet sind, wird auf die eigentliche Seite weitergeleitet
  4. Für einen Spieler darf die Statistik-Berechnung nur 1x ausgeführt werden.
    Ein Beispiel: Besucher 1 besucht die Seite von „Arjen Robben“ und stößt die Berechnungen an. Besucher 2 besucht wenige Sekunden danach die Seite von „Arjen Robben“, es liegen keine Daten vor und trotzdem soll das Skript erkennen: „Moment! Die Berechnungen laufen schon, stoße die Berechnungen kein zweites Mal an.“
  5. Sollten veraltete Berechnungen vorliegen, werden diese zwar angezeigt (mit einem Hinweis, dass die Daten veraltet sind), um keine Wartezeit zu haben, im Hintergrund wird allerdings die aufwändige Berechnung angestoßen. Hier gilt wieder: Die Berechnung darf nur 1x angestoßen werden.

Warteseite mit JQuery-Ajax

Die Warteseite wird nur dann ausgegeben, wenn noch nie Daten für den Spieler berechnet wurden.

Damit die Warteseite das Ende der Berechnung mitbekommt, muss diese über Ajax die Berechnungen anstoßen.
[code lang=’js‘]$(document).ready(function() {
$.ajax({
type: ‚GET‘,
url: ’spielerSeite.php‘,
cache: false,
data: ‚do=ajax‘,
success: function(msg){
if (msg == ‚true‘){
location.replace(‚http://localhost/spielerSeite.php‘);
}
else
{
$(‚div#responseAjax‘).empty();
$(‚div#responseAjax‘).addClass(’success‘);
$(‚div#responseAjax‘).html(msg);
}
}
});
});[/code]
Es wird also die Seite spielerSeite.php?do=ajax aufgerufen. Diese Seite führt die Berechnungen durch und gibt am Ende ‚true‘ aus, so dass auf die eigentliche Seite spielerSeite.php weitergeleitet wird. Wenn nicht ‚true‘ zurück gegeben wird, hat bereits ein anderer Benutzer die Berechnungen angestoßen und man gibt eine Meldung zurück.

Zwischenspeicherung der Berechnungen als JSON

Die fertigen Berechnungen, werden als json-Datei im Filesystem gespeichert:
[code lang=’php‘]function setJSON()
{
file_put_contents($this->json_dir.$this->spieler_string.‘.json‘, json_encode($this->berechnung_array));
}

function getJSON()
{
$this->berechnung_array = json_decode(file_get_contents($this->json_dir.$this->spieler_string.‘.json‘), true);
}[/code]
Vorteil von json ist die absolut problemlose Konvertierung vom PHP-Array zu einem json-String, der in einer Datei gespeichert wird. Anhand des Änderungsdatums der Datei mit filemtime kann man zudem einfach feststellen, ob die Daten wieder aktualisiert werden müssen.

Lock-Mechanismus zur einmaligen Ausführung

Wie kann man mit PHP feststellen, ob bereits eine Berechnung für einen Spieler läuft? Gar nicht. Also muss man es sich merken!

In einem Execution Array ($this->execution_array) werden die $this->spieler_string gespeichert, deren Berechnungen zur Zeit ausgeführt werden. Damit alle Skripte, die gleichzeitig laufen, auf dieses Array zugreifen können, wird das Array als JSON gespeichert:
[code lang=’php‘]function setExJSON()
{
file_put_contents(‚execution.json‘, json_encode($this->execution_array));
}

function getExJSON()
{
if (!file_exists(‚execution.json‘))
{
$this->execution_array = array();
}
elseif (file_get_contents(‚execution.json‘) === ’null‘)
{
$this->execution_array = array();
}
else
{
$this->execution_array = json_decode(file_get_contents(‚execution.json‘), true);
}
}[/code]
Nun muss bei jedem potentiellen Start der Berechnungen überprüft werden, ob für den Spieler schon Berechnungen laufen. Dabei ist darauf zu achten, dass $this->execution_array frisch aus der JSON-Datei geladen wurde. Sollte niemand die Berechnung gestartet haben, wird das Execution-Array um den entsprechenden Eintrag erweitert und als JSON gespeichert.
Nachdem die Berechnungen durchgeführt wurden, wird das Array wieder geladen, der entsprechende Eintrag gelöscht und das Array wieder gespeichert:
[code lang=’php‘]$this->getExJSON();
if (in_array($this->spieler_string,$this->execution_array))
{
exit();
}
else
{
$this->execution_array[] = $this->spieler_string;
$this->setExJSON();
$this->fuehreLangeBerechnungenDurch();
$this->getExJSON();
unset($this->execution_array[array_search($this->spieler_string, $this->execution_array)]);
$this->setExJSON();
}[/code]
Das Problem an dieser Implementation ist natürlich, dass Schreiben und Lesen ins Execution-Array kein kritischer Abschnitt ist und z.B. zeitgleich mehrere Berechnungsvorgänge gestartet werden könnten.. Da aber schlimmstenfalls „nur“ mehrere Berechnungen für einen Spieler gleichzeitig durchgeführt würden, ist das Problem vernachlässigbar.

Ablaufkontrolle

Und hier der komplette Ablauf des Skripts:
[code lang=’php‘]$this->execution_array = array();
if (isset($_GET[‚do‘]) AND $_GET[‚do‘] === ‚old‘)
{
//Daten veraltet: Alten Daten laden und anzeigen
$this->getJSON();
$this->toHTML();
}
elseif (isset($_GET[‚do‘]) AND $_GET[‚do‘] === ‚wait‘)
{
//Warteseite mit Ajax anzeigen
$this->doWaitHTML();
}
elseif (isset($_GET[‚do‘]) AND $_GET[‚do‘] === ‚ajax‘)
{
//Durch Ajax von Warteseite aus Berechnungen angestoßen
$this->getExJSON();
if (in_array($this->spieler_string,$this->execution_array))
{
//Anderer User hat Berechnungen schon angestoßen
$this->ajaxFalseResponse();
}
else
{
//Berechnungen durchführen
$this->execution_array[] = $this->spieler_string;
$this->setExJSON();
$this->fuehreLangeBerechnungenDurch();
$this->getExJSON();
unset($this->execution_array[array_search($this->spieler_string, $this->execution_array)]);
$this->setExJSON();
}
}
elseif (!file_exists($this->json_dir.$this->spieler_string.‘.json‘))
{
//Es liegen noch keine Berechnungen vor! Weiterleiten auf Warteseite.
$host = $_SERVER[‚HTTP_HOST‘];
$uri = $_SERVER[‚PHP_SELF‘];
$extra = ‚?do=wait‘;
header(„Location: http://$host$uri$extra“);
exit();
}
else
{
$this->getJSON();
if (filemtime($this->json_dir.$this->spieler_string.‘.json‘) < strtotime(' -1 day')) { //Die Daten sind veraltet. Weiterleiten zum Anzeigen der alten Daten, aber Berechnungen der neuen Daten starten $host = $_SERVER['HTTP_HOST']; $uri = $_SERVER['PHP_SELF']; $extra = '?do=old'; header("Location: http://$host$uri$extra"); $this->getExJSON();
if (in_array($this->spieler_string,$this->execution_array))
{
//Anderer User hat Berechnungen schon angestoßen
exit();
}
else
{
//Berechnungen durchführen
$this->execution_array[] = $this->spieler_string;
$this->setExJSON();
$this->fuehreLangeBerechnungenDurch();
$this->getExJSON();
unset($this->execution_array[array_search($this->spieler_string, $this->execution_array)]);
$this->setExJSON();
}
}
else
{
//Wenn Daten aktuell sind: Anzeigen!
$this->toHTML();
}
}[/code]

Google Analytics Alternative: Piwik

Google Analytics ist ohne Zweifel ein großartiges Tool, um seine Webzugriffe zu analysieren. Welche Seiten wurden am häufigsten besucht, wie hoch ist die Absprungrate, mit welchen Suchbegriffen landen Benutzer auf meiner Seite, bringt es etwas Tags zu verwenden, welche Begriffe bringen am meisten Besucher und viele weitere Fragen können mittels Webanalyse beantwortet werden. Den Traffic umfangreich zu analysieren, Schlüsse daraus zu ziehen und die eigene Webseite/Blog weiter zu optimieren, ist bestimmt ein guter Ansatz. So habe ich bspw. mit diesem Blog zu Beginn der Analyse 40 Besucher am Tag gehabt, mittlerweile liege ich bei 140 Besuchern durchschnittlich – neulich waren es sogar 220 am Tag.

Das Problem mit Google Analytics

Der Landesbeauftragte für Datenschutz von Schleswig-Holstein bringt es wie folgt auf den Punkt:

Derzeit ist die Nutzung des kostenlosen Google Analytics Services durch Webseitenanbieter unzulässig. Google muss dessen Konfiguration so ändern, dass die Betroffenen ihr Recht auf Widerspruch, Information und Auskunft sowie Löschung der Daten wirksam wahrnehmen können. Für den rechtswidrigen Einsatz des Dienstes haften die Webseitenbetreiber.

Verwendet man Google Analytics überträgt man die IP-Adresse des Besuchers an einen Dritten (Google), was auch dann nicht im Einklang mit dem Datenschutz ist, wenn man im Impressum den Besucher darauf hinweist, dass man Google Analytics verwendet. Der Besucher muss zustimmen oder dem Webseitenbetreiber muss eine gesetzliche Ermächtigung vorliegen, damit die personenbezogenen Daten (IP-Adresse) übermittelt werden dürfen. Andernfalls läuft man Gefahr von Abmahnungen..
Zwar wurde die zentrale Forderung der Datenschutz-Behörden durch Googles Nachbesserung durch die _anonymizeIp() Funktion erfüllt (wenn man denn Google den Glauben schenken kann, dass diese Funktion tatsächlich die IP anonymisiert), es bleibt jedoch die Frage der datenschutzrechtlich zulässigen Widerspruchsmöglichkeit erhalten – sprich: Es ist mir als Nutzer nicht möglich zu sagen, dass Google meine Daten nicht sammeln soll.

Ein weiteres Problem bei der Verwendung von Google Analytics ist, dass viele Benutzer Skripts von Google in ihrem Browser sperren, so dass keine Daten erfasst werden.

Google hat den Ruf einer Datenkrake und man muss sich fragen, warum Google solch ein aufwändiges Produkt wie Google Analytics kostenlos anbietet – sicherlich nicht aus reinem Gutmenschentum, sondern um intern die Daten weiter zu verarbeiten und Zugriffe zu tracken, die sich außerhalb des Google Universums abspielen.

Piwik – Eine kostenlose open source Alternative

Piwik Logo

Piwik hat den entscheidenden Vorteil, dass alle Daten bei einem selber bleiben. Die Installation ist einfach und lediglich PHP und MySQL ist notwendig. Mittlerweile bietet Piwik (Version 1.0) nahezu alle Möglichkeiten, die man von Google Analytics kennt.

Als ich im Dezember 2009 (Piwik 0.5.1) anfing, die open source Alternative zu nutzen, habe ich noch etliche Features vermisst. Die Entwicklung von Piwik war allerdings so rasant, dass ich bei dieser Software blieb. Ich bin gespannt, wohin sie sich noch entwickeln wird.

Mein Rat: Ausprobieren!

SQL IN und =

Da erschafft man einen völlig neuen Wettkampf, der von 31 Teams weltweit wahrgenommen wird. Hunderte von Menschen diskutieren in zig Foren in deutsch, englisch, französisch, russisch, chinesisch, spanisch, japanisch, tschechisch etc. über die beste Taktik, um diese Challenge zu gewinnen. Und man selber ist heilfroh, dass alles funktioniert: die Statistiken werden korrekt erstellt, die Daten werden richtig importiert, man hat sogar die Muße den Importvorgang in der laufenden Challenge noch ein wenig zu optimieren – Operation am offenen Herzen quasi…

Selbst der alte Server (im nächsten Monat gibt’s einen neuen) funktioniert trotz fast doppelt so hohen Besucherzahlen. Und man denkt: Mensch, wat biste fürn dufter Typ?! Klappt ja alles 1A!

Und dann kommt die Hiobsbotschaft der Serverload ist manchmal 10.00. Irgendetwas stimmt nicht. Irgendeine Anfrage dauert zu lange. Man überprüft die Seiten und stellt fest, dass eine Abfrage dabei ist, die anfangs nur wenige Millisekunden gebraucht hat, jetzt aber 70 Sekunden braucht.. Eine typische Anfrage:
[code lang=’sql‘]SELECT
[…]
output_24h
FROM
pentathlon_score
INNER JOIN
pentathlon_team
ON pentathlon_score.teamid = pentathlon_team.teamid
WHERE
zeitpunkt IN (SELECT max(zeitpunkt) FROM pentathlon_score WHERE disziplinid= 1) AND
disziplinid = 1 AND
Rank > 0
ORDER BY Rank ASC[/code]
Und schließlich stellt man fest, dass wenn man das IN durch ein = ersetzt, die Anfrage plötzlich nur noch 0,05 Sekunden dauert.
[code lang=’sql‘]SELECT
[…]
output_24h
FROM
pentathlon_score
INNER JOIN
pentathlon_team
ON pentathlon_score.teamid = pentathlon_team.teamid
WHERE
zeitpunkt = (SELECT max(zeitpunkt) FROM pentathlon_score WHERE disziplinid= 1) AND
disziplinid = 1 AND
Rank > 0
ORDER BY Rank ASC[/code]
und das, obwohl die Unterabfrage SELECT max(zeitpunkt) […] nur einen Wert zurück gibt..!?!