Velvon debriefieng I. — Technologie

Roman Pichlík
dagblog
Published in
10 min readFeb 17, 2020

--

Každý neúspěch si zaslouží post mortem. V egineeringu Velvonu jsme se o to snažili, proto bude v téhle dobré tradici pokračovat přestože Velvon byl 13.02.2020 zrušen a já se ocitl takřka po dvaceti letech bez práce a nějaký pátek v tom hodlám pokračovat. Proto na Dagblogu najdete pár bilančních příspěvků na téma Velvon. Rád bych se nejprve ohlédnul za tím, co se nám podařilo dokázat.

Velvon měla být moderní digitální banka pro Německo. Vývoj začal bezmála před dvěma roky. Já jsem se k týmu připojil po prvních pěti měsících a měl jsem pomoci s přenesením některých konceptů, které jsme úspěšně implementovali v Zonky. Zonky i Velvon patřily do stejného impéria Petra Kellnera.

Původní tým si prošel několika iteracemi stavby banky a v roce 2018 se začalo stavět na zelené louce podle moderních trendů. Architektura byla postavená na mikroslužbách, na konci jsme jich měli 93, Kotlin, aplikační deployment Docker, Kubernetes, Azure cloud, DevOps abych jmenoval pár z nich. Postavili jsme observability platform, určenou k monitoringu a provozu banky, postavenou nad Splunkem, Prometheus, Grafanou, Victoria metrics a PagerDuty. Měli jsme poměrně přímočarý deployment model, kdy se každá změna nasazovala do produkčního prostředí pouze s jedním skokem přes prostředí s automatizovanými testy. Po merge do master větve trvalo 40 minut než se změna objevila na produkčním prostředí.

Na papíře i v realitě jsme měli základní procesy, kterými jsme nejenom prováděli změny, ale drželi banku při životě, zátěžově testovali nebo prováděli chaos engineering experimenty, na základě kterých jsme zrobustnili jednotlivé části systému. Měli jsme mobilní bankovnictví pro iOS, Android (oboje reaktivní) i responsní internetové bankovnictví kde fungovala tlačítka Back a Forward (sic!). Všechno to fungovalo do té míry, že jsme si mohli simulovat provoz živé banky včetně příchozích plateb a plateb kartou.

Když nám ve čtvrtek 13.02.2020 Erich Čomor (CEO) oznámil, že je konec, málokdo tomu mohl uvěřit. Byli jsme tak blízko. Rád bych vypíchl pár věcí se všemi výhodami a neduhy, které jsme vnímal. Vnímání je to subjektivní, možná se mnou proto nemusí všichni bývalý kolegové souhlasit, protože můj pohled byl zkreslený odstupem Head of engineering v organizaci s 50+ vývojáři. Přiznávám, už jsem neviděl do všech detailů a denní hands-on zkušenost se zredukovala maximálně na řešení koncepčních problémů.

Mikroslužby

Protože jsme začínali na zelené louce, mohli jsme se vyvarovat typických antipatterns jako je například sdílená databáze, které vznikají při přerodu z monolitické architektury. Docker a Kubernetes pomohl s unifikací modelu nasazení, tohle byla jasná výhra. Občas jsme měli mikroslužby dost granulární a architektura mi připadala dost komplexní, ale to může být věc zvyku. Rozhodně se nám osvědčily následující techniky.

  • Kontrakt služeb byl dokumentován ve Swagger a při každém buildu byl publikován a tvořil součást dokumentace každé mikroslužby.
  • Kontrakty a zpětnou kompatibilitu jsme testovali pomocí kontraktových testů v Pactu. Kontrola kontraktu byla součástí build pipeline konzumenta i providera. Kontrakt se automaticky publikoval na Pact server. Platilo nejenom pro HTTP API, ale i pro zprávy, které se posílaly přes messaging.
  • 90% mikroslužeb bylo napsaných v Kotlinu nad Spring Bootem. Výhodné to bylo z pohledu sdílení zkušeností v napojení na infrastrukturu, Azure a monitoring. Nemuseli jsme znovuobjevovat kolo, když se něco prokoplo v jedné službě, bylo to možné zkopírovat do všech ostatních. Argument, že si s mikroslužbami můžete vyzkoušet různé technologie určitě platí, ale platíte daň v podobě složité rotace lidí a neustálého prošlapávání cest. U nás se nikomu moc z Kotlinu zpět do Javy nechtělo.
  • Striktně jsme se drželi toho, aby se kód nesdílel. Neměli jsme proto žádné závislosti kromě síťového API s kontraktem.
  • Řídili jsme se dle doporučení 12 factor applications. Konfigurace služeb byla nastavená přes environment proměnné. Logování směřovalo na standartní výstup (console appender) a z něj přest Outcold collector do Splunku.
  • Architekturu uvnitř mikroslužeb jsem neřídili. To bylo v gesci každého týmu. Zdali někdo používal pro přístup k datům JPA, Hibernate, iBatis nebo JDBC bylo jeho volbou. Neřešili jsme velikost ani komplexitu služeb. Ani testy a jejich pokrytí nebylo předepsané. Platilo pouze pravidlo pro veřejná API, která měla být dokumentovaná a měla mít kontraktový test. Vycházeli jsme z teze, že nechceme mít centralizované týmy, které řídí architekturu, měří kvalitu a podobně. Platilo: vy to navrhujete, programujete, testujete, nasazujete a potom provozujete. To jak si to děláte uvnitř je váš boj. Pokud by se to v nějaké mikroslužbě zvrhlo, předpokládali jsme, že jí bude jednoduché díky kontraktu přepsat a nahradit.
  • Služby měly vlastní GitLab repository a buildovací pipeline a produkující Docker image včetně scanu na 3rd party vulnerabilities. Když si vzpomenu na útrapy s Jenkinsem v Zonky, fungovaly GitLab pipelines naprosto skvěle. V GitLabu jsme mohli centrálně řídit pravidla, která formovala náš change management proces např. žádné přímé commity do master větvě, pouze přes merge request.
  • Integrace GitLab, JIRA a Argo (deploy do K8s) poskytovala dostatečný auditní záznam — kdo, co, proč.
  • Jádro testování leželo na úrovni služeb, služba se vždy testovala v izolaci před tím, než byl vybuildován docker image. Měli jsme pouze jedno prostředí (Stage), kde se služby potkaly a kde kontinuálně 24/7 střílela baterie testů (tzv. journeys viz Testing microservices). Testy jsme spouštěli z mobilní aplikace, ta běžela v emulátoru. Testy jsme psali BDD stylem.

@journey
Feature: Journeys which stop backend production pipeline

Scenario: Top up account | Team A | top_up_account
Given I type mPIN 1234
And I’m on ‘Product detail’ screen
Then I tap on ‘Top up account’ button

Scenario: Create new savings account | Team B | new_savings_account
Given I type mPIN 1234
And I’m on ‘Product detail’ screen
When I tap on ‘Add account’ button
And I tap on ‘Savings account’ button
And I tap on ‘Continue’ button
And I type ‘My savings’ account name
And I tap on ‘Continue’ button
And I type mPIN 1234
Then I’m on ‘Success’ screen

Scenario: Sending a simple future payment to own account | Team A | send_future_payment
Given I type mPIN 1234
And I’m on ‘Product list’ screen
And I tap on ‘my primary’ account
And I’m on ‘Product detail’ screen
When I tap on ‘New payment’ button
And I tap on the ‘My savings’ account
And I type amount of 42 EUR
And I tap on ‘Add more details’ button
And I switch to ‘Future’ payment term
And I tap on ‘Pay on’ button
And I select ‘1st of next month’ due date
And I tap on ‘Continue’ button
And I tap on ‘Confirm payment’ button
And I’m on ‘Success’ screen
And I tap on ‘OK’ button
Then I’m on ‘Product detail’ screen
And I tap on ‘Scheduled transfers’ button
And I tap on first future transaction

Pokud testy selhaly, ihned se vystřelil incident v PagerDuty, protože pokud neprocházely testy na Stage, nebylo možné nasadit na produkční prostředí. Nikomu. Říkali jsme tomu závora. Samozřejmě lidi dost štvalo, že se mohli vzájemně blokovat, ale nedokázali jsme přesně určit koho blokovat resp. heurestiku jsme neměli čas vyladit.

Realese nebyl na úrovni mikroslužeb, ale podle K8s namespaces. Byl to v podstatě release train pro daný cluster mikroslužeb, který tvořil část systému např. cluster riskových mikroslužeb nebo cluster payment mikroslužeb. O nasazení se staralo Argo.

Pod kapotou jsme měli Azure container registry a release měl následující kroků v tzv. promotion pipeline. Počkat 15m (garantovaný čas pro seběhnutí všech journeys testů), zkontrolovat PagerDuty zdali v něm není incident indikující selhání testů, přiřadit tagy docker images a o vlastní nasazení se postaralo Argo. V plánu byla kontrola podpisů a kontrola, že proběhl vulnerability scan Twistlockem.

Release trainy se mezi sebou neblokovaly a nebyly nijak koordinované. Týmy mohly nasazovat nezávisle na sobě. Každý den jsme měli 20–30 nasazení. Rozhodně to nebylo neprůstřelné, občas se něco rozbilo, ale pokud byly vážnější problémy, nikdo to nebylo způsobené tímto deployment modelem.

Zátěžové testy

Poslední tři měsíce jsme prováděli pravidelné zátěžové testování, kdy jsme 24 hodin simulovali operace očekávaného množství uživatelů (v roce plus jedna) včetně denních špiček. Pomohlo nám to jednak vychytat výkonnostní problémy a jednak naučit se systém správně škálovat včetně clodových zdrojů s ohledem na cenu. Architektura a chováni služeb mělo respektovat, že běžíme v cloudu a alokovat si ideálně zdroje podle zátěže.

Používali jsme Kubernetes autoscaler pod, kdy se počet podu mohl dle zátěže zvětšovat a zmenšovat. V tomhle jsme byli na začátku, protože jedna věc je správné škálování podů a jiná je škálování AKS (Azure managed Kubernetes) nodes. Stranou můžu nechat dynamické škálování databáze v očekávání pravidelných dávek na konci měsíce či roku. S tím jsme neměli moc možností experimentovat. Databáze se vždy manuálně zvětšila a zmenšila.

Chaos engineering experimenty

Pro ověření stability a dostupnosti jsme prováděli chaos engineering experimenty. Kluci udělali hypotézu, jak se bude systém chovat pokud dojde k určitému selhání, kterou jsme posléze ověřovali. Abych uvedl konkrétní příklad. Iniciální dashboard mobilního bankovnictví se skládal z několika nezávislých částí. Při výpadku jedné serverové části měl dashboard stále fungovat. Udělali jsme hypotézu, vyzkoušeli, nefungovalo to úplně podle představ a tak jsme to opravili. Experimentů byla pěkná řádka.

Synchronní vs. Asynchronní komunikace

Původně jsme měli většinu komunikace synchronně přes HTTP. Protože jsme potřebovali volnější vazby mezi jednotlivými částmi, přešli jsme na asynchronní komunikace. V Azure jsme použili Message bus (fronty) a Event hub (pub/sub ala Kafka). Sranda začala ve chvíli, kdy jsme se pustili do zátěžových testů.

Subsystémy měly sice volné vazby, ale pod zátěží nefungovaly, protože docházelo k lokálnímu přetížení — velké množství méně důležitých zpráv typu X vs malé množství důležitých zpráv Y. Muselo dojit k refactoringu X a Y do vlastních front, plus throttling na straně producenta i konzumenta. K tomu vybudovat monitoring, abychom to dokázali vyhodnotit. Rozhodně tato změna komunikace není jenom o přepsání HTTP controlleru na Messaging controller, ale o vyvažování zátěže, monitoringu a pochopení jak funguje messagingový middleware pod kapotou.

Eventual consistency

Celé řešení bylo navržené s ohledem na dostupnost a rychlost. Rychlosti jsme chtěli dosáhnout pomocí Google Firestore více podcast s Davidem Vávrou. Příklad, který to pomůže ozřejmit. Při loginu jsme synchroním voláním API získali data ze zdrojových systémů a uložili je v derivované podobě do lokální cache (Redis, TTL 24 hodin). Z ní jsme je nahráli do Google Firestore, na který byla pověšená reaktivní mobilní aplikace. Google Firestore se postaral o rychlou distribuci dat do mobilu, kde se dále cacheovali v lokálním obraze Firestore. Nemuseli jsme na naší straně řešit websockety a další srandy, abychom akcelerovali přenos.

Pokud se v systému něco událo např. jste zaplatili kartou, vygeneroval zdrojový systém událost, kterou si mikroslužba odpovědná za Firestore chytla a zaktualizovala stav (zjednodušeně řečeno). Všechno fungovalo krásně až do chvíle kdy se události někde zasekly viz lokální přetíženi nebo se někde ztratily. Do systému jsme museli doplnit rekonciliačni algoritmy, které měly po čase zaručit konzistenci. Prakticky všechna data u sebe musela mít verze nebo časová razítka, aby bylo možné systém zrekonciliovat do aktuálního stavu. Na to je potřeba myslet rovnou při návrhu.

Google Firestore

Už jsem něco málo naznačil. Ta technologie vypadá děsně sexy. Měli jsme jeden showcase s realtime platbou , na kterém jsem produktovému odděleni demonstrovali možnosti, které nám Google Firestore dává. Na jednom zařízení jsme provedli platbu a na druhém se to ihned projevilo.

Použití Firestore ovšem znamená, že musíte překopat způsob, kterým programujete aplikace včetně jejich architektury. Platíto pro backend i frontend. Je to úplně jiný svět oproti přístupu přes webové API. Bez ohledu zdali používáte REST, GraphQL, RPC víte jak dělat verzování, existují na to postupy. S Firestore si tohle musíte vyřešit po svém. Zjednodušeně řečeno si představte, že integrujete dva systémy přes databázi, ve které máte JSON dokumenty.

Firestore jsme používali pro čtení, zápis šel přes mutace v GraphQL. V jednoduchém případě např. změna názvu účtu se poslala mutace synchronně pres GraphQL a výsledek volání (povedlo/nepovedlo) jenom umožnil propsat uživatelem zadanou hodnotu. Nicméně změna názvu účtu se projevila ve Firestore až po určité chvíli (než doputovaly všechny async zprávy a než se zakutalizoval stav). Pokud by ta chvíle byla dost dlouhá na to, aby člověk například zapnul a vypnul aplikaci, viděl by předchozí název účtu. Použití Firestore vyžadovalo, abychom si vůbec vymysleli návrhové vzory, jak s Firestorem pracovat v mobilní aplikaci, na serveru či v UI. To samé platilo pro optimální výkonnost a verzovaní dat. Celkem jsme předělávali strukturu dat ve Firestore snad třikrát.

Protože klientská data ve Firestore byla opravdu citlivá, rozhodli jsme se pro end2end šifrování. Server při zápisu data šifroval a mobilní aplikace si je dešifrovala klíčem uživatele. Pokud by se někdo nedej bože dostal do Firestore, byla by mu data bez dešifrovacího klíče každého uživatele k ničemu. Z výkonnostního footprintu šifrování jsme měli trochu obavy, ale testy na low end zařízeních ukázaly, že není potřeba mít obavy.

Mobilní vývojáři Firestore milovali, protože se proti tomu skvěle vyvíjí. Máte stav, na který si bindnete UI a tím to pro vás hasne. Myslim si, že kluci na backendu z toho měli šedivé vlasy. Obzvláště pokud se někde neukazovaly aktuální data. Bylo potřeb zkontrolovat stav ve zdroji pravdy, v cache co držela derivovanou formu a potom v samotném Firestore. Vzhledem k tomu, že ve Firestore byly data šifrovaná (klíč per klient), museli jsme si napsat vlastní administrační nástroj, který data zobrazoval dešifrovaná.

Možná z toho Firestore vychází dost negativně, ale nenechte se zmýlit, napsat reaktivní UI bez toho aniž by člověk použil nějaký pooling nebo WebSockety je zhola nemožné. V tom Firestore posloužil skvěle. Mimochodem na problém škálování read operací, od kterých nás Firestore odstínil, jsme narazili při vývoji internetového bankovnictví. To nepoužívalo Firestore (chtěli jsme mít funkční alespoň jeden kanál při výpadku Google), ale získavalo data přes GraphQL a server dostával dost zabrat.

Reaktivní programování

Právě customer facing služby napsané podle návrhového vzoru Backend for frontend, které obstarávaly data do Firestore nebo obsluhovaly GraphQL volání, byly napsané reaktivně v Kotlinu a Reactoru. Neprogramoval jsem to, ale co jsem tak viděl a slyšel, zrekonstruovat stav aplikace, pokud nemáte stacktrace, bylo šílené. Kluci se v tom nakonec neučili chodit, ale problém byl, pokud do toho bylo potřeba dostat dav vývojářů. Myslim si, že pořádně reaktivně programovat jich umělo pár. Zpětně si říkám jestli nám to za to stálo při ceně za procesorový čas versus čas vývojářů a bariéry, kterou jsme si tím vytvořili, pro kontribuci z dalších týmů.

Kotlin

Jedno z důležitých a správných rozhodnutí bylo vsadit na Kotlin. Dost nám to pomohlo s hiringem, protože komunita Java vývojářů na to dost slyšela. Jestli byl kód skutečně lépe čitelný a ubylo boilerplate kódu nedokážu posoudit.

Observability platform

Tu jsme si postavili k monitorování a provozu celé banky. Podporovala události, metriky a včasnou notifikaci. Události v podobě toho co logovaly mikroslužby se ukládaly do Splunku. Nad Splunkem jsme postavili vrstvu dashboardů, reportů a alertů. Ve špiččce jsme logovali 600GB dat denně. Tomu odpovídal i sizing Splunk clusteru.

Z GoodData jsem byl zvyklý na velké použití Splunku, ale ve Velvonu to bylo posunute snad ještě na vyšší úroveň. Splunk tvořil páteř monitorovací infrastruktury, proto jsme ho museli neustále ladit, hlavně z výkonových důvodů. Konfigurace dashboardů a reportů ležela v Gitu a přes GitLab pipeline se nasazovala do Splunku. Metriky,které o sobě běžíci služby poskytovaly, jsme získávali přes Prometheus a ukládali do Victoria Metrics. K jejich vizualizaci jsme používali Grafanu.

Závěr

O našich zkušenostech by se dalo povídat dlouho. Pokud vás zajímají detaily, které už mi mohou unikat, zkuste se obrátít na následující kolegy. Případně mi napište a já vás na někoho nasměruju.

Slíbil jsem, že se podělím o tom co budu dělat, ale na to přijde řada někdy příšte.

--

--

software developer, kitesurfer, ironman, coffee & books lover, blogger, podcaster, speaker. @czpodcast, dagblog.cz, @_dagi