# Kö

## Vad det är

En kö är ett mellanlager mellan två program: den ena sidan lägger in meddelanden eller jobb, den
andra tar ut dem när den är redo, oftast det äldsta först. De flesta system kör flera köer, en per
sorts arbete. Programmet som lägger till arbete är producenten, programmet som tar det är
konsumenten, och det som lagrar meddelandena däremellan är brokern, som kan vara en egen server
eller en tabell i en databas som systemet redan har.

Upplägget gör att de två sidorna slutar vänta på varandra. En webbförfrågan som behöver skicka ett
mejl lägger in ett meddelande och svarar användaren direkt. En worker plockar upp meddelandet en
stund senare, och om hundra kommer på en gång betar den av dem i sin egen takt. En topp blir en
eftersläpning, och ett beroende blir en fördröjning.

Två idéer beskriver vad en kö lovar om leveransen. Den första är kvittering: en konsument talar om
för brokern när den är klar med ett meddelande, och först då tas meddelandet bort. I modellen för
AMQP 0-9-1, som RabbitMQ implementerar, går ett meddelande vars konsument dör innan den hinner
kvittera vidare till en annan konsument. Den andra är beständighet: en beständig kö med beständiga
meddelanden överlever att brokern startas om, medan en kö som bara hålls i minnet går förlorad.

Tre närliggande begrepp är värda att hålla isär. En meddelandekö bär data mellan tjänster. En
jobbkö bär arbetsenheter åt workers i bakgrunden, var och en med sina argument, ett tillstånd och
ett antal försök. En logg, modellen bakom Apache Kafka, ser ut som en kö utifrån men behåller
händelserna efter att de har lästs, under en lagringstid som väljs per ämne, så att flera läsare
kan spela upp samma historik igen. Att läsa från en kö tar bort meddelandet, medan läsning från en
logg bara flyttar läsarens position framåt.

En kö behöver inte alls vara en separat produkt. PostgreSQL kan spela rollen med en tabell med jobb
och en databasfråga som gör anspråk på nästa rad och kliver förbi rader som en annan worker redan
har låst. Handboken anger köliknande tabeller som själva skälet till att satsdelen `SKIP LOCKED`
finns:

```sql
WITH next AS (
  SELECT id
    FROM jobs
   WHERE state = 'available'
   ORDER BY id
   LIMIT 1
     FOR UPDATE SKIP LOCKED
)
UPDATE jobs
   SET state = 'running',
       attempt = attempt + 1
  FROM next
 WHERE jobs.id = next.id
RETURNING jobs.id, jobs.args;
```

## Varför det spelar roll

En kö avgör vad en användare väntar på. Arbete som läggs i kön sker efter att svaret har gått iväg,
så en sida som utlöser ett mejl, en PDF, en omindexering för sökning och en omräkning kan svara på
den tid det tar att skriva fyra rader, medan de fyra långsamma uppgifterna blir klara bakom den. För
en köpare är det skillnaden mellan en kassa som svarar på några hundra millisekunder och en som
svarar när e-postservern råkar vara redo.

Kön avgör också vad som överlever ett fel. Arbete som startas i en bakgrundstråd lever i den
processens minne, och en driftsättning som startar om processen tar med sig arbetet utan att lämna
något spår av att det någonsin skulle utföras. Arbete i en beständig kö är i stället en post: det
går att försöka igen, inspektera och räkna.

Den skiljer dessutom på två hastigheter. Producenterna rör sig i takt med den inkommande trafiken,
konsumenterna i takt med den långsammaste tjänst de anropar. En kö mellan dem tar upp skillnaden,
och dess djup blir ett av de mest sanningsenliga tal som ett system kan rapportera. En kö som har
vuxit i en timme säger att konsumenterna tappar mark, vad grafen över processorn än antyder.

Kostnaderna kommer tillsammans med fördelarna. Arbetet ligger nu utom synhåll mellan att det skickas
och att det blir gjort. Leveransen är nästan alltid minst en gång, så en konsument måste tåla att se
ett meddelande två gånger. Ordningen är svagare än den ser ut. En dedikerad broker är dessutom
ännu en server att driva, säkra, bevaka och uppgradera.

## Så ser det ut i praktiken

Det som gör en kö värd sin plats skiljer sig från fall till fall: här är det att jobb överlever en
omstart, att händelser behåller sin ordning och att eftersläpningen har ett tak.

På en plattform flyttades det långsamma arbetet bort från anropsvägen: att skicka e-post, bygga om
ett sökindex, generera dokument och räkna om placeringar. Arbete som lämnas till en goroutine
försvinner när processen startas om mitt under en driftsättning, så
[arbetet flyttades till en jobbkö i River som ligger i PostgreSQL](/sv/portfolio/moved-slow-work-onto-a-river-job-queue-97/).
Databasen var redan den auktoritativa källan, vilket gör att ett jobb läggs i kön i samma
transaktion som den data det gäller. Plattformen fick nya försök och en historik som någon kan
öppna och läsa. Den uttalade gränsen är att en kö inne i databasen inte skalar i all evighet.

I en app för macOS som byggdes ovanpå ett bibliotek i C fanns kön där för att hålla ordning.
Bibliotekets blockerande händelseloop skickar händelser i en ordning som spelar roll, eftersom en
redigering måste komma efter det meddelande den redigerar. Att starta en task per händelse låter
körmiljön schemalägga dem i godtycklig ordning, så att en redigering kan gå om meddelandet den
gäller. Designen som levererades har
[en producenttråd, en konsument och en kö mellan dem](/sv/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/),
följd av ett enda hopp över till main actor. Ordningen håller eftersom det finns exakt en väg
igenom. Priset är parallellismen: varje händelse passerar genom den enda konsumenten.

Det tredje fallet visar en kö som gör skada. En applikation växte med ungefär 41 MB minne i
sekunden, upp till 111 GB komprimerade sidor fast maskinen bara hade 36 GB. En av orsakerna var
en händelseström som konsumerades utan någon gräns: händelserna kom snabbare än gränssnittet
hann tillämpa dem, och eftersläpningen behölls.
[Varje obegränsad väg fick en gräns](/sv/portfolio/stopped-an-application-filling-memory-at-41mb-a-second-155/),
med en uttrycklig policy för en full buffert, eftersom en begränsad kö bredvid tre obegränsade,
som sidan uttrycker det, inte är någon lösning. Ett regressionstest verifierar nu att taket för
minnet håller.

## Var det går fel

Problem med en kö märks sent och någon annanstans, eftersom arbetet ligger utom synhåll per
konstruktion.

**Eftersläpningen växer tills minnet eller disken tar slut.** En kö utan gräns tar emot arbete
snabbare än det blir gjort och behåller skillnaden. Ge varje kö en största längd och bestäm i
förväg vad som händer vid den längden: bromsa producenten, vägra nytt arbete eller släpp det
äldsta. Bevaka sedan två tal, djupet och åldern på det äldsta meddelandet, eftersom en kort kö med
mycket gamla meddelanden är lika illa som en lång.

**Samma meddelande behandlas två gånger.** En konsument gör klart arbetet och dör innan den
kvitterar, och brokern levererar, helt korrekt, meddelandet igen. Dokumentationen till RabbitMQ
säger åt konsumenter att räkna med nya leveranser och att skrivas med idempotens i åtanke. Ge varje
meddelande en identifierare och registrera de identifierare som redan har hanterats, i samma
transaktion som själva arbetet. Skrivaren i
[anteckningen om en blixtrea på en enda het rad](/sv/notes/selling-limited-stock-without-overselling/)
är byggd kring precis det antagandet.

**Meddelanden kommer i en annan ordning än de skickades i.** Parallella konsumenter, förhämtning
och nya försök som lägger ett misslyckat meddelande bakom nyare meddelanden kastar alla om arbetet.
När ordningen spelar roll, skicka allt som rör ett konto eller en order genom samma partition eller
samma konsument. Kafka gör det per nyckel, till priset att en upptagen nyckel bara rör sig lika
snabbt som en enda konsument.

**Ett dåligt meddelande blockerar eller går i cirklar för alltid.** Ett meddelande vars innehåll
kraschar konsumenten varje gång läggs tillbaka i kön, levereras igen och kraschar den igen. Sätt ett
tak för antalet försök och flytta ett meddelande som har förbrukat dem till en separat plats där
en människa kan titta på det. RabbitMQ kallar det dead-lettering och tillämpar det på meddelanden
som har avvisats, som har gått ut och som skulle få kön att överskrida en gräns för längden.

**Arbete försvinner vid en omstart.** En kanal, en lista i minnet eller en bakgrundstråd är en kö
som inte behåller något över en omstart. I AMQP räcker inte heller en beständig kö ensam:
meddelandena måste också publiceras som beständiga. Bestäm för varje sorts arbete om det är
acceptabelt att förlora det.

**Databasen och kön är oense.** En tjänst skriver en rad, publicerar sedan ett meddelande och
kraschar mellan de två, och nu finns det ena utan det andra. Håll antingen kön i samma databas, så
att ett jobb blir synligt för workers först när dess transaktion har committats, vilket är så River
fungerar, eller skriv meddelandet till en utkorgstabell i den transaktionen och publicera det
därifrån.

## Läs mer

- [AMQP 0-9-1 Model Explained](https://www.rabbitmq.com/tutorials/amqp-concepts)
  — exchanges, köer, bindningar och kvitteringar, så som RabbitMQ beskriver dem.
- [Consumer Acknowledgements and Publisher Confirms](https://www.rabbitmq.com/docs/confirms)
  — när en broker betraktar ett meddelande som levererat och varför det levereras igen.
- [Dead Letter Exchanges](https://www.rabbitmq.com/docs/dlx) — villkoren som flyttar undan ett
  meddelande och vart det tar vägen.
- [The Locking Clause of SELECT](https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE)
  — `FOR UPDATE` och `SKIP LOCKED` och de köliknande tabeller de passar för.
- [River: Getting started](https://riverqueue.com/docs) — en jobbkö som lagrar sina jobb i
  applikationens egen databas.
- [Apache Kafka: Introduction](https://kafka.apache.org/41/getting-started/introduction/)
  — ämnen, partitioner och lagringstid i loggmodellen.

<https://engineer.company/sv/glossary/queue/>
