Skip to content

Virtualizace a kontejnery

Martin Skalicky edited this page Aug 7, 2026 · 1 revision

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, virtuální paměť a 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, 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í. 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í.

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 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 - odtud ty malé init programy jako tini.

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.

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ů, 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č 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ů - 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í, 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

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

Clone this wiki locally