# Containerkjøretid

## Hva det er

En containerkjøretid er programvaren mellom et containerimage på disk og en isolert prosess i
systemkjernen. Ordet dekker to lag, og folk mener ofte hvert sitt: kjøretiden på lavt nivå, også
kalt OCI-kjøretiden, og kjøretiden på høyt nivå, som oftere kalles en containermotor.

Kjøretiden på lavt nivå gjør én smal jobb. Den leser en bundle, som kjøretidsspesifikasjonen fra
Open Container Initiative definerer som en katalog med en `config.json`-fil og containerens
rotfilsystem, ber kjernen om de navnerommene (namespaces), cgroups og monteringene som
konfigurasjonen beskriver, og starter prosessen inni dem. Spesifikasjonen navngir operasjonene hver
kjøretid må støtte, nemlig create, start, kill, delete og en forespørsel om tilstand, og overlater
kommandolinjen til hver implementasjon. Versjon 1.0 ble publisert i juli 2017. runc, som OCI selv
vedlikeholder, beskriver seg som et verktøy på lavt nivå som ikke er laget med tanke på en
sluttbruker. crun gjør den samme jobben i C med mindre minnebruk og er standardvalget som
dokumentasjonen til Podman navngir. youki er en tredje, skrevet i Rust.

Motoren ligger over den. containerd beskriver seg som en daemon som styrer hele livssyklusen til
containerne på verten sin: overføring og lagring av images, kjøring av og tilsyn med containere, og
lagrings- og nettverkstilkoblingene rundt dem. CRI-O gjør en lignende jobb, bygd for Kubernetes
alene. En motor henter og pakker ut imaget, skriver bundlen, gir den videre til kjøretiden på lavt
nivå og fortsetter å følge med på prosessen etter at den kjøretiden har avsluttet. Docker Engine
ligger enda ett trinn høyere: dokumentasjonen sier at den bruker containerd til livssyklusen for
containere, og at containerd bruker runc som standard. Podman går den andre veien og holder ingen
daemon kjørende hele tiden. Hver kommando forbereder bundlen selv, kaller crun eller runc og
overlater den kjørende containeren til en liten monitor som heter conmon.

Kubernetes føyer ett begrep til vokabularet. Kubelet på hver node snakker med motoren over
Container Runtime Interface, CRI, en gRPC-protokoll som både containerd og CRI-O implementerer.
Docker Engine er eldre enn CRI, så i årevis hadde Kubernetes med seg en adapter for den, kalt
dockershim, og fjernet den i v1.24.

## Hvorfor det er viktig

Å velge en containerkjøretid er egentlig å velge hvilket lag du endrer, og de to lagene gir deg
ulike ting.

Motoren avgjør hvordan containere lever fra dag til dag: hvem som henter og lagrer images, hvem som
starter en krasjet container på nytt, hvor loggene havner, hvilket API andre verktøy snakker, og
hvem som eier prosesstreet. Å bytte Docker Engine mot Podman på en server, eller mot containerd på
en Kubernetes-node, endrer alt dette og lar selve containerne være i fred, fordi imageformatet og
bundlen bygger på de samme OCI-standardene under. Kubernetes-dokumentasjonen om fjerningen av
dockershim sier det rett ut: images bygd med `docker build` fungerer med alle CRI-implementasjoner.

Kjøretiden på lavt nivå avgjør hvor isolert prosessen er. runc og crun starter en vanlig prosess på
vertens kjerne, noe som er raskt og fullt kompatibelt, og som betyr at alle containerne på maskinen
deler den ene kjernen. Kjøretiden til gVisor, runsc, setter en applikasjonskjerne skrevet i Go
mellom containeren og verten og besvarer containerens systemkall selv. Dokumentasjonen til gVisor
oppgir prisen som redusert applikasjonskompatibilitet og høyere kostnad per systemkall. Kata
Containers starter hver arbeidslast i en lett virtuell maskin med en egen kjerne, bruker
maskinvarevirtualisering som et ekstra forsvarslag og trenger en hypervisor på hver node som kjører
den.

Kubernetes lar det andre valget tas per pod i stedet for per klynge. En RuntimeClass navngir en
handler som er satt opp i motoren, og en pod som setter `runtimeClassName`, kjører under den
handleren. Slik kan en arbeidslast du ikke stoler på, ligge i gVisor eller Kata mens alt annet blir
på runc. Dokumentasjonen beskriver dette som en avveining mellom ytelse og sikkerhet.

## Slik ser det ut i praksis

I dagligtale glir uttrykket mot motorlaget, og arbeidet på dette nettstedet bruker det i den
betydningen.

Tjue applikasjoner pakket med Docker,
[spredt over to skyer og noen få containerkjøretider](/nb/portfolio/deployed-and-maintained-20-docker-containerized-applications-troubleshooting-19/),
måtte produksjonssettes konsekvent og kunne diagnostiseres raskt når noe gikk galt. Docker pakket
dem, feilsøkingen gikk gjennom Podman, og feilsøkingen av orkestreringen gikk gjennom Kubernetes.
Å arbeide på tvers av motorer på den måten støtter seg på lagdelingen ovenfor: motorene er ulike,
og OCI-standardene for image og kjøretid under dem er felles.

På en nyere vert ble valget tatt i motorlaget.
[Containerlaget kjører på Podman og Quadlet i stedet for Docker](/nb/portfolio/deployed-a-container-plane-on-podman-and-quadlet-122/),
fordi standardmotoren publiserer containerporter over brannmurreglene som herdingsrollen
installerer, og fordi den ville lagt til en ekstra tjenestestyrer ved siden av systemd. Det valget
endret, er hvem som starter hver container, og hvem som følger med på den etterpå: containere ble
tjenester som starter slik tjenester starter, under den tjenestestyreren verten allerede stolte på.

Den samme tankegangen gjelder for
[en vert med 512 MB som driver hele selskapet](/nb/portfolio/ran-the-whole-company-on-one-512mb-host-109/),
der minnet avgjorde hver diskusjon. Containerlaget der er Quadlet-enheter under systemd i stedet
for en daemon med sin egen tjenestestyrer, et valg tatt på en maskin der rundt 464 MB minne er
brukbart.

## Hvor det går galt

Problemer med kjøretiden dukker gjerne opp i skjøtene mellom lagene, der hver side antar at den
andre er satt opp slik den forventer.

**Verktøyene på noden mister oversikten etter en klyngeoppgradering.** Fra Kubernetes v1.24 snakker
kubelet direkte med containerd eller CRI-O, så containerne klyngen planlegger, går aldri gjennom
Docker Engine. `docker ps` på noden lister bare det Docker selv har startet, og
overvåkingsagenter, oppstartsskript og privilegerte pods som kaller kommandolinjeverktøyet til
Docker eller monterer `docker.sock`, mister innsynet. Arbeidslastene fortsetter å kjøre. Det er
verktøyene rundt dem som må flyttes over til `crictl` eller motorens eget API.

**Kubelet og kjøretiden styrer cgroups på hver sin måte.** Begge må bruke den samme
cgroup-driveren, `cgroupfs` eller `systemd`, og kubelet bruker `cgroupfs` som standard. På en vert
der systemd er init-systemet, gir det to cgroup-styrere med to bilder av de samme ressursene, og
Kubernetes-dokumentasjonen advarer om at slike noder kan bli ustabile under ressurspress.
Rettelsen er systemd-driveren på begge sider, som også er den anbefalte driveren med cgroup v2.
For containerd 2.x med runc ser kjøretidssiden slik ut:

```toml
# /etc/containerd/config.toml, containerd 2.x
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
  SystemdCgroup = true
```

Kubelet-siden er `cgroupDriver: systemd` i konfigurasjonen, og containerd må startes på nytt
etterpå. containerd 1.x staver seksjonen annerledes, og Kubernetes-siden gir begge formene. Å bytte
driver på en node som allerede er med i en klynge, er ømfintlig: pods opprettet under én driver kan
få feil når de opprettes på nytt under den andre, så det dokumenterte rådet er å erstatte noden
eller installere den på nytt med den nye konfigurasjonen.

**En pod som flyttes til en sandkassekjøretid, stopper ved oppstart.** En RuntimeClass med en
handler som nodens motor ikke har, sender poden rett til fasen Failed, med årsaken i en hendelse.
Selv med handleren på plass må arbeidslasten passe til sandkassen. gVisor besvarer systemkall i sin
egen kjerne, så programvare som støtter seg på mindre vanlige kjernefunksjoner, kan oppføre seg
annerledes, og dokumentasjonen nevner arbeidslaster med mange systemkall som en dårlig match. Kata
trenger maskinvarevirtualisering på noden, og på en virtuell maskin i skyen betyr det nestet
virtualisering. En nodevelger i feltet `scheduling` på RuntimeClass-en holder slike pods på nodene
som kan kjøre dem.

**Et image importert for hånd forblir usynlig for kubelet.** containerd holder navn og metadata for
images per navnerom. Klienten `ctr` arbeider i navnerommet `default` hvis ikke noe annet er oppgitt,
mens CRI-pluginen som kubelet snakker med, arbeider i `k8s.io`. Et image som lastes inn med en
vanlig `ctr images import`, havner derfor der kubelet aldri ser, og poden går tilbake til
registeret eller stopper med en feil ved henting av imaget. Å importere med
`ctr -n k8s.io images import`, og å sjekke med `crictl images`, som ser det kubelet ser, lukker
gapet.

## Les mer

- [OCI Runtime Specification](https://github.com/opencontainers/runtime-spec) — standarden hver
  kjøretid på lavt nivå implementerer: bundlen, `config.json` og operasjonene i livssyklusen.
- [runc](https://github.com/opencontainers/runc) — OCIs egen kjøretid, med notater om hvordan
  verktøy på høyere nivå styrer den.
- [Container Runtimes](https://kubernetes.io/docs/setup/production-environment/container-runtimes/)
  — cgroup-drivere, CRI-versjoner og innstillingene for containerd og CRI-O som Kubernetes
  forventer.
- [Container Runtime Interface (CRI)](https://kubernetes.io/docs/concepts/containers/cri/) —
  gRPC-protokollen mellom kubelet og motoren.
- [Runtime Class](https://kubernetes.io/docs/concepts/containers/runtime-class/) — valg av en annen
  handler per pod, og hvordan slike pods holdes på noder som støtter den.
- [Alternative container runtimes](https://docs.docker.com/engine/daemon/alternative-runtimes/) —
  hvordan Docker Engine gir containere videre til gVisor, Kata Containers, youki eller Wasmtime.

<https://engineer.company/nb/glossary/container-runtime/>
