ADMIN-Magazin

Memcached als verteilter Datenbank-Cache

Hinter dem schlichten Namen Memcached versteckt sich ein hoch-performantes, verteiltes Caching-System. Obwohl weitgehend anwendungsneutral ausgelegt, puffert es meist in dynamischen Web-Anwendungen die zeitraubenden Datenbankzugriffe. Auf seine Hilfe zählen selbst Branchengrößen, wie Slashdot, Fotolog.com und natürlich sein Vater LiveJournal.com.

Tim Schürmann
Share this

Brad Fitzpatrick war frustriert: Zwar lief die von ihm gegründete und maßgeblich mitprogrammierte Blogger-Plattform LiveJournal.com schon auf über 70 potenten Maschinen, ließ aber bei der Geschwindigkeit noch arg zu wünschen übrig. Selbst die bis zu 8 GByte großen Caches der Datenbankserver verschafften kaum Linderung. Es musste also dringend Abhilfe her. Die in solchen Fällen üblichen Gegenmaßnahmen bestanden meist darin, bestimmte Inhalte vorab zu generieren oder einmal ausgelieferte Seiten komplett in einem Cache zu parken. Elemente, die auf mehreren Seiten vorkommen, speichert man damit jedoch doppelt und müllt so langsam den Cache unnötig zu. Sollte der Hauptspeicher knapp werden, könnte man zwar auf Festplatten ausweichen, die aber wieder relativ langsam sind.

In Brad Fitzpatricks Augen musste ein spezielles, neuartiges Cache-System her. Eines, das einzelne Objekte einer Seite separat speicherte und langsame Festplattenzugriffe vermied. Schon nach kurzer Zeit gab er die Suche nach einer bestehenden Lösung erfolglos auf, setzte sich hin und entwarf seinen eigenen Cache. Fitzpatrick hatte auf seinen Servern überall noch freien Hauptspeicher, den er unbedingt für sein Vorhaben einspannen wollte. Zudem sollten alle Maschinen gleichzeitig auf den Cache zugreifen können und geänderte Inhalte jedem Teilnehmer unverzüglich zur Verfügung stehen. Heraus kam schließlich Memcached, das die LiveJournal-Datenbank nach seinen Angaben schlagartig um über 90 Prozent entlastete, gleichzeitig die Seitenladezeiten für die Benutzer reduzierte und nebenbei noch zu einer besseren Ressourcenauslastung der eingesetzten Maschinen führte. Dies alles geschah vor rund vier Jahren. Nach verschiedenen Auf- und Verkäufen von LiveJournal.com kümmert sich heute Danga Interactive um die Weiterentwicklung von Memcached [1]. An der BSD-Lizenz, der das Caching-System seit Anbeginn untersteht, hat sich glücklicherweise nichts geändert.

Kleidertausch

Der Aufbau eines verteilten Caches mit Memcached ist so einfach wie simpel: Auf jedem Server, auf dem man etwas Hauptspeicher für den gemeinsamen Cache abknapsen kann, startet man einen memcached Daemon. Wer mag, darf auf einer Maschine auch gleich mehrere von ihnen aktivieren. Das ist insbesondere auf Betriebssystemen nützlich, die einem Prozess immer nur einen bestimmten Teil des gesamten, verfügbaren Hauptspeichers zugestehen. Dort startet man einfach mehrere Daemons, die jeweils den für sie maximal möglichen Speicher akquirieren und so doch noch den gesamten freien Platz dem Cache zur Verfügung stellen.

Die Schnittstelle zur eigenen (Web-)Anwendung bildet eine spezielle Client-Bibliothek. Sie nimmt die zu speichernde Information entgegen und legt sie unter einem frei wählbaren Schlüsselwort auf einem der vorhandenen Server ab (Abbildung 1). Welcher Memcached-Daemon dabei letztendlich die Daten erhält und in seinem Hauptspeicher parken darf, bestimmt die Client-Bibliothek über ein ausgeklügeltes, mathematisches Verfahren.

Abbildung 1: Die Client-Bibliothek nimmt die zu puffernden Daten von der Anwendung entgegen und wählt einen Daemon. Erst der übernimmt die eigentliche Speicherung.

Bildlich kann man sich den ganzen Ablauf wie die Garderobe in einem Theater vorstellen: Man gibt seinen Mantel der Dame hinter dem Tresen und nennt ihr eine Nummer. Die Garderobiere nimmt den Mantel entgegen, sucht mit ihm in der Hand den passenden Ständer und hängt das Kleidungsstück schließlich an den Haken mit der entsprechenden Nummer. Nach der Vorstellung läuft das ganz Spiel rückwärts ab: Man nennt der Garderobiere -- Pardon, der Client-Bibliothek -- wieder die Nummer, woraufhin diese abermals zum passenden Daemon stiefelt, dort die Daten vom Haken nimmt und schließlich bei ihrer Anwendung abliefert.

Das ganze Konzept erinnert frappierend an eine verteilte Datenbank oder ein verteiltes Dateisystem. Bei der Arbeit mit Memcached sollte man jedoch immer im Hinterkopf behalten, dass es sich hier um einen reinen Cache handelt. Mit anderen Worten: Die Garderobiere ist unzuverlässig und vergesslich. Reicht beispielsweise der Platz irgendwann nicht mehr für neue Elemente aus, wirft ein Daemon die am wenigsten nachgefragten Daten über Bord und schafft so neuen Platz. Ähnliches passiert, wenn ein Memcached-Daemon ausfällt -- in diesem Fall sind sogar sämtliche von ihm gespeicherten Informationen futsch. Es ist also durchaus möglich, dass man am Ende der Vorstellung seinen Mantel nicht mehr zurück bekommt. Die Anwendung ist dann gezwungen, wieder einmal der Datenbank einen kleinen Besuch abzustatten. Redundanz kennt das memcached System nicht. Muss es auch nicht, denn schließlich ist es nur ein Cache, der schnell Informationen zwischenspeichern und wieder rausrücken muss. Gleichsam ist es unmöglich, über alle Elemente des Caches zu iterieren oder den gesamten Zwischenspeicher ohne Verrenkungen auf die Festplatte zu bannen.

Lauschangriff

Den memcached Daemon stellt Danga Interactive auf einer eigenen Homepage zum Download bereit [1]. Als einzige Voraussetzung verlangt er nach der »libevent«-Bibliothek nebst passendem Entwicklerpaket. Den Daemon selbst übersetzt der bekannte Dreischritt:

configure
make
sudo make install

Einige größere Distributionen verstecken auch fertige, meist jedoch veraltete Memcached-Pakete in ihren Repositories. Nach erfolgreicher Installation aktiviert beispielsweise dieser Befehl den Daemon:

memcached -d -m 2048 -l 192.168.1.111 -p 11211 -u USERNAME

Damit startet Memcached als Daemon (»-d«), der auf dieser Maschine »2048« MByte Arbeitsspeicher für den gemeinsamen Cache abknapst (»-m 2048«). Auf Anfragen von Clients wartet er geduldig unter der IP-Adresse »192.168.1.111« an Port »11211«. Schließlich muss er noch den Benutzernamen erfahren, unter dem er künftig seine Arbeit verrichten soll. Wenn man den Daemon mit der aktuell eingeloggten Benutzer-ID starten möchte, fällt das Anhängsel »-u« einfach weg. Sicherheitsexperten dürften angesichts des letzten Satzes die Haare zu Berge stehen: Standardmäßig darf jeder Benutzer des Linux-Systems einen eigenen Memcached-Daemon starten. Wer das verhindern möchte, muss entsprechende Vorkehrungen treffen und beispielsweise die Zugriffsrechte entziehen. Dies ist nur eine von mehreren Sicherheitsfragen, denen Memcached bewusst aus dem Weg geht (dazu gleich mehr).

Partnerwahl

Nachdem alle Daemons in Stellung gebracht sind, entscheidet man sich für eine der zahlreichen Client-Bibliotheken. Diese gibt es mittlerweile für zahlreiche Programmier- und Skriptsprachen, teilweise besteht sogar die Wahl zwischen verschiedenen Paketen [2]. Wer lieber seinen eigenen Client zimmern möchte, findet eine ausführliche Beschreibung des zugrunde liegenden Protokolls unter [3].

Da Memcached in der Praxis häufig Web-Anwendungen beschleunigt, dürfte die Wahl meist auf einen PHP-Client fallen. Da diese Sprache zudem auch ohne weitere Vorkenntnisse verständlich ist, kommt sie bei allen nachfolgenden Beispielen zum Einsatz. C- und C++-Programmierer finden mehr Informationen im Kasten ,,libmemcached``. Die grundlegende Vorgehensweise ist in allen Sprachen gleich: Nachdem die passende Client-Bibliothek gefunden und installiert ist, bindet der Entwickler sie zunächst in das eigene Programm ein. Mit PHP und dem »memcache«-Client aus dem PECL-Repository, wie er auch Ubuntu im Paket »PHP5-memcache« beiliegt, erstellt diese Zeile ein neues »Memcached«-Objekt:

$memcached = new Memcached;

Anschließend teilt ein Funktionsaufruf der Bibliothek mit, auf welchen Servern überhaupt Memcached-Daemons warten:

$memcache->connect('192.168.2.1', 11211) or die ('Keine Verbindung zu memcached Server');

Ab jetzt können weitere Funktionsaufrufe den Cache nach Lust und Laune mit eigenen Informationen bestücken:

$memcache->set('schluessel', 'test', false, 10);

Diese Anweisung schiebt die Zeichenkette »test« unter dem Schlüsselwort »schluessel« in den Cache, wo sie für 10 Sekunden liegen bleibt. Die Schlüssellängen sind übrigens derzeit auf maximal 250 Zeichen begrenzt -- eine Limitierung, die vom Memcached-Daemon ausgeht.

Um wieder an die Daten heranzukommen, gibt man bei der Client-Bibliothek wieder den Schlüssel ab und fängt das zurückgeworfene Ergebnis auf:

$result = memcache->get('schluessel');

Listing 1 zeigt noch einmal das komplette PHP-Skript.

Listing 1: Eine einfache Cache-Abfrage in PHP.
01 <?php
02 $memcache = new Memcache;
03 $memcache->connect('localhost', 11211) or die ('Keine Verbindung zu memcached Server');
04 
05 $memcache->set('schluessel', 'datum', false, 10);
06 
07 $result = $memcache->get('schluessel');
08 
09 var_dump($result);
10 ?>
libmemcached

Die derzeit beliebteste Client-Bibliothek für C-und C++-Anwendungen heißt »libmemcached« [4] -- nicht zu verwechseln mit dem eingestampften Vorgänger »libmemcache« (ohne d am Ende). Selbst wer weder in C noch C++ programmiert, sollte einen Blick in das Paket riskieren, da es ein paar interessante Kommandozeilenwerkzeuge zur (Server-)Diagnose enthält. So holt beispielsweise »memcat« die Daten zu einem Schlüssel aus dem Cache und gibt sie auf der Konsole aus, während sich »memstat« nach dem aktuellen Zustand eines oder mehrerer Server erkundigt. Zur Übersetzung von »libmemcached« muss auf dem System neben dem C-, auch ein einsatzbereiter C++ Compiler installiert sein, den Rest erledigt wieder »./configure; make; make install«. Eine einfache Cache-Abfrage geschieht dann wie folgt:

#include <memcached.h>
#include <string.h>
#include <stdio.h>

main()
{
        /* memcached_st Struktur anlegen (enthält alle grundlegenden Infos über die memcached Server) */
        memcached_st *mcd = memcached_create(NULL);

        /* Einen Server hinzufügren: */
        memcached_server_add(mcd, "127.0.0.1", 11211);

        /* Objekt in den Cache schieben: */

        char *key = "schluessel";
        size_t keylength = strlen(key);
        char *value = "information";
        size_t valuelength = strlen(value);
        time_t expiration = 0;
        uint32_t flags = 0;

        memcached_add(mcd, key, keylength, value, valuelength, expiration,flags);

        /* Objekt wieder rausholen: */

        memcached_return fehlervariable;

        char *result = memcached_get(mcd, key, keylength, &valuelength, &flags, &fehlervariable);

        /* Objekt ausdrucken: */
        printf("Cache: %s\n", result);

        /* Aufräumen: */
        memcached_free(mcd);
}

Besonders interessant ist die Möglichkeit, mehrere Daten gleichzeitig sowohl in den Cache zu schreiben, als auch wieder auszulesen. Die Client-Bibliothek parallelisiert dabei automatisch ihre Anfrage an die Memcached-Server. Leider stellen nicht alle Client-Bibliotheken diese Funktionen zur Verfügung, das folgende Beispiel klappt beispielsweise unter PHP nur mit dem Memcached-Client (mit dem ,,d`` am Ende):

$mehrere = array(
        'schluessel1' => 'wert1',
        'schluessel2' => 'wert2',
        'schluessel3' => 'wert3'
);
$memcache->setMulti($mehrere);

Ihr Einsatz, bitte

Bei bestehenden Web-Anwendungen stellt sich schnell die Frage, an welchen Stellen man Memcached am besten nachträglich einbaut und nutzt. Die Antwort liefert ein kleines Profiling: Alle Datenbankabfragen, die den Server besonders stressen, leitet man in den Cache um. Wie so etwas in der Praxis aussieht, zeigen Listing 2 und 3: Vor der Datenbankabfrage prüft der Code, ob die gesuchten Informationen nicht in Memcached liegen. Nur wenn dieser kein passendes Objekt liefert, erfolgt einer Abfrage der Datenbank. Um sich beim nächsten Mal eine weitere Abfrage zu sparen, speichert man ihre Ergebnisse noch rasch in den Cache. Um diesen stets auf dem aktuellen Stand zu halten, wandern bei jedem Schreibvorgang auf die Datenbank die dabei fließenden Informationen auch noch in den Cache. In Listing 3 setzen sich die Schlüssel übrigens aus dem Wort »user« und der ID des Benutzerkontos zusammen -- eine weit verbreitete Strategie, um eindeutige Schlüssel zu erhalten.

Listing 2: Datenbankabfrage ohne Memcached...
01 function get_user($userid)
02 {
03         $result = mysql_query("SELECT * FROM users WHERE userid = '%s'", $userid);
04         return $result;
05 }
Listing 3: ... und nach seiner Anwendung
01 $memcache = new Memcache;
02 $memcache->connect('servername', 11211) or die ('Keine Verbindung zu memcached Server');
03 ...
04 function get_user($userid)
05 {
06         $result = memcache->get("user" + $userid);
07         if(!$result)
08         {
09                 $result = mysql_query("SELECT * FROM users WHERE userid = '%s'", $userid);
10                 memcache->add("user" + $userid, $result);
11         }
12         return $result;
13 }

Mit diesem Vorgehen ist Memcached schnell in die eigenen Anwendungen integriert. Das Cache-System weist allerdings auch ein paar Fußangeln auf, die sich erst bei einem Blick unter die Haube offenbaren.

Brockhaus

Erfahrene Programmierer haben es sicherlich schon erkannt: Memcached arbeitet intern mit einem Wörterbuch (Dictionary, bei einigen Programmiersprachen auch als Assoziatives Array bezeichnet). Ähnlich wie ein reales Wörterbuch speichert diese Datenstruktur jeden Wert unter einem bestimmten Schlüssel(-wort). Das Memcached-System realisiert dieses Dictionary mit zwei hintereinander geschalteten Hashtabellen [5]: Zunächst nimmt die Client-Bibliothek den Schlüssel und errechnet aus ihm mit einer ausgeklügelten, mathematischen Funktion eine Zahl, den so genannten Hashwert. Diese Nummer sagt der Bibliothek, an welchen Memcached-Daemon sie sich wenden muss. Nachdem dieser alle Daten erhalten hat, errechnet er mit einer eigenen Hashfunktion die Speicherstelle, unter der er die Daten schließlich einlagert. Die mathematischen Funktionen sind dabei so gestaltet, dass sie zu einem ganz bestimmten Schlüssel immer dieselbe Nummer ausspucken. Diese Arbeitsweise erlaubt extrem kurze Such- und Antwortzeiten: Um eine Information wieder aus dem Cache zu ziehen, muss das memcached-System lediglich wieder die beiden mathematische Funktionen auswerten. Den größten Teil der Antwortzeit schluckt somit die Datenübertragung im Netz.

Da jedoch die Client-Bibliothek bestimmt, welcher Daemon welche Daten speichert, sind auf allen beteiligten Maschinen die gleichen Bibliotheken in der gleichen Version nötig. Bei gemischtem Betrieb kann es sonst vorkommen, dass die Clients unterschiedliche Hashfunktionen nutzen und somit die gleiche Information auf unterschiedlichen Servern landet, was wiederum zu Inkonsistenzen und komplett durcheinander gewürfelten Daten führt. Wer die C- und C++-Bibliothek »libmemcached« verwendet, muss sogar besonders aufpassen, da diese mehrere Hashfunktionen zur freien Auswahl stellt.

Darüber hinaus verwendet jeder Client eine andere Methode zur Serialisierung. Unter Java kommt beispielsweise Hibernate zum Einsatz, während PHP seine Objekte über »serialize« speichert. Sobald also nicht mehr nur einzelne Zeichenketten, sondern ganze Objekte im Cache landen sollen, wird eine gemeinsame Nutzung aus verschiedenen Sprachen heraus unmöglich -- selbst dann, wenn alle Clients die gleiche Hashfunktion verwenden würden. Als Sahnehäubchen obendrauf dürfen die Bibliotheken die Daten noch mit einem beliebigen Verfahren komprimieren.

Gedächtnisschwund

Im Gegenzug bedient der Cache auch parallele Abfragen ohne Geschwindigkeitsverlust. Im Theater können mehrere Garderobieren gleichzeitig durch die Gänge flitzen und Mäntel auf- und abhängen, ohne dass eine auf die andere warten müsste. Genau das gleiche gilt hier für memcached: Jeder Client berechnet selbst, welchen Daemon er anfunken muss, im Idealfall rennt jede Garderobiere in einen anderen Gang. Allerdings verhindert auch niemand, dass die hilfreichen Damen munter ineinander laufen: Holt man Daten aus dem Cache, manipuliert diese und schreibt sie wieder zurück, so garantiert niemand, dass diese Daten nicht zwischenzeitlich von einer anderen Instanz geändert wurden. Abhilfe schaffen seit Version 1.2.5 die Kommandos »gets« und »cas«: Holt der Anwender per »gets« Daten ab, erhält er zusätzlich noch eine eindeutige ID (den Unique Identifier). Diese schickt er später zusammen mit den überarbeiteten Daten per »cas«-Befehl wieder Richtung Server. Dort stellt der Daemon anhand der ID fest, ob die Daten seit der letzten Abfrage noch unversehrt sind und überschreibt sie erst bei einer positiven Antwort mit dem neuen Wert.

Wie Memcached mit einem komplett ausgefallenen Server umgeht, hängt ebenfalls maßgeblich vom Client ab. Standardmäßig verhält er sich so, als wäre die gesuchte Information im Cache einfach nicht (mehr) vorhanden. Man sollte die Cache-Server somit besser ständig überwachen. Dank des modularen Aufbaus lässt sich ein solcher Daemon immerhin schnell durch einen neuen ersetzen. Dazu muss man meist neue IP-Adressen bei den Clients an-, beziehungsweise wieder abmelden. Einige Bibliotheken deklarieren dann jedoch den gesamten Cache auf einen Schlag für ungültig.

Schweizer Käse

Um eine Defragmentierung der Hauptspeicher zu verhindern, setzen die Daemon einen Slab Allocator [6] zur Speicherverwaltung ein. Dieses Verfahren ist darauf spezialisiert, häufig kleine Speicherbereiche zu reservieren und wieder freizugeben. Im Fall von Memcached bedeutet klein maximal 1 MByte, größere Datenmengen nimmt ein Daemon gar nicht erst an. Wer mehr speichern möchte, muss die Daten entweder auf mehrere Schlüssel aufteilen oder ein anderes Caching-System verwenden.

Anarchie

Memcached verschwendet keinerlei Gedanken an seine eigene Sicherheit. Die Clients müssen sich noch nicht einmal gegenüber einem Daemon authentifizieren. Jeder, der Zugriff auf das Netz besitzt, darf ungeniert und unbehelligt den Cache anzapfen. Weiß ein Angreifer beispielsweise, dass die Benutzernamen als Schlüssel dienen, so muss er nur systematisch alle Daemons nach ihm bekannten Namen befragen. Abhilfe schaffen hier möglichst kryptische Schlüssel. Um solche zu erhalten, könnte man beispielsweise den Benutzernamen schon in der eigenen Anwendung durch eine Hashfunktion laufen lassen und dann das Ergebnis als Schlüssel verwenden. Grundsätzlich sollte man sämtliche Kontodaten nach Gebrauch wieder aus dem Cache löschen, die Lebenszeit aller gespeicherten Daten begrenzen und unbedingt weitere Sicherheitsmaßnahmen einleiten. Hierzu gehört an erster Stelle eine Firewall, die den Rechnerverbund von der Außenwelt abschirmt.

Fazit

Memcached ist zwar kinderleicht aufzusetzen und auch in bestehende Anwendungen schnell integriert, diesen Komfort erkauft man sich aber mit einigen haarigen Sicherheitslöchern. Wer sich dessen bewusst ist, erhält einen pfeilschnellen, verteilten Cache, der selbst unter extremen Bedingungen nicht so leicht in die Knie geht. Dies beweist er im täglichlichen Einsatz auf LiveJournal oder Slashdot. Dabei bleibt das gesamte System extrem genügsam: Da im Wesentlichen nur Hashes berechnet werden, braucht memcached nur wenig CPU-Leistung, folglich lassen sich auch noch ältere Maschinen als Cache-Lieferant rekrutieren. (ofr)


Infos

[1] Memcached: [http://www.danga.com/memcached]
[2] Übersicht über die Client-Bibliotheken: [http://code.google.com/p/memcached/wiki/Clients]
[3] Einblick in das Protokoll: [http://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol]
[4] Libmemcached: [http://tangent.org/552/libmemcached.html]
[5] Funktionsweise einer Hashtabelle: [http://de.wikipedia.org/wiki/Hashtabelle]
[6] Funktionsweise eines Slab Allocator: [http://de.wikipedia.org/wiki/Slab_allocator]

Kommentare

bin mir nicht ganz sicher, ob

bin mir nicht ganz sicher, ob ich die Frage verstehe, aber man fügt die Server über die FunktionMemcache::addServer hinzu:  de2.php.net/manual/de/function.memcache-addserver.phpbeste GrüßeOliver Frommel

Fragen und Korrektur

Hi,vielen Dank für den guten Einblick in die Funktionsweise von Memcache.Der Beispielcode in Listing 3 enthält ein paar Flüchtigkeitsfehler:
$memcache->connect('servername', 11211) or die ('Keine Verbindung zu memcached Server');
function get_user($userid)
{
..global $memcache;
..$result = $memcache->get("user" + $userid);
..if(!$result)
..{
....$result = mysql_query("SELECT * FROM users WHERE userid = '%s'", $userid);
....$memcache->add("user" + $userid, $result);
..}
..return $result;
}
Ich habe diese mal unterstrichen eingefügt.Mir ist noch nicht ganz klar wie die Client-Bibliothek den Pool an möglichen Daemons kennenlernt, wenn sie direkt mit einem der Daemons verbunden wird?!GrußMatthias

Darstellungsoptionen

Wählen Sie hier Ihre bevorzugte Anzeigeart für Kommentare und klicken Sie auf „Einstellungen speichern“ um die Änderungen zu übernehmen.

Kommentar hinzufügen

CAPTCHA
Diese Frage hat den Zweck zu testen, ob Sie ein menschlicher Benutzer sind und um automatisierten Spam vorzubeugen.