# Virtualizace a kontejnery, čili proč kontejner není malý virtuální stroj

„Kontejner je odlehčený virtuální stroj." Tuhle větu najdeš i v oficiální dokumentaci a je nesprávná způsobem, který tě dřív nebo později kousne.

Ve virtuálním stroji běží druhé jádro - vlastní tabulky stránek, vlastní plánovač, vlastní ovladače. V kontejneru neběží žádné. Procesy uvnitř používají to samé jádro jako hostitel a jen nevidí, co se děje kolem.

Z toho plyne všechno ostatní: proč se kontejner spustí za desetiny vteřiny, proč v něm nespustíš jiné jádro a proč jeho izolace není totéž co izolace virtuálního stroje.

Stránka vysvětluje, jak obojí funguje uvnitř, co to stojí a kde to prasklo. Neřeší psaní Dockerfilů ani stavbu klastru. Předpokládá [jádro a uživatelský režim](Jadro-a-uzivatelsky-rezim), [virtuální paměť](Virtualni-pamet-a-strankovani) a [procesy](Procesy).

## Virtualizuje se železo, nebo výhled?

Tohle je nejdůležitější věta na celé stránce: **virtuální stroj virtualizuje hardware, kontejner virtualizuje pohled na operační systém.**

Hypervizor podvrhne procesor, paměť, disk a síťovku. Cokoli na tom železe poběží, se s tím musí vypořádat samo - proto v tom může běžet Windows nebo jádro o deset let starší než hostitel.

Kontejner podvrhne odpovědi jádra. Proces se zeptá „jaké procesy běží" nebo „jaké mám síťovky" a dostane oříznutou odpověď.

Jsou to dvě různé věci na podobné úlohy. Výkon, hustota i bezpečnost jsou důsledek téhle jedné odlišnosti.

## Hypervizor typu 1 a typu 2

**Typ 1 běží přímo na železe** a sám je tou vrstvou, která rozděluje procesor a paměť: Xen, VMware ESXi, Microsoft Hyper-V. Hostitelský systém pod ním žádný není.

**Typ 2 běží jako aplikace nad běžným systémem.** VirtualBox a VMware Workstation jsou obyčejné programy, které si na virtualizaci půjčují pomoc od jádra pod sebou.

**KVM je zvláštní případ, který tohle dělení láme.** Je to modul jádra (v Linuxu od verze 2.6.20, únor 2007), takže z Linuxu udělá hypervizor typu 1 - řízení přebírá kód v ring 0. Zvenku to přitom vypadá jako typ 2.

| Typ | Kde běží | Výkon | Na co je |
|---|---|---|---|
| Typ 1 | na železe | téměř nativní | servery a klastry |
| Typ 2 | nad hostitelem | horší | desktop, zkoušení |
| **KVM** | modul jádra, tedy typ 1 | téměř nativní | **výchozí volba na Linuxu** |

Doporučení: na serveru KVM, protože ho máš přímo v jádře. Na desktopu na hraní QEMU nebo VirtualBox - QEMU navíc umí emulovat cizí architekturu, což KVM neumí.

## Jak se to dá udělat rychle

Naivní hypervizor by musel přečíst každou instrukci hosta. Rychlé je to proto, že drtivou většinu z nich provede procesor přímo.

Intel VT-x (2005) a AMD-V (2006) k tomu přidaly vedle prstenců druhý rozměr: hostitelský režim pro hypervizor a hostovaný pro celý virtuální stroj **včetně jeho ring 0**. Hostované jádro si tedy myslí, že je v [režimu jádra](Jadro-a-uzivatelsky-rezim), a ve svém světě opravdu je.

### VM exit, čili to drahé místo

Když host sáhne na řídicí registr nebo na zařízení, procesor přepne zpátky do hostitelského režimu. Tomu se říká **VM exit**: uloží se stav hosta, načte se stav hypervizoru, zahodí se část predikce skoků.

Jeden VM exit stojí řádově stovky nanosekund, tedy jako pár [systémových volání](Systemova-volani). Celé ladění virtualizace je o tom, aby jich bylo co nejmíň.

### Dvojí překlad adres

Host má vlastní tabulky stránek. Jenže „fyzická adresa" v nich znamená jen adresu uvnitř paměti přidělené virtuálnímu stroji a musí se přeložit ještě jednou.

Dřív to hypervizor řešil **stínovými tabulkami stránek**: držel si vlastní kopii tabulek hosta se skutečnými adresami a každý zápis hosta odchytával VM exitem. Fungovalo to a bylo to drahé.

Dnes to dělá hardware - AMD NPT (2007), Intel EPT (2008). MMU projde dvě sady tabulek za sebou, takže při čtyřech úrovních na obou stranách to je až dvacet čtyři přístupů do paměti místo čtyř. **Dvojí překlad adres je hlavní zbývající cena virtualizace**, a proto se ve virtuálu vyplatí velké stránky 2 MiB víc než na železe.

### Co se dnes už nedělá: binární překlad

Před rokem 2006 hardwarová podpora nebyla a x86 nebyl virtualizovatelný: některé citlivé instrukce v uživatelském režimu tiše selhaly místo toho, aby vyvolaly výjimku. `POPF` v ring 3 ignoruje změnu příznaku přerušení a pokračuje dál, takže hypervizor neměl kde zasáhnout.

VMware to řešilo **binárním překladem**: kód hostovaného jádra se za běhu přepisoval a sporné instrukce se nahrazovaly voláním hypervizoru. V návodech se o tom pořád píše, na dnešním procesoru se to nepoužívá.

## Paravirtualizace a virtio

Emulovat skutečnou síťovku znamená napodobit její registry a každý dotyk registru je VM exit. Odeslání jednoho paketu tak vyjde na desítky přechodů.

**Paravirtualizace to obejde tím, že host ví, že běží ve virtuálu.** Místo ovladače pro konkrétní hardware používá zjednodušené rozhraní navržené pro virtualizaci: **virtio**, v Linuxu od verze 2.6.24 (2008).

Virtio nepředstírá žádné zařízení. Je to sdílený kruhový buffer v paměti, do kterého host uloží popisy požadavků a hypervizora upozorní jednou, až jich je dost. **Na Linuxu používej vždycky `virtio-net` a `virtio-blk`.**

## Tři věci, na kterých kontejner stojí

Jádro nezná slovo „kontejner". Zná tři nezávislé mechanismy a kontejner je jejich kombinace.

**Jmenné prostory určují, co proces vidí** - vlastní seznam procesů, vlastní síťová rozhraní, vlastní strom přípojných bodů.

**Cgroups určují, kolik toho proces smí spotřebovat** - procesor, paměť, vstup a výstup, počet procesů.

**Vrstvený souborový systém určuje, co proces má.** Obraz je stoh vrstev jen pro čtení a nad ním tenká zapisovatelná vrstva; skládá se to přípojnými body, což patří na [VFS a montování](VFS-a-montovani).

**Docker není technologie, je to nástroj, který tyhle tři věci skládá dohromady** a přidá k tomu formát obrazů a registr. Totéž si uděláš ručně na tři řádky.

## Jmenné prostory jednotlivě

| Prostor | Co odděluje | Od jádra |
|---|---|---|
| `mnt` | strom přípojných bodů | 2.4.19 (2002) |
| `uts` | jméno stroje a domény | 2.6.19 (2006) |
| `ipc` | fronty zpráv a sdílenou paměť, viz [IPC](Meziprocesova-komunikace) | 2.6.19 (2006) |
| `pid` | čísla procesů | 2.6.24 (2008) |
| `net` | rozhraní, adresy, směrování | 2.6.29 (2009) |
| `user` | mapování UID a GID | 3.8 (2013) |
| `cgroup` | pohled na strom cgroups | 4.6 (2016) |
| `time` | `CLOCK_MONOTONIC` a `CLOCK_BOOTTIME` | 5.6 (2020) |

Nejzajímavější je `pid`. První proces uvnitř dostane **PID 1** a s ním povinnosti init procesu: sklízet osiřelé potomky. Když to nedělá, hromadí se ti tam [zombie procesy](Procesy) - odtud ty malé init programy jako `tini`.

```bash
ls /proc/self/ns/                                   # deset odkazů = deset prostorů, ve kterých jsi
sudo unshare --pid --fork --mount-proc /bin/bash    # --mount-proc přepojí /proc, jinak bys viděl staré PID
```

V tom shellu je `echo $$` rovno jedničce a `ps -e` ukáže dva procesy. Přepnutí PID prostoru chce `CAP_SYS_ADMIN`, proto to `sudo`; bez roota se přidá `--user --map-root-user`.

## cgroups v2, čili kolik toho smíš spotřebovat

Hierarchie v2 (stabilní od jádra 4.5, na Debianu výchozí od verze 11) je jeden strom adresářů v `/sys/fs/cgroup` a limity jsou obyčejné soubory.

```bash
cat /sys/fs/cgroup/user.slice/memory.max   # „max" znamená bez limitu
systemd-cgtop                              # co skutečně žere, po skupinách
```

`cpu.max` je dvojice „kolik mikrosekund za jakou periodu", `memory.max` tvrdý strop v bajtech, `io.max` propustnost zařízení a `pids.max` počet procesů - ten se hodí proti fork bombě.

**Překročení `memory.max` znamená OOM uvnitř kontejneru, i když má stroj paměti dost.** Jádro zabije proces ve skupině, která limit přerostla, a hostitel si toho nevšimne. Je to týž mechanismus jako u [výměnných algoritmů](Vymenne-algoritmy), jen počítaný po skupinách.

### Aplikace, která si myslí, že má dvanáct jader

Klasický průšvih. Aplikace se zeptá, kolik má jader, jádro odpoví za celý stroj, a ona spustí dvanáct vláken proti kvótě jednoho jádra.

Vlákna se perou o jednu kvótu a [plánovač](Planovani-procesu) je uprostřed periody uspí. Latence vyskočí, vytížení procesoru přitom vypadá nízké.

**Počet vláken nastav ručně podle limitu.** Jmenný prostor pro počet procesorů neexistuje - `nproc` čte masku afinity, ne kvótu.

## Kde izolace kontejneru končí

Sdílené jádro znamená jednu věc: **díra v jádře je útěk z kontejneru.** Hypervizor, který bys musel prolomit, tu žádný není.

### Dirty COW 2016

CVE-2016-5195, zveřejněno v říjnu 2016. Souběh v obsluze copy-on-write umožnil neprivilegovanému uživateli zapsat do mapování, které měl jen pro čtení, a získat tím práva roota. Chyba byla v jádře od roku 2007 a devět let si jí nikdo nevšiml.

Veřejně se předvedl i únik z kontejneru na hostitele, protože táž zranitelnost fungovala skrz hranici, která se prodává jako izolace. **Poučení: zabezpečení kontejneru je zabezpečení jádra hostitele, nic víc.**

### Čím si to podřízneš sám

**`--privileged` vypne skoro všechno.** V návodech to bývá jako rychlá oprava a je to horší než žádná izolace, protože jako izolace vypadá.

**Připojený `/var/run/docker.sock` je root na hostiteli.** Kdo umí mluvit s démonem, spustí si privilegovaný kontejner s připojeným kořenem.

**UID 0 v kontejneru je UID 0 na hostiteli.** Bez user namespace jsou to tatáž čísla, jak je popsané u [práv a uživatelů](Prava-a-uzivatele) - odtud soubory na svazku, které patří rootovi.

Doporučení: zapni user namespace, aplikaci pouštěj pod nerootovským UID a přidej `--cap-drop=ALL`. Výchozí seccomp profil, který zakazuje zhruba čtyřicet [systémových volání](Systemova-volani), nech běžet.

### Virtuální stroj, nebo kontejner

| Vlastnost | Virtuální stroj | Kontejner |
|---|---|---|
| Start | vteřiny až desítky vteřin | desetiny vteřiny |
| Režie | vlastní jádro a jeho paměť | jednotky procent |
| Izolace | hranice hypervizoru | hranice systémových volání |
| Jiné jádro | **ano** | ne |
| Hustota na stroji | desítky | **stovky až tisíce** |
| Kdy sáhnout | nedůvěryhodný kód, cizí zákazník | **vlastní služby nad stejným jádrem** |

**Vlastní služby do kontejnerů, cizí kód do virtuálních strojů.** Hranice mezi zákazníky patří na hypervizor.

## Co je mezi tím

**gVisor** (Google, 2018) je jádro napsané v uživatelském prostoru. Zachytává systémová volání kontejneru a většinu jich vyřídí sám, takže ke skutečnému jádru se dostane zlomek. Platí se za to zpomalením volání.

**Firecracker** (AWS, 2018) jde z druhé strany: minimální hypervizor nad KVM, který umí jen virtio. MikroVM na něm startuje kolem 125 ms a má přitom vlastní jádro.

Obojí říká, že „lehké a bezpečné" se dá mít najednou, když se za to zaplatí rychlostí systémových volání, nebo tím, že v každé instanci znovu běží jádro.

## Diagnostika

```bash
docker stats                        # živě: procesor, paměť proti limitu, síť
lsns                                # kdo v jakém jmenném prostoru sedí
ps -eo pid,pidns,comm               # PIDNS je číslo i-uzlu, stejné číslo = stejný prostor
sudo nsenter -t 1234 -a /bin/bash   # vlez do všech prostorů procesu 1234
```

| Příznak | Kde je problém |
|---|---|
| Kontejner zabit s kódem 137 | 128 + 9, `SIGKILL` od OOM killeru; malý `memory.max` |
| Aplikace vidí všechna jádra | `cpu.max` je kvóta, ne cpuset; nastav vlákna ručně |
| Soubory z kontejneru patří rootovi | chybí user namespace, UID 0 se mapuje na UID 0 |
| Kontejner nemá síť | vlastní `net` prostor bez rozhraní nebo chybějící most |
| Kontejner se restartuje dokola | skončil hlavní proces; kontejner žije jen s PID 1 |

## Co si odnést

**Virtuální stroj virtualizuje hardware, kontejner pohled na systém.** Odtud plyne každý další rozdíl.

**V kontejneru neběží druhé jádro.** Proto startuje hned a proto v něm nespustíš jiné jádro.

**Kontejner jsou jmenné prostory, cgroups a vrstvený souborový systém.** Docker to jen skládá dohromady.

**Dvojí překlad adres je hlavní cena virtualizace, VM exit ta druhá.** Virtio je odpověď na tu druhou.

**Limit paměti v cgroups zabíjí i na stroji, kde je paměti dost.** Kód 137 znamená přesně tohle.

**Díra v jádře je útěk z kontejneru.** Dirty COW 2016 to předvedla naživo.

**Kontejner je izolace provozní, ne bezpečnostní.** Hranice mezi cizími subjekty patří na hypervizor.

## Kam dál

- **[Jádro a uživatelský režim](Jadro-a-uzivatelsky-rezim)** - prstence, ke kterým VT-x přidalo další rozměr
- **[Virtuální paměť a stránkování](Virtualni-pamet-a-strankovani)** - tabulky stránek, které se ve virtuálu procházejí dvakrát
- **[VFS a montování](VFS-a-montovani)** - přípojné body a `overlayfs`, ze kterých je obraz kontejneru
- **[Práva a uživatele](Prava-a-uzivatele)** - capabilities a UID, na kterých stojí zabezpečení kontejneru
- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - `strace` a `perf`, když tabulka výš nestačí

