Apache CouchDB – Deo II

U prvom delu smo pomenuli šta je CouchDB i demonstrirali osnovne komande za manipulaciju podacima preko grafičkog interfejsa i konzole. U ovom delu ćemo se baviti njegovim naprednijim mogućnostima, kao što su praćenje promena, repliciranje i razrešavanje konflikata.

Praćenje promena u CouchDB-u

CouchDB sadrži ugrađeni interfejs za praćenje promena koje se događaju u bazi – _changes API. Slanje GET zahteva bazi sa parametrom _changes će vratiti sortiranu listu promena nad dokumentima u bazi. Praćenje promena može biti korisno za kasnije dodatno procesiranje i sinhronizaciju. Neprestano praćenje promena je dobar pristup za generisanje dnevnika promena u realnom vremenu (eng. real-time logging).

Najlakši način pristupa interfejsu promena je slanje zahteva preko komandne linije:

$ curl http://localhost:5984/music/_changes
{"results":[{        "seq":1,        "id":"370255",        "changes":[{"rev":"1-a7b7cc38d4130f0a5f3eae5d2c963d85"}]},{"seq":2, "id":"370254", "changes":[{"rev":"1-2c7e0deec3ffca959ba0169b0e8bfcef"}]},{... jos 97 rezultata ...},{"seq":100, "id":"357995", "changes":[{"rev":"1-aa649aa53f2858cb609684320c235aee"}]}], "last_seq":100}

Ako se pošalje GET zahtev sa parametrom _changes, CouchDB će poslati sve rezultate koje ima. Kao i kod pogleda, može se poslati parametar limit da ograniči broj vraćenih podataka, a dodavanje include_docs=true će vratiti kompletne dokumente u odgovoru.

Kod tipičnog slučaja korišćenja nije potrebno potrebno dobijati sve promene ikad napravljene u odgovoru. Logično je tražiti samo promene nastale od poslednje provere. U tom slučaju se koristi parametar since :

$ curl http://localhost:5984/music/_changes?since=99
{"results":[{        "seq":100,        "id":"357995",        "changes":[{"rev":"1-aa649aa53f2858cb609684320c235aee"}]}], "last_seq":100}

Ako se specificira since vrednost koja je veća od poslednjeg broja sekvence seq, rezultat će biti prazan:

$ curl http://localhost:5984/music/_changes?since=9000
{"results":[    ], "last_seq":9000}

U odgovoru, polje last_seq sadrži broj sekvence poslednjeg rezultata, dok results sadrži, logično, rezultate. Results sadrži seq, id i changes. Seq je redni broj rezultata, a id je identifikator dokumenta. Changes sadrži niz polja u kojima je podrazumevana vrednost broj revizije dokumenta, ali se u njemu mogu sadržati i drugi podaci.

Anketiranje

Anketiranje (polling), ili ispitna operacija, je proces u kome jedan entitet proverava status drugog entiteta u vezi. Jedan uređaj će proveravati status drugog uređaja sve dok nije spreman i, kada jeste, pristupiće mu. Tip anketiranja se može izabrati slanjem feed parametra u zahtevu. CouchDB podržava tri tipa anketiranja:

  1. Normalno anketiranje je podrazumevano u CouchDB-u i za njega ne moraju da se navode dodatni parametri u zahtevu. Primeri koji su malopre demonstrirani su zapravo anketiranja normalnog tipa. Kod normalnog tipa se sve promene odmah šalju natrag u telu odgovora.
  2. Dugo anketiranje (long polling) kao parametar zahteva feed=longpoll. Nakon slanja zahteva, korisnik i CouchDB neće prekinuti HTTP konekciju, već će je držati otvorenom. Konekcija traje sve dok se ne desi promena ili dok ne istekne neki vremenski period. Kada se promena desi i doda se nova vrednost u rezultate, CouchDB će kao odgovor vratiti rezultate (zajedno sa promenom) i veza će biti prekinuta. Dugo anketiranje ima najviše smisla kada promene nisu učestale. Ako jesu i biva kreirano mnogo novih zahteva, korisnost dugog anketiranja će opasti.
  3. Kontinualno anketiranje (continuous polling) je slično dugom anketiranju. Kao i dugo anketiranje, ovaj tip anketiranja će poslati zahtev i držati konekciju otvorenom, primiće odgovor od servera kada se promena desi, ali se konekcija neće prekinuti. Za kontinualno anketiranje treba u zahtev dodati feed=continuous.

Razmotrimo sledeći pseudoprimer dugog anketiranja:

00:00:> curl -X GET "$HOST/db/_changes?feed=longpoll&since=2"00:00:{"results":[00:10: {"seq":3,"id":"test3","changes":[{"rev":"1-02c6b758b08360abefc383d74ed5973d"}]}00:10: ], 00:10:"last_seq":3}

Primeru su sa strane dodati vremenski intervali da bi mehanizam rada bio uočljiviji. U 00:00 se šalje novi zahtev tipa longpoll. Isto u 00:00 će stići odgovor od servera, koji nam govori da nema rezultata, ali da se čeka na promenu stanja. U 00:10 neko dodaje novi dokument i CouchDB uredno šalje svoj odgovor, a zatim i zatvara vezu.

Mreže mogu biti nesigurne i nestabilne i nekad je teško zaključiti da li odgovora od servera nema ili se konekcija sa mrežom umrtvila. U tim situacijama je zanimljiv parametar heartbeat. Dodavanjem heartbeat+N, gde je N broj milisekundi, CouchDB će slati karakter za novi red svakih N milisekundi. Sve dok se primaju znaci za novi red, korisnik će znati da nema novih notifikacija, ali da je server i dalje spreman da pošalje novu notifikaciju kada se desi promena.

Razmotrimo sada pseudoprimer kontinualnog anketiranja:

00:00:> curl -X GET "$HOST/db/_changes?feed=continuous&since=3"00:10:{"seq":4,"id":"test4","changes":[{"rev":"1-02c6b758b08360abefc383d74ed5973d"}]}00:15:{"seq":5,"id":"test5","changes":[{"rev":"1-02c6b758b08360abefc383d74ed5973d"}]}

U 00:00 se šalje zahtev. U 00:10 i 00:15 se kreiraju novi dokumenti. Kod kontinualnih pregleda nema JSON objekta sa poljem results koje sadrži niz rezultata, već se svaka promena šalje u posebnom redu. Parametar heartbeat se takođe može upotrebiti i funkcioniše na isti način.

Filtriranje promena

API promena u CouchDB-u nudi jedinstveni uvid u stvari koje se dešavaju unutar baze tako što šalje izvršene promene u tokovima podataka. Pa ipak, ponekad je bolje pogledati samo delić tih promena umesto mnoštva podataka odjednom. Za odabir određenih podataka u promenama koje želimo da vidimo se koriste funkcije filtriranja. Funkcija filtriranjailifilter je JavaScript funkcija koja kao argumente prima dokument i objekat sa detaljima o zahtevu. Funkcija treba da odluči da li dokument ispunjava uslov prolaska kroz filter, što znači da je povratna vrednost funkcije true ili false. Funkcije filtriranja se čuvaju u dokumentima dizajna unutar filters ključa pod imenom koje korisnik odabere.

Pretpostavimo da u dokumentima imamo ključ country koji sadrži oznake dužine tri karaktera za države. Funkcija filtriranja bi mogla da izgleda ovako:

function(doc){return doc.country ==="SRB";}

Ako sačuvamo funkciju unutar dokumenta dizajna, mogli bismo da filtriramo podatke samo ako je u pitanju država Srbija. Ovakvo rešenje je suviše kruto. Bilo bi bolje kada bismo u zahtevu kao parametar mogli da specificiramo državu koju želimo kao filter. Sledeći primer radi upravo to:

function(doc, req){return doc.country === req.query.country;}

Sada u zahtevu treba da pošaljemo ime države kao vrednost parametra country. Pošaljimo prvo zahtev u kome ćemo kreirati novi dokument dizajna samo za geografsko filtriranje:

$ curl -X PUT \ http://localhost:5984/music/_design/wherabouts\ -H "Content-Type: application/json"\-d'{"language":"javascript","filters":{"by_country":"function(doc,req){return doc.country === req.query.country;}"}}'{"ok":true, "id":"_design/wherabouts", "rev":"1-c08b557d676ab861957eaeb85b628d74"}
$ curl "http://localhost:5984/music/_changes?\filter=wherabouts/by_country&\country=SRB"
{"results":[    {"seq":10,"id":"5987","changes":[{"rev":"1-2221be...a3b254"}]}, {"seq":57,"id":"349359","changes":[{"rev":"1-548bde...888a83"}]}, {"seq":73,"id":"364718","changes":[{"rev":"1-158d2e...5a7219"}]}, ...

Razmotrimo sada sledeći primer. Recimo da imamo bazu u kojoj korisnici mogu da šalju poruke jedni drugima. Umesto posebne baze za svakog pojedinačnog korisnika, koristićemo jednu bazu za sve poruke od svih korisnika. Recimo da korisnici mogu videti svoje poruke tako što će poslati _changes zahtev sa svojim imenom u parametru. Funkcija filtriranja vodi računa da svaki korisnik dobije tačno one poruke koje mu sleduju:

function(doc, req){return doc.name == req.query.name;}

Sada korisnici mogu poslati svoje ime i dobiti svoje poruke. Međutim, korisnici mogu poslati tuđe ime kao parametar i slobodno pročitati tuđe poruke.

Srećom, ova situacija se može vrlo lako preduprediti. Req parametar funkcije filtriranja sadrži člana po imenu userCtx (user context – kontekst korisnika). Ovaj član sadrži korisne podatke o korisniku koji je već autentifikovan preko HTTP-a u ranijoj fazi zahteva. Ime korisnika koji šalje zahtev se nalazi u polju req.userCtx.name. Možemo biti sigurni da je korisnik taj koji tvrdi da jeste, jer ga je CouchDB već identifikovao. Sa ovim poljem nam dinamički filter imena više nije neophodan, ali on i dalje može biti koristan u nekim drugim slučajevima:

function(doc, req){return doc.name == req.userCtx.name;}

Replikacija podataka

Master-master replikacija

CouchDB je dizajniran imajuću na umu asinhrona okruženja i trajnost podataka. Prema CouchDB-u, najsigurnije mesto za čuvanje podataka je svuda, i CouchDB pruža alate koji to omogućavaju. Većina baza podataka pruža jedan glavni master čvor koji garantuje konzistentnost podataka ili se postiže kvorum između svih čvorova. CouchDB ne koristi ni jednu od ovih metoda; umesto toga koristi više master čvorova. Alat koji to omogućava se zove master-master replikacija.

Svaki CouchDB server, umesto dela podataka, sadrži sve podatke i može da prima zahteve klijenata, ažurira i briše podatke nezavisno od drugih servera. U jednom trenutku, svi serveri mogu razmeniti podatke između sebe i rešiti eventualne konflikte koji nastaju usled konkurentnih promena nad istim podacima na različitim serverima.

Replikacija se može sprovesti preko Futon-a. Odlazak na stranu željene baze i klik na dugme Replicator otvara stranu za replikaciju baze. Replikacija se odvija između dve baze. Obe baze se mogu nalaziti ili u lokalu ili na nekom udaljenom serveru – za CouchDB je to nebitno, jer je postupak replikacije isti. Za bazu koju želimo replicirati odabraćemo music, dok ćemo drugu bazu nazvati music-repl. Klik na dugme Replicate će obaviti sav ostali posao. Sada obe baze imaju skoro identične podatke. Neki podaci se neće uvek poklapati, kao npr. ukupan broj ažuriranja dokumenata, ali to je i očekivano. Bitno je da su dokumenti sa korisničkim podacima i pogledima identični.

Replikacija se može izvršiti i preko komandne linije, slanjem POST zahteva na _replication:

POST /_replicateHTTP/1.1{"source":"database","target":"http://example.org/database"} -H "Content-Type: application/json"

Source označava bazu koja se kopira, a target bazu na koju će se podaci kopirati.

Kopiranje baza je jednosmerno. Ukoliko želimo da kopiramo promene u oba smera, potrebno je u novom zahtevu zameniti source i target vrednosti:

POST /_replicateHTTP/1.1 Content-Type:application/json{"source":"http://example.org/database","target":"database"}

Nastanak konflikta

U jednom trenutku, isti podaci na različitim bazama će se različito ažurirati i pri sledećoj replikaciji će nastati konflikt. Kako CouchDB rešava konflikte? Najbolje je demonstrirati na primeru. U komandnu liniju treba upisati sledeći zahtev:

$ curl -X PUT "http://localhost:5984/music/theconflicts"\ -H "Content-Type: application/json"\-d'{ "name": "The Conflicts" }'{"ok":true, "id":"theconflicts", "rev":"1-e007498c59e95d23912be35545049174"}

Sada treba ponovo pokrenuti replikaciju između dve baze. Možemo proveriti da li je replikacija uspela slanjem zahteva music-repl bazi:

$ curl "http://localhost:5984/music-repl/theconflicts"{"_id":"theconflicts", "_rev":"1-e007498c59e95d23912be35545049174", "name":"The Conflicts"}

Sada ćemo ažurirati dokument u music-repl bazi dodavanjem novog albuma:

$ curl -X PUT "http://localhost:5984/music-repl/theconflicts"\ -H "Content-Type: application/json"\-d'{"_id": "theconflicts","_rev": "1-e007498c59e95d23912be35545049174","name": "The Conflicts","albums": ["Conflicts of Interest"]}'{"ok":true, "id":"theconflicts", "rev":"2-0c969fbfa76eb7fcdf6412ef219fcac5"}

U bazi music ćemo napraviti konfliktno ažuriranje dodavanjem drugačijeg albuma:

$ curl -X PUT "http://localhost:5984/music/theconflicts"\ -H "Content-Type: application/json"\-d'{"_id": "theconflicts","_rev": "1-e007498c59e95d23912be35545049174","name": "The Conflicts","albums": ["Conflicting Opinions"]}'{"ok":true, "id":"theconflicts", "rev":"2-cab47bf4444a20d6a2d2204330fdce2a"}

Sada music i music-repl baze podataka imaju dokument sa _id vrednošću theconflicts. Oba dokumenta su verzije 2 i imali su istu osnovnu reviziju. Šta će se desiti ako pokrenemo replikaciju između ove dve baze?

Razrešenje konflikata

Nakon što smo kreirali konfliktne vrednosti između dva dokumenta u bazi, pokrenimo replikaciju ponovo. Rezultati replikacije mogu biti iznenađujući zato što će se replikacija izvršiti u potpunosti i bez greške.

CouchDB zapravo nema mehanizme da odredi koji podaci su „ispravni“, a koji nisu, već bira jedan dokument i proglašava ga pobednikom. Algoritam određivanja pobednika je deterministički, što znači da će za iste instance konflikta uvek davati istog pobednika. Svaka revizija sadrži liste prethodnih revizija. Revizija sa najdužom istorijom promena postaje pobednik. Ako revizije imaju istorije iste dužine, karakteri _rev vrednosti dokumenata se porede redom po ASCII tabeli. Pobednik je ponovo onaj koji ima veću vrednost.

Dokument koji je „izgubio“ se ne briše, već se čuva da bi korisnik ili klijentska aplikacija kasnije mogli da preprave podatke kako im odgovara i razreše konflikt. Da bi se videlo koji dokument je izašao kao pobednik tokom replikacije, može se poslati GET zahtev uz dodati parametar conflicts=true, da bi se pogledale informacije o konfliktnim revizijama:

$ curl http://localhost:5984/music-repl/theconflicts?conflicts=true{"_id":"theconflicts", "_rev":"2-cab47bf4444a20d6a2d2204330fdce2a", "name":"The Conflicts", "albums":["Conflicting Opinions"], "_conflicts":[        "2-0c969fbfa76eb7fcdf6412ef219fcac5"    ]}

Sa primera možemo videti da je drugo ažuriranje pobedilo. Odgovor ima i _conflicts polje, koje sadrži listu revizija koje su u konfliktu sa glavnom revizijom dokumenta. GET zahtevu možemo priključiti ime konfliktne revizije da bismo videli konfliktne podatke i odlučili šta želimo da radimo sa njima:

$ curl http://localhost:5984/music-repl/\ theconflicts?rev=2-0c969fbfa76eb7fcdf6412ef219fcac5 {"_id":"theconflicts", "_rev":"2-0c969fbfa76eb7fcdf6412ef219fcac5", "name":"The Conflicts", "albums":["Conflicts of Interest"]}

Još jednom je bitno naglasiti da CouchDB neće pokušavati da inteligentno reši konflikte. Iako se čini da bi CouchDB to mogao inteligentno da reši – u jednom od dokumenata treba sve albume spojiti u istu listu, primeri iz realnog sveta neće uvek biti jednostavni ili očigledni. Zato CouchDB ostavlja korisniku da sam svoje podatke dovede u red onako kako želi.

Za kraj

Prednosti CouchDB-a

CouchDB je stabilan i robustan član NoSQL zajednice. Zasnovan na filozofiji da su mreže nepouzdane i otkaz hardvera neizbežan, CouchDB nudi poseban decentralizovan pristup čuvanju podataka. Dovoljno mali za mobilni uređaj i dovoljno veliki za preduzeće, CouchDB je dovoljno raznovrstan za sve situacije.

Mane CouchDB-a

CouchDB nije baza podataka pogodna za sve situacije. Njegovi mapreduce pogledi, iako dobri, ipak ne mogu da podrže baratanje sa podacima na način na koji to čine relacione baze podataka. Takođe, njegova strategija replikacije nije uvek pravi izbor. Replikacija ide na sve ili ništa, što znači da će svi replicirani serveri imati iste podatke. Ne postoji particionisanje i raspodela podataka na više čvorova. Glavni razlog za dodavanje novih čvorova nije raštrkivanje podataka, već postizanje većeg protoka operacija čitanja i pisanja.

Zaključak

CouchDB poseduje robustan sistem koji ga čini pravim izborom ukoliko je mreža nestabilna. Pošto koristi standarde kao što su HTTP/REST i JSON, CouchDB se uklapa na mestima gde pretežno vladaju internet tehnologije. U današnje vreme, to je skoro bilo gde. Postoji još ogroman broj opcija koje u ovom dokumentu nisu pokrivene. Ostaje nada da je priloženi rad probudio apetit čitalaca da o ovoj neverovatnoj tehnologiji sazna nešto više.

Korisni linkovi