Dizajn baze podataka: projektiranje predvidljivosti pod opterećenjem
Većina incidenata s bazom podataka ne počinje dramatičnim kvarom. Počinju upitom koji izgleda bezazleno, praktičnim tipom stupca ili krajnjom točkom koja savršeno radi s malim skupom podataka. Zatim promet raste, retci se gomilaju, stižu istodobni zahtjevi, a baza podataka postaje mjesto na kojem se mali prečaci u dizajnu pretvaraju u nepredvidivu latenciju.
Dobar dizajn baze podataka ne znači težiti idealnom dijagramu sheme. Riječ je o projektiranju predvidljivog ponašanja pod stvarnim opterećenjem: poznatim putanjama upita, jasnom vlasništvu nad podacima, ograničenom radu po zahtjevu i načinima otkaza koji su razumljivi kad je sustav zauzet.
Dizajnirajte prema obrascima pristupa, a ne samo prema entitetima
Odnosi među entitetima važni su, ali performanse u produkciji oblikuje način na koji aplikacija čita i zapisuje podatke. Shema može izgledati savršeno normalizirano, a ipak svaki važan zahtjev prisiljavati na skupa spajanja, široka skeniranja ili ponovljena povratna putovanja.
Započnite imenovanjem upita koje vaš sustav mora izvršavati. Za API narudžbi to može uključivati:
- Dohvaćanje jedne narudžbe prema njezinu javnom identifikatoru.
- Prikaz nedavnih narudžbi kupca uz paginaciju.
- Pronalaženje narudžbi koje čekaju obradu plaćanja.
- Atomsko bilježenje narudžbe i njezinih stavki.
Svaki upit podrazumijeva ograničenja za ključeve, indekse, redoslijed i paginaciju. Ako zahtjev često filtrira prema customer_id i sortira prema created_at, indeks usklađen s tom putanjom obično je korisniji od zasebnih indeksa odabranih samo zato što se svaki stupac pojavljuje u filtru.
SELECT id, status, total_amount, created_at
FROM orders
WHERE customer_id = :customer_id
AND created_at < :cursor
ORDER BY created_at DESC
LIMIT 50;
Podržavajući indeks trebao bi odražavati filtar i redoslijed koje upit koristi. Točna sintaksa indeksa i ponašanje optimizatora razlikuju se ovisno o pogonu baze podataka, stoga provjerite plan na pogonu koji koristite umjesto da bilo koje pravilo smatrate univerzalnim.
Pažljivo birajte identifikatore
Primarni ključevi su infrastruktura. Utječu na spajanja, veličinu indeksa, lokalnost zapisa, URL-ove, migracije i granice integracije. Numerički interni ključ kompaktan je i učinkovit za mnoga relacijska opterećenja. Generirani javni identifikator može spriječiti jednostavno nabrajanje u vanjskim API-jima. Te se potrebe ne moraju natjecati: često je razumno zadržati interni primarni ključ i zasebno indeksirani javni ID.
Najvažnija je dosljednost. Ako jedna usluga ID tretira kao cijeli broj, a druga ga pohranjuje kao niz, greške se tiho uvlače u validaciju, serijalizaciju i spajanja. Odlučite gdje se identifikatori stvaraju, koji prikaz prelazi granice API-ja i smiju li se pozivatelji oslanjati na njihov redoslijed.
Normalizacija je alat, a ne ritual
Normalizacija štiti ispravnost tako što svakoj činjenici daje jedno autoritativno mjesto. Adresa e-pošte kupca ne bi se trebala kopirati u svaku narudžbu samo zato što ju je praktično prikazati. Ako se promijeni, duplicirani podaci postaju problem popravka.
Ali neko je dupliciranje namjerno. Narudžba može sačuvati snimku adrese za dostavu jer je povijesna točnost važnija od odražavanja trenutačnog profila kupca. Tablica za izvještavanje može pohranjivati unaprijed izračunate vrijednosti jer bi njihovo ponovno izračunavanje tijekom svakog zahtjeva bilo preskupo.
Korisno pitanje nije „je li ovo normalizirano?” nego „koju invarijantu ovaj prikaz čuva i tko je održava?” Denormalizacija bez strategije ažuriranja odgođena je korupcija podataka. Denormalizacija s jasnim izvorom istine, transakcijskom putanjom ažuriranja i planom usklađivanja može biti pragmatična odluka za performanse.
Zapise učinite izričito atomskima
Mnoge greške u pozadinskim sustavima jesu greške djelomičnog zapisa. Aplikacija umeće narudžbu, ažurira zalihe i stvara zapis o plaćanju; treći korak ne uspije, a prva su dva ostala potvrđena. Sretna putanja može mjesecima izgledati ispravno, dok putanja oporavka ostaje nedefinirana.
Koristite transakcije baze podataka za promjene koje moraju zajedno uspjeti ili ne uspjeti. Neka budu kratke: validirajte ulaze prije otvaranja transakcije, izbjegavajte mrežne pozive unutar nje i potvrdite je čim se uspostavi invarijanta.
$pdo->beginTransaction();
try {
$orderId = createOrder($pdo, $customerId, $items);
reserveInventory($pdo, $items);
recordOrderEvent($pdo, $orderId, 'created');
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
To ne čini svaku distribuiranu operaciju atomskom. Primjerice, slanje e-pošte ili pozivanje pružatelja usluge plaćanja ne može se sigurno uključiti u lokalnu transakciju baze podataka. Umjesto toga, zabilježite promjenu stanja i događaj za kasniju isporuku, a zatim učinite potrošača idempotentnim. Ponovni pokušaji sigurniji su kada obrada istog događaja dvaput ne stvara dvije naplate ili dvije pošiljke.
Indeksi trebaju proračun
Indeks ubrzava neka čitanja uz cijenu dodatne pohrane, pritiska na memoriju i rada pri zapisivanju. Svako umetanje, ažuriranje ili brisanje možda mora održavati svaki relevantni indeks. Stoga odgovor na spor upit nije „indeksirajte svaki stupac”. Treba izmjeriti upit, pregledati njegov plan izvršavanja i dodati najmanji indeks koji podržava smisleni obrazac pristupa.
Pazite na uobičajene zamke:
- Filtriranje prema funkciji primijenjenoj na stupac, što može spriječiti učinkovito korištenje indeksa.
- Korištenje širokih upita
SELECT *kada krajnja točka treba samo nekoliko polja. - Offset paginacija za duboke skupove rezultata, pri kojoj baza podataka mora preskakati sve više rada.
- Indeksi koji se nikada ne koriste, ali i dalje usporavaju zapise.
Paginacija temeljena na pokazivaču često bolje odgovara velikim, uređenim popisima. Sljedeću stranicu čini nastavkom od poznatog retka, a ne zahtjevom za odbacivanje sve većeg prefiksa. Pokazivač mora koristiti stabilan redoslijed, najčešće vremensku oznaku uz jedinstveni razrješivač izjednačenja.
Zaštitite bazu podataka od aplikacije
Baza podataka dijeljeni je kapacitet. Jedna krajnja točka s neograničenim upitom može naštetiti nepovezanim zahtjevima. Postavite ograničenja na granici aplikacije: ograničite veličine stranica, validirajte polja za sortiranje prema popisu dopuštenih vrijednosti, postavite odgovarajuća vremenska ograničenja i izbjegavajte prihvaćanje proizvoljnih izraza filtra od klijenata.
Važno je i upravljanje vezama. Otvaranje previše istodobnih veza može preopteretiti bazu podataka prije nego što CPU ili pohrana postanu vidljivo usko grlo. Konfigurirajte radnike aplikacije i spremišta veza imajući na umu ograničenja baze podataka. U kontejneriziranim implementacijama imajte na umu da skaliranje replika aplikacije umnožava potencijalne veze, osim ako se istodobno ne projektira spremanje veza.
Također razlikujte sporu bazu podataka od sporog zahtjeva. Bilježite korisnu telemetriju o trajanju upita, klasi pogreške, ponovnim pokušajima transakcije i vremenu čekanja na vezu. Prema zadanim postavkama izbjegavajte bilježenje osjetljivih parametara. Kada se pojavi regresija, plan izvršavanja i reprezentativan upit obično su vrijedniji od nagađanja.
Migracije su produkcijski kod
Migracija sheme nije samo datoteka koja se uspješno pokreće na prijenosnom računalu. U zauzetom sustavu dodavanje indeksa, promjena tipa ili popunjavanje novog stupca može zaključati tablice, potrošiti I/O ili trajati dovoljno dugo da poremeti uobičajeni rad. Planirajte migracije kao operacije spremne za implementaciju, imajući na umu vraćanje unatrag i nadziranost.
Dajte prednost kompatibilnim, postupnim promjenama. Dodajte nullable stupac prije nego što ga učinite obveznim. Implementirajte kod aplikacije koji može obraditi i stari i novi oblik. Popunjavajte podatke u ograničenim serijama. Dodajte ograničenja tek nakon što ih postojeći podaci zadovoljavaju. Uklonite zastarjele stupce tek kada svi implementirani potrošači više ne ovise o njima.
Predvidljivost je stvarna značajka
Najjači dizajni baza podataka rijetko djeluju domišljato. Uobičajene operacije čine dosadnima: zahtjev ima poznat oblik upita, zapis ima jasnu granicu transakcije, indeks ima razlog postojanja, a migracija siguran put kroz produkciju.
Ta se disciplina isplati kada se opterećenje poveća ili se zahtjevi promijene. Baza podataka prestaje biti tajanstveno usko grlo i postaje pouzdan dio sustava: dio čije troškove, ograničenja i ponašanje tim može objasniti prije nego što ih sljedeći incident mora objasniti umjesto njih.