Apache CouchDB: Eine NoSQL-Datenbank zum Entspannen

Für kaum eine Herausforderung in der Informatik wurden und werden so viele unterschiedliche Lösungsansätze entwickelt wie für die effiziente Speicherung und Verwaltung sehr großer Datenmengen. Doch was vor zehn Jahren noch als sehr groß galt, ist heute nicht mehr sonderlich beeindruckend. In großen sozialen Netzwerken sind Datenmengen im Terabyte-Bereich an der Tagesordnung, bei denen klassische Lösungen nicht mehr greifen. NoSQL-Datenbanken wie Apache CouchDB sind genau für solche Szenarien entwickelt worden und werden immer populärer.

Nachdem wir im Artikel NoSQL: Eine kurze Einführung in Theorie und Praxis einen Überblick über die Probleme herkömmlicher Datenbanklösungen wie MySQL und die Vorteile von NoSQL-Implementationen gegeben haben, soll dieser Beitrag einen praxisorientierten Einblick in diese neue Generation von Datenbanken am konkreten Beispiel bieten.

Entspannung mit CouchDB

Beim Start von CouchDB wird man direkt mit "Time to relax" dazu aufgefordert, sich zu entspannen, und das offizielle Logo zeigt eine auf der Couch herumlungernde Person. Diese originelle, wenn vielleicht auch etwas merkwürdig wirkende, Symbolik ist vom Entwickler Damien Katz sehr bewusst gewählt worden: Als dieser im Jahr 2005 mit der Entwicklung von CouchDB begann, waren viele Strategien, die das Projekt zur Speicherung und Verwaltung großer Datenmengen nutzt, noch frisch und wenig erprobt. Sein Hauptziel bestand stets darin, eine Datenbank zu realisieren, die in Sachen Bedienung und Erweiterbarkeit nicht nur effizient, sondern auch unkompliziert ist.

Als ehemaliger Entwickler von Lotus Notes hat Katz so manche Features dieses Systems direkt übernommen und um weitere ergänzt: So ist auch CouchDB ein dokumentenorientiertes Datenbanksystem, das speziell für die Benutzung auf verteilten Systemen ausgelegt ist.

Hinzugekommen ist unter anderem der MapReduce-Ansatz von Google, der paralleles Arbeiten im Cluster ermöglicht und auch bei Datenmengen im Tera- oder gar Petabyte-Bereich noch immer performante Ergebnisse erzielt.

Womit wir auch bei unserer ersten Frage wären:

Wie speichert CouchDB Daten und wie wird auf diese zugegriffen?

Anders als in relationalen Datenbanken, gibt es in der NoSQL-Datenbank CouchDB keinerlei Tabellen. Stattdessen werden die Daten in voneinander unabhängigen Dokumenten abgelegt, die in der JavaScript Object Notation (JSON) formatiert sind. Das hat mehrere Vorteile: JSON bietet eine einfache Syntax, die auch für Menschen gut lesbar ist, und die Unabhängigkeit der Dokumente erlaubt den Einsatz von MapReduce.

Aber was ist MapReduce eigentlich? Zunächst handelt es sich "lediglich" um ein Patent des Unternehmens Google, das ein Framework beschreibt, mit dem sich paralleles Rechnen in riesigen Datenmengen auf verteilten Systemen in einem Computer-Cluster realisieren lässt. Was unspektakulär klingt, ist für CouchDB allerdings ein wahrer Segen: Wer schon einmal mit Datenbanken wie MySQL gearbeitet hat, wird vermutlich auch von der Structured Query Language (SQL) Gebrauch gemacht haben. In der Welt von NoSQL gibt es diese Datenbanksprache jedoch ebenso wenig wie starre Tabellen mit fest definierten Schemata. Stattdessen gibt es map- und reduce-Funktionen in JavaScript.

Eine map-Funktion ist in CouchDB nichts anderes als eine JavaScript-Funktion mit genau einem Paramater (doc), der als Referenz auf ein JSON-Dokument der Datenbank dient. Der Blick aufs Detail lohnt sich: Dank der Unabhängigkeit der vielen verschiedenen JSON-Dateien wird die map-Funktion für jedes einzelne Dokument aufgerufen – und zwar parallel. Auf diesem Wege lassen sich dann auch Petabyte an Daten noch in akzeptabler Zeit durchsuchen.

Als Ergebnis liefern die vielen parallelen map-Aufrufe letztlich eine Liste von Key-Value-Paaren zurück, die wiederum in JSON formatiert sind und von der JavaScript-Funktion reduce nach Wunsch weiterverarbeitet werden können.

Beispiel: Mit map und reduce eine Abfrage durchführen

Wer bisher nur mit SQL-Abfragen gearbeitet hat, den dürfte der MapReduce-Ansatz zunächst womöglich etwas verwundern. Spielen wir zur Veranschaulichung also ein kleines Beispielszenario durch.

Nehmen wir der Einfachheit halber an, dass unsere Datenbank nur aus zwei kleinen JSON-Dokumenten besteht, die jeweils den Namen eines beliebigen Produkts und dessen Preis speichern:

{
   "_id": "buch",
   "_rev": "AE19EBC7654",

   "product": "Buch",
   "price": 19.95
}
{
   "_id": "dvd",
   "_rev": "4A3BBEE711",

   "product": "DVD",
   "price": 9.95
}

Normalerweise würde _id eher eine zufällige Zeichenkette sein, die sich durch CouchDB leicht erzeugen lässt, und _rev dient allein der Versionierung und ist in unserem Fall nur der Vollständigkeit halber vorhanden.

Wollen wir nun eine Ergebnisliste erzeugen, die alle Dokumente der Datenbank nach Produkten durchsucht und mit den entsprechenden Preisen zurückliefert, müssen wir zuerst eine map-Funktion schreiben. In dieser muss sichergestellt werden, dass wir in einem Dokument gelandet sind, das die Attribute product und price auch tatsächlich enthält. Das ist wichtig, denn wie erwähnt führt CouchDB die map-Funktion für alle (!) Dokumente aus; es ist unsere Aufgabe, dafür zu sorgen, dass wir nur bestimmte Dokumente weiterverarbeiten und andere ignorieren.

Haben wir ein Dokument mit den Attributen product und price entdeckt, übergeben wir die Werte der speziellen emit-Funktion, die sie dann als Key-Value-Paar zurückliefert.

function(doc) {
   if (doc.product && doc.price) {
      emit(doc.product, doc.price);
   }
}

Das Ergebnis unserer map-Aufrufe wäre ein JSON-Dokument, das in etwa wie folgt aussehen könnte:

{
   "total_rows": 2,
   "offset": 0,
   "rows": [
      { "id": "buch", "key": "Buch", "value": 19.95 },
      { "id": "dvd", "key": "DVD", "value": 9.95 }
   ]
}

Auch wenn wir das Attribut _id nicht bewusst an die emit-Funktion übergeben haben, sorgt CouchDB automatisch dafür, dass dieser Wert nie fehlt. Die Werte von key und value entsprechen genau unseren Erwartungen.

Abschließend könnten wir nun mithilfe einer reduce-Funktion die einzelnen Werte aufsummieren und wüssten so, wie viel uns der Kauf aller verfügbaren Produkte kosten würde:

function(keys, values, rereduce) {
   return sum(values)
}

Der Parameter rereduce dieser JavaScript-Funktion ist ein boolescher Wert, der entweder auf true oder false gesetzt wird und es erlaubt, große Ergebnisse in mehreren Zwischenschritten zu verarbeiten.

Die Kombination der notwendigen map- und optionalen reduce-Funktion nennt sich in der CouchDB-Welt View und kann temporär oder permanent sein. Eine temporäre View ist in etwa das Pendant zu einer einzelnen SQL-Abfrage und eine permanente View kann man mit einer Stored Procedure vergleichen, die sich wiederverwenden lässt und auf dem Server als JSON-Dokument hinterlegt wird.

CouchDB mit einfachen HTTP-Requests bedienen

Die obigen Beispiele einer View sind zwar leicht verständlich, beantworten aber noch nicht die Frage, wie man nun eigentlich auf die Datenbank zugreift. Um mit CouchDB zu interagieren, ist letztlich nur eine Möglichkeit erforderlich, HTTP-Anfragen zu versenden und die Antworten zu empfangen. Ob dies nun mithilfe eines eigenen kleinen Programms, dem Konsolen-Tool curl, über die Weboberfläche Futon oder auf ganz anderem Wege passiert, ist dem Nutzer überlassen.

Nehmen wir für einige rudimentäre Beispiele das Konsolen-Tool curl zur Hand, da es sich besonders gut dazu eignet, die Funktionsweise von CouchDB zu beleuchten. Standardmäßig wartet die Datenbank auf Port 5984, an dem wir nun anfragen, ob alles in Ordnung ist:

curl http://127.0.0.1:5984/

Eine Antwort wie die folgende wäre wünschenswert:

{"couchdb":"Welcome","version":"0.10.1"}

Eine Liste aller verfügbaren Datenbanken können wir uns mit dem folgenden GET-Request zukommen lassen:

curl -X GET http://127.0.0.1:5984/_all_dbs

Möchten wir eine neue Datenbank anlegen, müssen wir lediglich einen kurzen PUT-Request schicken:

curl -X PUT http://127.0.0.1:5984/foo

Wollten wir die die Datenbank wieder löschen, ginge dies wie folgt:

curl -X DELETE http://127.0.0.1:5984/foo

Um nun ein Dokument in der Datenbank zu hinterlegen, fragen wir zuerst nach einem Universally Unique Identifier (UUID):

curl -X GET http://127.0.0.1:5984/_uuids

Die Antwort sollte jedes Mal eine andere sein, niemals sollte uns CouchDB zwei identische UUIDs liefern.

{"uuids":["6e1295ed6c29495e54cc05947f18c8af"]}

Mit diesem UUID legen wir nun ein drittes Produkt in unserem Beispielszenario von oben an:

curl -X PUT \
http://127.0.0.1:5984/foo/6e1295ed6c29495e54cc05947f18c8af \
-d '{"product":"CD","price":14.95}'

Erinnern wir uns an unser Beispiel, in dem das Attribut _id die Werte buch und dvd trägt. Um Kollisionen zu vermeiden, würde man eine solch kurze Zeichenkette vermutlich nie in einem Produktivsystem wählen, sondern auf einen UUID wie den gerade verwendeten zurückgreifen.

(Für weitere Beispiele und weiterführende Erläuterungen sei dem interessierten Leser das frei verfügbare Buch CouchDB: The Definitive Guide ans Herz gelegt, von dem dieser Artikel zum Teil inspiriert wurde.)

Fazit

Der Umstieg auf eine NoSQL-Datenbank stellt für SQL-Veteranen sicherlich eine gewisse Hürde dar, aber die Vorteile sind zu verlockend, um sich der Herausforderung nicht zu stellen. Gerade die schnelle und kostengünstige Erweiterbarkeit durch horizontale Skalierung ist ein nicht zu unterschätzender Pluspunkt, um Projekte für künftige Herausforderungen zu wappnen. Zusammen mit dem MapReduce-Ansatz, der parallele Berechnungen erlaubt und so auch noch im Petabyte-Bereich eine gute Performance liefert, ist man für Datenmengen gerüstet, die man sich heute noch nicht einmal vorstellen kann.

Und auch die Bedienung spricht für sich: Offenbar stets Usability-Aspekte im Blickfeld, haben die Entwickler von Apache CouchDB eine NoSQL-Datenbank geschaffen, die dem Nutzer die lästigen Aufgaben so weit wie möglich abnimmt und ihm gleichzeitig viel Freiraum zur Lösung der wichtigen "Probleme" lässt.

Weiterführende Informationen

Ihr neues Projekt soll mit modernsten Technologien entwickelt werden? Sie möchten ein bestehendes System erweitern oder eine Software-Plattform migrieren? Sie benötigen Schnittstellen zwischen Anwendungen im Unternehmen? //SEIBERT/MEDIA ist der richtige Partner. Wir legen größten Wert auf iterative Erweiterbarkeit, Performanz, Skalierbarkeit, Testbarkeit und Plattformunabhängigkeit und schaffen so individuelle High-End-Software-Lösungen, die sich auch im Nachhinein flexibel ausbauen und verändern lassen. Bitte sprechen Sie uns unverbindlich an!


Mitwirkender an Konzeption und Umsetzung: Benjamin Borbe


Mehr über die Creative-Commons-Lizenz erfahren