Containerkörmiljö
programmet som faktiskt startar en container
Vad det är
En containerkörmiljö är programvaran som står mellan en containeravbildning på disken och en isolerad process i kärnan. På engelska heter den container runtime, och i svenskt tal hörs ofta bara runtime eller körmiljö. Uttrycket täcker två lager, och folk menar ofta olika saker med det: körmiljön på låg nivå, som också kallas OCI-körmiljön, och körmiljön på hög nivå, som oftare kallas containermotor (container engine).
Körmiljön på låg nivå gör ett enda smalt jobb. Den läser in ett paket, i
specifikationen kallat bundle, som körmiljöspecifikationen från Open Container
Initiative definierar som en katalog med en config.json-fil och containerns
rotfilsystem. Den ber kärnan om de namnrymder, cgroups och monteringar som
konfigurationen beskriver och startar processen inuti dem. Specifikationen
namnger de operationer som varje körmiljö måste stödja, nämligen create, start,
kill, delete och en fråga om tillståndet, och lämnar kommandoraden åt varje
implementation. Version 1.0 publicerades i juli 2017. runc, som OCI självt
underhåller, beskriver sig som ett verktyg på låg nivå som inte är tänkt för en
slutanvändare. crun gör samma jobb i C med ett mindre minnesavtryck och är den
standard som Podmans dokumentation anger. youki är en tredje, skriven i Rust.
Motorn ligger ovanpå. containerd beskriver sig som en daemon som hanterar hela livscykeln för containrarna på sin maskin: överföring och lagring av avbildningar, körning och tillsyn av containrar och de lagrings- och nätverkskopplingar som hör till dem. CRI-O gör ett liknande jobb, byggt enbart för Kubernetes. En motor hämtar och packar upp avbildningen, skriver paketet, lämnar över det till körmiljön på låg nivå och fortsätter att bevaka processen efter att den körmiljön har avslutats. Docker Engine ligger ytterligare ett steg högre: enligt dess dokumentation använder den containerd för containrarnas livscykel, och containerd använder i sin tur runc som standard. Podman går den andra vägen och har ingen daemon som körs hela tiden. Varje kommando förbereder paketet självt, anropar crun eller runc och lämnar en liten övervakare som heter conmon att ta hand om den körande containern.
Kubernetes lägger till ytterligare ett ord i vokabulären. Kubelet på varje nod talar med motorn via Container Runtime Interface, CRI, ett gRPC-protokoll som både containerd och CRI-O implementerar. Docker Engine är äldre än CRI, så i flera år bar Kubernetes med sig en adapter för den som hette dockershim, och den togs bort i v1.24.
Varför det spelar roll
Att välja containerkörmiljö är i själva verket att välja vilket lager man byter, och de två lagren ger olika saker.
Motorn avgör hur containrarna lever från dag till dag: vem som hämtar och lagrar
avbildningar, vem som startar om en container som har kraschat, vart dess loggar
tar vägen, vilket API andra verktyg talar och vem som äger processträdet. Att
byta Docker Engine mot Podman på en server, eller mot containerd på en nod i
Kubernetes, ändrar allt detta och lämnar själva containrarna orörda, eftersom
avbildningsformatet och paketet bygger på samma OCI-standarder därunder.
Kubernetes dokumentation om borttagningen av dockershim säger det rakt ut:
avbildningar som har byggts med docker build fungerar med alla
CRI-implementationer.
Körmiljön på låg nivå avgör hur isolerad processen är. runc och crun startar en vanlig process på maskinens egen kärna, vilket är snabbt och helt kompatibelt och betyder att varje container på maskinen delar just den kärnan. gVisors körmiljö, runsc, lägger en applikationskärna skriven i Go mellan containern och maskinen och besvarar containerns systemanrop själv. Projektets egen dokumentation anger priset som sämre kompatibilitet för applikationer och högre overhead per systemanrop. Kata Containers startar varje arbetslast i en lätt virtuell maskin med en egen kärna, använder hårdvaruvirtualisering som ett andra försvarslager och behöver en hypervisor på varje nod som kör den.
Kubernetes låter det andra valet göras per pod i stället för per kluster. En
RuntimeClass namnger en hanterare som är konfigurerad i motorn, och en pod som
sätter runtimeClassName körs under den hanteraren, så att en arbetslast man
inte litar på kan ligga i gVisor eller Kata medan allt annat stannar på runc.
Dokumentationen beskriver detta som en avvägning mellan prestanda och säkerhet.
Så ser det ut i praktiken
I vardagligt tal glider uttrycket mot motorlagret, och arbetet på den här webbplatsen använder det i den betydelsen.
Tjugo applikationer paketerade med Docker, utspridda över två moln och några körmiljöer för containrar, behövde driftsättas konsekvent och kunna felsökas snabbt när något gick sönder. Docker paketerade dem, felsökningen gick genom Podman och felsökningen av orkestreringen genom Kubernetes. Att arbeta över flera motorer på det sättet vilar på lagerindelningen ovan: motorerna skiljer sig åt, men OCI-standarderna för avbildningar och körmiljöer under dem är gemensamma.
På en nyare server gjordes valet i motorlagret. Containerskiktet körs på Podman och Quadlet i stället för Docker, eftersom standardmotorn publicerar containerportar ovanför de brandväggsregler som härdningsrollen installerar, och eftersom den skulle lägga till en andra tillsynsprocess bredvid systemd. Det valet ändrade är vem som startar varje container och vem som bevakar den efteråt: containrarna blev tjänster som startar på samma sätt som tjänster startar, under den tillsyn som maskinen redan litade på.
Samma resonemang gäller för en server med 512 MB som kör hela företaget, där minnet avgjorde varje diskussion. Containerskiktet där är Quadlet-enheter under systemd snarare än en daemon med en egen tillsynsprocess, ett val som gjordes på en maskin där ungefär 464 MB minne går att använda.
Var det går fel
Problem med körmiljön brukar dyka upp i skarvarna mellan lagren, där varje sida utgår från att den andra är uppsatt på det sätt den själv väntar sig.
Verktygen på noden tappar sikten efter en uppgradering av klustret. Från och
med Kubernetes v1.24 talar kubelet direkt med containerd eller CRI-O, så de
containrar som klustret startar passerar aldrig genom Docker Engine.
docker ps på noden listar bara det som Docker själv har startat, och
övervakningsagenter, startskript och privilegierade poddar som anropar Dockers
CLI eller monterar docker.sock förlorar sin överblick. Arbetslasterna
fortsätter att köras. Det är verktygen runt dem som behöver flyttas till
crictl eller till motorns eget API.
Kubelet och körmiljön hanterar cgroups på två olika sätt. Båda måste använda
samma cgroup-drivrutin, cgroupfs eller systemd, och kubelet använder
cgroupfs som standard. På en maskin där systemd är init-systemet blir det två
hanterare av cgroups med två bilder av samma resurser, och Kubernetes
dokumentation varnar för att sådana noder kan bli instabila när resurserna
tryter. Lösningen är systemd-drivrutinen på båda sidor, som också är den
rekommenderade drivrutinen med cgroup v2. För containerd 2.x med runc ser
körmiljöns sida ut så här:
# /etc/containerd/config.toml, containerd 2.x
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = true
Kubelets sida är cgroupDriver: systemd i dess konfiguration, och containerd
behöver startas om efteråt. containerd 1.x stavar avsnittet annorlunda, och
Kubernetes sida visar båda formerna. Att byta drivrutin på en nod som redan har
gått med i ett kluster är känsligt: poddar som skapades under den ena
drivrutinen kan stöta på fel när de skapas på nytt under den andra, så det
dokumenterade rådet är att ersätta eller installera om noden med den nya
konfigurationen.
En pod som flyttas till en sandlådad körmiljö stannar vid start. En
RuntimeClass vars hanterare nodens motor saknar skickar podden direkt till fasen
Failed, med orsaken i en händelse. Därefter måste arbetslasten passa sandlådan.
gVisor besvarar systemanrop i sin egen kärna, så programvara som lutar sig mot
mindre vanliga funktioner i kärnan kan bete sig annorlunda, och dokumentationen
pekar ut arbetslaster med många systemanrop som en dålig matchning. Kata behöver
hårdvaruvirtualisering på noden, vilket på en virtuell maskin i molnet betyder
nästlad virtualisering. En nodväljare under scheduling i RuntimeClass håller
sådana poddar på de noder som kan köra dem.
En avbildning som har importerats för hand syns aldrig för kubelet.
containerd håller namn och metadata för avbildningar per namnrymd. Klienten
ctr arbetar i namnrymden default om inget annat anges, medan
CRI-insticksmodulen som kubelet talar med arbetar i k8s.io. En avbildning som
läses in med ett vanligt ctr images import hamnar därför där kubelet aldrig
tittar, och podden går tillbaka till registret eller stannar med ett fel vid
hämtningen av avbildningen. Att importera med ctr -n k8s.io images import och
kontrollera med crictl images, som ser det som kubelet ser, täpper till
glappet.
Läs mer
- OCI Runtime Specification —
standarden som varje körmiljö på låg nivå implementerar: paketet,
config.jsonoch operationerna i livscykeln. - runc — OCI:s egen körmiljö, med anteckningar om hur verktyg på högre nivå styr den.
- Container Runtimes — cgroup-drivrutiner, CRI-versioner och de inställningar för containerd och CRI-O som Kubernetes förväntar sig.
- Container Runtime Interface (CRI) — gRPC-protokollet mellan kubelet och motorn.
- Runtime Class — att välja en annan hanterare för körmiljön per pod och hålla de poddarna på noder som stöder den.
- Alternative container runtimes — hur Docker Engine lämnar över containrar till gVisor, Kata Containers, youki eller Wasmtime.