Pamätáte si, keď som ručne spúšťal runc
? Vtedy som len nakopíroval celý obsah ubuntu
do rootfs
a neriešil som. Ale, čo ak máme spustených desiatky kontajnerov?
Každé spustenie kontajnera obnáša vytvorenie vlastného rootfs
a nakopírovanie množtva súborov. Niektoré obrazy kontajnerov sú malé. Taký alpine
má niečo nad 2MB. Niektoré su stredne veľké okolo 30MB, lenže sú aj také, ktoré majú viac ako 500MB alebo 1GB.
Mať kópiu rootfs
pre každý bežiaci kontajner nieje nič moc. Takto by nám veľmi rýchlo došlo miesto na disku. A to nehovoriac o množstve IO operácii. Každý štart by znamenal kopírovanie súborov do rootfs
a následne mazanie, aby v kontajneri neostal neporiadok.
Riešením je použiť Overlay2 - súborový systém, ktorý organizuje súbory a adresáre vo vrstvách. Spodná vrstva je len na čítanie a horná vrstva umožňuje aj zápis. Ich obsah sa nakoniec prekrýva do výsledného súborového systému.
Skusim si vytvoriť adresár s takýmto súborovým systémom. Pripravím si adresáre.
$ mkdir ./upperdir && \
mkdir ./lowerdir && \
mkdir ./workdir && \
mkdir ./merged
Do lowerdir
si ešte umiestnim súbor s nejakým obsahom:
$ echo "Hello lowerdir" > ./lowerdir/hello.txt
Tento lowerdir
slúži ako spodná vrstva, ktorá bude len na čítanie. Adresár upperdir
je vrchná vrstva pre zápis zmien. No a ešte je tu taký pracovný adresár workdir
. Ten slúži na zaručenie akejsi atomickosti zmien v upperdir
. Čiže súbor sa najprv vytvorí vo workdir
a potom sa presunie do upperdir
. Preto je tu dôležité pravidlo - upperdir
a workdir
musia byť rovnakého typu.
Súborový systém namontujem do merged
.
$ mount -t overlay overlay -o \
lowerdir=./lowerdir,\
upperdir=./upperdir,\
workdir=./workdir \
./merged
V adresári merged
teraz vidím súbor zo spodnej vrstvy
$ ls ./merged
hello.txt
$ cat ./merged/hello.txt
Hello lowerdir
Modifikácie súborov
Skúsim taký experiment. Upravím existujúci hello.txt
a vytvorím nový new.txt
súbor.
$ echo "Update of file" >> ./merged/hello.txt
$ echo "New file" > ./merged/new.txt
Teraz sa pozriem do hornej vrstvy.
$ ls ./upperdir
hello.txt new.txt
$ cat ./upperdir/hello.txt
Hello lowerdir
Update of file
V podstate je to presne to, čo vidím vo výslednom merged
adresári. Obsah súboru hello.txt
je presne ten obsah, ktorý som zmodifikoval. Ale čo mám v spodnej vrstve?
$ cat ./lowerdir/hello.txt
Hello lowerdir
V spodnej vrstve súbor ostal nezmenený. Taktiež sa tu nenachádza ani new.txt
súbor. Čo som aj očakával.
Mazanie súborov
Čo sa stane ak zmažem hello.txt
? Zmaže sa v spodnej vrstve?
$ rm ./merged/hello.txt
$ ls ./lowerdir
hello.txt
Súbor tu ostal. Ale v merged
nieje, lebo som ho zmazal. Ako to funguje? Pozriem sa do hornej vrstvy:
$ ls -l ./upperdir
total 4
c--------- 2 root root 0, 0 Oct 8 00:01 hello.txt
-rw-rw-r-- 1 sn3d sn3d 9 Oct 7 23:57 new.txt
Tu vidím zmazaný súbor, ale má však iné vlastnosti. Totižto súborový systém súbor označil ako vymazaný. Tento proces sa volá aj whiteout.
Takýto súborový systém môže pomôcť s problémom pri rootfs
. Stačí, ak rootfs
adresár namontujeme tento súborový systém tak, že spodná vrstva lowerdir
bude obraz kontajnera a horná vrstva upperdir
bude prázdna.
Mnohonásobné spodné vrstvy
Stále tu však je veľa duplicitného obsahu. Zoberme si také mysql-apline
a redis-alpine
. Obe obrazy sú odvodené od apline
, čo znamená, že oba obrazy budu duplikovať jeho obsah.
Čo keby sme vedeli spraviť ďalšiu spodnú vrstvu, kde by bol alpine? Takto by kontajnery opäť dokázali ušetriť diskový priestor. V systéme by som mal len jednu kópiu obrazu alpine
.
Linux od verzie jadra 4.0 podporuje mnohonásobné spodné vrstvy. Stačí len trochu upraviť montovanie súborového systému.
$ mount -t overlay overlay -o \
lowerdir=./lowerdir-alpine:/lowerdir-redis,\
upperdir=./upperdir,\
workdir=./workdir \
./merged
Overlay v kontajneroch
Už teda viem, ako by sa dal použiť Overlay2.Ako to však používajú skutočné kontajnery? To sa dá jednoducho zistiť.
Najprv budem potrebovať nejaký testovací obraz založený na alpine
. Aby som si odsledoval ako vrstvenie funguje v kontajneroch, tak vytvára dvojicu súborov first.txt
a second.txt
. Aby som mal dosť času sa v ňom hrabať, tak kontajner spusti sleep 100
.
FROM alpine
RUN echo "First" >> /first.txt
RUN echo "Second" >> /second.txt
CMD sleep 1000
Obraz zostavím a spustím:
$ docker build . -t hello
$ docker run -d hello:latest
8a025a03789bd47e50eb6ffd9732d03225abc3efbcaa0143ccc5ea7263523370
Z predchádzajúcich článkov viem, že za chod kontajnera je zodpovedné behové prostredie runc
. To spusti kontajner podľa konfigurácie v config.json
. Ten sa nachádza v /run/container.d/
Výborne miesto kam sa pozrieť. Mňa bude zaujímať nastavenie root
:
$ cd /run/containerd/io.containerd.runtime.v2.task/moby/8a025a03789bd47e50eb6ffd9732d03225abc3efbcaa0143ccc5ea7263523370
$ cat config.json | jq .root
{
"path": "/var/lib/docker/overlay2/edda3ff0f5eef18f3cf840b16dbda6d0a41d9eb2b6198b2875733d58421c817b/merged"
}
Čo sa pred štartom kontajnera stalo? Proces containerd
pripravil /var/lib/docker/overlay2/{id}/merged
adresár, ktorý je root adresarom bežiacého kontajnera. Príprava dát nieje v kompetencii behového prostredia runc
. Ten už len používa tento adresár. Skúsim si pozrieť, ako je tento súbor namontovaný pomocou findmnt
:
$ cd /var/lib/docker/overlay2/edda3ff0f5eef18f3cf840b16dbda6d0a41d9eb2b6198b2875733d58421c817b/merged
$ findmnt -J . | jq .
{
"filesystems": [
{
"target": "/var/lib/docker/overlay2/edda3ff0f5eef18f3cf840b16dbda6d0a41d9eb2b6198b2875733d58421c817b/merged",
"source": "overlay",
"fstype": "overlay",
"options": "rw,relatime,lowerdir=/var/lib/docker/overlay2/l/SDF4TI3RLBNGOE3YKDKGG2TDHO:/var/lib/docker/overlay2/l/BVVZ7CMX4ZZPT6CTHIQJBG52GC:/var/lib/docker/overlay2/l/5EXGXUCOPQAFEED4GDLPYR3CDR:/var/lib/docker/overlay2/l/NRKPSWFD4UEYYPNG6M52RHQ5NH,upperdir=/var/lib/docker/overlay2/edda3ff0f5eef18f3cf840b16dbda6d0a41d9eb2b6198b2875733d58421c817b/diff,workdir=/var/lib/docker/overlay2/edda3ff0f5eef18f3cf840b16dbda6d0a41d9eb2b6198b2875733d58421c817b/work"
}
]
}
Je tu niekoľko spodných vrstiev. A tu si už môžem zodpovedať, čo reprezentuje vrstva. Je to Dockerfile
? Je každá ďalšia vrstva vlastne výsledkom FROM
? V takom prípade by som však mal mať len 2 vrstvy. Lenže ja ich tu mám viac. Skúsim teda námatkovo pozrieť, čo obsahuje druhá a tretia vrstva:
ls /var/lib/docker/overlay2/l/BVVZ7CMX4ZZPT6CTHIQJBG52GC
etc second.txt
ls /var/lib/docker/overlay2/l/5EXGXUCOPQAFEED4GDLPYR3CDR
etc first.txt
Toto je ale veľmi zaujímavé. Každá vrstva je výsledkom nejakého príkazu. Čiže nieje to Dockerfile
. Ak by som mal v mojom Dockerfile
10x spomenutý RUN
, výsledkom by bolo 10 vrstiev. Preto skoro všade vidieť snahu o RUN
, ktorý spustí hromadu príkazov.
Obmedzenia prekrývania
S prekrývaním je spojených viacero vecí.
V prvom rade, keď som si čítal niečo o overlay2
, tak som natrafil na limit vrstiev. Možné je použiť maximálne 128 vrstiev. Aby to nebolo také priamočiare, tak problém môže nastať už aj pri počte 122. Aj ten text pre options
môže mať len určitú dĺžku.
Keď som skúšal testovať tento limit, tak som zistil, že Docker do toho všetkého ešte dolepí jednu vrstvu s rôznymi súbormi ako .dockerenv
, hostnames
atď.
Uvedomil som si, že overlay
je niečo, čo sa vloží medzi aplikáciu a skutočný súborový systém. To môže priniesť určitú penalizáciu z pohľadu rýchlosti. Zaujímalo ma, či ide o niečo signifikantné alebo ide o zanedbateľnú penalizáciu. K tejto téme som však nenašiel žiaden relevantný test.
Je tu však zopár prípadov kedy je dobre byť ostražitý. Napríklad, keď chcem otvoriť existujúci súbor na zápis. Ak je tento súbor v spodnej vrstve, tak systém ho najprv musí nakopírovať do hornej vrstvy. Keďže iba horná vrstva slúži na zápis. Je úplne jedno aký veľký súbor to je. Pri veľkom dátovom súbore to môže napáchať zlobu.
Aby to nebolo s IO operáciami až také čierne, tak VOLUME
obchádza celé prekrývanie a ide priamo na súborový systém, ktorý je namontovaný v mennom priestore daného kontajnera. Preto sa všade uvádza, aby sme nezapisovali do kontajnera priamo, ale na IO operácie proste použili VOLUME
.
Záver
Na záver už len také to moje programátorské. Doteraz som používal príkaz mount
. To je fajn, ale ako to programovo rieši containerd
? Veľmi jednoduchá odpoveď je - cez systémové volanie mount()
. V Go je toto volanie dostupné v golang.org/x/sys/unix
. Ale aby to nebolo až také jednoduché, keď som prechádzal kód, tak som natrafil na FUSE. O tom, čo to je a hlavne akú úlohu zohráva pri vytvárani root
adresára pre kontajner ale niekedy inokedy.