V poslední době je v módě termín NOSQL (Not Only Sql) a tak se i já o něm zmíním. Není to proto, že bych potřeboval být nutně cool, ale byl jsem nucen se na NOSQL podívat, protože nabízí efektivní řešení některých specifických úkolů, u kterých klasické relační databáze ztrácí dech. Je trošku zavádějící mluvit o NOSQL databázích (přestože to jsou báze dat), proto upřednostňuji termín NOSQL úložiště. NOSQL úložišť existuje mnoho typů, já se ale zaměřím jen na úložiště typu BigTable, konkrétně na Cassandru. Co zde popíšu ale do jisté míry platí i pro HBase.

V čem byste implementovali úložiště dat, aby bylo opravdu rychlé? C/C++? Asm? Některá NOSQL úložiště jdou touto cestou (např. MongoDB), ale Cassandra a HBase jsou implementovány v Javě. Ano, v tom strašné, pomalém a zdroježeroucím molochu, jak by Javu mnozí počastovali. Ale vězte, že na tomto molochu běží celý internet…tedy, ehm, Facebook ;-) Cassandra je totiž zopensourcovatělé jádro Facebooku, ale dnes ji používá i Twitter a další služby.
Implementace v Javě určila i ekosystém okolo těchto úložišť, takže prakticky všechny nástroje a knihovny okolo jsou také implementovány v Javě. A zde vyvstává otázka jak komunikovat s úložištěm z .NETu. Protože celá komunikace probíhá přes síť, není to nic složitého. Mnozí by očekávali nějaký známý protokol jako SOAP nebo OData, ale Cassandra i HBase používají (IMHO mezi .NET komunitou zcela neznámý) Thrift.

Thrift je síťový binární protokol pro RPC, který je ale i přes svou binárnost otevřený k rozšíření (není nutné se vázat na jednu verzi služby). Thrift ale není jen definice protokolu. Thrift dále přináší vlastní IDL (jazyk pro popis rozhraní služby), který umí dodávané nástroje transformovat do mnoha jazyků (C# nevyjímaje). Takže spolu s Cassandrou si stáhneme Thrift API a z toho si nagenerujeme klientské třídy v C#. Je to tedy stejný postup, jako když z WSDL generujeme třídy pro komunikace přes SOAP.

API Cassandry není moc bohaté a vlastně nám umožňuje jen vybírat hodnoty podle klíčů a hodnoty ukládat. Více v dokumentaci.

Datový model

Nejvyšší úrovní dělení je tzv. KeySpace (~databáze), který obsahuje ColumnFamilies (~tabulky). Každá ColumnFamily obsahuje řádky, které jsou identifikovány pomocí klíče. Každý řádek může obsahovat libovolný počet sloupců. Každý sloupec (identifikovaný jménem) má přiřazenu hodnotu a timestamp (který dodává aplikace!). Hodnotou sloupce nemusí být jen byte[], ale opět to může být další Mapa – pak mluvíme o tzv. SuperColumn.
Datový model pro programátory:
Map<byte[], Map<byte[], Map<byte[], Tuple<byte[], TimeStamp>>>>.
Neboli:

  • KeySpace name (typicky string) -> ColumnFamilies
  • ColumnFamily name (typicky string) -> Rows
  • Row key (byte[]) -> Columns
  • Column name (string) -> ColumnValue (byte[], timestamp)

Často se datový model vysvětluje (a je to tak přesnější) tak, že se obrátí ColumnFamily a řádek, tedy že řádek obsahuje ColumnFamilies.
Důležité je, že v konfiguraci se nastavují pouze KeySpaces a ColumnFamilies. Všechno ostatní je variabilní a může se to měnit – a to na každé úrovni. To mj. znamená, že každý řádek může mít libovolný počet sloupců a u každého řádku mohou být sloupce úplně jiné.
Data jsou fyzicky setříděná podle klíčů a sloupce také seřazeny podle názvů. Protože se všude pracuje s blobama byte[], máme možnost definovat, jakým způsobem se má třídit.

Velmi pěkný popis datového modelu Cassandry naleznete v tomto článku.

Jak jsem psal, Cassandří API je poměrně jednoduché a tak Vás možná napadne, že vybírání hodnot jen podle klíčů není dostatečné. A je to možné! Opravdu Vaše aplikace nemusí být vůbec vhodná pro nasazení na NOSQL úložišti. Ale i tak je vybírání podle klíčů poněkud svazující a proto byla ve verzi Cassandry 0.6 přidána podpora pro Hadoop.

Hadoop

Hadoop je v Javě napsaný open-source framework pro psaní spolehlivých, škálovatelných a distribuovaných výpočtů. Sestává se z několik částí, přičemž nás bude zajímat hlavně MapReduce, což je implementace MapReduce algoritmu. To je zjednodušeně řečeno algoritmus, který nám umožní položit dotazy, které se dají snadno paralelizovat.
Tento Map/Reduce se pak typicky využívá nad NOSQL úložištěm HBase (další část Hadoopu), které je postavené nad distribuovaným file systémem HDFS (také součást Hadoopu).

Když chceme zadat nějakou Map/Reduce úlohu, musíme předat do jobtrackeru jar, který obsahuje třídy implementující rozhraní Mapper a Reducer.
Pokud chceme dělat něco jednoduchého a nechce se nám kódit v Javě, můžeme použít Hadoop Streaming, což není nic jiného než konkrétní implementace Mapperu a Reduceru, která volá námi zadaný program a komunikuje s ním přes stdin/stdout. Konkrétně to funguje tak, že do Mapperu, resp. Reduceru, přichází řádky oddělené znakem nového řádku. Na řádku je nejprve klíč následovaný tabulátorem. Zbytek do konce řádku je hodnota.
Na jednoduché věci to určitě stačí, ale pokud chceme absolutní svobodu, pak musíme sáhnout po Hadoop Pipes. Jeho základem je opět implementace Mapperu a Reduceru, která tentokrát volá tenký wrapper v C++ (přes sockety). S tímto wrapperem je už možné komunikovat z téměř jakéhokoliv jazyka, protože je pro něj dostupné SWIG rozhraní (něco jako Thrift, ale funguje to lokálně). Takže díky tomuto můžeme psát Map/Reduce funkce v jakém jazyce chceme.

Psaní Map/Reduce dotazů se nám může ale pěkně zkomplikovat, zvláště když dotazy vyžadují více iterací. Pak můžeme sáhnout po projektu Pig (součást Hadoopu), který nám umožní zapsat dotazy v jazyce Pig Latin (ne nepodobný Linqu). Pig Front-end zadaný dotaz naparsuje, zoptimalizuje a vytvoří LogicalPlan, který popisuje, jak by se měl daný skript spustit. O samotné vykonání se pak postará Pig Back-end. Nyní existují dvě implementace Pig Back-endu – nad lokálním úložištěm (méně zajímavé) a nad Map/Reduce (obsahuje imlementaci Mapperu a Reduceru).
Abychom tedy mohli napsat Map/Reduce dotaz, nemusíme tedy nutně psát kód v Javě, ale místo toho předáme jen string obsahující skript v jazyce Pig Latin.

Podobný účel jako Pig má také projekt Hive, který přináší ještě vyšší stupeň abstrakce a umožňuje dotazovat do libovolného úložiště (pomocí jazyka QL), jehož strukturu jsme schopni popsat (musíme tedy zavést nad úložištěm strukturu). Hivem se ale nebudu teď zabývat, protože ještě není v production quality.

Shrnutí

Jak tedy pracovat s Cassandrou z .NET? Pokud si vystačíme s API, které nám nabízí Cassandří Thrift, pak si stačí nagenerovat třídy pro C# a máme vystaráno. Pokud Vám připadá Thriftové rozhraní příliš low-level, jsou k dispozici i nějaké nadstavby (třeba connection pooling je dobrý nápad).

Pokud chceme provádět Map/Reduce operace, je situace složitější, protože v jádru jde vždy o Javu.
Můžeme mít např. jednoduchou web-service, do které budeme posílat jary s Mapperem a Reducerem. Nebo můžeme mít na serveru předpřipravené jary a modifikovat soubor s parametry uložený uvnitř nich (je to jen zip).
Další možností je posílat do naší webové služby pouze dotazy v jazyce Pig Latin – pak už jsme blízko klasické situaci, kdy na SQL server posíláme dotazy v SQL.

Pokud se někdo rozhodne nasadit Cassandru na svůj projekt, měl by se nejprve důkladně obeznámit s jejím datovým modelem a zjistit, jestli bude mít k dispozici věci, které potřebuje a třeba je považuje za samozřejmost (díky SQL světu). Není to všespásný zlatý grál a nehodí se na všechny typy projektů.