Freitag, 13. August 2010

RIAK und Innostore

Für jeden ernsthaften Test mit RIAK sollte man Innostore als Datenbackend verwenden. Auf meinem Mac waren erst damit wirklich performante Abfragen zu realisieren.
Aus Lizenzgründen ist Innostore nicht direkt mit RIAK in einem Bundle, auf der RIAK Seite ist aber gut beschrieben wie man Innostore compiliert und konfiguriert.

Reduce Funktion

Auch in RIAK müssen reduce Funktionen idempotent sein und werden ggfs. mehrfach aufgerufen um mehrere Zwischenergebnis zu einem Gesamtergebnis zusammen zu fassen.
Das bedeutet in der Regel, dass die Funktion mit unterschiedlichen Eingabewerten umgehen können muss und man irgendwie unterscheiden muss, ob es sich um einen reduce oder rereduce Aufruf handelt.
In CouchDB ist dieses Problem recht einfach gelöst; hier gibt der Parameter "rereduce" an, ob die Methode mit dem Ergebnis aus einem vorangegangenen Reduce aufgerufen wird.
In RIAK muss man sich das selber nachbauen, z.B. indem man ein JSON-Objekt mit einem entsprechenden Feld zurückliefert: return [{'rereduce': true, 'value': value}].
Ich stelle jedoch gerne zur Diskussion ob das so der optimale Weg ist.
Generell sollte man bei den reduce Methoden von vornherein an den rereduce Fall denken, ansonsten kann es einem schnell passieren dass die Sache mit wenigen Datensätzen noch funktioniert, bei größeren Datenmengen aber auf die Nase fällt.
Dies ist jedoch kein Problem von RIAK, sondern liegt in der Natur des Konzepts.

Donnerstag, 12. August 2010

RIAK DB Quick Test

Seit einiger Zeit beschäftige ich mich mit unterschiedlichen "NoSQL" Datenbanken. Dabei bin ich vor kurzem auf RIAK gestoßen. RIAK ist ein Key-Value-Store der hervorragende Möglichkeiten zum skalieren (horizontal) verspricht und Abfragen mit Hilfe von MapReduce ermöglicht. In diesem Punkt ähnelt RIAK durchaus etwas CouchDB, mit dem kleinen aber feinen Unterschied dass die Abfragen ad hoc ausgeführt werden (CouchDB erstellt Views, die dann inkrementell aktualisiert werden).
Hier interessiert mich vor allem die Abfrageperformance von RIAK.

Ich werde RIAK zunächst auf einem einzelnen Rechner mit Mac OS X 10.6 testen und wie hier beschrieben drei Instanzen auf einem Rechner laufen lassen, also quasi einen Cluster mit drei Knoten installieren.

Download und Installation


Leider funktioniert die Installation auf meinem Mac nicht so wie beschrieben. Ich hab zunächst Mercury installiert und dann die Quellen aus dem Repository ausgecheckt.
Das anschließende "build all" endet leider mit dem Fehler


./rebar get-deps
env: escript: No such file or directory
make: *** [deps] Error 127


Nachtrag 13.08.2010:
der Fehler scheint auf eine unvollständige Erlang Installation zurück zu gehen. Ich hab Erlang auf Basis der Quellen neu gebaut und installiert und seitdem funktionieren auch die RIAK Builds einwandfrei.

Da ich keine Lust habe mich jetzt durch irgendwelche Makefiles zu wühlen, habe ich beschlossen eine fertig compilierte Version von RIAK herunterzuladen und die Konfiguration dann so anzupassen dass ich drei Instanzen auf einem Rechner laufen lassen kann.
Ich hab mir RIAK in die drei Ordner "riak-node1", "riak-node2" und "riak-node3" kopiert. Den ersten Knoten kann man aus dem RIAK-Verzeichnis mit "./bin/riak start" direkt starten.
Für die anderen beiden Knoten muss man noch den Namen und die Ports anpassen.
Die Anpassung der Ports nimmt man in der Datei

/etc/app.config

vor. Für jeden Knoten muss ein eigener Port für "web_port", "handoff_port" und "pb_port" eingestellt werden. Der Name des Knoten wird leider nicht auch in app.config eingetragen, sondern muss in der Datei  vm.args eingetragen werden. Ich hab hier einfach "riak1@127.0.0.1", "riak2@127.0.0.1" und "riak3@127.0.0.1" gewählt.
Anschließend kann man auch die Knoten zwei und drei über "./bin/riak start" starten.
Mit Hilfe des tools "riak-admin" fügt man nun die beiden Knoten zum Cluster hinzu:

/riak-node2/bin/riak-admin join riak@127.0.0.1
/riak-node2/bin/riak-admin join riak@127.0.0.1


Nach dem Start der Knoten kann man mit "riak-admin status" (an einem beliebigen Knoten) de Clusterstatus überprüfen.
Auf meinem System sieht die Ausgabe so aus (Auszug):


connected_nodes : ['riak@127.0.0.1','riak2@127.0.0.1',
                   'riak3_maint_23884@127.0.0.1']
sys_driver_version : <<"1.5">>
sys_global_heaps_size : 0
sys_heap_type : private
sys_logical_processors : 2
sys_otp_release : <<"R13B04">>
sys_process_count : 152
sys_smp_support : true
sys_system_version : <<"Erlang R13B04 (erts-5.7.5) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:5] [hipe] [kernel-poll:true]">>
sys_system_architecture : <<"i386-apple-darwin10.3.0">>
sys_threads_enabled : true
sys_thread_pool_size : 5
sys_wordsize : 8
ring_members : ['riak2@127.0.0.1','riak3@127.0.0.1','riak@127.0.0.1']
ring_num_partitions : 64
ring_ownership : <<"[{'riak@127.0.0.1',21},{'riak3@127.0.0.1',21},{'riak2@127.0.0.1',22}]">>

Die Informationen lassen sich auch über das http Interface von RIAK abrufen. Man kann dafür entweder curl verwenden oder aber natürlich jeden beliebigen Browser.
Die Status-Url http://127.0.0.1:8091/stats liefert ein JSON-Dokument zurück, mit Hilfe des Firefox-Plugins "JSONView" kann man sich das Dokument jedoch auch direkt im Browser ansehen.
Wie auf der RIAK-Seite beschrieben hab ich auch noch schnell mal ein Bild in der Datenbank gespeichert und wieder abgefragt:

curl -X PUT http://127.0.0.1:8091/riak/bilder/riak-logo.png -H "Content-type: image/png" --data-binary @/riakcluster/riak-logo.png

Und unter dieser URL ist das Bild dann auch wieder zu finden: http://127.0.0.1:8091/riak/bilder/riak-logo.png !
Aufgrund der Architektur von RIAK spielt es keine Rolle an welchen Knoten man die Daten schickt oder von welchem man sie abruft.

Daten in RIAK speichern

Als nächstes möchte ich ein paar Testdaten von Hand in RIAK speichern. Ich verwende dazu das REST Interface und schreib ein paar Datensätze (im JSON Format) mit Hilfe von curl:

curl -X PUT -d '{"type":"website","name":"www.heise.de"}' -H "Content-Type: application/json" http://127.0.0.1:8091/riak/websites/www.heise.de
curl -X PUT -d '{"type":"website","name":"www.n-tv.de"}' -H "Content-Type: application/json" http://127.0.0.1:8091/riak/websites/www.n-tv.de

Über http://127.0.0.1:8091/riak/websites?keys=true kann man Informationen zu einem Bucket (hier: websites) abfragen. Ist der Parmeter keys auf true gesetzt, werden auch die Schlüssel aller in dem Bucket gespeicherten Werte ausgegeben.

Als nächstes würde ich gerne mal einen MapReduce Job auf RIAK laufen lassen. Das geht zwar auch über curl, ist mir aber für größere Tests zu umständlich. Daher werde ich mir zunächst mal ein kleines Java-Programm bauen um Daten in RIAK zu speichern und abzufragen.


RIAK Java Library

Für RIAK gibt es eine offizielle Java-Library, die man hier herunterladen kann. Ein fertiges JAR konnte ich nicht finden, man muss sich das JAR daher mit Maven selber bauen (was aber reinbungslos funktioniert): mvn clean install !
Zusätzlich werden noch ein paar Libraries der Apache commons benötigt, die sich aber alle im lib Verzeichnis befinden.
Zum (de)serialisieren der Java-Objekte in/von JSON verwende ich die svenson library.


Lesen und Schreiben eines Datensatzes (Java)

Als erstes will ich über ein Mini Java-Programm einen von Hand angelegten Datensatz wieder auslesen:

RiakClient riak = new RiakClient("http://127.0.0.1:8091/riak");
FetchResponse r = riak.fetch("websites", "www.google.de");
System.out.println(r.getObject().getValue());


Das funktioniert auf Anhieb. Das Anlegen eines Datensatzes funktioniert ähnlich einfach (in meinem Beispiel wird der JSON-String aus einem einfachen 'Website' Objekt erzeugt):

Website ard = new Website();
ard.setName("www.ard.de");
ard.setType("website");
riak.store(new RiakObject("websites", ard.getName(), JSON.defaultJSON().forValue(ard).getBytes(), "application/json"));


Abfragen mit Hilfe von MapReduce

Jetzt kommt der für mich wirklich spannende Teil: das Abfragen von Datensätzen mit MapReduce.
Als Einstieg möchte ich eine einfache Map-Funktion schreiben die mir alle Websites heraussucht die ein 'r' im Namen enthalten. Eine reduce-Funktion braucht man für diese einfache Suche noch nicht.
An dieser Stelle läuft es zum ersten Mal nicht ganz so problemlos.
Daher hab ich mir relativ schnell Möglichkeiten gesucht meine MapReduce Funktionen zu debuggen.
Fehler die beim Ausführen der Map bzw. Reduce Methode (Javascript) auftreten, werden in der Datei

log/sasl-error.log

protokolliert. Besonders aussagekräftig sind die Fehlermeldungen zwar meistens leider nicht, aber es ist immer noch besser als nichts.
Man kann aber auch selber Einträge in ein eigenes Log-File schreiben.
Dazu steht die Funktion

ejsLog(pathtologfile, message)

zur Verfügung. Ich verwende diese Möglichkeit um erst mal die Struktur der übergebenen Parameter zu lernen.
Hier ein kleines Java-Beispiel:

MapReduceResponse mr =  riak.mapReduceOverBucket("websites").map(JavascriptFunction.anon("function (value,keydata,arg) { ejsLog('/riakcluster/mapred.log', JSON.stringify(value)); return [value]; }"), true).submit();

Diese Map-Funktion schreibt den Parameter 'value' in das Logfile 'mapred.log'.
In meinen Fall sieht ein Eintrag so aus:



08/12/2010 (15:42:41): {"bucket":"websites","key":"www.n-tv.de","vclock":"a85hYGBgzmDKBVIsjCe5TmQwJTLmsTKoud84ygcVZlrBIAAVrsuEC7M1J7H8YT2BLJEFAA==","values":[{"metadata":{"Links":[],"X-Riak-VTag":"3XWRElappCZDe2AgLa7xkG","content-type":"application/json","X-Riak-Last-Modified":"Thu, 12 Aug 2010 12:47:58 GMT","X-Riak-Meta":[]},"data":"{\"type\":\"website\",\"name\":\"www.n-tv.de\"}"}]}

Bei dem Parameter 'value' handelt es sich um ein JSON-Objekt, die eigentlichen Daten (in meinem Fall auch ein JSON-Objekt) findet sich im Attribut 'data'.
Aus meiner Sicht wäre es nun logisch und konsequent, wenn man auf das eigene JSON-Objekt in der Form 'value.data.name' zugreifen könnte (um den Namen der Website zu ermitteln).
Das funktioniert so jedoch nicht. Der Grund dafür ist mir erst beim zweiten hinschauen aufgefallen: mein eigenes JSON-Objekt liegt in dem Feld 'data' gar nicht als Objekt, sondern als String vor (man beachte dass die Anführungszeichen daher escaped sind).
Wenn man daher auf die Attribute des eigenen Objektes in der Map-Funktion zugreifen möchte, muss man den String zunächst in ein Objekt umwandeln:

var website = Riak.mapValuesJson(value)[0];

Aus meiner Sicht gibt es keinen wirklich guten Grund weshalb Dokumente vom Typ application/json nicht direkt als Objekt eingebunden werden. Das würde den Javascript-Code ein wenig vereinfachen.
Folgende Map-Funktion liefert nun alle Websites zurück, die den Buchstaben 'i' im Namen haben:

function (value,keydata,arg) { var website = Riak.mapValuesJson(value)[0]; if (website.name.indexOf('i')>=0) return [website]; else return []; }

Ich möchte diesen Test nun noch um eine kleine Reduce-Funktion erweitern, die am Ende die Anzahl der Websites zurückgibt, die ein 'i' im Namen haben.
In diesem Fall ist die Reduce-Funktion ziemlich übersichtlich. Den Parameter 'keep' der Map-Funktion sollte man auf 'false' setzen, wenn man die Ergebnisse der Map-Funktion nicht im Gesamtergebnis haben möchte.

MapReduceResponse mr = riak.mapReduceOverBucket("websites").
  map(JavascriptFunction.anon("function (value,keydata,arg) { var website =   Riak.mapValuesJson(value)[0]; if (website.name.indexOf('com')>=0) return [website]; else return []; }"), false).
                    reduce(JavascriptFunction.anon("function (values, arg) { return values.length; }"), true).submit();

In den nächsten Tagen möchte ich noch

  • Tests mit vielen Daten durchführen
  • mit komplexeren Abfragen arbeiten
  • Abfragen mit mehreren Map-Phasen erstellen
  • das Link-Feature von RIAK ausprobieren
  • eine andere Java-Library testen, die nicht mit http sondern ProtocolBuffers arbeitet
Die Ergebnisse werde ich hier posten.