Mikroservisna arhitektura: dizajn i razvoj

Mikroservisna arhitektura dijeli jednu veliku aplikaciju na skup manjih servisa koji se objavljuju nezavisno, pri čemu svaki servis pokriva jednu poslovnu funkciju i upravlja vlastitim podacima. Kada je dobro urađena, timovi isporučuju funkcionalnosti bez međusobnog čekanja, skalira se samo dio sistema koji je pod opterećenjem, a greška u jednom dijelu ne obara sve ostalo. Kada je loše urađena, nastaje „distribuirani monolit”: sva složenost mreže servisa, a nijedna prednost nezavisnosti.

U CodeWizu dizajniramo i gradimo mikroservisne sisteme u Node.js-u, Laravelu/PHP-u, Go-u i Pythonu – a jednako često pomažemo firmama da procijene da li im mikroservisi uopšte trebaju. Granice servisa povlačimo oko vaših poslovnih domena, postavljamo API gateway, biramo način na koji servisi komuniciraju i od prvog dana projektujemo skalabilnost, nadzor i otpornost na otkaze, a ne tek nakon prvog pada sistema.

Usluga je namijenjena produktnim kompanijama kojima je monolit postao kočnica za nove verzije, SaaS platformama sa neujednačenim opterećenjem po funkcijama i timovima koji su prerasli fazu u kojoj svi mogu sigurno mijenjati isti kod. Ako svaki deployment zahtijeva usklađivanje pola firme ili jedan spori izvještaj obori cijelu aplikaciju, dobro isplaniran prelazak na servise vrijedi ozbiljno razmotriti.

Kod teme arhitektura alati otvorenog koda su prvi izbor kada odgovaraju poslu.

Arhitektura: Staklena fasada zgrade sastavljena od odvojenih ćelija u obliku saća naspram svijetlog neba

Podjela na servise: kako povući prave granice

Podjela sistema na servise je najvažnija odluka u svakoj mikroservisnoj arhitekturi i ujedno odluka koju je najteže poništiti. Granice povučene po tehničkim slojevima („servis za bazu”, „servis za validaciju”) daju servise koji stalno razgovaraju i moraju se mijenjati zajedno. Granice povučene oko poslovnih funkcija – katalog, narudžbe, naplata, obavještenja – daju servise koji se mogu razvijati samostalno.

Arhitektura kreće od sistema koji već radi, a ne od praznog šablona.

Kako pronalazimo granice

  • Event storming radionice sa ljudima koji poznaju vaše poslovanje: na vremenskoj liniji mapiramo poslovne događaje („narudžba kreirana”, „plaćanje naplaćeno”, „paket poslan”) i tražimo prirodne grupe.
  • Bounded contexti iz domain-driven designa: riječ „kupac” ne znači isto prodaji, naplati i podršci, a upravo ta razlika često označava granicu servisa.
  • Analiza zajedničkih izmjena kroz Git commitove: fajlovi koji se uvijek mijenjaju u istim commitovima pripadaju zajedno, bez obzira na to u kojem se folderu nalaze.
  • Opterećenje i struktura timova: dijelovi sa vrlo različitim potrebama za skaliranjem ili oni koje vode različiti timovi dobri su kandidati za izdvajanje (Conwayev zakon djeluje u oba smjera).

Pravila koja primjenjujemo na svaki servis

  • Svaki servis je vlasnik svojih podataka. Drugi servisi nikada ne čitaju njegove tabele direktno – koriste njegov API ili njegove događaje.
  • Servis mora moći da se objavi bez objavljivanja bilo čega drugog. Ako se dva servisa uvijek objavljuju zajedno, to je zapravo jedan servis.
  • Počnite krupnije. Kasnije podijeliti servis je lako; spojiti deset sitnih servisa koji dijele jednu bazu nije.

Rezultat: mapa konteksta, spisak servisa sa odgovornostima i podacima, ugovori između njih i redoslijed migracije.

Rad na temi arhitektura ostaje u vašem repozitoriju i na vašoj infrastrukturi.

Monolit, modularni monolit ili mikroservisi?

Mikroservisi su kompromis: dobijate nezavisan deployment i skaliranje, a plaćate mrežnim pozivima, eventualnom konzistentnošću i znatno zahtjevnijim održavanjem. Mnogim proizvodima modularni monolit – jedna aplikacija sa strogo razdvojenim internim modulima – donosi većinu prednosti uz djelić troška, a kasnije se može podijeliti na servise duž istih granica modula.

CodeWiz provjerava arhitektura u produkciji, ne samo na demonstraciji.

KriterijKlasični monolitModularni monolitMikroservisi
DeploymentJedna cjelina, sve ili ništaJedna cjelina, čiste granice modulaSvaki servis nezavisno
SkaliranjeCijela aplikacijaCijela aplikacijaPo servisu, prema njegovom opterećenju
Konzistentnost podatakaACID transakcije svudaACID transakcije, šema po moduluEventualna konzistentnost, sage između servisa
Operativni trošakNizakNizakVisok: orkestracija, tracing, mnogo pipelinea
Izolacija otkazaSlabaSlaba do srednjaJaka, ako je tako projektovano
Kada je najbolji izborMali timovi, rani proizvodiProizvodi u rastu, jedan do dva timaViše timova, neujednačeno opterećenje, česte nezavisne verzije

Izbor mikroservisa treba biti zaključak, a ne polazna tačka.

Odluke o temi arhitektura vežu se za ograničenje koje stvarno koči poslovanje.

Postavljanje API gatewaya za mikroservise

Klijenti ne bi trebali znati koliko servisa stoji iza vašeg API-ja. API gateway web, mobilnim i partnerskim klijentima daje jednu ulaznu tačku i preuzima zajedničke zadatke koje bi inače svaki servis morao sam implementirati: rutiranje, TLS terminaciju, autentifikaciju, rate limiting, ograničenje veličine zahtjeva, CORS i logovanje pristupa.

Predaja teme arhitektura uključuje uputstvo koje tim može koristiti.

Šta podešavamo

  • Rutiranje i verzionisanje po putanji ili hostu, kako bi se servisi mogli premještati, dijeliti ili mijenjati bez posljedica po klijente.
  • Autentifikacija na ulazu: gateway jednom provjerava JWT ili API ključ i prosljeđuje potvrđeni identitet servisima, koji i dalje sami provode svoja pravila autorizacije.
  • Rate limiti i kvote po klijentu ili paketu – tehnike smo opisali u našem vodiču za API rate limiting.
  • Backend for frontend (BFF) slojevi kada mobilna aplikacija i web dashboard trebaju potpuno različite odgovore, kako nijedan klijent ne bi morao zvati pet servisa za jedan ekran.

U zavisnosti od vašeg stacka koristimo Kong, Traefik, gatewaye zasnovane na Envoyu (uključujući Kubernetes Gateway API), NGINX ili managed rješenje poput AWS API Gatewaya. Deklarativna konfiguracija u Gitu čini gateway preglednim i ponovljivim:

Za arhitektura CodeWiz prvo mjeri ograničenje, pa bira alat.

_format_version: "3.0" services: - name: orders url: http://orders.internal:8080 routes: - name: orders-api paths: [/api/v1/orders] plugins: - name: jwt - name: rate-limiting config: { minute: 600, policy: local } - name: catalog url: http://catalog.internal:8080 routes: - name: catalog-api paths: [/api/v1/products]

Upozorenje koje često ponavljamo: gateway mora ostati tanak. Poslovna logika ubačena u plugine gatewaya postaje novi monolit oko kojeg se svi timovi moraju usklađivati.

Arhitektura ostaje prilagođen poslu, a ne obrnutno.

Crno-bijeli prikaz izbliza serverskog kućišta sa povezanim kablovima

Komunikacija između servisa: sinhrona i asinhrona

Način na koji servisi razgovaraju određuje kako se sistem ponaša pod opterećenjem i tokom otkaza. Svaki mrežni poziv može biti spor ili propasti, zato se komunikacija mora projektovati, a ne improvizovati.

Arhitektura je tema ovog članka, od ograničenja koje koči do predaje posla.

StilTipični alatiPrednostiKompromisi
REST preko HTTP-aJSON, OpenAPIUniverzalan, lak za debugging, može se keširatiOpširni odgovori, vremenska povezanost servisa
gRPCProtocol Buffers, HTTP/2Brz, strogo tipizirani ugovori, streamingTeže se pregleda, za browsere treba prevođenje na gatewayu
Asinhrone porukeRabbitMQ, Kafka, Redis Streams, NATSVremenski nezavisno, apsorbuje udare, prirodno za događajeEventualna konzistentnost, teži debugging od početka do kraja
GraphQL federacijaApollo Federation, GraphQL MeshJedan graf za klijente preko mnogo servisaDodatni sloj za održavanje, potrebna kontrola cijene upita

Naša podrazumijevana pravila

  • Sinhrone pozive koristimo za upite na koje odgovor treba odmah; događaje za stvari koje su se „desile” i na koje drugi servisi mogu reagovati.
  • Izbjegavamo duge sinhrone lance. Ako zahtjev prolazi kroz četiri servisa redom, dostupnost se množi naniže, a kašnjenje sabira.
  • Svaki poziv ima timeout, ponovne pokušaje sa eksponencijalnim backoffom i jitterom samo za idempotentne operacije, te circuit breaker koji odmah odustaje kada je zavisni servis pao.
  • Ugovori su eksplicitni i verzionisani: OpenAPI ili Protobuf definicije u repozitoriju, uz contract testove (na primjer Pact) u CI-ju.

Minimalan Node.js primjer poziva zaštićenog timeoutom, circuit breakerom i rezervnim odgovorom:

CodeWiz dokumentuje kako arhitektura radi u produkciji i kako se predaje timu.

import CircuitBreaker from 'opossum'; const getStock = (sku) => fetch(`http://inventory.internal:8080/stock/${sku}`, { signal: AbortSignal.timeout(800), }).then((res) => { if (!res.ok) throw new Error(`inventory ${res.status}`); return res.json(); }); const breaker = new CircuitBreaker(getStock, { timeout: 1000, errorThresholdPercentage: 50, resetTimeout: 10000, }); breaker.fallback(() => ({ available: null, stale: true })); const stock = await breaker.fire('SKU-123');

Za interni saobraćaj velikog obima često koristimo gRPC, a za sve što prelazi granice timova ili firmi radije biramo dobro dokumentovan REST ili događaje. U većim Kubernetes okruženjima service mesh poput Linkerda ili Istia može dodati mutual TLS, ponovne pokušaje i metrike saobraćaja bez izmjene koda – ali samo kada broj servisa opravdava taj dodatni sloj.

Kod teme arhitektura alati otvorenog koda su prvi izbor kada odgovaraju poslu.

Upravljanje podacima između servisa

Baza podataka je mjesto na kojem većina mikroservisnih projekata tiho propada. Ako servisi dijele tabele, povezani su bez obzira na to koliko repozitorija imate. Zato svaki servis dobija vlastitu šemu ili bazu, a konzistentnost između servisa rješavamo eksplicitno:

  • Sage (sagas) umjesto distribuiranih transakcija: niz lokalnih transakcija sa kompenzacionim akcijama („oslobodi rezervisanu robu”) kada kasniji korak ne uspije.
  • Transactional outbox: servis upisuje promjenu stanja i događaj koji je opisuje u jednoj lokalnoj transakciji, a poseban proces objavljuje događaj – bez izgubljenih ili fantomskih poruka.
  • Read modeli: kada ekran treba podatke iz više servisa, gradimo poseban, denormalizovan prikaz koji se puni događajima, umjesto spajanja podataka između servisa u trenutku zahtjeva.
  • Idempotentni consumeri, tako da poruka isporučena dva puta ne naplati kupca dva puta.

Kada podaci jednog servisa prerastu jedan server baze, sljedeći korak je sharding ili particionisanje – razliku objašnjavamo u članku sharding i particionisanje baze podataka.

Projektovanje skalabilnosti mikroservisa

Skalabilnost se ne dodaje na kraju kupovinom novih servera; ona proizlazi iz ranih odluka u dizajnu. Kod projektovanja skalabilnosti mikroservisa fokusiramo se na to da svaki servis bude jeftino umnožiti i sigurno pokretati u mnogo kopija.

Principi dizajna

  • Servisi bez stanja (stateless): sesije, uploadi i cache žive u Redisu, object storageu ili bazi, pa bilo koja instanca može obraditi bilo koji zahtjev.
  • Horizontalni autoscaling prema signalu koji stvarno odražava opterećenje – procesor za API-je koji puno računaju, dubina reda ili kašnjenje consumera (preko KEDA-e) za workere.
  • Keširanje na pravom sloju: CDN za javne odgovore, Redis za česte upite i HTTP cache zaglavlja između servisa gdje podaci to dozvoljavaju.
  • Back-pressure i odbacivanje viška: ograničeni redovi, limiti istovremenih zahtjeva i uredni 429/503 odgovori umjesto kolapsa pod udarom saobraćaja.
  • Bulkheadi: odvojeni pulovi konekcija i workera po zavisnom sistemu, kako jedan spori servis ne bi potrošio sve niti.

Na Kubernetesu Horizontal Pod Autoscaler drži servis između sigurnog minimuma i gornje granice troška:

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: orders spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: orders minReplicas: 2 maxReplicas: 12 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 65

Dizajn provjeravamo testovima opterećenja u k6 ili Locustu koji oponašaju stvarne obrasce saobraćaja i dokumentujemo gdje će se pojaviti sljedeće usko grlo. Izbor infrastrukture slijedi iz brojki; o tome smo pisali u članku o strategijama deploymenta na DigitalOceanu, AWS-u, Hetzneru i bare metal serverima.

Dva poslovna tornja sa ponavljajućom mrežom identičnih staklenih prozora

Nadzor: pogled u unutrašnjost distribuiranog sistema

U monolitu vam stack trace kaže šta je pošlo po zlu. U mikroservisnom sistemu jedan zahtjev korisnika može proći kroz deset servisa, pa su vam potrebna tri signala koja rade zajedno:

  • Distribuirani tracing sa OpenTelemetryjem, uz izvoz u Jaeger, Grafana Tempo ili komercijalni alat, pri čemu se trace ID prenosi kroz HTTP zaglavlja i metapodatke poruka.
  • Strukturirani logovi u JSON formatu, sa trace ID-jem, centralno prikupljeni u Loki, ELK stack ili sličan sistem.
  • Metrike u Prometheusu i Grafani: broj zahtjeva, stopa grešaka i kašnjenje po servisu i endpointu, uz kašnjenje redova i zasićenost resursa.

Za ključne korisničke tokove definišemo service-level ciljeve (SLO) i alarmiramo na njih, a ne na svaki skok procesora.

Postepena migracija sa monolita

Gotovo nikada ne preporučujemo prepisivanje svega odjednom. Umjesto toga koristimo strangler fig obrazac: gateway stoji ispred postojeće aplikacije, a funkcije se jedna po jedna izdvajaju u nove servise dok ostatak saobraćaja i dalje ide na monolit.

  1. Postavljamo gateway i nadzor ispred monolita, bez funkcionalnih izmjena.
  2. Izdvajamo funkciju sa najjasnijom granicom i najvećom vrijednošću – često obavještenja, pretragu ili naplatu.
  3. Selimo njene podatke, privremeno održavamo obje strane usklađenim, pa saobraćaj prebacujemo feature flagom.
  4. Uklanjamo stari kod, mjerimo i ponavljamo postupak.

Posao nijednog trenutka ne staje, a možete se zaustaviti kad god preostali monolit bude dovoljno dobar.

Najčešće greške u mikroservisima koje ispravljamo

  • Zajednička baza za više servisa, zbog koje svaka izmjena šeme postaje pregovaranje između timova.
  • Servisi podijeljeni po tehničkim slojevima, pa svaka nova funkcija dira pet repozitorija.
  • Sinhroni lanci poziva bez timeouta, gdje jedan spori servis izaziva lančani pad.
  • Ponavljanje neidempotentnih operacija, što pravi duple narudžbe ili plaćanja.
  • Nema distribuiranog tracinga, pa se incidenti istražuju pretraživanjem logova na deset servera.
  • Previše sitnih servisa za mali tim – više pipelinea nego programera.

Od čega zavisi cijena mikroservisnog projekta

Procjenu dajemo nakon pregleda arhitekture, a ne naslijepo. Glavni faktori cijene su broj servisa i koliko je zapetljan postojeći model podataka, da li gradimo od nule ili migriramo živi monolit, ciljna platforma (Docker Compose, Swarm ili Kubernetes), zahtjevi za dostupnošću i usklađenošću s propisima, te koliko CI/CD-a i nadzora već postoji. Više servisa znači i više stvari za ažuriranje i nadzor, zato o dugoročnom održavanju razgovaramo od početka.

Kako radimo

  1. Analiza: pregledamo kod, model podataka, saobraćaj, strukturu tima i probleme sa objavljivanjem novih verzija, te potvrđujemo da li su mikroservisi pravi odgovor.
  2. Arhitektura: vodimo radionice o domenu, definišemo granice servisa, ugovore, stilove komunikacije i vlasništvo nad podacima, pa usaglašavamo redoslijed migracije.
  3. Temelji: prije prvog servisa u produkciji postavljamo API gateway, CI/CD po servisu, kontejnerske slike, tracing, logove i metrike.
  4. Izrada i izdvajanje: nove servise gradimo ili ih izdvajamo iz monolita jedan po jedan, iza feature flagova.
  5. Testiranje: contract testovi, testovi opterećenja i namjerno izazivanje otkaza – gasimo instance i usporavamo zavisne servise da vidimo kako sistem reaguje.
  6. Puštanje u rad i podrška: postepeno prebacivanje saobraćaja, pojačan nadzor, runbookovi i, po želji, kontinuirano održavanje.

Zašto CodeWiz

  • Full-stack iskustvo: servise sami pišemo u Node.js-u, Laravelu, Go-u i Pythonu, a gradimo i infrastrukturu na kojoj rade.
  • Iskren savjet o arhitekturi: rješenja po mjeri umjesto šablona – ako vam modularni monolit više odgovara, reći ćemo vam to.
  • Open-source na prvom mjestu: Kubernetes, Traefik, Kong, OpenTelemetry, Prometheus i Grafana vas oslobađaju zavisnosti od jednog dobavljača.
  • Fer cijene: vrhunski razvoj bez vrhunskih cijena, od tima iz Banje Luke koji radi na daljinu sa klijentima iz regiona i svijeta.
  • Performanse i sigurnost na jednom mjestu: keširanje, baze podataka, Cloudflare i hardening su dio našeg svakodnevnog rada – saznajte više o CodeWizu.

Često postavljana pitanja

Kada firma treba preći na mikroservise?

Kada više timova blokira jedan isti kod, kada dijelovi sistema imaju vrlo različite potrebe za skaliranjem ili kada usklađivanje usporava objavljivanje novih verzija. Ako imate jedan mali tim i umjeren saobraćaj, dobro strukturiran modularni monolit je obično brži i jeftiniji izbor.

Koliko košta mikroservisna arhitektura?

Zavisi od broja servisa, toga da li migrirate živi sistem, ciljne platforme i zahtjeva za dostupnošću. Pregled arhitekture sa planom migracije je mali, jasno definisan posao, a nakon njega dobijate preciznu procjenu implementacije.

Koliko traje migracija sa monolita?

Izdvajanje prvog servisa, zajedno sa gatewayem i osnovom za nadzor, obično traje nekoliko sedmica. Potpuna migracija je niz takvih koraka kroz više mjeseci, a svaki korak sam po sebi donosi vrijednost – možete stati kad god preostali monolit bude dovoljno dobar.

Da li mikroservisi zahtijevaju Kubernetes?

Ne. Nekoliko servisa sasvim dobro radi na Docker Composeu ili Swarmu. Kubernetes se isplati kada imate mnogo servisa, potrebu za autoscalingom ili više timova koji nezavisno objavljuju izmjene.

Koje jezike i frameworke koristite za mikroservise?

Najčešće Node.js (Express, Fastify, NestJS), Laravel i PHP, Go i Python. Servisi mogu biti pisani u različitim jezicima kada to ima smisla, ali stack držimo dovoljno malim da ga vaš tim može održavati.

Planirate mikroservisnu arhitekturu?

Recite nam gdje vas postojeći sistem najviše koči – spore nove verzije, granice skaliranja ili krhak deployment – a mi ćemo predložiti arhitekturu koja odgovara vašem timu i budžetu. Kontaktirajte nas da razgovaramo o projektu ili pogledajte naše usluge mikroservisne arhitekture.