Otisnuli smo se prema pučini i očekivali da će naš pothvat biti brz i jednostavan praćen sjajnim i vedrim nebom. Ali na vidiku opazih kako se naoblačilo i da moramo mijenjati kurs.
Sve je započelo, kao mali projekt. Vrijeme je prolazilo, ali stvari se nisu odvijale onako kako smo planirali, a kada sam shvatio da radimo Cloud-native aplikacije, već je bilo prekasno.
Paradigma mikroservisne arhitekture
Iako na samom početku to nismo razmatrali, konačno smo prihvatili činjenicu da će biti potrebno razdvojiti kod na manje dijelove te definirati i organizirati komunikaciju između tih manjih dijelova. Još uvijek smo u toj fazi, a kraj našega puta je daleko. Ali ovako je sve to krenulo:
Dizajn smo započeli sa strangler vine patternom i prvi poslovni zahtjevi su riješeni ciljanom mikroservisnom arhitekturom (MSA) – sve što je dalje slijedilo, pratilo je tok po novoj paradigmi. Puno je jednostavnije usredotočiti se na samo jedan dio sustava i implementirati mikroservisne pattern-e, nego ponovno graditi cijeli sustav od nule.
Odvojili smo entitete iz tablica specifične za domenu autorizacijskog servisa (tj. shemu pravila) u zasebni spremnik podataka namijenjenog samo za ovaj servis. S obzirom da govorimo o našem prvom mikroservisu, omogućili smo REST API metode klijentima (za SPA aplikacije i za pozadinske servise), a onemogućili pristup domeni krajnjim korisnicima. Stoga je domena postala dostupna isključivo mikroservisu, čije se REST sučelje prikazuje zajedno sa setom evenata (u slučaju da je došlo do promjene domene, a potrošači su zainteresirani za istu).
Odvajanje podataka i definiranje domena mikroservisa
Zatim smo uklonili servisne reference iz odgovarajućih projekata. Osigurali smo samo NuGet pakete koji su sadržavali nove API definicije i odgovarajuće modele (DTO) koji se dijele radi verzioniranja i dosljednosti koda. To bi značilo da imamo API registar stvoren od svih servisa za autorizaciju. Svaki servis koja treba konzumirati usluge servisa ima na raspolaganju svoju dokumentiranu API definiciju. Budući da koristimo Swagger kao OpenAPI specifikaciju za REST API-je, HTTP modeli se automatski generiraju, ali modele smo proslijedili u pisanim dokumentima za ostale komunikacijske protokole.
Takav pristup je drastično povećao dostupnost podataka i omogućio autonomni razvoj servisa, bez ometanja postojećeg koda unutar ostalih servisa. Razvojni timovi mogu se sada koncentrirati na datu poslovnu domenu i riješiti potencijalne probleme koji se odnose samo na određenu domenu.
Poticanje event-driven komunikacije
Budući da su servisi bili izloženi vanjskom svijetu sa svojim definiranim API sučeljem, bilo je potrebno promijeniti i strukturu i ponašanje pri implementaciji šesterokutnog dizajna aplikacije. Servis ne komunicira nužno samo s vlastitim spremnikom podataka (koji može biti baza podataka, ali i bilo koji drugi izvor poput blob-a, tablica, data lake itd.), on čita ili pohranjuje podatke preko svog podatkovnog pristupnog sloja, ali i izlaže svoje krajnje točke Web API-ja klijentima, poziva druge servise putem HTTP ili gRPC protokola i na kraju, prenosi poruke koristeći asinkronu event-driven komunikaciju.
Najvažniji segment podatkovne komunikacije je slanje i primanje poruka kroz event bus koji nam služi kao primarna komunikacija između slabo povezanih mikroservisa. Dodavanjem asinkronih event-driven poruka, komunikacija je otpornija i učinkovitija jer pozivi nisu blokirani ili podložni mrežnoj latenciji. Svaki servis po definiciji izlaže nekoliko evenata kada dođe do promjene domene (notification events) i omogućava slanje poruka preko drugih servisa (commands). Sve će se poruke poslati pomoću jednog ili više kanala poštujući primijenjene uzorke poruka.
Komunikacija kao vitalni dio arhitekture mikroservisa
Primjenom event-driven arhitekture, servisi su postali samodostatni i neovisni jedni o drugima, što je omogućilo međuprocesnoj komunikaciji (IPC) da postane transparentna, pouzdana i skalabilna. Ovisno o primijenjenom protokolu i tipu komunikacije, servisi također uključuju elastičnost kroz rukovanje djelomičnim kvarovima (partial failures), dosljednost podataka i rješavanje cross-cutting problema s laganom kodnom bazom.
Pregledan interni dizajn aplikacije
Unutarnja struktura servisa znatno se promijenila u skladu s njihovom vanjskom arhitekturom. Alito uopće ne mora biti slučaj, s obzirom na to da je svaki servis odgovoran za svoje unutarnje ponašanje. Ali shvatili smo da kanonski dizajn aplikacije jednostavno ne odgovara novoj paradigmi mikroservisa.
Pretpostavimo da je složenost poslovanja velika, a servisa još veća. U tom je slučaju primjenjivo uvesti domain-driven dizajn (DDD), a u većini slučajeva i uzorak command-query responsibility segregation (CQRS). Prilikom razvoja servisa, dokazano je da je potrebno razdvojiti brzi pristup samo za čitanje i ERM podatkovni sloj koji se koristi za CRUD operacije na domeni. Prvi bi se pristup mogao primijeniti i pomoću neke lagane biblioteke za pristup podacima (poput Dappera), a drugi s cjelovitim rješenjem temeljenim na transakcijama kao Entity Framework Core. Pomaže i razdvajanje servisa koji rade s podacima drugih domena, onih koji komuniciraju bilo s drugim internim uslugama ili s drugim mikroservisima korištenjem izravne ili neizravne komunikacije. Ali to sve ovisi o poslovnom slučaju, veličini projekta i strategiji pohrane podataka.
Iako smo na sličan način modelirali svoje mikroservise, izbjegavamo bilo kakvu uniformnost. To je zato što se svaki servis bavi vlastitim poslovnim potrebama, a nasljeđivanje iz iste zajedničke baze čini nam se anti-uzorkom. Dizajniramo mikroservise i (mikro) aplikacije po mjeri, čineći ih sigurnijima za stalne promjene i proširivost. Naučili smo lekciju o izgradnji monolitne strukture koja se zatim primjenjuje na bezbroj usluga; stoga je izrada velikog broja monolita čak gora od izgradnje jednog velikog monolita.
Testiranje i kontejnerizacija
S obzirom na šesterokutni dizajn aplikacije, svoje servise modeliramo na temelju različitih adaptora koji čine skup kontrolera, upravitelja evenata, naredbi, upita, usluga domene, infrastrukturnog koda i klasa saga. Sve bi to trebalo temeljito testirati – s potrebnim jediničnim testovima i sa složenijim integracijskim testovima.
Što se tiče unit testova, stvar je očita: sve kao jedinicu rada (SUT) treba testirati odvojeno – neki testovi se mogu provesti kao usamljeni, a neki testovi kao društveni testovi (također uključuju funkcionalnost drugih SUT-ova). Počeli smo unit testovima s in-memory bazom, ali to se pokazalo pogrešnim. Mnogo spremnika podataka, osim relacijske SQL baze, to ne podržava, pa čak i tada baza podataka u memoriji nije potpuno ista kao stvarna. Broj unit testova mora biti velik kako bi pokrili većinu poslovnih slučajeva, a moraju biti i brzi (pogotovo ako se pokreću pri svakom buildu).
Naprotiv, pretpostavlja se da će unit testovi znatno nadmašiti integracijske testove u njihovom broju. Također ih je teže implementirati, a uskoro ćete naići i na fizičke poteškoće pri njihovom pokretanju, jer na lokalnom stroju nema dostupnih servisa, osim ako se ne vratite na monorepo pristup (koji je u većini slučajeva nije preporučen).
Alternative u tom slučaju su mock serveri i kontejnerizacija ili neka kombinacija ovo dvoje. U nekim je slučajevima moguće osigurati alternativnu funkcionalnost (poput poslužitelja identiteta), a ponekad će biti lakše omogućiti mocked server API (Postman ili MockAPI). Ipak, u većini slučajeva trebat će vam Docker na vašem lokalnom razvojnom stroju. DevOps pipeline će se sigurno razviti kako kako bi mogao generirati servisni image iz Docker datoteka. Vjerojatno ćete kontejnerizirati svoje interne komponente za testove integracije (poput baze podataka, pošiljatelja poruka itd.), umjesto da ih instalirate na svoj razvojni stroj.
Izazovi i prepreke
Naravno, uvijek dolaze novi izazovi i prepreke koji će se pojaviti prilikom usvajanja Cloud design patterna i distribuiranih stilova arhitekture, a naš slučaj nije iznimka.
Jedna od stvari s kojom smo se morali suočiti vrlo rano je preoblikovanje postojećeg servisa kako bi se upotrijebili mikroservisi i kako bi radio pouzdanije. Napokon se pojavljuju različiti čimbenici kvalitete, budući da je programabilnost prikladnija za upotrebu. Stvari poput skalabilnosti, pouzdanosti, sigurnosti i otpornost podataka rješavamo, ali u širem smislu.
Ne možemo očekivati da će se legacy sustavi vrlo rano prilagoditi, pa smo usvojili obrazac anti-corruption layer za one usluge koje trebaju raditi na ‘stari način’, ali i dalje biti dostupne klijentima. Stoga ove usluge tretiramo kao i do sada, dok stari kod ne zagađuje novi.
Konzistentnost podataka nam je velika briga, uglavnom zato što tek sada možemo jamčiti dosljednost. Dakle, podaci bi na kraju trebali biti dosljedni. Moramo računati da bi dijelovi sustava trebali raditi s replikama drugih izvora podataka i povremeno se sinkronizirati (bilo s određenim rasporedom ili potaknuti događajima integracije). U tom smislu, neophodno je osigurati neku vrstu početnih strategija sinkronizacije podataka te pratiti i nadzirati stanje podataka i procesa koji uključuju servisi.
Što se tiče skalabilnosti, posebno kada kontejnerizacija dođe u obzir, servisi moraju biti relativno mali i raditi kao izdržljiva pojedinačna jedinica rada, uz veliku korist od računskih cloud patterna . U tom smislu, upotreba serverless paradigme u Azureu (Azure funkcije, Logic Apps, Event Grid), zajedno s odvojenom pozadinskom obradom (Azure WebJobs, pozadinski servisi, worker role itd.), Imaju ključnu ulogu u izgradnji distribuirane arhitekture koja se može dobro skalirati.
Jedno od ograničenja su operativni troškovi. To uključuje ne samo osigurane upravljane usluge prvenstveno kao SaaS ili FaaS rješenja (koja će vas općenito skuplje koštati), već i svu infrastrukturu (IaaS i djelomično PaaS) resurse koje treba prilagoditi. Ponekad je teško izračunati koliko ćete kredita potrošiti za svaku ponudu na temelju toga kako se koristi na modulu i kako će se vjerojatno skalirati u budućnosti. Pogotovo jer gradimo multi-tenant rješenje, svi resursi moraju uključivati particioniranje ili nekoliko procesa obrade podataka koji bi se mogli odvojeno skalirati, što komplicira izračun.
Kolaboracija i povjerenje su prioritet
Najvažnija činjenica bila je da je uprava shvatila važnost uvođenja Cloud arhitekture i svega što ona donosi, a ja sam vrlo zahvalan na koraku koji su poduzeli da to učine. Budući da su velika tvrtka sa strogom i podijeljenom hijerarhijom, bilo je izazovno probiti se u njihovom mišljenju da stvaramo nešto što ne donosi novac unaprijed. Umjesto toga, potrebno je ulagati u Azure infrastrukturu, ljudske resurse i povezati sve uključene. To nije bilo samo moje postignuće i uspjeh moga tima, već zajednički napor mnogih voditelja na projekatu, menadžera i programera koji su se potrudili prihvatiti novi način razmišljanja i stav pozitivne kritike prema zajedničkom cilju. Slomili smo monolit u strukturi njihove organizacije zahvaljujući inicijativi za uključivanjem svakog pojedinog člana koji doprinosi i dijeli svoje znanje. Održavam predavanja svom timu, organiziram radionice, svakodnevno ih uvjeravam da sudjeluju i prilagođavam svoje razmišljanje onome što mislimo da je najbolje u datom trenutku.
Svi mi svakodnevno učimo i napredujemo u novim tehnologijama i konceptima. Otvoreni smo za nove izazove, nove suigrače, promjene paradigmi, načina razmišljanja i pravila. Usvojili smo novu krilaticu: “svaki tjedan pothvat jedan” i razmišljamo o svim izazovima s kojima bismo se mogli suočiti.
Ako ste dovoljno otvoreni za nove stvari, sve je moguće, jedino ostaje odrediti: kada, tko i kako.