Přecházím z CosmosDB na MongoDB

CosmosDB je NoSQL databázový systém který provozuje Microsoft na Azure. Chtěl jsem na něm stavět novou aplikaci z následujících důvodů:

  • CosmosDB je PaaS (Platform As A Service). Nechci vůbec řešit provoz konkrétních aplikací na serverech (IaaS), chci, aby všechno bylo čistě cloudové a pokud možno se nechci vůbec zabývat monitorováním VMek.
  • V CosmosDB člověk musí pochopit jen logiku toho, jakým způsobem volba partition klíče v dokumentu ovlivňuje distribuci mezi logickými a fyzickými partitions.
  • Skrz dobře zvolenou partition strategii pak v CosmosDB nemusíte vůbec řešit škálování a platíte pouze za propustnost v jednotkách RU (Request Units) za vteřinu.
    (Rok 2021: minimum je 400 RU/s, 100 RU/hodina = 0,008 USD = ~$23 USD za měsíc)
  • V serverless režimu platíte za určitý celkový počet RU a ne za propustnost. Server se škáluje automaticky.
  • Indexování nad všemi hodnotami dokumentu by default (v MongoDB je by default indexace pouze nad primárním klíčem).

Na CosmosDB se mi ale od začátku některé věci nelíbily.

  • Chybějící podpora hromadného smazání/aktualizace všech dokumentů.
  • V platebním režimu RU/s CosmosDB vyhazuje chybu při překročení této propustnosti. Při programování aplikace člověk musí myslet na to, že každé volání CosmosDB může tuto chybu vyprodukovat.
    • Jasně, aplikace by se měly stavět resilientně a v dobře postavené aplikaci by se vždy mělo počítat s pádem jakékoliv externí komponenty.
    • Jenže tohle prostě znamená práci navíc. Toto mě nutí monitorovat metriky RU/s a monitorovat, jestli je z pohledu této metriky aplikace pro koncové uživatele vůbec použitelná.
  • Serverless mi nešlo založit v data centru Germany West Central (Frankfurt). Ind z Azure podpory mi řekl, že na portálu to sice není ale že to založím přes az cli nebo powershell. To jsem zkoušel ale dostal jsem HTTP 503 Service Unavailable. Ve West Europe mi to fungovalo ale WE (Nizozemí) je o 10ms dál než Frankfurt a u API pro mobilní aplikace se každých 10ms počítá. Dál jsem to neřešil.
  • Úchylné SDK pro C#, které se nedá prostě používát normálně napřímo a člověk si spíš k němu musí napsat nějaké vlastní extension metody. Viz. kód níže z oficiálního ukázkového repa
QueryDefinition query = new QueryDefinition("SELECT * FROM Families f WHERE f.id = @id AND f.Address.City = @city");

List<Family> results = new List<Family>();
using (FeedIterator<Family> resultSetIterator = container.GetItemQueryIterator<Family>(...))
{
    //Abych dostal všechny resulty, musím čekat na HasMoreResults=false
    //a dokud je to true, musí přečíst dostupnou kolekci dat skrz ReadNextAsync.
    while (resultSetIterator.HasMoreResults)
    {
        FeedResponse<Family> response = await resultSetIterator.ReadNextAsync();
        results.AddRange(response);
    }
}

//Nelíbí se mi, že tohle je způsob, který MS prezentuje jako to, jak by se měl CosmosDB používat.
//Proč prostě by default neexistuje v SDK metoda, která vrací IEnumerable<Model> nebo IAsyncEnumerable<Model> která skrývá tento implementační detail s HasMoreResults a ReadNextAsync?
//Proč musím psát vlastní extension metody?

CosmosDB Emulator = naprostý konec

Všechny výše zmíněné nevýhody pro mě ale stále nebyly tak významné. Automatická škálovatelnost schovaná za jednotky propustnosti RU/s a serverless varianta jsou prostě fakt skvělá lákadla. Jako programátor jsem tak nucený se soustředit pouze na to, že správně používám klíče a mám rozdistribuované partition klíče tak, aby CosmosDB mohl automatizovaně shardovat moje data bez větších omezení podle libosti. (Nutno dodat, že nemám zkušenosti s tím, jak CosmosDB doopravdy partitionuje ve vysoké zátěži, vycházím z informací zde a zde.)

Důvod, proč od CosmosDB odcházím je ten jejich posranej emulator.

Like seriously.

Microsoft místo toho, aby napsal emulátor který emuluje chování skutečného CosmosDB tak podle mého názoru vzali zdrojáky, kterým provozují skutečný CosmosDB na infrastruktuře Azure, ten zdroják jenom totálně ořezali a vznikla tak naprostá sračka, se kterou se nedá dobře pracovat.

Pokud si stáhnete emulator napřímo pod Windowsama a z C# kódu se na ten emulator připojíte, všechno funguje celkem dobře. Některý věci teda trvaj fakt relativně dlouho: funkce jako CreateDatabaseIfNotExistsAsync nebo CreateContainerIfNotExistsAsync trvají stovky milisekund až vteřiny. Funkce pro vytvoření databáze/kontejnerů jsou fakt zrovna podstatný pro psaní integračních testů (o tom ale víc níže).

Nefunkční a nepoužitelný docker container a vynucené HTTPS

Co už ale začne bejt dost na hovno je rozeběhnutí CosmosDB v docker containeru. Taková věc je totiž fakt docela potřeba, že jo, kvůli integračním testům. Já prostě nechci provozovat integrační testy proti ostré, placené verzi na cloudu. Já chci v rámci CI v nějaké build pipelajně rozchodit CosmosDB docker kontejner a proti němu spustit integrační testy.

Jenže ejhle.

Existují 2 verze docker kontejneru s CosmosDB emulátorem. Jedna je pro Windows, jehož image má 3GB a v nějakém build agentovi to prostě zabere deset minut, než se stáhne a nastartuje. Další verze je Linuxová, ta je podstatně menší.

Obě verze mají ale problém v tom, že CosmosDB emulátor generuje HTTPS certifikát při každém startu nejspíš proti IP adrese kontejneru. Takže v daném prostředí je nutné po nastartování kontejneru certifikát importovat. Takže musíte v CI pipelajně řešit a testovat sadu příkazů jako je tato:

ipaddr="`ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' | head -n 1`"
curl -k https://$ipaddr:8081/_explorer/emulator.pem > ~/emulatorcert.crt
cp ~/emulatorcert.crt /usr/local/share/ca-certificates/
update-ca-certificates

A to prostě zabírá mrtě zbytečnýho času tohle v CI pipelajnách otestovat a zprovoznit. Velmi brzy zjistíte, kvůli problémům popsaným v kapitolách níže, že ten kontejner je prostě kurevsky nestabilní.

Proč to prostě nemůže fungovat hned? Proč nemůžu udělat něco jako docker run -p 27017:27017 mongo a všechno začne magicky fungovat, aniž bych se musel srát s nějakými certifikáty?

Řešením je vypnout ověřování certifikátu na straně aplikace ale s linuxový docker containerem se mi to stejně nepodařilo stabilně zprovoznit.

Divná a nesmyslná omezení

Na téhle stránce se dočtete podivnost, kterou je emulátor zatížen. Napovídá to tomu, že Microsoft nedělal žádný emulátor ale vzal zdrojáky z kódu, který provozuje CosmosDB na Azure infrastruktuře a nějak ho brutálně ořezal, aby to vůbec bylo použitelné. Některé věci dávají smysl z logiky věci, např. žádná podpora consistency levelů. Správný emulátor by dle mého názoru měl simulovat pouze API a neměl by vůbec obsahovat žádný sdílený kód s emulovanou věcí.

Nejzvláštnější je toto:

The emulator is not a scalable service and it doesn’t support a large number of containers. When using the Azure Cosmos DB Emulator, by default, you can create up to 25 fixed size containers at 400 RU/s (only supported using Azure Cosmos DB SDKs), or 5 unlimited containers. For more information on how to change this value, see Set the PartitionCount value article.

https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21#differences-between-the-emulator-and-the-cloud-service

Proboha proč? Proč mě ten emulátor omezuje na 25 kontejnerů s fixní propustností nebo jen 5 kontejnerů s neomezenou propustnosti? Proč nemůžu založit neomezené množství kontejnerů? Proč si nemůžu nastavit propustnost tak jak chci stejně jako v ostré verzi? Tohle je zrovna věc, kterou emulátor vůbec emulovat vůbec nemusí nebo jen volitelně. Vždyť i všechny ty kvóty lze support requestem rozšířit.

Okay, pokud to chci zvýšit, musím při zapnutí emulátoru nastavit ručně PartitionCount argument. Tohle už je technologický detail emulátoru, který nechci řešit a absolutně mě nezajímá. V cloudové verzi vůbec žádný PartitionCount neřešíte tak proč to musím řešit v emulátoru?

Nepredikovatelné HTTP 503 errory

Při vývoji C# aplikace náhodně dochází k HTTP 503 chybám. Na tohle prostě nevím, co říct víc. Když jsem zkusil přejít na ostrou, cloudovou verzi CosmosDB tak k těm chybám nedocházelo.

Nepoužitelnost pro automatizované integrační testy

V rámci integračního testu chci postavit kompletní infrastrukturu aplikace se všemi souvisejícími komponentami jako databáze, cache apod. a proti této simulaci chci postavit čistou databázi a nad touto databází provést nějaký test (toto má smysl pouze u projektů kde dává smysl koncept „čistého prostředí“).

U integračních testů je v pořádku, že nějakou dobu trvají. Nevadí mi, pokud integrační test běží vteřinu ale už mi vadí, pokud běží desítky vteřin. U CosmosDB jsem zjistil, že prostě opakované volání vytvoření databáze, vytvoření kontejneru a následně shození kontejneru a shození databáze se prostě nasčítává až na desítky vteřin a to jen při hrstce testů.

Vzhledem k těm limitacím musíte taky hodně myslet na to, že nad emulátorem prostě nelze pouštět integrační testy paralelně. CosmosDB se z toho úplně sesype, jak nějaká traumatizovaná žába a emulátor vám začne vracet HTTP 503 i při vysoce nastaveném PartitionCount. Když jsem všech těch 10 integračních testů v xUnitu hodil do stejné [Collection] tak, aby se spouštěly v sérii za sebou, tak vše fungovalo ale běželo to skoro minutu.

Přecházím na MongoDB

S CosmosDB mi došla trpělivost, hlavně tedy kvůli tomu emulátoru. Je to nástroj, na který se nedá spolehnout a pokud databázové prostředí, nad kterým chci pracovat, nemohu dobře provozovat zdarma a lokálně z emulátoru a jsem nucený řešit záležitosti emulátoru, který mi má život zjednodušit a ne ztížit, pokud se nemohu soustředit na vývoj aplikace a musím řešit technologické záležitosti emulátoru, pak je prostě načase jít jinam.

Rozhodl jsem se pro MongoDB konkrétně MongoDB Atlas protože chci PaaS a vážně se mi nechce se starat o žádná VMka.

Jak se nastartuje lokálně MongoDB?

docker run --name mongo -p 27017:27017 mongo

A funguje to.

A jak se k němu připojím?

new MongoClient("mongodb://localhost:27017");

A funguje to. A neřeším žádný hovadiny.

Jak rychle běží integrační testy? Rychle a paralelně, 20 integračních testů je za vteřinu hotových.