# Skalerbarhet

## 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.

```sql
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](/nb/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/),
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](/nb/portfolio/designed-deployed-and-maintained-10-postgresql-and-ms-13/).
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](/nb/notes/optimise-before-you-shard/):
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](https://www.rfc-editor.org/rfc/rfc9110#section-3.3)
  — tilkoblinger, klienter og servere, og den tilstandsløse utformingen som lar
  implementasjoner fordele forespørsler over flere servere.
- [PostgreSQL: Table Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)
  — 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](https://www.postgresql.org/docs/current/pgstatstatements.html)
  — utvidelsen som tar vare på antall kall og kjøretid for hver normaliserte
  setning.
- [PostgreSQL: Hot Standby](https://www.postgresql.org/docs/current/hot-standby.html)
  — skrivebeskyttede spørringer på en standby, og konfliktene som kan avbryte
  dem.
- [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
  — hvordan HTTP-cacher lagrer og gjenbruker svar, slik at gjentatte
  forespørsler slipper å gå helt til opprinnelsesserveren.

<https://engineer.company/nb/glossary/scalability/>
