sobota 24. dubna 2010

Key/Value databáze

Na jednom z projektů jsem použil pro ukládání dat key/value databázi a rád bych se podělil o několik postřehů, které jsem získal. V mém případě jsem jako key/value databázi zvolil Voldemort a to z toho důvodu, že je kompletně napsaná v Jave. Původně vzniknul Voldemort jako interní projekt používaný službou LinkedIn, který byl posléze uvolněn jako open source a je dále rozšiřován.

Před tím než se pustíme do větších detailů, musím ukázat klientské API, přes které s Voldemortem komunikujete a můžete ho očekávat od každé key/value databáze. Jedná se o rozhraní podobné mapě, které má tři metody put, get a delete.

String key = "dagblog.url";
client.put(key, "http://dagblog.cz");
String value = client.get(key);
client.delete(key);

Kdy použít key/value databázi

Jednoduchá odpověď se nabízí, všude tam kde se nehodí relační či jiná databáze. Já osobně jsem key/value databázi zvolil ze dvou důvodů výkonnost a jednoduchost ukládaných dat (to byl vstupní předpoklad zadupaný dalším vývojem ;-). Díky tomu, že všechny operace jsou řízené přes klíč, lze velice jednoduše škálovat (architektura Voldemortu) a rychlost odezev je relativně stabilní bez ohledu na to kolik dat je uloženo.

S tím souvisí i další věc a to je vlastní API, které máte k dispozici. Možná jste si již všimli, že API nemá žádnou metodu, kterou by bylo možmé hledat dat. K dispozici není ani žádný dotazovací jazyk typu SQL. Z toho vyplývá několik poměrně nezanedbatelných architektonických omezení, které je potřeba vzít v potaz.

  • Nemůžete udělat projekci dat, kdy spojíte dvě či více entit přes join (o tom jak řešit joiny si povíme později.). K datům se nedostanete jinak než přes znalost jejich klíče.
  • Není možné vytáhnout pouze část dat. Převedeno do prostředí relačních databází a SQL to znamená nemožnost v select části dotazu specifikovat sloupečky.
  • Není možné udělat dotaz, který najde data jednoho typu. Převedeno do řeči SQL select * from table.
  • API nenabízí žádnou možnost transakčního zpracování či hlídání integrity dat. Konzistence dat tedy plně leží na bedrech aplikace.

Otázka číslo jedna proč tomu tak je a otázka číslo dvě komu to prospěje. Všechna výše uvedená architektonická omezení mají za cíl umožnit právě jednoduché škálování a tím i brutální výkonnost. Tato kritéria vám naopak naznačují, že key/value databáze nebude vhodná pro aplikace, které potřebují dělat složité dotazy nad daty. Případně aplikace, kde by přechodná nekonzistence dat (ve smyslu přechodu stavu několika svazaných hodnot pod různými klíči) mohla znamenat problém, například převod peněz z jednoho účtu na druhý.

Samozřejmě key/value databáze mají kromě výkonnostních i další výhody. Díky jednoduchému API se dají jednoduše zamockovat. Všechny testy i vývoj částí závisejících na key/value databázi pohodlně obsloužíte mockem, který představuje mapu držící ve potřebné v paměti. Data, které ukládáte jako hodnoty, nemusíte normalizovat jako v případě relační databáze.

Voldemort a praktické zkušenosti

Nakonec se počet entit ukládaných pomocí key/value databáze zastavil na počtu 15. Mluvím o entitách v případě key/value databáze, protože jsem potřeboval ukládat celkem komplexní graf složený právě z různých entit (tříd) a k nim jsem potřeboval pár pomocných struktur.

Díky tomu, že jsem nakonec ukládal graf a potřeboval jsem vytahovat jeho části, muselo dojít k jisté dekompozici. Každá separátně ukládaná část grafu měla svůj vlastní dedikovaný prostor (tabulku v řeči relační databáze) nazývaný store.

Každý store může používat různé strategie pro serializaci a deserializaci klíčů a hodnot, které ukládáte. Voldemort podporuje celou řadu formátu JSON, Thrift , Protocol Buffers, Java serializaci a další. To prakticky znamená, že můžete vaše objekty serializovat jako celky. Já jsem použil JSON, protože díky tomu měl každý store svoji definovanou strukturu.

public class User {
 private String firstName;
 private String lastName;
    
    ...
}

V případě JSONu byl formát storu {"firstName":"string","lastName":"string"} a tato struktura byla v Jave reprezentovaná jako java.util.Map, kterou vyžaduje Voldemort pro ukládání dat v tomto formátu.

User user = new User("john", "doe");
Map value = new HashMap();
value.put("firstName", user.getFirstName());
value.put("lastName", user.getLastName());

//uložení dat
client.put(1, value);

//načtení dat
Map map = client.get(1);
String firstName = map.get("firstName");
String lastName = map.get("lastName");

Při konfiguraci storu na použití Java serializace by kód vypadal následovně (nutný předpoklad, že objekt User implementuje rozhraní java.io.Serializable) ).

User user = new User("john", "doe");

//uložení dat
client.put(1, user);

//načtení dat
User user = client.get(1)

Jak na joiny

Už jsem naznačil, že key/value databáze nenabízejí možnost dělat joiny, proto pokud je potřebujete, musíte je udělat programově a nebo zohlednit při návrhu storu. Nejdříve jak to uděláme programově. Řekněme, že entity User se ukládá do jednoho storu a entita Address do jiného storu. V takovém případě musím být chopen na základě znalosti uživatele zjistit všechny jeho adresy. Nejjednodušší možností je poznamenat si při ukládání uživatele klíče všech adres.

Address address = new Address("Na konci světa", "Zatavi", "12345");
User user = new User("john", "doe", address);

Map addressValue = new HashMap();
addressValue.put("street", address.getStreet());
addressValue.put("city", address.getCity());
addressValue.put("zipcode", address.getZipcode());

addressClient.put(1, addressValue);

Map userValue = new HashMap();
userValue.put("firstName", user.getFirstName());
userValue.put("lastName", user.getLastName());
//uložíme se klíče asociovaných adress
userValue.put("addressKeys", Arrays.asList(new Integer[]{1}); 

userClient.put(1, userValue);

Programový join při čtení vypadá analogicky

Map userValue = userClient.get(1);
List addressKeys = userValue.get("addressKeys");
for(int key: addressKeys) {
 Map addressValue = addressClient.get(key);
}

Tímto kouskem kódu si dotáhnu všechny asociované adresy. Voldemort umožňuje i hromadný get, abych nemusel bych v kódu procházet přes jednotlivé klíče, ale rovnou bych předhodil jejich množinu. Ne vždy je ovšem žádoucí, a v případě key/value databází to platí dvojnásob, dělat dekompozici ukládání na úrovni entit. V podstatě jediný důvod pro dekompozici je v případě, že je těch dat velké množství a nebo s nimi potřebujeme manipulovat (get/put) bez ohledu na zbytek systému. Pokud u vašich dat vidíte velkou nutnost dekompozice je to známka toho, že není vhodné používat key/value databázi.

Pokud se joinu chcete vyhnout, múžete data uložit společně. Tedy jeden společný storage. V našem případě by to mohlo vypadat následovně.

Address address = new Address("Na konci světa", "Zatavi", "12345");
User user = new User("john", "doe", address);

Map addressValue = new HashMap();
addressValue.put("street", address.getStreet());
addressValue.put("city", address.getCity());
addressValue.put("zipcode", address.getZipcode());

Map userValue = new HashMap();
userValue.put("firstName", user.getFirstName());
userValue.put("lastName", user.getLastName());
userValue.put("addresses", Arrays.asList(new Map[]{addressValue});

client.put(1, userValue);

Nebo v případě, že bychom použili Java serializaci.

Address address = new Address("Na konci světa", "Zatavi", "12345");
User user = new User("john", "doe", address);

client.put(1, user);
 

Postřehy

V projektu, kde jsem Voldemort použil, jsem si napsal tenkou vrrstvu, která mi umožňovala transparentním způsobem ukládat jednotlivé entity. Každá entita měla tři metody metody, jednu pro vráceni klíče, vlastní reprezentace jako mapy (hodnota) a jednu metodu, která měla zrekonstruovat stav objektu z mapy. Zároveň jsem naimplementoval lazy loading pro entity, které v grafu objektů nebyly ještě načtené. Tím se zefektivnil počet dotazů na storage a celkový objem přenášených dat.

Při použití Voldemortu a myslím si, že to jde obecně generalizovat na jakoukoliv key/value databázi, došlo k tomu, že persistentní vrstva aplikace pěkně narostla. Bylo to právě nutností dělat všechnu tu logiku jako joiny, dekompozici objektu a jejich uložení, kontrolu konzistence atd. Bylo to tedy docela pracné, ale na oplátku jsem dostal kód 100% napsaný v Jave, ke kterému jsem mohl napsat velice jednoduše testy.

Závěr

Key/value databáze se nehodí pro jakýkoliv projekt, to je myslím celkem jasné. Není možné o ní uvažovat jako o plnohodnotné náhradě relační databáze, protože její koncept je naprosto odlišný. To mimo jiné znamená, že není možné při jejím použití aplikovat ty samé vzory a postupy, které jsme se naučili právě na relačních databázích. Jakmile začnete uvažovat o key/value databázi v inténcích relační databáze, je potřeba zvážit jestli vám key/value databáze nabízí to co potřebujete a nebo jestli jste někde v návrhu neudělali chybu.

Key/value databáze lze velice dobře používat jako doplněk relačních (dokumentových,objektových) databází. Pro specifické účely ve vaší aplikace se může hodit key/value databáze a zbytek dat může ležet v klasické relační databázi.