by:

Ich habe mir gedacht, nachdem ich ja sonst nix zu tun habe, ein paar
Tage lang eine bestehende Applikation so umzuschreiben, dass
sie auf mehreren Servern läuft.
Als Basis werde ich meinen Reminder Service verwenden.

Der erste Schritt um die Performance zu verbessern ist immer
die Analyse der bestehenden Applikation, dazu gehört auch das Profiling, um
Bottlenecks besser zu lokalisieren.

Um die Skalierbarkeit zu verbessern, werde ich
gewisse Teile ausgliedern, man nennt dies auch funktionale
Partitionierung.
Man könnte bei einem typischen Blog zum Beispiel anfangen,
Kommentare, Benutzer und Nachrichten jeweils auf eine eigene Datenbank auszulagern.
Weiters könnte man mit Hilfe eines Lastenverteilers und mehreren Webservern die
Last aufteilen.

In unserem Beispiel haben wir zwei Kernbereiche, erstens das Userinterface,
worüber der Benutzer Daten anlegen, verändern und löschen kann und
zweitens das Backend, welches die eingegebenen Daten verarbeitet.

Im Idealfall können wir am Ende das Front und Backend auf N Servern horizontal skalieren.
Das bedeutet, wir erhöhen nicht nur die Geschwindigkeit, sondern schaffen zusätzlich Redundanzen,
die eine gewisse Ausfallsicherheit garantieren.

Schauen wir uns das bestehende Backend mal an:
Code

 
<? 
include("./config.php"); 
$connect=@mysql_connect("$db_host", "$db_user", "$db_pass") 
or die("<li>Cant connect to: $db_host  with user: $db_user</li>"); 
@mysql_select_db($db, $connect)or die("Cant select DB"); 
 
// hier wird jeder job aus der datenbank geholt, 
// der den staus planned hat und in der vergangenheit liegt. 
 
$sql="SELECT jid, job.rid, subject, description, email, reminder.interval, 
      reminder.remind_date, reminder.auth  
      FROM job, reminder  
      WHERE  
      status='planned' AND  
      planned<UNIX_TIMESTAMP() AND  
      reminder.rid=job.rid"; 
       
$result=mysql_query($sql); 
while($row=mysql_fetch_array($result)){ 
    $header  = 'MIME-Version: 1.0' . "n"; 
    $header .= 'Content-type: text/html; charset=iso-8859-1' . "n"; 
    $header .= 'To: "'.$row['email'].'" <'.$row['email'].'>' . "n"; 
    $header .= 'From: "Remind ME" <remind@codejungle.org>' . "n"; 
    $msg=' 
    <html> 
    <head> 
      <title>RemindME</title> 
    </head> 
    <body> 
      '.$row['description'].' 
          <br/><hr><a href="http://www.codejungle.org/reminder/uid/'.$row[auth].'">Modify your Reminder</a> 
    </body> 
    </html> 
    '; 
    if(mail($row['email'], $row['subject'], $msg, $header)){ 
        //wenn die email verschickt werden konnte, wird der job auf done gesetzt 
        //und ein neuer job mit neuem versand datum angelegt 
        $update="update job set status='done' where jid=".$row['jid']; 
        mysql_query($update); 
        $time=getdate($row['remind_date']); 
        if($row['interval']=="d"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'], 
            $time['mon'], $time['mday']+1, $time['year']));     
        } 
        if($row['interval']=="w"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'], 
            $time['mon'], $time['mday']+7, $time['year']));     
        } 
        if($row['interval']=="m"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'],
            $time['mon']+1, $time['mday'], $time['year']));     
        } 
        if($row['interval']=="y"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'],
            $time['mon'], $time['mday'], $time['year']+1));     
        } 
        if($row['interval']!=="n"){ 
            $update_setting="update reminder set remind_date=".$dstr." where rid=".$row[rid]; 
            mysql_query($update_setting);         
            $insert="insert into job values ('', ".$row['rid'].", 'planned', ".$dstr.")"; 
            mysql_query($insert); 
        } 
    } 

?> 


Im Grunde passiert in diesem Script folgendes:

Hole mir alle unerledigten Jobs
Versende eMail
Markiere Job als erledigt
Erstelle gegebenenfalls neuen Job

Dieser Teil zeigt ganz schön, was man beachten muss, wenn man eine Applikation umschreibt, damit
Sie auf mehreren Server laufen kann.

Da zwischen "Hole mir alle unerledigten Jobs" und "Markiere Job als erledigt" etwas Zeit vergehen kann,
kann es passieren das ein zweiter Server die gleiche Routine startet und
ebenfalls den gleichen Job zugeteilt bekommt. Das Resultat wäre, dass der Benutzer
zwei oder mehr gleiche Nachrichten erhält.

Dies werden wir umgehen, indem wir nach "Hole mir alle unerledigten Jobs" einen Task einbauen,
der dafür sorgt, dass andere Server nicht den gleichen Task bekommen.

Hole mir 10 unerledigte Jobs
Markiere Job als in Bearbeitung
Versende eMail
Markiere Job als erledigt
Erstelle gegebenenfalls neuen Job

Desweiteren werden wir die Anzahl der zu erledigenden Jobs pro Prozess auf 10 verringern.
Daher sollte unser Code nun folgendermaßen aussehen:
Code
 
<? 
include("./config.php"); 
$connect=@mysql_connect("$db_host", "$db_user", "$db_pass") 
or die("<li>Cant connect to: $db_host  with user: $db_user</li>"); 
@mysql_select_db($db, $connect)or die("Cant select DB"); 
 
// hier werden die letzen 10 jobs aus der datenbank geholt, 
// der den staus planned hat und in der vergangenheit liegt. 
 
$sql="SELECT jid, job.rid, subject, description, email, reminder.interval,
      reminder.remind_date, reminder.auth  
      FROM job, reminder  
      WHERE  
      status='planned' AND  
      planned<UNIX_TIMESTAMP() AND  
      reminder.rid=job.rid  
      limit 10 
      "; 
       
$result=mysql_query($sql); 
while($row=mysql_fetch_array($result)){ 
    //setze job auf in bearbeitung, damit kein anderer prozess ihn ebenfalls erledigt 
    mysql_query("UPDATE job set staus='in_progress' where id='".$row['jid']."'"); 
    $header  = 'MIME-Version: 1.0' . "n"; 
    $header .= 'Content-type: text/html; charset=iso-8859-1' . "n"; 
    $header .= 'To: "'.$row['email'].'" <'.$row['email'].'>' . "n"; 
    $header .= 'From: "Remind ME" <remind@codejungle.org>' . "n"; 
    $msg=' 
    <html> 
    <head> 
      <title>RemindME</title> 
    </head> 
    <body> 
      '.$row['description'].' 
          <br/><hr><a href="http://www.codejungle.org/reminder/uid/'.$row[auth].'">Modify your Reminder</a> 
    </body> 
    </html> 
    '; 
    if(mail($row['email'], $row['subject'], $msg, $header)){ 
        //wenn die email verschickt werden konnte, wird der job auf done gesetzt 
        //und ein neuer job mit neuem versand datum angelegt 
        $update="update job set status='done' where jid=".$row['jid']; 
        mysql_query($update); 
        $time=getdate($row['remind_date']); 
        if($row['interval']=="d"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'], 
            $time['mon'], $time['mday']+1, $time['year']));     
        } 
        if($row['interval']=="w"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'], 
            $time['mon'], $time['mday']+7, $time['year']));     
        } 
        if($row['interval']=="m"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'],
            $time['mon']+1, $time['mday'], $time['year']));     
        } 
        if($row['interval']=="y"){ 
            $dstr=date("U", mktime($time['hours'], $time['minutes'], $time['seconds'],
            $time['mon'], $time['mday'], $time['year']+1));     
        } 
        if($row['interval']!=="n"){ 
            $update_setting="update reminder set remind_date=".$dstr." where rid=".$row[rid]; 
            mysql_query($update_setting);         
            $insert="insert into job values ('', ".$row['rid'].", 'planned', ".$dstr.")"; 
            mysql_query($insert); 
        } 
    } 

?> 

Nachdem wir den Code nun soweit umgeschrieben haben, dass er auf mehren Servern laufen kann,
wäre eine Variante, ihn dort via Cron alle x Minuten auszuführen.

Im zweiten Teil möchte mit Hilfe dieses Beispiels, Gearman vorstellen, es ist ein Framework,
mit dem man parallele und lastenverteile Systeme aufbauen kann.

"It allows you to do work in parallel, to load balance processing, and to call functions between languages"

Außerdem möchte ich im dritten Teil noch etwas näher auf die Datenbankebene, memcache und verschiedene Lastenverteiler eingehen.

To be continuted

Andreas