markdown
Virtualizace-a-kontejnery.md
markdown
# 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`. ```bashls /proc/self/ns/ # deset odkazů = deset prostorů, ve kterých jsisudo 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. ```bashcat /sys/fs/cgroup/user.slice/memory.max # „max" znamená bez limitusystemd-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 ```bashdocker 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ý prostorsudo 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čí