Skalerbarhet
om mer last krever mer enn flere maskiner
Hva det er
Skalerbarhet sier hvor mye mer et system klarer når du gir det flere ressurser. Et system skalerer godt når dobbelt så mange maskiner bærer omtrent dobbelt så mye last med de samme svartidene. Det skalerer dårlig når den nye maskinvaren for det meste står og venter på noe den ikke kan hjelpe med. Ordet beskriver altså et forhold mellom last og kostnad, ikke en egenskap et system enten har eller mangler.
Et system kan vokse i to retninger. Å skalere opp, eller vertikal skalering, betyr en større maskin: flere kjerner, mer minne, raskere disker. Programvaren trenger ingen endring, men veksten stopper ved den største maskinen det lønner seg å kjøpe. Å skalere ut, eller horisontal skalering, betyr flere maskiner som deler på arbeidet. Der finnes ikke det samme taket, men bare arbeid som lar seg dele opp, får noe igjen for det.
HTTP ble formet med tanke på å skalere ut. RFC 9110 påpeker at mange implementasjoner er avhengige av at HTTP er tilstandsløs, for å kunne gjenbruke tilkoblinger gjennom en proxy eller fordele forespørsler dynamisk over flere servere. Et webnivå som ikke husker noe mellom forespørslene, kan vokse ved å legge til kopier av seg selv bak en lastbalanserer, og hvilken som helst kopi kan svare på hvilken som helst forespørsel.
Last har mer enn én dimensjon, og et system kan skalere langs én og stå fast langs en annen: forespørsler per sekund, mengden lagrede data, antall leietakere og antall skrivere som slåss om den samme posten. Den siste er den vanskeligste, fordi én rad bare kan endres av én transaksjon om gangen, uansett hvor mange servere som står rundt den.
Regnestykket bak dette heter Amdahls lov. Når en del av arbeidet må kjøres i rekkefølge, gjør parallell maskinvare bare resten raskere, og den sekvensielle delen setter grensen: er 5 % av arbeidet serielt, kan hele jobben aldri gå mer enn 20 ganger raskere, selv på tusen maskiner. I en database er den serielle delen ofte en lås. I en applikasjon er den ofte én delt teller, én kø eller ett felles lager for sesjoner.
Hvorfor det er viktig
Skalerbarheten avgjør hva vekst koster. Skalerer systemet godt, blir en dobling av trafikken en ny linje på fakturaen. Skalerer det dårlig, vokser regningen raskere enn lasten, eller ny maskinvare slutter å hjelpe i det hele tatt, og løsningen blir en omlegging av arkitekturen som må gjøres mens trafikken strømmer inn.
Mer maskinvare hjelper dessuten bare mot én slags treghet: ekte metning, der prosessorer, minne eller disker er opptatt med nyttig arbeid. En spørring som leser hele tabellen, er like treg på en større server. En kø av transaksjoner som venter på én rad, blir heller ikke kortere av en større maskin, og flere applikasjonsservere legger bare flere skrivere til i køen. De fleste systemer som ser ut til å trenge flere maskiner, trenger først å bli lest.
I PostgreSQL begynner lesingen i pg_stat_statements, en utvidelse som
registrerer hver normaliserte setning med antall kall og kjøretid. Sortert etter
total tid i stedet for gjennomsnitt viser den hvor serveren faktisk bruker
kreftene sine: en spørring som tar 2 ms og kjøres en million ganger i timen,
koster 2 000 sekunder av den timen, mens en rapport på to sekunder som kjøres to
ganger, koster fire.
SELECT calls, round(total_exec_time) AS total_ms,
left(query, 60) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Hvert steg mot flere maskiner har sine egne driftskostnader, og derfor betyr rekkefølgen noe. En strammere setning eller en bedre indeks koster ingenting å holde ved like. En lesereplika er konfigurasjon. Partisjonering endrer én tabell. Sharding, å dele dataene mellom separate databaseservere, endrer alt rundt dem: hver databasemigrering, sikkerhetskopi, gjenoppretting og rapport må kjøres én gang per shard, en join på tvers av shards blir applikasjonskode, og en spørring som ikke kjenner shardnøkkelen sin, må spørre alle.
Slik ser det ut i praksis
En PostgreSQL-database bak en mellomstor applikasjon holdt brukerkontoer,
leietakere, geografiske data og daglige målinger av strømforbruk. Trafikken var
lav, men databasen ble tregere etter hvert som datamengdene vokste. Oppdraget
var å gjøre den
10 ganger raskere uten å bygge om arkitekturen,
og plass til framtidig datavekst var en del av målet. Arbeidet besto av
GiST-indekser for de romlige dataene, sammensatte indekser på leietaker og
datointervall, tidsbasert partisjonering av strømtabellen, hash-partisjonering
av de geografiske dataene, omskrevne spørringer sjekket med EXPLAIN ANALYZE,
og tilkoblingspooling. Svartidene ble 10 ganger bedre, databasens ressursbruk
falt med 40 %, og systemet tok imot voksende datamengder uten å bli tregere,
med mindre behov for maskinvareoppgraderinger.
Hos et mellomstort teknologiselskap med ti databaseservere på PostgreSQL og SQL Server ble infrastrukturen skalert for 40 % økning i brukertrafikken uten forringelse av tjenesten. Samtidig ble svartidene for spørringer 30 % bedre, gjennom justering av konfigurasjonen og optimalisering av indekser.
Rekkefølgen arbeidet følger, står i et notat om å optimalisere før du sharder: se, stram inn, ta lesingene bort fra primærnoden, partisjoner tabellene rundt det hete punktet, og shard først når én maskin er full av godt arbeid. Notatet nevner tre tilfeller der sharding er rimelig: primærnoden er mettet av setninger som allerede er slanke, dataene har vokst fra vedlikeholdet sitt, eller isolasjon er selve kravet.
Hvor det går galt
Flere webservere foran én database. Hver applikasjonsserver åpner sine egne tilkoblinger, og når mange ganger flere økter er aktive enn databasen har kjerner, går serverens tid med til å bytte mellom dem. Tilkoblingspooling løser det før ny maskinvare gjør det, og enda en webserver gjør det verre.
Én varm rad. Når hver forespørsel skriver til den samme posten, f.eks. en lagerteller eller en saldo, styres gjennomstrømningen av hvor lenge låsen på raden holdes: ett sekund delt på låsetiden gir omtrent taket, på et hvilket som helst antall maskiner. Alt som er tregt inne i låsen, senker det taket. Hold transaksjonen kort, legg eksterne kall utenfor den, og når én vare vokser fra maskinen sin, del lageret opp i mindre biter som hver har nøyaktig én eier.
Tilstand som bor på én server. Sesjoner i minnet til én prosess, opplastinger skrevet til lokal disk eller en planlagt jobb som tar for gitt at den er den eneste kopien, hindrer alle sammen at en ny instans blir likeverdig med den første. Legg den tilstanden i et delt lager, slik at hvilken som helst instans kan svare på hvilken som helst forespørsel.
Replikaer behandlet som kopier av primærnoden. En strømmende replika henger litt etter, så den som nettopp har skrevet noe og så leser fra replikaen, kan få se den gamle verdien. Les ferske skrivinger fra primærnoden. En lang rapport på en standby kan også bli avbrutt når den holder igjen endringene som spilles av. PostgreSQL dokumenterer dette som spørringskonflikter, sammen med innstillingene som veier replikaforsinkelse opp mot dem.
Partisjoner som spørringene aldri nevner. Partisjonering deler én tabell etter en nøkkel, f.eks. måned eller leietaker, slik at en spørring som oppgir nøkkelen, bare leser partisjonene den trenger. En spørring som utelater nøkkelen, leser alle partisjonene og får ingenting igjen for oppdelingen. Partisjoner etter det spørringene allerede filtrerer på, og regn med at det lønner seg først for store tabeller: tommelfingerregelen i PostgreSQL-manualen er en tabell som er større enn databaseserverens fysiske minne.
Les mer
- RFC 9110: HTTP Semantics, section 3.3 — tilkoblinger, klienter og servere, og den tilstandsløse utformingen som lar implementasjoner fordele forespørsler over flere servere.
- PostgreSQL: Table Partitioning — hva partisjonering er, når den lønner seg, og hvordan partition pruning hopper over partisjonene en spørring ikke kan trenge.
- PostgreSQL: pg_stat_statements — utvidelsen som tar vare på antall kall og kjøretid for hver normaliserte setning.
- PostgreSQL: Hot Standby — skrivebeskyttede spørringer på en standby, og konfliktene som kan avbryte dem.
- RFC 9111: HTTP Caching — hvordan HTTP-cacher lagrer og gjenbruker svar, slik at gjentatte forespørsler slipper å gå helt til opprinnelsesserveren.