Do hlubin tlačítka Líbí se: výběr databáze, API a backend

21. 10. 2020
Doba čtení: 5 minut

Sdílet

 Autor: Seznam.cz
Nedávno jsme se podívali, jak dokáže Seznam spustit novou službu za 14 dní. Některé z vás zaujal výběr databáze, API a backend, proto se na ně v tomto článku podíváme zblízka.

Vraťme se nejdřív zpátky do momentu kdy se Seznam.cz rozhodl umístit tlačítko „Líbí se“ na další svoje služby a nabídnout ho také partnerům. V té době byly hlasy uživatelů spravovány v databázi nekonečného feedu článků hlavní stránky Seznam.cz, což je multi-master MariaDB cluster.

Hlasy bylo možné evidovat pouze pro články v databázi. Počty hlasů pro jednotlivé články nebyly nijak agregovány, vždy se počítaly jako součet hlasů pro daný článek. Získat počet hlasů k článku bylo možné pouze z API, které vydávalo doporučovaný výpis článků, nebylo možné se doptávat na konkrétní články. Frontendu se tento článek nebude věnovat.

Nutné minimum v takové situaci by bylo přidat do API možnost dotazovat se na počet hlasů ke konkrétním článkům a neznámé odkazy automaticky přidávat do databáze jako nepublikované položky v momentě, kdy dostanou první hlas. Tím bychom sice umožnili používat tlačítko „Líbí se“ na dalších službách, ale, jak již bylo zmíněno v předešlém článku, nelíbily se nám dopady na výkon hlavní stránky Seznamu způsobené používáním tlačítka na jiných službách a měli jsme pochybnosti o vhodnosti tou dobou aktuálního technologického řešení pro takový objem provozu. Primárně za účelem zachování spolehlivosti hlavní stránky Seznamu nezávisle na této službě jsme se rozhodli z tlačítka „Líbí se“ udělat samostatnou mikroservice.

Hlavní problém, který jsme museli adresovat v návrhu služby, bylo škálování pro očekávaný provoz při spuštění na všech zvažovaných službách a u partnerů. Hrubým odhadem jsme došli k průměrně 6 000 požadavkům za sekundu a až 14 000 požadavků za sekundu ve špičkách. Jelikož víme jak postavit backend, který (skoro) lineárně horizontálně škáluje, jediným potenciálním úzkým hrdlem byla databáze.

Výběr databáze

Při výběru databáze byla prioritou jednak možnost horizontálního škálování, a jednak vysoká datová propustnost pro čtení. Přijatelným omezením byla asynchronní replikace s eventuální konzistencí.

Výběr jsme konzultovali s administrátory provozujícími databáze pro SKlik, jelikož mají nejvíc zkušeností s provozem databází pod velkou zátěží. Tyto varianty jsme zavrhli:

  • MariaDB / MySQL – má složitější a omezenější horizontální škálovatelnost v porovnání s jinými možnostmi.
  • MongoDB – má pouze primary–secondary replikaci, primary je potenciální Single Point of Failure (automatický failover nebylo přijatelné řešení tohoto omezení), a jelikož všechny zápisy jsou koordinovány primární replikou, primární replika je úzkým hrdlem pro zápisové operace.
  • Cassandra / Scylla – mají náročnější instalaci a údržbu, vyžadují pravidelnou údržbu. Toto řešení je vhodné spíš pro sekvenční čtení, my vyžadujeme náhodný přístup. Globální sekundární indexy jsou míň výkonné a agregace dat jsou složitější.

Jako databázi jsme nakonec zvolili Couchbase. Důvodem byla jednoduchá instalace, propracované UI pro administraci a monitoring, automatický failover, horizontální škálovatelnost i napříč datacentry pro čtení i zápis a výkonné globální sekundární indexy. Dalším krokem bylo zvolit reprezentaci dat, která by vyhovovala vlastnostem zvolené databáze.

Datový model pro databázi

Původně jsme navrhli datový model tak, že kromě jednotlivých hlasů byly evidovány také sumy hlasů pro jednotlivé mediální entity (články, videa…). Tento návrh ovšem trpěl nekonzistencemi stavu vychádzejícího z chování cross-data center replikace (XDCR) Couchbase. XDCR v Couchbase je založena na časové známce nebo sekvenčních číslech s fallbackem na porovnávání metadat. Zjednodušeně řečeno: obvykle zvítězí chronologicky poslední operace nad záznamem. Pro evidování jednotlivých hlasů uživatelů toto chování vyhovuje, ovšem pro počítadla může nekonzistenci způsobit i rozptyl latencí na síti k databázi.

Z toho důvodu jsme upravili datové schéma tak, aby se synchronizovaly pouze jednotlivé hlasy uživatelů. Součty hlasů reprezentujeme jako cachovaný pohled na data – ten je počítán každým datacentrem samostatně, ale jelikož data na kterých jsou založena, jsou konzistentní, spočítané pohledy jsou také konzistentní.

API pro komunikaci s frontendem

Pro komunikaci mezi frontendem a backendem jsme zvažovali GraphQL, FastRPC (binární kódování pro XML-RPC), REST, ale zvolili jsme GraphQL. Důvodů bylo několik:

  • Součástí každého GraphQL API je schéma, kterým runtime pak validuje vstup i výstup, a slouží jako vždy aktuální forma dokumentace samostného API.
  • V každém dotazu pro čtení je možné číst víc zdrojů najednou. Zpracování těchto zdrojů je možné paralelizovat, a tuto paralelizaci je možné aplikovat na každé úrovni každé vlastnosti u stromové struktury dat.
  • Klient API vždy musí specifikovat které vlastnosti požaduje v odpovědi API. To umožňuje některá data zpracovávat a zpřístupňovat klientovi podmíněně, pouze pokud jsou zrovna potřeba.

V týmu tlačítka „Líbí se“ je v současnosti pro psaní backendu oblíben jazyk GoLang. Ten, v kombinaci s knihovnou GraphQL-Go, nám umožňuje využít výše uvedené výhody, proto jsme se rozhodli využít tuto kombinaci.

Pro maximální využití paralelizmu jsme rozdělili dotazy pro čtení na dotaz pro informace o aktuálním uživateli a informace o jedné konkrétní mediální entitě. To nám umožňuje načíst na klientovi informace o uživateli spolu s informacemi o entitách odkazovaných všemi tlačítky na stránce v jednom dotazu na API, přičemž runtime automaticky bude paralelizovat načítání dat pro jednotlivé entity.

Všechen tento paralelizmus ovšem nese potenciál pro race conditions a další problémy.

Backend, tak jako databáze, je nasazen ve více data centrech, což může při hlasování uživatelů a následné aktualizaci dat na klientovi vést k dočasným nekonzistencím, než proběhne replikace. Každé hlasování toho samého uživatele může být navíc zpracováno v jiném data centru. Navíc, jak jsme rychle zjistili, může být zrcadlení stavu databáze na klientovi pro uživatele matoucí, obzvlášť pokud hlasuje víc uživatelů „najednou“ nebo „současně“ s uživatelem přidávajícím hlas jiný uživatel svůj hlas odebere.

Tento problém jsme se rozhodli vyřešit tak, že na klientovi při hlasování provedeme optimistickou aktualizaci UI jako kdyby byl hlas zpracován okamžitě, a zvýšíme/snížíme zobrazený počet hlasů o 1. Pokud by se stalo, že by nastala v průběhu zpracování hlasování chyba, vrátíme tuto aktualizaci UI zpátky. Toto řešení spolehlivě odstraňuje oba problémy a vede k lepšímu uživatelskému zážitku.

Backend v provozu

Backend je aktuálně nasazený ve dvou ze tří datacenter Seznamu jako pody v Kubernetes. Abychom měli neustálý přehled o jeho stavu, využíváme monitoring založený na metrikách a analýze logů aplikace.

bitcoin_skoleni

Logy aplikace agregujeme do ELK stacku (Elasticsearch, Logstash, and Kibana) a máme nastavená automatická upozornění na chyby. Monitorovací metriky sbíráme instancemi Promethea v každé lokalitě, agregujeme je pomocí Thanose a vizualizujeme pomocí Grafany. Tento monitoring obvykle zobrazujeme na televizi připevněné na zdi u týmu a pro vhodné metriky využíváme automatická upozornění.

Zaujalo vás naše řešení? Máte nějaký dotaz? Tak jako minule také tentokrát se budeme snažit zodpovědět všechny vaše dotazy v diskuzi.

Autor článku

Martin Jurča pracuje v Seznam.cz jako vedoucí týmu vývoje hlavní stránky Seznam.cz. Ve volném čase se věnuje vývoji open source, případně drobným hobby projektům. Přednášel např. na konferencích JSConf Iceland, Cloud Native Brno a BrnoJS.