In-Memory OLTP v dobe Covidovej

Po tom ako predminuly tyzden popadali u nas aj v Cesku weby na registraciu na Covid testy a ockovanie som zacal rozmyslat, ako by slo navrhnut system, ktory znesie velku (narazovu) zataz, pricom spracovava citelive udaje a sucasne sa pouzivatelia biju o obmedzene zdielane zdroje (terminy testov/ockovania).

Pokúsil som sa navrhnut architekturu a nasledne si ju prakticky vyskusat. S pouzitim In-Memory OLTP v MS SQL 2019 som na svojom notebooku dokazal spracovat (tranasakčne) 10000 requestov za skundu (teda aj registracii).

Viac na mojom blogu.

Ako by si riesil pri sql memory tabuľkách prípadný výpadok. Dáš tam SQL cluster? Alebo nejak sa to perzistuje raz za čas aj z tej pamäti? Alebo to poskladáš z nejakého logu?
Zaujíma ma to práve preto, že podobný prípad sa stal v Čechách, pri registráciu na podporu v covide a vraj im “vyhorel” server a prišli o všetky dáta, backup mali raz za deň.

V in-memory tabulke mam DURABILITY = SCHEMA_AND_DATA, takze sa tieto data odkladaju na disk same (malo by to byt vramci transakcie), ak som prednasky o tejto technologii pochopil, tak sa neperzistuju v tranzakcnom logu, ale idu do vlastnej filegrupy.

Co sa tyka zhorenia serveru… Samozrejme cluster alebo replikacia by bola v produkcii nutna. No neviem aky vpliv na vykon by to malo.

Mozno som s tym event sourcingom uz trapny, ale podla mna toto je ucebnicovy priklad :slight_smile: , ktory by bol jednak este vykonnejsi, horizontalne skalovatelny s mnozstvom dalsich vyhod.

Eventy by boli nieco ako:

  1. RegistrationRequested, ktore by sa zapisovali bez validacie a potom spracovavane nejakym background workerom, ktory by produkoval:
  2. RegistrationConfirmed.

Alternativne, RegistrationRequested by nemusel byt event, ale command, to znamena ze by sa neperzistoval.

Read cast by potom mohol byt obycajny in-memory dictionary, alebo cokolvek co sa ti hodi a malo by to byt radovo rychlejsie ako in memory SQL

Trik je v tom, ze read cast si vies jednak denormalizovat a optimalizovat a druhak nemas problem si vytvorit tolko replik, kolko sa ti zapaci.

Event Store je append only, to znamena ze vies do neho velmi rychlo zapisovat a aj z neho velmi rychlo citat, kedze data sa nemenia a vies ho aj jednoducho replikovat (kedze data sa nemenia).

Navyse, v tomto pripade by sa dala horizontalne skalovat nielen read cast, ale aj write cast. V tvojom pripade by mohol byt DistributionKey PlaceId. Na to aby si zistil, ci je nejake miesto obsadene, potrebujes eventy len z toho jedneho miesta, takze kadze miesto by mohlo byt obsluhovane osobitnym nodom, ale neverim ze by si vytazit co i len jeden server.

Mimochodom, vedel by si zakomponovať do tvojho testu aj nejake rezervacie. Napr kazdy 5 request by bol POST a nie GET?

Skor som rozmyslal nad celym CQRS.
Len som uvazoval ako riesit to, ze pri velkom navale ludi bude Command cakat vo fronte na spracovanie, nejaky cas. A moze sa stat, ze ked uz pouzivatela notifikujem o tom, ze dany termin je uz obsadeny, tak budu obsadene aj vsteky ostatne terminy.

Alebo si mam niekde in memory ukladat obsadenost terminov este nespracovanych commandov?

Pod tym in-memory dictionary si viem predstavit Redis. No neviem co pouzit ako EventStore. Vies dat typ? Rad si to vyskusam a tiez som zvedavy na vysledky.

Ten test testoval iba rezervacie, nie citanie.

Funguje to tak, ze mam GET metodu kontroleru, v nej sa vyberu nahodne data (nahodne miesto, clovek, cas) a zavola sa “POST” metoda, ktora sa pokusi registrovat cloveka na dany cas.

Po 1 minute testu bola zaplnenost terminov priblizne 30%.

GET pouzivam len pre to, ze Netling vie robit len GET requesty. Druhy nastroj, co pouzivam na load tetsing vegeta vie aj akukolvek metodu s akymkolvek telom a hlavickami, dokonca aj viac requestov. Ale nevie generovat nahodne data.

Tak som to proste hekol pomocou Netlingu, lebo ma gui. Ale co request, to pokus o registraciu terminu.

A teraz otazka, kolko by trvalo vytvorit taketo riesenie, ked vies, ze ide o jednorazovy projekt, ktory z principu chces aby bol jednoduchy, lebo mas na jeho realizaciu tak 2 tyzdne (nepredpokladam, ze mali viac casu vzhladom ako teraz funguje planovanie). V tvojom pripade by si musel vystavat nejaky event processing stack vs web a sql db.
Tiez som si kolko krat povedal, keby mal finstat proper navrh cez event sourcing, proper CQRS (co z pohladu toho ako funguje RavenDB ciastocne mame) vs moznost rychlo sa adaptovat na zmeny.
Taky januar a april su mesiace, ked sa rozpadnu vsetky druhy importov, lebo sa nasadzuju nove projekty vsade a ty proste chces aby si zakaznik nic nevsimol.

To sa urcite nestane :slight_smile: Ak uz mas asynchronne commandy, tak ich mozes spracovavat paralelne. Jediny bottleneck je EventStore, ale ten je vysoko vykonny (append only - data sa nemenia, ziadne locky alebo relacie) a aj ten vies distribuovat per seat.
Navyse, treba si uvedomit, ze okrem forwardovania eventov do denormalizerov budes uz iba requestovat eventy podla agregatu. Cize “daj mi vsetky eventy pre dany seat”. Prakticky budes robit iba tento jeden dotaz nad EventStore,

Kludne staci aj obycajny C# thread safe dictionary. Ak mas viacero nodov, tak kazdy moze mat vlastnu kopiu v pamati. QueryModel nemusis presistovat. Ak mas miliony eventov, ktore by si potreboval spracovat na obnovenie querymodelu, tak mozes urobit nejake snapshoty a tie persitovat. Potom nebudem musiet “prehravat” vsetky eventy nato, aby si ziskal aktualny stav.

Pridavam aj pseudokod. Zaviedol som dva druhy stavov - Ci je seat rezervovany a komu je seat rezervovany. Ci seat rezervovany totiz potrebujeme dat vediet userom velmi rychlo. Ked uz user klikne na nejaky volny seat, mozeme si dovolit cakat par sekund na potvrdenie rezervacie.

Poznamka: EventStore ma tu vlastnost, ze checkuje unikatnost SeatId, EventNumber

// Command Stack
IActionResult ReserveSeat(seatId, userName...) {
   //just save -> very fast!!
   EventStore.Save(new SeatReservationRequested { seatId, userName... }
   // tu user este nevie aky je vysledok
}


//query stack
SeatReservationsDenormalizer {
   Dictionary<seatId, isReserved> SeatsOccupancy; //extremly fast queries

   Handle(SeatReservationRequested event) {
      SeatsOccupancy[event.SeatId] = true;
      NotifyClients(seatId, true);
      // tu user este nevie aky je vysledok, ale mozeme s istotou povedat, ze Seat je rezervovany
   }
}

//Command Stack
SeatReservationRequestedHandler  {
   Handle(SeatReservationRequested event) {
      //retry at least 2 times:
      seat = SeatsRepository.get(event.seatId);
      seat.Reserve(evet.UserName);
      SeatsRepostory.Save(seat); //ensures consistecy using EventNumber
   }
}

Seat {
   SeatId;
   ReservedTo = userName;
   Events; //This seat events

   Reserve(userName) {
     if (ReservedTo != null) {
        Events.Add(new SeatReservationCancelled { SeatId, userName };
     }
     else {
        Events.Add(new SeatReserved { SeatId, userName, EventNumber = Events.Max(e => e.EventNumber) + 1 });
     }
   }
   
    Handle(SeatReserved event) {
       Events.Add(event);
       ReservedTo = userName;
    }
}

SeatReservedHandler  {
   Handle(SeatReserved event) {
      NotifyUser(..);
   }
   Handle(SeatReservationCancelled event) {
      NotifyUser(..);
   }
}

Ked to je jednorazovy projekt, urob to cez Azure Function a CosmosDB a za vikend si hotovy.
Ano, mas pravdu, ze vyskladat ten stack je narocne. Hlavne som bol sklamany z toho, ze vacsina kniznic a “tutorialov” boli dost naprd. Jedina realne pouzitelna vec bol Event Store - Event Sourcing Database od Grega Younga.

Mimochodom, teraz pozeram, ze Greg ponuka uz aj “fully managed” EventStore Cloud na Azure a AWS, co ma mimoriadne tesi, lebo ako hovoris, vyskladat si ten stack je riadna fuska.

Hovoril som o fronte commandov, lebo to chapem tak, ako hovoria poucky o CQRS, command moze byt odmietnuty spracovat, udalost nie, lebo udalost je nieco co sa uz stalo.

Chapem to tak, ze pred zapisanim udalosti do Event Store budem musiet “transzakcne” skontrolovat, ci dany termin nie je uz obsadeny aj v ES aj ulozicku ( este nematerializovane udalosti a perzistetny stav).

Kludne staci aj obycajny C# thread safe dictionary. Ak mas viacero nodov, tak kazdy moze mat vlastnu kopiu v pamati. QueryModel nemusis presistovat.

Uf, toto by sa mi kodit nechcelo. aj ked pouzijem nejaku implementaciu RAFT protokolu pre dotnet, tiez by to nebolo sranda vyvjat pod casovym tlakom.

Poznamka: EventStore ma tu vlastnost, ze checkuje unikatnost SeatId, EventNumber

Takze, tu ma byt event store nieco inteligentnejsie ako len ulozisko udalosti?

Hovoril som o fronte commandov

No a ja hovorim, ze command fronta je zbytocna. A ked uz, mozes ju spracovat paralelne. A ked sa ti tam nakopi vela commandov mozes vratit server too busy.

Chapem to tak, ze pred zapisanim udalosti do Event Store budem musiet “transzakcne” skontrolovat

Nie. Nacitas si eventy pre dany seat, overis ci je rezervovaný a pokusis sa zapisat novy event. Ak sa eventy v danom seate medzitym zmenili, tak sa ti to nepodari. Tymto mas zarucenu konzistentnost bez tranzakcii

A co by sa ti nechcelo kodit? Ved to je ten najjedoduchsi mozny sposob. Co za RAFT protokol? Asi sa nerozumieme :blush:

Synchronizaciu in memry reprezentacie Event Stroru. Raft alegoritmus/protokol sa pouziva na synchronizaciu stavu na viacerich v nodoch - https://en.wikipedia.org/wiki/Raft_(algorithm) (https://github.com/ThreeMammals/Rafty, pouziva ho napriklad Consul).

No zistujem, ze nakodit to nie je taka sranda, zabil som tym uz asi 4 hodiny. Zo zapisom eventov do suboru som sa dostal na uzasnych 150 req/sec. Ked som to preskocil a zapisujem len query cast do Redisu, tak som na 750 req/sec.

Vyzera, ze hlavnou brzdou tu je MediatR.

Priamy zapis do redisu cez ASP.NET (zapisem natvrdo jeden kluc) mi dalo 14 000 req/sec.

Moc sa mi s tym uz nechce trapit.

Mas jeden thread, ktory forwarduje eventy do denormalizera a ten a zapisuje do Dictionary. Tam netreba nic synchronizovať. Potom mas N paralelných threadov ktore z neho citaju. Aj s obycajnym ConcurrentDictionary by si si vystacil.

Tak potom by bola richlost zapisu zhodna s rycholstou zapisu do Event Store.