# Skalerbarhed

## Hvad det er

Skalerbarhed er den måde, et systems kapacitet vokser på, når der tilføjes
ressourcer. Et system skalerer godt, når dobbelt så mange maskiner bærer omtrent
dobbelt så meget belastning med de samme svartider, og dårligt, når den ekstra
hardware mest venter på noget, den ikke kan hjælpe med. Ordet beskriver et
forhold mellem belastning og omkostning, ikke en egenskab, et system enten har
eller mangler.

Der er to retninger at vokse i. At skalere op, vertikal skalering, betyder en
større maskine: flere kerner, mere hukommelse, hurtigere diske. Det kræver
ingen ændring af softwaren og stopper ved den største maskine, det kan betale
sig at købe. At skalere ud, horisontal skalering, betyder flere maskiner, der
deler arbejdet. Her er der ikke samme loft, men kun arbejde, der kan deles op,
får gavn af det.

HTTP er formet med udskalering for øje. RFC 9110 bemærker, at mange
implementeringer bygger på HTTP's tilstandsløse design for at genbruge
forbindelser gennem proxyer eller fordele forespørgsler dynamisk på flere
servere. Et weblag, der ikke gemmer noget mellem forespørgslerne, kan vokse ved
at sætte flere kopier af sig selv bag en load balancer, og enhver kopi kan
besvare enhver forespørgsel.

Belastning har mere end én dimension, og et system kan skalere i én og gå i stå
i en anden: forespørgsler i sekundet, mængden af lagrede data, antallet af
lejere og antallet af skrivere, der kæmper om den samme post. Den sidste er den
sværeste, fordi en enkelt række kun kan ændres af én transaktion ad gangen,
uanset hvor mange servere der står omkring den.

Regnestykket bag det er Amdahls lov. Når en del af arbejdet skal køre i
rækkefølge, gør parallel hardware kun resten hurtigere, så den sekventielle del
sætter grænsen: er 5 % af arbejdet serielt, kan hele jobbet aldrig køre mere end
20 gange hurtigere, heller ikke på tusind maskiner. I en database er den
serielle del ofte en lås. I en applikation er den ofte én delt tæller, én kø
eller ét sessionslager.

## Hvorfor det betyder noget

Skalerbarheden afgør, hvad vækst koster. Når et system skalerer godt, er en
fordobling af trafikken en linje på en faktura. Når det skalerer dårligt, vokser
regningen hurtigere end belastningen, eller mere hardware holder helt op med at
hjælpe, og løsningen bliver et redesign, der skal udføres, mens trafikken
strømmer ind.

Hardware løser desuden kun én slags langsomhed: reel mætning, hvor processorer,
hukommelse eller diske er optaget af nyttigt arbejde. En forespørgsel, der
scanner en hel tabel, er lige så langsom på en større server. En kø af
transaktioner, der venter på én række, bliver heller ikke kortere af en større
maskine, og flere applikationsservere føjer blot flere skrivere til køen. De
fleste systemer, der ser ud til at have brug for flere maskiner, skal læses
først.

I PostgreSQL er stedet at begynde at læse `pg_stat_statements`, en udvidelse,
der registrerer hver normaliseret sætning med antal kald og udførelsestid.
Sorteret efter samlet tid frem for gennemsnit viser den, hvor serveren faktisk
bruger sig selv: en forespørgsel, der tager 2 ms og kører en million gange i
timen, koster 2.000 sekunder af den time, mens en rapport på to sekunder, der
kører to gange, 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 skridt mod flere maskiner har sine egne driftsomkostninger, og derfor
betyder rækkefølgen noget. En strammere sætning eller et bedre indeks koster
intet at holde i drift. En læsereplika er konfiguration. Partitionering ændrer
én tabel. Sharding, hvor data deles ud på separate databaseservere, ændrer alt
omkring sig: hver migrering, backup, gendannelse og rapport kører én gang pr.
shard, en join på tværs af shards bliver applikationskode, og en forespørgsel,
der ikke kender sin shardnøgle, må spørge dem alle.

## Sådan ser det ud i praksis

En PostgreSQL-database bag en mellemstor applikation rummede brugerkonti,
lejere, geografiske data og dagligt elforbrug. Den kørte under lav belastning,
men blev langsommere, efterhånden som datamængderne voksede. Opgaven var at
gøre den
[ti gange hurtigere uden at ændre arkitekturen](/da/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/),
med plads til fremtidig datavækst som en del af målet. Arbejdet var GiST-indeks
til de geografiske data, sammensatte indeks til filtre på flere kolonner,
eltabellen partitioneret efter tid, hash-partitionering af de geografiske data,
omskrevne forespørgsler tjekket med `EXPLAIN ANALYZE` og connection pooling.
Svartiderne blev ti gange bedre, databasens ressourceforbrug faldt med 40 %, og
systemet kunne tage imod voksende datamængder uden ydeevnetab og med mindre
behov for hardwareopgraderinger.

I en mellemstor virksomhed med ti PostgreSQL- og SQL Server-databaseservere
blev infrastrukturen
[skaleret til 40 % mere brugertrafik uden servicetab](/da/portfolio/designed-deployed-and-maintained-10-postgresql-and-ms-13/).
Samtidig blev svartiderne for forespørgsler 30 % bedre gennem
konfigurationstuning og indeksoptimering.

Rækkefølgen, arbejdet følger, er beskrevet i
[en note om at optimere før sharding](/da/notes/optimise-before-you-shard/):
kig, stram op, tag læsningerne væk fra den primære, partitionér tabellerne
omkring det varme punkt, og shard først, når én maskine er fuld af godt
arbejde. Noten nævner tre tilfælde, hvor sharding er rimeligt: den primære er
mættet af sætninger, der allerede er slanke, data er vokset fra deres
vedligeholdelse, eller isolation er selve kravet.

## Hvor det går galt

**Flere webservere foran én database.** Hver applikationsserver åbner sine egne
forbindelser, og når mange gange flere sessioner er aktive, end databasen har
kerner, bruger serveren sin tid på at skifte mellem dem. En forbindelsespulje
løser det, før nogen hardware gør, og endnu en webserver gør det værre.

**Én varm række.** Når hver forespørgsel skriver til den samme post, for
eksempel en lagertæller eller en saldo, er gennemløbet begrænset til omtrent de
skrivninger, én lås kan bære, divideret med den tid, låsen holdes, uanset
antallet af maskiner. Alt langsomt inde i låsen sænker loftet. Hold
transaktionen kort, hold eksterne kald uden for den, og når én vare vokser fra
sin maskine, så del dens lager op i mindre stykker, der hver har præcis én ejer.

**Tilstand, der bor på én server.** Sessioner i én proces' hukommelse, uploads
skrevet til dens lokale disk eller et planlagt job, der går ud fra, at det er
den eneste kopi, forhindrer alle, at en anden instans kan være ligeværdig. Læg
den tilstand i et fælles lager, så enhver instans kan besvare enhver
forespørgsel.

**Replikaer behandlet som kopier af den primære.** En streaming-replika ligger
lidt bagefter, så en bruger, der lige har skrevet noget og derefter læser fra
replikaen, ser det måske ikke; læs nylige skrivninger fra den primære. En lang
rapport på en standby kan også blive annulleret, når den holder de ændringer
tilbage, der afspilles. PostgreSQL dokumenterer det som
forespørgselskonflikter, sammen med de indstillinger, der afvejer
replikaforsinkelse mod dem.

**Partitioner, forespørgslerne aldrig nævner.** Partitionering deler én tabel
efter en nøgle, for eksempel en måned eller en lejer, så en forespørgsel, der
nævner nøglen, kun læser de partitioner, den har brug for. En forespørgsel uden
nøglen læser hver eneste partition og vinder intet ved opdelingen. Partitionér
efter det, forespørgslerne allerede filtrerer på, og forvent kun en gevinst for
store tabeller: PostgreSQL-manualens tommelfingerregel er en tabel, der er
større end databaseserverens fysiske hukommelse.

## Læs mere

- [RFC 9110: HTTP Semantics, section 3.3](https://www.rfc-editor.org/rfc/rfc9110#section-3.3)
  — forbindelser, klienter og servere, og det tilstandsløse design, der lader
  implementeringer fordele forespørgsler på flere servere.
- [PostgreSQL: Table Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)
  — hvad partitionering er, hvornår det betaler sig, og hvordan partition
  pruning springer de partitioner over, en forespørgsel ikke kan få brug for.
- [PostgreSQL: pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html)
  — udvidelsen, der holder styr på antal kald og tider for hver normaliseret
  sætning.
- [PostgreSQL: Hot Standby](https://www.postgresql.org/docs/current/hot-standby.html)
  — skrivebeskyttede forespørgsler på en standby og de konflikter, der kan
  annullere dem.
- [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
  — hvordan HTTP-caches gemmer og genbruger svar, så gentagne forespørgsler
  ikke behøver at nå originserveren.

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