Overlay

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.

part1

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.

part2

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.

<