Hlavní navigace

CouchDB a skriptování v C++

28. 4. 2017 22:00 (aktualizováno) | Ondřej Novák

Následující článek bude skriptování pohledů v NoSQL databázi CouchDB pomocí jazyka C++. Pokusím se představit nástroj, který jsem k tomuto účelu vytvořil včetně popisu implementace.

Při vymýšlení titulku jsem si představil šokované oči odmítačů jazyka C++ při představě, že by se v něm dalo „skriptovat“. Ale proč ne? Motivací bylo více. Vedle příslibu vyšší rychlosti při zpracování pohledů také i skutečnost, že jazyku C++ nejvíce rozumím, je mi příjemné v něm pracovat a také mohu sdílet některé části kódu mezi klientem a skripty nahrané na serveru (serverem myslím zejména databázi CouchDB). Výhodou je také syntaktická kontrola „skriptu“ před nahráním na serveru což díky silnému typování odhalí většinu chyb a překlepů ve skriptech ještě během vývoje.

Abych některé nedráždil, budu nadále používat slovo „fragment“ nebo „fragment kódu

Na představení CouchDB nemám prostor, proto bych zájemce odkázal na pěkný serial na Zdrojáku. NoSQL databáze CouchDB je autory prezentována jako databáze, ve které se indexace záznamů řídí JavaScriptem. Propojení CouchDB a JavaScriptu je tak zažité, že drtivá většina projektů dostupných z vyhledávače jsou též napsané v JavaScriptu. To celé nakonec dovršuje projekt PouchDB, který je vlastně implementací celé CouchDB v JavaScript (nejde jen o klienta, opravdu dělá vše co CouchDB) a je určen hlavně pro prohlížeče.

Samotná CouchDB je naprogramovaná v jazyce Erlang. Méně známa je skutečnost, že Javascript je sice vestavěný interpretr, ale nikoliv nativní. Indexaci v CouchDB lze řídit samotným Erlangem. Takto psané skripty přitom dosahují maximálního výkonu. Mají však bezpečnostní rizika daná právě tím, že Erlangový skript je úzce spjat s databází a skripty, kterou jsou součástí dat se v něm vykonávají, aniž by byly odděleny v nějakém sandboxu. Z tohoto důvodu je ve výchozím stavu Erlang, jako skriptovací jazyk, zakázán.

Zůstaňme u vestavěného Javascriptu. To poodhalí mechanismus, jakým lze do CouchDB integrovat další jazyky. CouchDB nabízí otevřené rozhraní, které se nazývá „query server protocol“ a jeho popis lze snadno dohledat v dokumentaci. Komponentě, která pak představuje protistranu k databázi, se říká „query server“. V rámci query serveru se realizuje mapování dokumentů (funkce map), operace reduce a rereduce, a samozřejmě neméně známé funkce show, list, update, validate a filter (více zde). Databáze komunikuje se serverem prostřednictvím obyčejné roury, samotný query server nachází požadavky na standardním vstupu a výsledky odesílá na standardní výstup. Základem komunikačního protokolu je formát JSON.

Vyvinout query server postavený na tomto jednoduchém schématu – pouhé čtení stdin a zápis do stdout, žádné vlákna, žádné porty, žádná synchronizace, žádné hlavičkové soubory a knihovny – zvládne každý průměrný programátor za pár hodin. Může si zvolit libovolný jazyk, může to psát třeba Perlu, nebo v Pythonu, v Php, v Javě nebo i v C++. Jediným požadavkem je, aby query server byl samostatně spustitelný proces, který pracuje se standardním vstupem a výstupem. Před vznikem tohoto projektu jsem si několik takových serverů napsal, zpravidla však byly jednoúčelné, určené pro konkrétní aplikace, a všechny operace uvnitř byly psané v C++. Kompletní sadu nástrojů pro práci s CouchDB z C++, včetně možností vyvinout si vlastní query server jsem pak zahrnul do knihovny couchit (stále ve vývoji)

U těchto jednoúčelových query serverů však nebyl kód v design dokumentu, jak je obvyklé, ale byl „zapečen“ přímo v query serveru a v design dokumentech se nacházel pouze odkaz na patřičný „zapečený“ fragment kódu. Mnohem větší výzvou tedy bylo, pokud by šlo do design dokumentu umístit C++ kód přímo. Mělo by to řadu výhod. Asi největším problémem jednoúčelových query serverů byla nutnost aktualizace binárek na všech uzlech kam se databáze replikovala a to při každé změně některého pohledu. Obecný query server by se aktualizovat nemusel, řízen by byl přímo design dokumenty, které se do každého uzlu replikují samy, tedy tak, jak se přirozeně děje u jiných skriptovacích jazyků

Projekt couchcpp

Projekt couchcpp je plnohodnotná implementace query serveru přijímající fragmenty psané v jazyce C++, kterými se pak zpracovávají data, jež serverem protečou. Podporuje všechny známé funkce, které design dokumenty nabízí až do verze 1.6, tedy nepodporuje nově zavedenou funkci „rewrite“ od verze 2.0 (ale to je jen otázkou času). Kódové fragmenty se píšou jako funkce s daným jménem a parametry, ale mohou volitelně obsahovat vložené hlavičkové soubory a v rámci fragmentu lze i ovlivnit, jaké knihovny mají být při spuštění fragmentu k dispozici.

#include <cstring> //standardní knihovna
#include <moje/custom/knihovna.h>  //jiná knihovna
//direktiva pro linker
//!link -lmylib

using namespace std;  //import namespace

void mapdoc(Document doc) {
    emit(doc[“_id”],doc[“_rev”]);
}

Příklad ukazuje obecný formát fragmentu, přičemž vložené hlavičky a importy namespace jsou volitelné. Povinná je vlastní funkce. Příklad realizuje operaci „map“ (a aby nebyla v kolizi se std::map, je záměrně pojmenovaná jako „mapdoc“). Jako parametr přijímá typ Document, což je předdefinovaný typ vhodný pro přístup k datům dokumentu.

Další funkce mají tyto prototypy

void mapdoc(Document doc);
Value reduce(RowSet rows);
Value rereduce(Value values);
void show(Document doc,  Value request);
void list(Value queryhead,  Value request);
void update(Document &doc,  Value request);
bool filter(Document doc,  Value request);
ValidateResult validate(Document doc, Context context);

Mnoho hodnot je předáváno jako JSON objekt a to včetně typu Document, což je jen trochu vylepšený JSON objekt. JSON objekty jsou reprezentovány typem Value, který je importován z knihovny imtjson, kterou jsem představil v minulém příspěvku

Uvnitř kódového fragmentu jsou pak k dispozici následující funkce (podle kontextu):

void emit(Value key, Value value); //funkce mapdoc()
ListRow getRow(); //funkce list()
void start(Value heades, int status); //funkce show(), list(), update()
void send(StrViewA text); //funkce show(), list(), update()
void log(StrViewA text); //všude

Význam těchto funkcí je shodný s tím, co nabízí rozhraní Javascriptu. Předsto má rozhraní ale jisté odlišnosti

Odlišnosti od Javascriptového API

  • První odlišností je rozpad funkce reduce na dvě: reduce a rereduce. Interně jde skutečně o dvě funkce, každá má jiné parametry. Javascriptová verze přitom následně parametry upravuje a volá jednu funkci „function(keys, values, rereduce)“. Program rozpoznává jednu operaci od druhé příznakem. Má zkušenost je taková, že drtivá většina funkcí má na začátku příkaz if, ve kterém kód větví na oba případy, které jsou naprosto neslučitelné. Proto C++ verze má funkce dvě a větvení již není třeba. Obě funkce se přitom napíší do stejného fragmentu pod klíč „reduce“.
  • Další významnou změnou je generování výstupu funkcí show a update. Zatímco v javascriptu se výstup vrací, v C++ verzi se výstup posílá na výstup funkcí send(). Před prvním send() je dobré zavolat start(). Je to stejný přístup jako u funkce list(). Samotná funkce list() nic nevrací, místo toho se patička výpisu obyčejně pošle příkazem send() stejně jako se poslal výpis samotný. Snahou bylo API maximálně sjednotit, aby se stejná věc volala stejným způsobem. Má-li tedy skript něco poslat jako odpověď, zavolá send().
  • Do třetice je ještě změna ve funkci update(). Tato funkce dostává dokument volaný odkazem. Pokud se něco v dokumentu změní, musí nový dokument přiřadit zpět do proměnné doc. Když se tak neučiní, má se za to, že dokument nebyl změněn. V knihovně imtjson každá změna v JSONu vede na nový JSON a platí to i pro dokumenty. 
void update(Document &doc,  Value request) {
    doc = doc.replace(„counter“,doc[„counter“].getUInt()+1);
}

Výše uvedený příklad zvýší o jedničku pole „counter“. Funkce doc.replace nahradí hodnotu v patřičném poli a vrací nový dokument. Zápisem do proměnné doc se zajistí změna dokumentu v databázi. 

  • Drobná změna je v návratové hodnotě validate(), která vrací objekt, jenž nese výsledek validace. Sama funkce některé parametry přijímá sdruženě jako Context

Sdílení fragmentů

V Javascriptu lze využít klíč „lib“ k importu knihoven CommonJS modulů. Ty se pak dají volat ve všech funkcí, kromě „reduce“. V couchcpp lze klíč „lib“ velice podobně použít k importu sdíleného kódu.

{
   “views”:  {
     “lib”: {
        “vendors”:{
           “test”:{
              “hello”:”std::string hello() {return \”Hello world!\”;}”,
              "magic":"const int response = 42;"
           }
        }
      }
   }
}

Tento sdílený kód lze pak do ostatních fragmentů vložit takto: (vyjma reduce)

#include “vendors/test/hello”
#include “vendors/test/magic”

(Kvůli chybě nelze sdílený kód používat ve verzi CouchDB 2.0, protože v jednom okamžiku tam autoři zapomínají sdílený kód připojit k fragmentu a query server následně odmítne fragment přeložit, protože sdílený kód nenajde. Chyba už byla nahlášena, případný pull request už je na cestě).

Implementace

Určitě někoho bude zajímat, jakým způsobem se C++ fragmenty kompilují a spouští. Není to nic složitého. Když se podíváte do projektu couchcpp, zjistíte, že se skládá z dvou *.cpp souborů. V jednom (couchcpp.cpp) je funkce main() a mnoho funkcí implementující jednotlivé části query server protocolu. V druhém souboru (module.cpp) je realizováno přeložení a načtení fragmentu do paměti.

Překlad fragmentu se provádí pomocí „g++“. Nicméně, Couchcpp má i konfigurační soubor, kde lze vybrat i jíný překladač a také upravit příkazovou řádku. Výsledkem překladu je dynamická knihovna *.so. Tato knihovna je následně načtena do paměti. Třída Module poskytuje unifikované rozhraní k takto přeloženému fragmenu a interface IProc pak poskytuje přímý přístup k jednotlivých funkcím daného fragmentu (podle typu). Kromě toho umožňuje inicializovat funkce log, emit, start, send a getRow, podle aktuální situace.

CouchDB nijak fragmenty neoznačuje, prostě posílá plain-text daného kódu. Couchcpp z každého fragmentu spočítá hash přes funkci FNV-1a, a prohleda cache již přeložených modulů. Pokud najde modul se stejným hashem, rovnou jej načte do paměti. V případě, že modul neexistuje, je spuštěn překlad fragmentu a výsledný modul je uložen do cache. Protože i samotné načítání fragmentů stojí nějaký výkon, podobná cache ještě spravuje načtené moduly v paměti a jejich uvolnění při delší době nečinnosti.

Výkon

Jednou z motivací bylo dosažení rychlejší indexace pohledů než třeba u javascriptu. Tady je potřeba dodat, že rychlosti zrovna nepřidá nutnost serializace a deserializace JSONů předávaných rourou mezi CouchDB a query serverem. Touto nevýhodou však trpí jak javaskriptový query server tak Couchcpp. Jak bylo zmíněno výše, Erlangové pohledy toto dělat nemusí, proto mohou být výkonější.

Provedl jsem několik srovnání a vždy byla indexace v C++ rychlejší než u JS. U složitější skriptů i o 100%. Na obrázku je ukázkový skript, který vytváří pohled typů a indexů, který se sestavuje po importu dat z MySQL, kdy je třeba data hledat podle původního automatické číslování. Výsledkem je tedy pohled dvojsloupcových klíčů, kde v prvním sloupci je typ záznamu (v jaké tabulce byl záznam uložen) a v druhém pak jeho tamní ID. V databázi je něco přes 6000 záznamů a skripty jsou puštěny na plný re-index (což je speciální, nejpomalejší případ, kdy se sestavuje celý index projitím všech záznamů)

Porovnání indexace rychlosti Javascript vs C++

Javascriptová verze (vlevo) trvala zhruba 2.5 sekundy, zatímco C++verze jen tři čtvrtě druhé sekundy. JS verze je zhruba o 40% pomalejší.

Je třeba říct, že do času C++ verze není započítán čas kompilace fragmentu. I s kompilací fragmentu trvá celý reindex 3.358 sekundy, čili pomaleji, než JS. Nicméně, kompilace není závislá na počtu záznamů a s vyšším počtem záznamů se snižuje její význam. U inkrementální indexace, která je v CouchDB nejčastější se už pracuje se zkopmilovaným fragmentem, takže i tady bude C++ verze rychlejší.

Bezpečnost

Přítomnost fragmentů C++ v databázi mezi uživatelskými daty moc pocitu bezpečí nepřidá. Jiné skriptovací jazyky se dají omezit tím, že nenabízí funkce, které by mohly poškodit systém. Couchcpp však umožňuje fragmentu použít cokoliv, co je dostupné v systému, přitom již samotná stdlib nabízí funkce pro přístup na filesystem a spouštění procesů.

Dobrou zprávou je, že fragmenty běží v odděleném procesu, takže nemohou přímo ovlivňovat běh databáze. Nicméně ve výchozím nastavení se proces spouští v kontextu stejného uživatele, jako sama databáze, takže process má teoreticky možnost číst i zapisovat do souborů patřící CouchDB. Což je ošklivé. Naštěstí to má snadné řešení. V systému založit uživatele, který má maximálně omezená práva. Tomuto uživateli věnovat process couchcpp a nastavit mu SUID příznak. CouchDB nebude protestovat, že se couchcpp spouští s právy jiného uživatele. Je třeba akorát zajistit, aby takový process měl přístup i do své cache, a víc už nepotřebuje.

Pokud někoho napadne ještě lepší izolace, chroot, fakechroot, LD_PRELOAD napište do komentářů.

Použití ve vývoji

Hlavní uplatnění najde couchcpp tam, kde se vyvíjí v C++. Umožní tak sdílet kód mezi klientskou aplikací a fragmenty a není třeba některé algoritmy psát ve dvou jazycích. Při psaní fragmentů používám couchapp, která drží fragmenty v souborech. Couchcpp také nabízí možnost nechat před sestavením design dokumentu fragmenty cvičně zkompilovat a vypsat případné chyby. Při implementaci jsem také počítal se všelijakými IDE pro vývoj. Couchcpp například správně nahlásí místo chyby, tedy přesně soubor a řádek, přestože se při kompilaci fragment vkládá do jiného souboru a kompiluje se na jiném místě.

I API počítá s IDE. Pokud se do fragmentu vloží #include <couchcpp/api.h>, budou správně fungovat kontextoví asistenti a obarvování syntaxe, protože v tomto souboru jsou všechny potřebné definice (byť se pak fragment překládá v jiném kontextu)

Závěrem

Projekt Couchcpp je v zásadě ve fázi bety, je zdarma pod licencí MIT a pro programátory C++ píšící pro CouchDB plně k dispozici. Pokud používáte jiné databáze, doporučuji rozhodně CouchDB dát šanci a pořádně si projít její možnosti. Programovat pohledy v C++ se vám jinde nepoštěstí.

Stránky projektu: https://github.com/ondra-novak/couchcpp