středa 29. března 2017

CZ Podcast 168 - Převážně nevážně


V 168. díle jsme to vzali převážně nevážně. Témata oscilují od práce na stavbě, sport, audioknihy až po muzeum herních konzolí.

pondělí 27. března 2017

Papírová škálovatelnost, Bcrypt a ladění výkonostních problémů Zonkyho

V poslední době často odpovídám na technické otázky našich uživatelů z řad investorů ohledně problémů s přetíženým Zonkyho v době půjček s vysokým úrokem, o které se pravidelně strhává litý boj. Rád bych na jednom místě sepsal seznam problémů a řešení, které jsme udělali.

Jakékoliv naše řešení je nutně determinované architekturou aplikace a zdroji, které máme k dispozici. Zonky je postavený nad klasickým Java stackem - Spring, JPA a jako databáze se používá Postgres. Jako v každém jiném startupu jsme nejdříve validovali business model a jeho růst namísto toho, abychom tunili architekturu. Podle mého soudu je to naprosto validní a jediná správná cesta. K čemu je vám super škálovatelná architektura, když nemáte platící uživatele. Řečnická otázka samozřejmě.

Naše architektura je tedy resp. byla do jisté míry poplatná tomu, že jsme startup, který šel na trh s MVP. jednalo se o klasický monolit. Škálovatelnost vertikální ano, horizontální na papíře. Proč na papíře, když svět je přece plný technologií s neomezenými škálovacími možnostmi, máme Docker, Kubernetes, Redis, cloud, virtualizaci?

Při použití klasického Java stacku, máte teoretickou možnost škálovat horizontálně. V našem případě nasadit aplikaci na více strojů. Ještě jednou, tohle je teorie. Každý kdo někdy provozoval nějaký distribuovaný systém ví, že jakmile do hry začne vstupovat sdílený stav a síť, začnou se věci dost komplikovat viz CAP theorem a jeho důsledky.

Krátká odbočka k Zonky. V našem případě je sdílený stav reprezentovaný in-process cache, kterou offloadujeme zátěž z databáze. V cache leží všechny kritická data, která jsou nutná pro provedení investice - aktuální stav zainvestování, autentizační tokeny atd.

A teď zpátky k CAPu a jeho dopadům. Máte sice kód a technologie, které se tváří, že fungují v distribuovaném prostředí, ale prakticky nevíte jaké problémy se na vás řítí - rozpady sítí, zpomalení komunikace/zahazování packetu, rozjetí systémových hodin atp., které k vám stejně probublají.

Rozdíl mezi tím, kdy máte aplikaci horizontálně škálovatelnou teoreticky a prakticky, je v tom že tyhle okrajové případy selhání máte podchycené. Nemůžete si říci: můj technologický stack tohle podporuje, fajn jdeme na to. Musíte se podívat na sdílený stav a ten vyřešit. Žádná technologie, která by magicky za vás řešila všechny problémy sdíleného stavu neexistuje.

Náš charakter provozu vypadá následovně: stabilní počet požadavků a při půjčce s vysokým úrokem řádový nárůst, protože faktor času/rychlosti je klíčový. To co nám hraje do karet je, že chvilku před víme, že k takové události dojde. Zase je to teorie, protože pokud nedokážete horizontálně vyškálovat je vám tahle informace k ničemu.

Většina prvotních výkonových problémů šla jednoduše odstranit na databázi. Prostě se přidal chybějící index případně se zoptimalizoval JPA dotaz či jiné další známé ORM optimalizace. Před nějakým časem jsme ovšem narazili na problém, který vyžadoval mnohem větší invenci.

Problém byl v tom, že aplikace začala konzumovat veškerý výpočetní výkon a pak se na chvíli zasekla a pak zase rozběhla. Podle telemetrie všechno ukazovalo na Bcrypt funkci, která se používá na hashování hesel. Vzhledem k době platnosti autentizačního tokenu došlo k tomu, že se stovky uživatelů chtěly v jeden okamžik zalogovat. Bcrypt je jako bezpečná funkce pro hashovani hesel z principu pomalá.

To co nedávalo smysl, bylo zatuhnutí a samovolné rozběhnutí aplikace. Z telemetrie bylo vidět, že jsou obsazené všechny databázové připojení. Při bližším průzkumu jsme zjistili, že za to může použítí návrhového anti-vzoru OpenSessionInView v kombinaci se sideloaderem do cache.

Po celou dobu odbavení požadavku se drželo aktivní databázové připojení. Při saturovaném výkonu CPU to znamenalo, že vlákna, která fakticky databázová připojení nepotřebovala, (neb čekala na CPU) spotřebovala všechnu jejich kapacitu. Buďto se dostala k lizu a nebo skončily timeoutem. V tu chvíli se celý systém rozpohyboval zpět k životu. Jak to?

Máme read-heavy provoz, praktický veškerou komunikaci obsluhujeme z cache a data do cache (Hazelcast) se nahrávají side loaderem z databáze. Ten je reprezentován dedikovaným thread poolem. Čili mezi aplikačním vláknem či vlákny, které čekají na tu cache a vlaknem sideloaderu, které do ní nahrává data z databáze, došlo k vyčerpání připojení. Sideloader(y) do cache čekaly na spojení, která byla zbytečně držena aplikačními vlákny (díky OpenSessionInView), která čekala na CPU plně saturované Bcryptem. Když došlo k timeoutu nebo se uvolnilo spojení, ke kterému se dostalo sideloader vlákno systém se pomalu pohnul dopředu.

Pořadí našich patchů:

  1. Vertikální škálovaní - zvýšit výkon přidáním CPU
  2. Odložit alokaci databázového spojení na nejzašší možný okamžik (typicky zahájení transakce)
  3. Odstranit sideloading do cache pro nejčastější použítí
  4. Dedikovaný databázový pool pro sideloadery, aby nedocházelo k vzájemnému vyčerpání.
  5. Předřazení semaforu před Bcypt, abychom limitovali maxímánlí možmý paralelismus, který systém neochromí (počet CPU - 1)

Úplné odstranění OpenSessionInView nebylo možné a stejně tak přepsání sideloaderu vzhledem k urgenci toho problému.

Dalším krokem, který chystáme, je offloadování Bcryptu na volnou kapacitu, kterou máme v datovém centru k dispozici. Máme mikroslužbu, kterou nasadíme na N strojů a jediné co bude dělat, je hashování s tím, že tam bude vždycky možnost fallbacku zpět na lokální výpočet.

Zde se nabízí otázka jestli neudělat scale out do cloudu. Pro tenhle případ by se perfektně hodil serveless v podobě AWS Lambda. Už teď máme hybridní deployment, kdy nám server side rendering pro SPA běží nad AWS Elastic Beans Talk. Lambda má tu nevýhodu, že by tam byla daleko větší latence a mám trochu obavy, že neexceluje v schopnosti elasticky reagovat na prudký nárůst požadavků během pár desítek sekund. Pak by to znamenalo AWS Lamdy “nažhavit” tedy generovat syntetická provoz, aby se patřičně nafoukl počet instancí schopných odbavit očekávaný provoz. Potřebný infrastrukturní by byl mnohem složitější než samotná mikroslužba.