CouchDB – Slacker database in Erlang

6 April 2009 11:17 Peter Maas Databases

De laatste tijd zijn de zogenaamde ‘slacker’ databases weer helemaal hot. Een aantal prototypes voor de architectuur die ik momenteel ontwerp zijn ook gebaseerd op het gebruik van een dergelijke database. Een goed moment om er eens een blogpost aan te wijden.

Wat men momenteel verstaat onder de term ‘slacker’ database is eigenlijk een document geörienteerde database zonder schema. De database biedt CRUD operaties voor documenten en mogelijkheden om deze te filteren en te aggregeren.

CouchDB

CouchDB is zo’n database. Een toplevel apache project geschreven in Erlang (net als de simpledb van Amazon trouwens). De belangrijkste kracht van Erlang is de ondersteuning voor parallelle verwerking. De taal heeft een kleine maar krachtige set van primitieven voor de aanmaak van threads en om de communicatie tussen deze threads te verzorgen. De fysieke locatie van een thread is transparant (dit kan in hetzelfde lopende systeemproces zijn of zelfs op een andere computer).

Documenten in CouchDB zijn in JSON formaat. Praten met CouchDB doe je via REST. CouchDB komt met een webinterface voor het beheren van de database en het schrijven van design documenten. Design documenten bevatten de operaties die je op je data wilt uitvoeren.

Een JSON document zou er als volgt uit kunnen zien:

{
   "_id": "example",
   "_rev": "1-1137355506",
   "newsitem": {
       "title": "Mijn nieuwsbericht",
       "subtitle": "een bericht om couchdb te testen",
       "author": "Peter Maas",
       "tags": [
           "test",
           "news",
           "artikel"
       ],
       "synopsis": "...",
       "body": "..."
   }
}

Het _id element wordt door CouchDB ingevult aan de hand van het path waar the PUT operatie naar werd uitgevoerd. Het _rev element bevat de interne versie die CouchDB gebruikt voor locking en het berekenen van de proxy headers voor GET requests (het _rev element wordt ook als etag gebruikt).

Als je bovenstaande document in een database stop kun je het opvragen middels een HTTP GET. Bijvoorbeeld:

curl http://localhost:5984/myDatabase/example

views

Een view is een set filters en aggregaties op een bepaalde database met documenten. Een filter is een ‘map’ operatie en een aggregatie een ‘reduce’ naar de respectievelijke delen van het MapReduce framework. Een view wordt niet berekend op het moment van opvragen maar wordt als index op disk opgeslagen, deze wordt incrementeel bijgewerkt. De standaard distributie biedt support voor het schrijven van views in Javascript (serverside dus). Er zijn ook plugins die je views laten definiëren in Ruby of Python. Hierbij moet ik wel zeggen dat er weinig support is als je je views met een andere taal dan Javascript gaat ontwikkelen.

map
Stel dat we een set hebben van soortgelijke artikelen en deze zouden willen filteren op een bepaalde tag dan ziet een map functie er in Javascript er als volgt uit:

function(doc) {
  var newsitem = doc.newsitem
  if(newsitem && newsitem.tags.indexOf('test') != -1){
    emit(null, {title: newsitem.title, tags: newsitem.tags});
  }
}

Als je deze functie als ‘map’ function in een design document opneemt kun je deze via HTTP benaderen:


http://localhost:5984/myDatabase/_design/news/_view/articles

Geeft bijvoorbeeld als resultaat:

{"total_rows":2,"offset":0,"rows":[
  {"id":"example","key":null,"value":{"title":"Mijn nieuwsbericht","tags":["test","news","artikel"]}},
  {"id":"example:2","key":null,"value":{"title":"Mijn tweede nieuwsbericht","tags":["test","news","peter","artikel","couchdb"]}}
]}

Tevens kun je pagineren, limiteren en sorteren via request parameters. Informatie over de parameters is te vinden in de view wiki pagina van couchdb.

Het volledige design document kun je hier vinden. Er zijn tools voor het bewerken van je views binnen en buiten de database. Als je het in de database doet kun je resultaten van de view bekijken zonder ze op te slaan:

Views editen via de webinterface

Momenteel kun je de output van een map alleen met absolute key-waarden filteren. Er wordt gewerkt aan een fulltext search oplossing waarmee flexibelere query modellen denkbaar zijn.

reduce
Reduce functies werken op het resultaat van een map. Stel dat we van de bovenstaande map het gemiddelde aantal tags per post zouden willen weten dan zou de reduce functie er als volgt uit zien:

function (keys, values, rereduce) {
  var count = 0;
  for(var k in values){
    count += values[0].tags.length
  }
 
  return count / values.length;
}

Deze geeft een enkele regel als resultaat in JSON.

{"rows":[
  {"key":null,"value":5}
]}

Dit is natuurlijk pas het topje van de ijsberg met betrekking tot mogelijke map en reduce functies, meer voorbeelden zijn te vinden op de couchdb wiki en de meegeleverde test suite.

Shows

Naast functies voor uitvragen van data biedt CouchDB ook de mogelijkheid deze om te zetten in andere representatie dan JSON. Bijvoorbeeld XML of HTML. Het is ook mogelijk om content negotiation te implementeren in een show functie. Zowel documenten als lijsten kunnen op deze manier worden getransformeerd.

Zo zou je bijvoorbeeld het resultaat van een view kunnen omzetten naar een atom feed (met dank aan Nils):

function(head, row, req, row_info) {
    ////////////////////////////////////////////
    // These functions should be moved elsewhere
    // here for demonstration purposes only
    ////////////////////////////////////////////
    function f(n) {    // Format integers to have at least two digits.
        return n < 10 ? '0' + n : n;
    }
    Date.prototype.rfc3339 = function() {
        return this.getUTCFullYear()   + '-' +
        f(this.getUTCMonth() + 1) + '-' +
        f(this.getUTCDate())      + 'T' +
        f(this.getUTCHours())     + ':' +
        f(this.getUTCMinutes())   + ':' +
        f(this.getUTCSeconds())   + 'Z';
    };
    ////////////////////////////////////////////
 
    if (head) {
        var feed = <feed xmlns="http://www.w3.org/2005/Atom">;
        feed.title = "Cinema feed titel";
        feed.subtitle = "Cinema feed subtitel";
        feed.generator = "Cinema on CouchDB";
        feed.updated = new Date().rfc3339();
        feed.author.name = "Cinema.nl";
        feed.author.email = "redactie@cinema.nl";
        var feedString = feed.toXMLString();
        return {
            headers: { "Content-Type": "application/atom+xml" },
            body: feedString.substring(0, feedString.lastIndexOf("</feed>"))
        };
    } else if (row) {
        var movie = row.value;
        var entry = <entry>;
        entry.title = movie.title;
        entry.link.@href = movie.url;
        entry.summary = "Een summary over "+ movie.title + ".";
        return { body: entry };
    } else {
        return { body: "</feed>" };
    }
}

De ontwikkelaars van CouchDB hebben zelf ook een volledige blog applicatie geschreven die in CouchDB draait. Je kunt je natuurlijk afvragen of je dat moet willen.

Replicatie

CouchDB heeft goede ingebouwde mogelijkheden om replicatie tussen verschillende databases te faciliteren. Door het versie systeem en ingebouwde conflict detectie is het mogelijk bi-directioneel te repliceren:

Couch Replication

View more presentations from p3t0r.

Bovenstaande slides zijn afkomstig uit een presentatie op slideshare, als je met CouchDB aan de gang gaat is het doorkijken van een aantal presentaties een aanrader.

Performance

Ik heb op een set van 15000 documenten benchmarks uitgevoerd om te testen hoe de performance van het systeem is. Op mijn laptop moet je denken aan ongeveer 500 requests/sec (in 128 concurrent mode) op een view zonder extere caching terwijl er op volle kracht nieuwe documenten werden geschreven. Tijdens deze test gebruikte couchdb 8 MB geheugen. Omdat alle lees-operaties nette voor proxies begrijpelijke headers zetten is het eenvoudig om bestaande producten zoals Squid of Varnish of bijvoorbeeld een loadbalancer om de performance of beschikbaarheid nog verder te verbeteren.

Het CouchDB team geeft zelf aan dat zij systemen hebben die ongeveer 6000 schrijfacties per seconde verwerken en waar miljoenen documenten per dag bij komen. Er zijn in dat geval wel zaken waar je rekening mee moet houden. In batches schrijven is bijvoorbeeld sneller dan per document, en geclusterde ID’s schrijven sneller dan ID’s zonder clustering.

Robuustheid

CouchDB overschrijft bestaande data niet. Er wordt een nieuwe versie aangemaakt bij een update. Doordat het schrijven van een nieuwe versie een atomair proces (CouchDB voldoet aan de ACID Properties) is kan de database niet in een inconsistente staat geraken. Het is ook ten aller tijde mogelijk de database hard te stoppen of bijvoorbeeld de onderliggende files te rsyncen naar een ander systeem.

Conclusie

CouchDB is een erg krachtig product. Je moet het niet vergelijken met een relationele database, het is ook geen vervanging voor de relationele database. In de architectuur waar ik nu aan werk is het een complementair product. De vorm waarin ik het gebruik is het beste te omschrijven als ‘cache’, maar dan met erg krachtige query functionaliteit en taal onafhankelijk dataformaat en REST interface. Het replicatiemodel zorgt ervoor dat je je database eenvoudig mee kunt schalen met je applicatie waardoor de database opeens de bottleneck niet meer is. Het feit dat alles via REST gaat maakt het mogelijk om zaken als loadbalancers, proxies, accelerators etc. te gebruiken!

Natuurlijk zijn er wel nadelen. Er is bijvoorbeeld nog geen 1.0 release en dat merk je vooral aan het ontbreken van complete documentatie, de testsuite is op dit moment de meest complete beschrijving van de mogelijkheden. Ook zijn de ontwikkelmodellen nog niet zo uitgekristalliseerd als in de relationele wereld; soms moet je dus zelf het wiel uitvinden.

10 reacties »

  1. Je javascript voorbeeld is wel een beetje eng zeg…

    - Waarom is die functie anoniem?
    - Waarom elke keer bij de (anonieme) functie het rfc3339 prototype method herdefinieren?
    - Waarom zit die f in de scope van je view en niet in je rfc3339 prototype method?
    - Waarom moet < escaped worden dmv <? (in die f private)
    - Waarom retourneer je soms een xml node en soms een string?
    - Moet de laatste else niet zijn: return { body: "” };? Kan je anders effe uitleggen wat het doet, want dan volg ik het niet.
    - wat doe je met req en row_info?

    wel hip dat e4x gewoon werkt though, welke js engine wordt er gebruikt btw?

    Rikkert Koppes April 6, 2009 11:53

  2. fok, waarom wordt html niet geescaped in een reactie…

    - Waarom moet < escaped worden dmv &lt;? (in die f private)
    - Moet de laatste else niet zijn: return { body: “</entry>” };?

    Rikkert Koppes April 6, 2009 11:55

  3. Ja je hebt gelijkt wat de scope van f betreft, dit komt uit een voorbeeld applicatie…. bovenstaande is “for demonstration purposes only”. De escape van het groter-dan teken is denk ik een bug in mijn omzetting van textile naar dit blogartikel.

    De engine is trouwens spidermonkey.

    Op die return uit de else kom ik nog terug… ik ga even met de auteur van dat stukje code overleggen daarvoor ;)

    Peter Maas April 6, 2009 12:07

  4. Netjes Pet0r! Wilde altijd al weten hoe CouchDB nou in elkaar stak (en wat het nou überhaupt was ;-) )

    Diederick April 6, 2009 13:09

  5. relax! ;)

    Iain Hecker April 6, 2009 14:30

  6. Hoi, hier Nils van het Atom feed-voorbeeldje. In mijn demo-applicatie is het inderdaad niet meer zo dat die functies bij elke aanroep van de showfunctie opnieuw gedefinieerd worden, maar is die code in een aparte helpers/date.js opgenomen. De functies zelf heb ik ook niet zelf geschreven, maar kwamen rechtstreeks uit de voorbeeld CouchDB-blogapplicatie.

    Met req en row_info doet deze functie niets, maar die zijn wel onderdeel van de signature van de show-functie. Je zou er dus iets meer kunnen doen als je wil. Zie de CouchDB-documentatie over het gebruik van show en list-functies voor verdere uitleg: http://wiki.apache.org/couchdb/Formatting_with_Show_and_List Overigens was deze feature van CouchDB pas net beschikbaar in de trunk (ondertussen is versie 0.9.0 uit waar dit ook in beschikbaar is), dus het is allemaal nog wat nieuw.

    En nee, die laatste return moet de list (de feed in dit geval) afsluiten, niet de entry, dus dat is gewoon goed.

    Nils Breunese April 6, 2009 15:09

  7. Pardon, voor de duidelijkheid: mijn code retourneert in in die else. Ik heb geen idee hoe het gekomen is dat er staat in het artikel.

    Nils Breunese April 6, 2009 15:27

  8. Goed… even wat verwarring. Maar de return in de laatste else moet:

    else {
            return { body: "</feed>" };
        }

    zijn. Ergens fout gegaan met heen en weder slepen van code.

    Peter Maas April 6, 2009 15:28

  9. Grr, nou is m’n laatste comment weer van HTML gestript. Anway, wat Peter hierboven zei klopt.

    Nils Breunese April 6, 2009 15:30

  10. Heel interessant, dank je wel!

    Volgens mij zag ik nog wel een kleine typo. Als ik me niet vergis moet dit blok:

    for(var k in values){
    count += values[0].tags.length
    }

    vervangen worden met:

    for(var k in values){
    count += values[k].tags.length
    }

    mvgrt, Kevin

    PS In het engels zou dit artikel zo maar eens dzone voorpaginanieuws kunnen zijn..

    Kevin van Zonneveld June 8, 2009 2:25

Reageer

RSS feed for comments on this post · TrackBack URI