by:

Ich habe in einem Artikel von der Möglichkeit geschrieben mit
MySQL-Proxy eine lastenverteilte Datenbank aufzubauen.

Heute möchte ich jedoch eine andere Alternative vorstellen.
Ich habe zu diesem Zweck eine Datenbank Lastenverteilungsklasse geschrieben.
Sie stellt drei Methoden zur Lastenverteilung bereit:

Simple Load Balancing verteilt die Last völlig willkürlich:
Vorteil: relativ schnell
Nachteil: Server können einseitig belastet werden

Load Balancing by Weight verteilt die Last nach Gewichtung:
Hat man z.B. eine starke und zwei schwache Maschinen
ist es möglich der starken mehr Gewichtung zu geben.
Nachteil: Der richtige Gewichtungswert von Maschinen ist schwer zu ermittelen
und kann sich im regulären Betrieb unerwartet ändern.

Load Balancing by Load verteilt die Last nach Systemauslastung:
Vorteil: Die Last wird an den Server verteilt, der am wenigsten zu tun hat.
Nachteil: Die zusätzliche Zeit, die benötigt wird, um die Systemlast der Server abzufragen.
Läuft nur unter Linux, benötigt memcache.

Derzeit wird die Systemlast 60 Sekunden im memcache zwischengespeichert um unnötige Anfragen
an den Webserver zu sparen,
evtl. muss dieser Wert angepasst werden, damit unter hoher Last die Maschinen nicht einseitig
belastet werden.


Desweiteren stellt die Klasse eine Methode zum Abfragen der durchschnittlichen Systemlast bereit
und ausserdem eine Methode die endscheiden kann, ob eine Abfrage auf den Master oder Slave laufen soll.


Code


<?
/**
 * DBLoadBalancing implements three different methods for LoadBalancing
 * and one Method for Read / Write Splitting
 *
 * Simple Load Balancing - random dissision
 * Load Balancing by Weight - dissision by weight
 * Load Balancing by Load - dissision by load
 *
 * @todo benchmark and test bevore useing it on productive system
 * @author Andreas Beder <office@codejungle.org>
 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
 */
class DBLoadBalancing{

    var $config =array();

    function __construct() {

        //Config for Simple Load Balanncing

        $this->config['slaves']=array("10.0.0.1","10.0.0.2", "10.0.0.3");
        /*
        //Config for Load Balancing by Weight

        $this->config['slaves']=array("10.0.0.1"=>2,"10.0.0.2"=>0, "10.0.0.3"=>1);

        //Config for Load Balancing by Load

        $this->config['slaves']=array(
        "10.0.0.1"=>"http://slave1/getload.php",
        "10.0.0.2"=>"http://slave2/getload.php",
        "10.0.0.3"=>"http://slave3/getload.php"
        );
        $this->config['memcache']=array(
        "localhost"=>"11211",
        "slave2", "11211"
        );
        */
    }

    /**
     * This Method checks if a Query
     * can be run on Master or Slaves
     *
     * @param string $sql
     * @return boolen
     *
     * @author Andreas Beder <office@codejungle.org>
     */

    function ModifingQuery($sql){

        $sql=strtolower(substr($sql, 0, 6));

        switch ($sql) {

            //selects can be run on slaves, because it's not a modifiny query
            case "select":
                return false;
                break;
            //insert, update, delete, alter must be executed on master
            case "default":
                return true;
        }
        return true;
    }

    /**
     * Simple Load Balanncing
     * Just random Load Balancing
     *
     * @return string ip
     *
     * @author Andreas Beder <office@codejungle.org>
     */
    function SimpleLoadBalanncing(){

        $i=count($this->config['slaves'])-1;
        $rand=rand(0, $i);
        return $this->config['slaves'][$rand];
    }

    /**
     * Load Balancing by Weight
     *
     * @return string ip
     *
     * @author Andreas Beder <office@codejungle.org>
     */
    function LoadBalancingByWeight(){

        $i=count($this->config['slaves']);
        foreach($this->config['slaves'] as $server => $weight){
            $rand=rand(1, $i);
            $lb[$server]=$rand+$weight;
        }
        arsort($lb);
        return key($lb);

    }

    /**
     * LoadBalancingByLoad
     *
     * This method checks the load on n server.
     * Cache the result for 60 sec (because we don't want to ask too many times)
     * Finaly it returns the Server ip with the lowest load
     *
     * @return string ip
     *
     * @author Andreas Beder <office@codejungle.org>
     */
    function LoadBalancingByLoad() {

        $memcache = new Memcache;
        foreach($this->config['memcache'] as $memcahe_server => $port){
            $memcache->addServer($memcahe_server, $port);
        }

        foreach($this->config['slaves'] as $server => $checkloadurl){
            if($memcache->get($server)){
                $load[$server]=$memcache->get($server);
            }
            else
            {
                $curl = curl_init();
                curl_setopt($curl, CURLOPT_URL, $checkloadurl);
                curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
                $output = curl_exec($curl);
                $memcache->set($memcache, $server, $output, 0, 60);
                $load[$server]=$output;
                curl_close($curl);
            }
        }
        asort($load);
        return key($load);
    }

    /**
     * Get Load simply returns the laod average
     * from the last minute
     *
     * @return float load avg
     *
     * @author Andreas Beder <office@codejungle.org>
     */
    function GetLoad(){
        $load=sys_getloadavg();
        return $load[0];
    }
}

?>


Ich habe versucht die Methoden so generisch zu schreiben, dass man sie auch für andere
Lastenverteilung nutzen kann.
Es ist ebenso denkbar, gewisse Methoden zu kombinieren.

Ich hoffe euch hat mein kleiner Artikel zur Lastenverteilung gefallen,
über Feedback würd' ich mich wie immer sehr freuen.

Eine Warnung noch zum Schluss, bitte verwendet die Klasse nicht ohne gezieltes
Benchmarking und Testing im Produktivsystem.


LG

Andreas


Kommentare

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


Kommentare

by:

Schon wieder Omas Geburtstag vergessen?
Der Arzttermin war doch am Montag, oder war es doch Dienstag?
Was war noch mal die wichtige Sache, die ich heute unbedingt noch machen wollte?
Und wann fängt eigentlich das Meeting an?

Peinliche Situationen, die man sich mit meinem neuen Reminder Service
ersparen kann.
Der Sourcecode ist wie immer OpenSource, doch müsst
ihr euch noch ein paar Wochen gedulden.
Ich möchte zuerst sicherstellen, dass der Reminder Service
auch wirklich stabil läuft und dafür brauch ich möglichst viele Betatester.
Wenn du mir dabei helfen magst kannst du Remind ME hier testen:
http://www.codejungle.org/reminder/

LG
Andreas


Kommentare

by:

Es ist nun mehr als ein Monat her, als ich mein letztes Blogpost veröffentlicht habe.
Nun, ich verfahre eben nach dem Motto "Qualität statt Quantität". Ich glaube, einfach dass es besser ist,
weniger dafür aber sinnvolle Posts zu veröffentlichen, als jeden Tag über das Wetter und weiß Gott was zu bloggen.
Auf der anderen Seite, wurde ich schon darauf angesprochen, wieso ich meine Homepage nicht aktualisiert habe.

Also was hat sich bei mir so im letzten Monat getan?

Ich war in Berlin, kam nicht auf den ccc Congress (Karten ausverkauft),
habe angefangen in Python zu hacken (django.codejungle.org),
war auf einem Advanced Security Vortrag im Metalab und
musste meine Wlan Antenne enteisen, damit ich wieder ins Internet komm,
hab mir mal die Endian Firewall angeschaut,
bin morgen auf der Abschiedsfeier von einem Freund der nach Japan siedelt (Goodbye Jay Jay)...

Soweit mal die Kurzfassung, doch bevor ich zur Vorschau auf dieses Jahr komme,
möchte ich noch meine Wörter des Jahres 2009 mit euch teilen.

auf Platz 1 : Terrorverdächtige Minderjährige
auf Platz 2 : Kognitive Dissonanz
auf Platz 3 : Stockholm Syndrom im Zusammenhang mit Apple Usern

Gut, wäre das auch gesagt, also zum Jahr 2010...

Wie immer ist der Terminkalender ziemlich voll, hier einige Events die ich 2010 gerne Besuchen würde.

Fosdem 10 - Feb 6th - 7 th. Brussels, Belgium
Black Hat Europe 2010 - Apr 12th to 15th , Barcelona, ES
Linuxwochen Vienna 2010 - Mai 6th to 8, Vienna AT
EuroPython 2010 - 19th to 22nd July, Birmingham, UK
DEF CON 18 2010 - July 29th - August 1, Las Vegas US
CCC - 27C3 2010 - Dez 27th - 30 th, Berlin GER

Mal sehen was sich so ausgeht. Und was gab es sonst noch, was mich bewegt hat, im noch relativ jungen neuen Jahr?

Haiti
Oracle buys Sun

So, genug davon, ich schätze mal, dass dieses Blogpost in der Form das einzige in diesem Jahr sein wird.
In Zukunft gibt es auch wieder weitere Bsp. Programme, Dokumentationen und aktuelle Themen rund um die IT Welt.
Voraussichtlich werde ich auch mein Blog bald auf Django umstellen, wenn ich das Syntax Highlighting endlich in
den Griff bekomme und das Design auch etwas hergibt... (django.codejungle.org).

In dem Sinne

Ein gutes neues Jahr

Andreas


Nachtrag:
Mir ist gerade noch eingefallen, dass ich und zwei Arbeitskollegen, demnächst ein Killer Ninja Coder
Weekend machen werden, bin gespannt was da raus kommen wird... ;D


Kommentare

by:

Ich freue mich sehr nach einigen Jahren Abstinenz,
dieses Jahr wieder auf den Chaos Communication Congress in Berlin
sein zu können.

Der Congress wird wieder vom 27. bis 30. Dezember im Berliner Congress Center (BCC) beim Alexanderplatz statt finden.

Berliner Congress Center

Happy Hacking, Xmas and New Year

Andreas


Kommentare


Seiten: