Iza API Gatewaya: projektiranje interakcija usluga za predvidljivo skaliranje
API pristupnik može učiniti da distribuirani sustav izgleda umirujuće jednostavno: jedna javna krajnja točka, jedan sloj autentikacije, jedno mjesto za usmjeravanje i ograničenja broja zahtjeva. Ta je jednostavnost vrijedna, ali pristupnik je također lako zamijeniti za samu arhitekturu.
Predvidivo skaliranje određeno je onime što se događa nakon što zahtjev prijeđe tu granicu. Koja usluga posjeduje podatke? Koji su pozivi sinkroni? Što se događa kada je ovisnost spora? Može li potrošač sigurno ponovno pokušati? To su pitanja dizajna interakcije i važnija su od broja usluga na dijagramu.
Dajte svakoj usluzi jasnu granicu vlasništva
Usluga bi trebala posjedovati poslovnu sposobnost i podatke potrebne za upravljanje njome. „Usluga korisnika”, „usluga baze podataka” ili „zajednička usluga” obično su znakovi upozorenja jer opisuju tehničku infrastrukturu, a ne odgovornost.
Na primjer, usluga narudžbi može posjedovati stanje narudžbe, dok usluga zaliha posjeduje rezervacije zaliha. Usluga narudžbi ne bi trebala izravno ažurirati tablice zaliha, čak i ako obje koriste isti pokretač baze podataka. Izravan pristup stvara skriveno povezivanje: promjena sheme u jednom području može tiho narušiti ponašanje drugdje.
Vlasništvo također olakšava zaključivanje o ponašanju pri neuspjehu. Ako zalihe nisu dostupne, usluga narudžbi može zabilježiti stanje na čekanju i zatražiti od usluge zaliha rezervaciju robe putem definiranog ugovora. Ne mora razumjeti indekse zaliha, pravila zaključavanja ni migracije pohrane.
Koristite sinkrone pozive za odluke, a ne za duge lance
Sinkroni HTTP ili RPC pozivi prikladni su kada zahtjev doista treba trenutačan odgovor. Provjere autentikacije, dohvaćanje trenutačne cijene ili potvrda da račun smije izvršiti radnju mogu odgovarati ovom modelu.
Problem počinje kada jedan dolazni zahtjev pokrene lanac ovisnih poziva:
Gateway -> Order -> Customer -> Inventory -> Payment -> Shipping
Svaka ovisnost dodaje latenciju i još jednu točku neuspjeha. Ispravna usluga ipak može uzrokovati neuspjeh zahtjeva zato što je nepovezana nizvodna usluga preopterećena. Pod opterećenjem ponovni pokušaji mogu pojačati štetu stvaranjem još više posla za ovisnost koja se muči.
Zadržite sinkroni put uskim. Tražite samo informacije potrebne za prihvaćanje, odbijanje ili točnu potvrdu zahtjeva. Posao koji može završiti kasnije premjestite u asinkronu obradu.
Dizajnirajte eksplicitne asinkrone radne tijekove
Nakon što se narudžba prihvati, usluga narudžbi može objaviti događaj poput OrderPlaced. Komponente za zalihe, plaćanje i ispunjenje narudžbi mogu reagirati neovisno. To ne znači da radni tijek postaje neodređen; znači da njegova stanja moraju biti namjerna.
- Zabilježite trajno stanje narudžbe prije objave događaja.
- Učinite potrošače idempotentnima kako bi se ista poruka mogla obraditi više puta.
- Modelirajte kompenzacijske radnje, poput oslobađanja rezervacije zaliha nakon neuspjelog plaćanja.
- Izložite smislene statuse poput
pending_payment,confirmedicancelled.
Asinkroni rad mijenja trenutačnu konačnost za otpornost. Ta je zamjena često ispravna, ali samo kada je korisničko iskustvo proizvoda može iskreno objasniti. „Narudžba je primljena; potvrda je u tijeku” bolje je od vraćanja uspjeha uz skrivanje nepouzdanog lanca rada.
Tretirajte ponovne pokušaje kao rezultat dizajna ugovora
Mreže zakazuju na nezgodne načine. Klijentu može isteći vrijeme nakon što je poslužitelj već dovršio operaciju. Posrednik poruka može ponovno isporučiti poruku. Radnik se može srušiti nakon pisanja u bazu podataka, ali prije potvrđivanja reda čekanja.
Stoga su ponovni pokušaji uobičajeno ponašanje, a ne iznimno ponašanje. Ugovor ih mora učiniti sigurnima.
Za operacije pisanja prihvatite ključ idempotentnosti i povežite ga s rezultirajućom operacijom. Ako klijent ponovno pošalje isti ključ, vratite izvorni rezultat umjesto stvaranja druge narudžbe ili dvostruke naplate kartice. Za potrošače događaja pohranite dovoljno informacija da prepoznaju kako je poruka već primijenjena.
public function createOrder(string $idempotencyKey, array $payload): Order
{
$existing = $this->orders->findByIdempotencyKey($idempotencyKey);
if ($existing !== null) {
return $existing;
}
return $this->transaction->run(
fn () => $this->orders->create($idempotencyKey, $payload)
);
}
Primjer je namjerno nepotpun: granica transakcije, ograničenje jedinstvenosti i ponašanje pri istodobnim zahtjevima moraju biti usklađeni. U praksi bi baza podataka trebala provoditi jedinstvenost ključa idempotentnosti. Provjere samo na razini aplikacije mogu se utrkivati kada dva identična zahtjeva stignu istodobno.
Držite podatke lokalnima, dijelite činjenice putem ugovora
Zajednička baza podataka može se u početku činiti učinkovitom, osobito u PHP aplikaciji koja se razvija iz modularnog monolita. No ona uslugama omogućuje zaobilaženje međusobnih pravila i pretvara migracije sheme u koordinirana izdanja.
Snažniji je obrazac omogućiti svakoj usluzi da posjeduje vlastitu perzistenciju i objavljuje činjenice koje druge usluge trebaju. Usluga izvještavanja, primjerice, može održavati model za čitanje na temelju poslovnih događaja umjesto spajanja operativnih tablica iz više domena.
To uvodi eventualnu konzistentnost, pa zahtijeva operativnu disciplinu. Potrošači bi trebali tolerirati zakašnjele i duplicirane događaje. Tereti događaja trebali bi sadržavati stabilne identifikatore i dovoljno konteksta da budu korisni. Promjene bi trebale biti aditivne gdje je to moguće: dodajte polje prije nego što ga počnete zahtijevati i podržavajte stare potrošače dok se ne povuku.
Nemojte zamijeniti događaj za udaljeni redak baze podataka. Događaj bi trebao izražavati nešto što se dogodilo u poslovnoj domeni, a ne izlagati svako interno polje tablice.
Ugradite kontrole pritiska u svaku granicu
Predvidivo skaliranje ovisi o sprječavanju da lokalni problem postane problem cijelog sustava. Pristupnici pomažu s grubim kontrolama poput autentikacije, ograničenja veličine zahtjeva i javnih ograničenja broja zahtjeva. Unutarnje usluge također trebaju vlastite zaštite.
- Postavite vremenska ograničenja za svaku mrežnu ovisnost; izostavljeno vremensko ograničenje neograničena je obveza resursa.
- Koristite ograničene redove čekanja i konkurentnost radnika koja odgovara nizvodnom kapacitetu.
- Primijenite odgodu s nasumičnim odstupanjem za neuspjehe koji se mogu ponovno pokušati, umjesto trenutačnih sinkroniziranih ponovnih pokušaja.
- Koristite prekidanje kruga ili odbacivanje opterećenja kada je ovisnost dosljedno nezdrava.
- Odvojite kritična radna opterećenja od nekritičnog rada, poput obavijesti ili analitike.
Docker čini pakiranje usluga ponovljivim, ali spremnici ne uklanjaju ograničenja resursa. Definirajte očekivanja za CPU i memoriju, osigurajte graciozno rukovanje gašenjem te osigurajte da radnici prestanu prihvaćati novi rad prije završetka procesa. Implementacija koja prekida poslove u tijeku bez semantike oporavka nije pouzdana samo zato što radi u spremnicima.
Promatrajte interakciju, a ne samo uslugu
Nadzorna ploča koja pokazuje da je svaka usluga „aktivna” nije dovoljna. Korisnici doživljavaju putove zahtjeva i poslovne ishode. Instrumentirajte identifikatore korelacije kroz zahtjeve pristupnika, pozadinske poslove i odlazne pozive. Bilježite latenciju, kategorije pogrešaka, dubinu reda čekanja, broj ponovnih pokušaja i starost nedovršenog rada.
Najkorisnija upozorenja opisuju stanje koje utječe na korisnike: potvrde plaćanja kasne, rezervacije zaliha ne uspijevaju ili red čekanja raste brže nego što ga radnici mogu isprazniti. Zapisnici ostaju važni, ali strukturirani zapisnici s identifikatorima zahtjeva i domene daleko su korisniji od izoliranih poruka o iznimkama.
Skalirajte odluke prije skaliranja infrastrukture
Pristupnik je ulazna vrata, a ne zamjena za dobre granice. Usluge se predvidivo skaliraju kada posjeduju svoje podatke, minimiziraju sinkrone ovisnosti, pretpostavljaju da će se ponovni pokušaji dogoditi i čine asinkrona stanja vidljivima.
Rezultat nije savršeno sustav bez neuspjeha. To je sustav čiji neuspjesi ostaju ograničeni, čije se ponašanje može objasniti i čiji timovi mogu promijeniti jedan dio bez pretvaranja svake implementacije u pregovaranje. To je vrsta skaliranja za koju vrijedi dizajnirati.