Sieťová komunikácia 2

Kto zabezpečí sieťové rozhranie dostupné v kontajneri? Naivne som si myslel, že behové prostredie je to miesto, kde sa deje celá mágia ohľadom menného priestoru pre sieť. Lenže ono to je trochu inač.

Začal som prehľadávať zdrojáky runc. Hľadal som, kde všade sa pracuje s Netlinkom cez knižnicu vishvananda/netlink. Tu som našiel len jediné miesto. Lenže nebolo tam žiadne nastavovanie smerovacích tabuliek, žiaden most. Zisťoval sa tam len stav sieťových rozhraní, či sú nastavené. Sieťovanie teda nie je v kompetencii behového prostredia. Behové prostredie už očakáva, že sieťové rozhranie pre kontajner bude vytvorené. V tom som sa utvrdil ďalej aj pri preštudovaní si OCI špecifikácie behového prostredia. Tak som začal pátrať po tom, kto nastavuje sieť pre kontajner.

Pátračka

Kde začať? Skúsil som sledovať, ako sa spúšťa behové prostredie runc. Spustil som si môj obľúbený ubuntu kontajner a zapamätám si jeho ID.

docker run ubuntu:latest sleep 5000

docker ps
CONTAINER ID   IMAGE           COMMAND        CREATED         STATUS         PORTS     NAMES
6c7cda6819bc   ubuntu:latest   "sleep 5000"   4 minutes ago   Up 4 minutes             dreamy_visvesvaraya

Tento kontajner má ID 6c7cda6819bc. Už vieme, že kontajner je vlastne proces. Tak pomocou príkazu ps nájdem príslušný proces v systéme.

> ps -fax | grep "6c7cda6819bc" -A 3
    75110 ?        Sl     0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 6c7cda6819bc9312c06addadb0a56536cb27887066293656b8aeedd9555ce2fb -address /run/containerd/contnerd.sock
  ai
    75191 pts/0    S+     0:00  \\\\_ sleep 5000

Ide o PID 75110 a je to podložka containerd-shim-runc-v2. O podložkách som tiež už písal. Možno si ešte pamätáte článok o architektúre Docker. Táto konkrétna podložka sa spúšťa s parametrami, ktoré mi však veľa odpovedí nedali. Teda okrem ID kontajnera tu nie je nič zaujímavé.

part1

Skúsim sa teda pozrieť do pracovného adresára, v ktorom sa táto podložka spúšťa. To sa dá vyčítať z adresára /proc. Ten obsahuje zoznam procesov a kvantum užitočných informácii o každom procese. Vyčítať v akom pracovnom adresári beží proces je veľmi jednoduché. Stačí sledovať, kam ma dostane /proc/75110/cwd a čo obsahuje?

ls -la /proc/75110/cwd
lrwxrwxrwx 1 root root 0 Jun 17 23:57 /proc/75110/cwd -> /run/containerd/io.containerd.runtime.v2.task/moby/6c7cda6819bc

ls -1 /run/containerd/io.containerd.runtime.v2.task/moby/6c7cda6819bc
address
config.json
init.pid
log
log.json
options.json
rootfs
runtime
work

V tomto prípade pracovný adresár podložky je /run/containerd/io.containerd.runtime.v2.task/moby/6c7cda6819bc Tu je veľmi dôležitý súbor config.json. Ide o konfiguráciu behového prostredia runc pre konkrétny kontajner. Jeho obsah je veľmi zaujímavé čítanie.

cat /proc/75110/cwd/config.json | jq -r

{
  "ociVersion": "1.0.2-dev",
  "process": {
    "user": {
      "uid": 0,
      "gid": 0
    },
    "args": [
      "sleep",
      "5000"
    ],
    "env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "HOSTNAME=d06e750f7ac9"
    ],
...

Súčasťou konfigurácie behového prostredia sú aj hooks. Tieto hooks dovoľujú v spustiť ľubovoľný kód v rôznych fázach behového prostredia. Ide o prestart, createRuntime, createContainer, startContainer, poststart, poststop. Asi vysvetľovať ktorý hook sa kedy zavolá nejak veľmi netreba.

Pozrime sa lepšie na prestart. Ten je tu veľmi zaujímavý:

cat /proc/75110/cwd/config.json | jq -r '.hooks'
  {
    "prestart": [
      {
        "path": "/proc/1264/exe",
        "args": [
          "libnetwork-setkey",
          "-exec-root=/var/run/docker",
          "6c7cda6819bc9312c06addadb0a56536cb27887066293656b8aeedd9555ce2fb",
          "c9be0a9597f0"
        ]
      }
    ]
  }

Čiže pred tým, ako sa spustí samotný kontajner, runc zavolá /proc/1264/exe s podpríkazom libnetwork-setkey. Žeby som bol na správnej stope?

part1

Ale čo je /proc/1264/exe? Stačí sledovať symbolickú linku:

ls -la /proc/1264/exe
  lrwxrwxrwx 1 root root 0 Jun 15 11:55 /proc/1264/exe -> /usr/bin/dockerd

Ide o dockerd. Ten sa spustí s dvojicou parametrov: ID kontajnera a druhé krátke číslo je short controller ID. V OCI špecifikácii som ešte našiel že okrem parametrov sa na stdin odošle stav kontajnera vo forme JSON. Niečo ako:

{
    "ociVersion": "0.2.0",
    "id": "6c7cda6819bc",
    "status": "running",
    "pid": 75110,
}

JSON obsahuje ID procesu kontajnera 75110 . Vďaka tomu Dockeru už nič nebráni v tom, aby ovplyvnil sieťový menný priestor kontajnera. Čiže dockerd pripraví sieť a potom ju kontajneru podhodí.

Ale poďme ďalej. Čo sa skrýva za podpríkazom libnetwork-setkey? Tu už som musel zabŕdnuť do zdrojového kódu. Ide o nezdokumentovaný podpríkaz. Autori ho nechceli veľmi ukazovať bežnému používateľovi. Po krátkom hľadaní som sa dopátral k funkcii processSetKeyReexec() v moby projekte. Práve táto funkcia sa skrýva za týmto podpríkazom. Jej kód však nenastavuje žiadnu sieť. Funkcia sa len pripojí sa na tzv. unix domain socket na ceste /var/run/docker/libnetwork/{short controller ID}.socket. Na tento socket sa odošle požiadavka setKeyData a potom sa podpríkaz ukončí.

part1

Čo je unix domain socket? Ide o jeden z IPC (Inter process communication) spôsobov, ktorým si viacero aplikácii môže vymieňať dáta. Unix domain socket sa tvári ako súbor ale je to socket. Jedna aplikácia zapisuje dáta do súboru a iná z neho dáta číta. Niekomu sa to môže podobať na pub-sub.

Kto je na druhej strane tohto socketu? Na to mi dá odpoveď netstat:

netstat -panel | grep "c9be0a9597f0"
unix  2      [ ACC ]     STREAM     LISTENING     27960    1264/dockerd         /var/run/docker/libnetwork/c9be0a9597f0.sock

Podľa tohto je to opäť dockerd služba. Lenže nie je to žiaden podpríkaz, ale ide o servis, ktorý beží nepretržite.

Pomaličky sa dostávame k záveru pátračky. Kto teda príjme a spracuje setKeyData? Po malom hrabaní prichádzam na ďalšiu funkciu startExternalKeyListener() . Práve táto funkcia vytvára zmieňovaný unix domain socket a spracováva setKeyData požiadavku, podľa ktorej vytvorí celý ten cirkus okolo siete.

part1

Už teda môžeme spokojne prehlásiť, že za nastavenie siete je zodpovedný Docker samotný. Nie containerd, ani behové prostredie runc.

Prečo však takto komplikovane? Toto má jednu obrovskú výhodu. Umožňuje to demokratizáciu sieti vo svete kontajnerov. Ak by sieť bola súčasťou behového prostredia runc, boli by sme odkázaní len na jeden štandard. To by rozhodne nepomohlo inováciám v kontajnerovom svete.

Libnetwork a CNM

Všimli ste si, že akosi sa mi často opakovalo slovíčko libnetwork ? Čo to presne je?

Je to referenčná knižnica CNM (Container Networking Model) štandardu. CNM definuje to, ako sieť v kontajneroch vnímame. Je akousi abstrakciou. CNM je architektonicky o troch entitách a o ich vzťahoch. Ide o network, sandbox a endpoint.

cnm

S pojmom network sa asi väčšina stretla. Je to presne to, čo sa skrýva za docker network ls. Táto entita tvorí skupinu kontajnerov, ktoré dokážu priamo medzi sebou komunikovať. Najčastejšie sa na našom počítači stretneme s bridge.

Druhá dôležitá entita je sandbox. Táto entita je silno spätá so samotným kontajnerom. Reprezentuje sieťovú konfiguráciu kontajnera. Pod týmto si môžeme predstaviť napríklad smerovacie tabuľky kontajnera alebo DNS. Práve sandbox je zodpovedný za vytvorenie unix domain socketu /var/run/docker/libnetwork/*.sock. Nie je to ale konkrétne sieťové rozhranie v kontajneri.

Poslednou je endpoint a je to vlastne pár virtuálnych rozhraní, ktoré prepájajú konkrétny network a sandbox.

Aká by to bola abstrakcia, pre ktorú si nevieme napísať vlastnú implementáciu? CNM teda špecifikuje, ako môžeme do Dockera dodať vlastnú implementáciu. Definuje ovládač - driver a rozhranie, ktoré ovládač musí implementovať. To rozhranie je pomerne jednoduché a vyzerá nasledovne:

driver.Config
driver.CreateNetwork
driver.DeleteNetwork
driver.CreateEndpoint
driver.DeleteEndpoint
driver.Join
driver.Leave

Čo sa týka ovládačov, tak Docker pozná 2 základne typy - interné a externé.

Interné implementácie sú tie, s ktorými Docker už prichádza. Ich kód je v libnetwork/drivers. Medzi najznámejšiu patrí práve bridge, ale je tu aj host, macvlan, alebo overlay.

Externé implementácie sú dostupné vo forme tzv. Docker pluginov. Sú to samostatné bežiace procesy, ktoré s Dockerom komunikujú tak, že vytvoria unix domain socket v /docker/plugins. Docker tak prechádza tento adresár a pripája sa na pluginy práve skrz tieto sockety. Takýmto príkladom externého ovládača je napríklad Weave alebo aj Calico.

Celé to v Libnetwork funguje tak, že sa na začiatku vytvorí NetworkController pre príslušný ovládač a pošlú sa mu Options, s akými je ovládač nakonfigurovaný. Napríklad pre bridge to bude:

driverOptions := options.Generic{}
genericOption := make(map[string]interface{})
genericOption[netlabel.GenericData] = driverOptions
controller, err := libnetwork.New(config.OptionDriverConfig("bridge", genericOption))

Tento kontrolér čaká na svoju príležitosť. Tá nastáva v momente, keď zadáme:

docker network create -d bridge my-bridge

Za týmto príkazom sa schováva:

myBridge, err := controller.NewNetwork("bridge", "my-bridge", "")

Teraz je sieť pripravená na použitie. Keď pre túto sieť vytvorím nový kontajner príkazom:

docker run --network=my-bridge ubuntu:latest

Tak prebehne celá ta mágia, ktorú som si prešiel v pátračke a Docker vytvorí endpoint pre môj myBridge. Takto zabezpečí sieťové rozhranie a priradí mu IP adresu:

ep, err := myBridge.CreateEndpoint("ExammpleEndpoint")

A nakoniec ešte zabezpeči existenciu sandbox, do ktorého sa pridá endpoint:

sbx, err := controller.NewSandbox("ubuntu_container",
		libnetwork.OptionHostname("ubuntu"),
		libnetwork.OptionDomainname("docker.io"))

err = ep.Join(sbx)

Za týmito fragmentami kódu sa skrýva konkrétna implementácia, ktora cez Netlink nastaví všetký potrebne rozhrania a smerovacie tabuľky. Ale to už záleží od príslušného ovládača.

Záver

Posledné 3 články som sa venoval výhradne sieti. Nezameriaval som sa na otázku na čo je dobrý macvlan alebo host.Myslím, že na toto veľmi dobre odpovedá oficiálna dokumentácia. Skôr ma zaujímalo, ako by som si taký vlastný ovládač mohol naprogramovať a čo to odnáša. Na teraz je to k sieťam všetko. Týmto ale problematika sieti vo svete kontajnerov nekončí. Je to veľká, takmer nevyčerpateľná a fascinujúca téma, hlavne vo svete. Rozhodne sa k nej ešte vrátim.

CNM a libnetwork nie je jediná možnosť. Je to skôr možnosť typická pre Docker. Svet kontajnerov je veľmi rozumne navrhnutý s prihliadaním na demokratizáciu. Vďaka tomu vznikla aj alternatíva - CNI (Container Network Internface). Je to o niečo iná abstrakcia, ktorá prichádza zo sveta Kubernetes. Ako CNI funguje a aká bola potreba pre iný štandard, to by som si nechal na neskôr, na samostatný článok.

<