-
Notifications
You must be signed in to change notification settings - Fork 0
Virtualizace a kontejnery
„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.
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.
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í.
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.
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íň.
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.
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á.
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.
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.
| 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é PIDV 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.
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áchcpu.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.
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.
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í.
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.
--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.
| 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.
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.
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 |
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.
- Jádro a uživatelský režim - prstence, ke kterým VT-x přidalo další rozměr
- Virtuální paměť a stránkování - tabulky stránek, které se ve virtuálu procházejí dvakrát
-
VFS a montování - přípojné body a
overlayfs, ze kterých je obraz kontejneru - Práva a uživatele - capabilities a UID, na kterých stojí zabezpečení kontejneru
-
Nástroje a diagnostika -
straceaperf, když tabulka výš nestačí
Našel jsi chybu nebo něco chybí? Založ issue nebo pošli pull request.
Základy
- Co dělá operační systém
- Režim jádra a uživatelský režim
- Systémová volání
- Přerušení a výjimky
- Architektury jádra
Procesy
Souběh
Paměť
Soubory a zařízení
Systém v provozu
Praxe