Docker vs LXC – czym to się różni?

Docker vs LXC – czym to się różni?

Na przebiegu kilku ostatnich lat, rosnąca popularność Dockera wprowadziła podział na jego zwolenników, jak i przeciwników. Osoba „bezstronna”, na samym początku może szukać odpowiedzi na kilka pytań: Dlaczego Docker? Dlaczego nie wykorzystać czegoś, co było od dawna — LXC? Jaka jest różnica między LXC a Dockerem? Kiedy wybrać kontenery LXC, a kiedy kontenery Dockerowe?

W tym artykule postaram się rozwiać wszystkie te wątpliwości i odpowiedzieć na pytania osoby „bezstronnej”.


Historia konteneryzacji i LXC

Konteneryzacja sama w sobie ma dość długą historię. Pierwsze próby stworzenia czegoś, co mogłoby przypominać dzisiejsze kontenery, podjęto już w roku 2000. Twórcy systemu FreeBSD (system operacyjny z rodziny UNIX) wprowadzili polecenie jail (więzienie).

Docker vs LXC
Zródło: https://unsplash.com

Dzięki jail otrzymywaliśmy możliwość izolowania systemu plików, użytkowników oraz sieci.
Dodatkowo jail wprowadziło możliwość przypisania adresu IP czy konfigurowanie niestandardowych instalacji pakietów i bibliotek. Główną ideą jail, było odizolowanie aplikacji działających na hoście. Jak na rok 2000, pomysł był bardzo dobry, aczkolwiek nie obyło się bez problemów. Aplikacje w obrębie jail miały ograniczoną funkcjonalności i finalnie to rozwiązanie nie przyjęło się na większą skalę.

Przełom nastąpił w 2006 roku, gdy inżynierowie firmy Google zaprezentowali światu mechanizm przeznaczony do izolowania procesów oraz ograniczania zużycia zasobów (CPU, pamięć, dysk, sieć) przez proces. W 2007 roku mechanizm ten został określony jako Control Groups (cgroups). Rok później, cgroups zostało przyłączone do jądra Linuksa 2.6.24, co doprowadziło do powstania projektu znanego teraz jako LXC — Linux Containers.


Idea kontenerów LXC

Główną zaletą LXC jest możliwość uruchomienia pełnoprawnego systemu operacyjnego w kontenerze, który uruchamiany jest na współdzielonym jądrze systemu hosta. Jeśli chodzi o zasadę działania, LXC jest bardziej „zbliżone” do maszyn wirtualnych.

Na oficjalnej stronie ubuntu, o kontenerach LXC możemy przeczytać:

Containers are a lightweight virtualization technology.
They are more akin to an enhanced chroot than to full virtualization like Qemu or VMware, both because they do not emulate hardware and because containers share the same operating system as the host.

O kontenerach LXC możemy myśleć jako czymś zbliżonym do tego, za co w systemie Linux odpowiedzialne jest polecenie chroot. Chroot to polecenie uniksowe, pozwalające uruchomić dany program ze zmienionym korzeniem (root) – katalogiem głównym systemu plików


Docker vs LXC – czyli inne typy kontenerów

Zanim przejdziemy do omawiania różnic, warto wspomnieć o tym, co było kiedyś. Mianowicie, na samym początku (rok 2013) Docker korzystał z rozwiązań kontenerów LXC. Z czasem jednak, przedstawiciele Dockera doszli do wniosku, że błędy w LXC mają bezpośredni wpływ na działanie Dockera. Jako że LXC było rozwijane przez społeczność, Docker nie miał nad tym pełnej kontroli.

W 2014 roku Docker stworzył i zaczął wykorzystywać własne narzędzie do uruchamiania kontenerów o nazwie libcontainer.


Inne podejście do kontenerów

Podstawowa różnica pomiędzy Dockerem a kontenerami LXC to podejście do tworzenia kontenerów.

Kontenery LXC zostały stworzone z myślą uruchomienia pełnoprawnego systemu operacyjnego, zawierającym wszystko to, co moglibyśmy mieć w wirtualnej maszynie. Różnica pomiędzy wirtualną maszyną a kontenerami LXC polega na tym, że system operacyjny wewnątrz kontenerów LXC nie posiada własnego jądra (kernela) i działa bezpośrednio na jądrze systemu hosta.

Docker z kolei zaadoptował całkiem inne podejście. Zamiast uruchamiać cały system operacyjny wraz ze wszystkimi jego komponentami, takimi jak: syslog, cron, czy systemd — Docker został zaprojektowany do uruchamiania JEDNEJ aplikacji per kontener.


Pojedynczy proces vs wiele procesów

Opierając się na zasadzie SRP (Single Responsible Principle), każdy kontener dockerowy powinien mieć jedną odpowiedzialność. W Dockerfile definiujemy główny proces startowy kontenera, za pomocą instrukcji CMD lub ENTRYPOINT.

W przypadku Dockera, niezalecane jest tworzenie kontenerów, wewnątrz których działają dwa, niezależne od siebie procesy. Oczywiście, jak to zwykle bywa, istnieją pewne wyjątki, gdzie zachodzi potrzeba uruchomienia więcej niż jednego procesu. W takich sytuacjach kluczowa kwestia to moment, w którym jeden proces nagle kończy swoje działanie. Sami musimy zarządzić tym, co ma się wtedy wydarzyć.  Czy kontener powinien się wtedy “wysypać”? Czy działać dalej? – na to pytanie musisz sobie odpowiedzieć.

Z kolei wewnątrz kontenera LXC, działa wiele procesów. Wynika to z tego, że na starcie uruchamiany jest cały system operacyjny.

Idąc dalej, można by posunąć się o stwierdzenie, że kontenery LXC zostały stworzone z myślą o administratorach a kontenery dockerowe z myślą o developerach. Oczywiście trzeba to traktować nieco z przymrużeniem oka (sys-admini też korzystają z Dockera), aczkolwiek jest w tym trochę prawdy.


Wbudowane mechanizmy

Docker zawiera w sobie niemal wszystkie niezbędne mechanizmy służące do spakowania aplikacji do obrazu, dystrybucję obrazów, aż do uruchomienia kontenera:

  • Dockerfile, dzięki któremu możemy określić zachowanie kontenera
  • Docker Hub, dzięki któremu nie musimy tworzyć obrazów “od zera”
  • Mechanizm do budowania i dystrybucji obrazów (docker build, docker push/pull)
  • Mechanizm uruchamiania kontenerów na podstawie wcześniej zbudowanego obrazu (docker run)

LXC z kolei jest rozdzielone na kilka aplikacji/komponentów. Przykładowo, aby móc zbudować własny obraz, potrzebne jest nam narzędzie distrobuilder.

Definicję obrazu tworzymy za pomocą pliku .yaml. Przykład takiego pliku znajdziesz TUTAJ. Co można o tym powiedzieć? Mechanizm ten wydaje się nieco bardziej skomplikowany niż jego odpowiednik — plik Dockerfile.

Może to wynikać z tego, że definicja pozwala naprawdę na wiele. Wewnątrz niej możemy określić nawet listę repozytoriów dla managera pakietów (apt) oraz listę paczek, która ma być zainstalowana (np. openssh-client, curl, git itp.).


Docker vs LXC – cechy wspólne


Cgroups

Bez tego mechanizmu pewnie w ogóle nie było by kontenerów. Mowa tutaj o Control Groups (cgroups), które zapoczątkowało narodziny LXC. Zarówno Docker jak i LXC wykorzystują ten mechanizm. 

Namespace

Drugi obok cgroups kluczowy mechanizm wykorzystywany przez kontenery. Wyróżniamy następujące typy namespace’ów

  • Process ID (pid) – każdy kontener posiada własne drzewo procesów (proces w kontenerze A, nie wie nic o istnieniu procesu w kontenerze B)
  • Network (net) – czyli IP routing table
  • Mounts (mnt) – montuje system plików
  • Inter-proc comms (ipc) – pozwala procesom wewnątrz kontenera na dostęp to tej samej współdzielonej pamięci, ale rozdziela pamięć od innych procesów z innych kontenerów
  • UTS (uts) – nadaje każdemu kontenerowi hostname
  • User (user) – pozwala na zmapowanie kont wewnątrz kontenera na konta na hoście. Przykład: root w kontenerze zmapowany na readonly user na hoście
Root w kontenerze

Domyślnie zarówno kontenery dockerowe jak i kontenery LXC startują w trybie root’a. Oznacza to, root w kontenerze jest mapowany na root’a na hoście. Ma to oczywiście zły wpływ na bezpieczeństwo. Gdyby atakującemu udało by się wyjść “z kontenera”, może skończyć się to przejęciem całego hosta.

Mamy jednak możliwość re-mapowania roota w kontenerze na użytkownika nieuprzywilejowanego na hoście. Zarówno Docker jak i LXC posiadają taki mechanizm, który wykorzystuje user namespace.


LXC szybki start

Aby skorzystać z LXC na dystrybucjach pochodnych od Debiana, potrzebujemy doinstalować pakiet o nazwie lxc-utils.

Kontenery LXC działają również na innych dystrybucjach takich jak CentOS. Jednak “ojczystą” dystrybucją omawianego rozwiązania jest Ubuntu.

$ apt install lxc-utils

Tworzenie kontenerów LXC

Po zainstalowaniu niezbędnych pakietów, możemy przystąpić do tworzenia kontenerów LXC. Pierwsza możliwość to stworzenie kontenera w trybie interaktywnym. Po wpisaniu poniższego polecenia, zostaniemy poproszeni o wskazanie:

  • Dystrybucji
  • Wersji
  • Architektury

W moim przypadku było to kolejno: ubuntu, bionic, amd64

$ sudo lxc-create --template download --name u1
Distribution:
ubuntu
Release:
bionic
Architecture:
amd64
Downloading the image index
Downloading the rootfs
Downloading the metadata
The image cache is now ready
Unpacking the rootfs
---
You just created an Ubuntu bionic amd64 (20200328_07:42) container.

Właśnie utworzyliśmy kontener o nazwie u1, bazujący na ubuntu w wersji bionic i architekturze amd64.

Ten sam efekt możemy uzyskać jednym poleceniem:

$ lxc-create -t download -n u1 -- --dist ubuntu --release bionic --arch amd64

Do wyświetlanie kontenerów służy polecenie lxc-ls

$ lxc-ls --fancy
NAME STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
u1   STOPPED 0         -      -    -    false

Po utworzeniu, kontener posiada status STOPPED. Aby go uruchomić, należy użyć polecenia lxc-start

UWAGA: Chcąc uruchomić kontener LXC, nie musimy wskazywać głównego procesu. Kontener domyślnie uruchomi polecenie /sbin/init co spowoduje uruchomienie jednocześnie wielu procesów (o czym za chwilę)

Poniższe polecenie uruchamia kontener LXC o nazwie u1, w trybie dettached (w tle).

$ lxc-start -n u1 -d

Docker vs LXC – procesy w kontenerze

Aby przenieść się do terminala kontenera używamy polecenia lxc-attach

$ lxc-attach u1 bash

Sprawdźmy teraz, jakie procesy działają wewnątrz kontenera:

root@u1:/$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.4 159152  8476 ?        Ss   06:31   0:00 /sbin/init
root         38  0.0  0.4  78452  9392 ?        S<s  06:31   0:00 /lib/systemd/systemd-journald
systemd+     42  0.0  0.2  80052  5368 ?        Ss   06:31   0:00 /lib/systemd/systemd-networkd
systemd+     65  0.0  0.2  70636  5112 ?        Ss   06:31   0:00 /lib/systemd/systemd-resolved
root         67  0.0  0.1  31288  3144 ?        Ss   06:31   0:00 /usr/sbin/cron -f
root         68  0.0  0.2  62024  5620 ?        Ss   06:31   0:00 /lib/systemd/systemd-logind
root         69  0.0  0.8 170376 17428 ?        Ssl  06:31   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
message+     70  0.0  0.2  49924  4248 ?        Ss   06:31   0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
syslog       71  0.0  0.2 263032  4252 ?        Ssl  06:31   0:00 /usr/sbin/rsyslogd -n
root         76  0.0  0.1  15952  2452 pts/2    Ss+  06:31   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/2 115200,38400,9600 vt220
root         77  0.0  0.1  15952  2324 pts/1    Ss+  06:31   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/1 115200,38400,9600 vt220
root         78  0.0  0.1  15952  2324 pts/3    Ss+  06:31   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/3 115200,38400,9600 vt220
root         79  0.0  0.1  15952  2316 pts/1    Ss+  06:31   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud console 115200,38400,9600 vt220
root         80  0.0  0.1  15952  2236 pts/0    Ss+  06:31   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/0 115200,38400,9600 vt220
root         88  0.0  0.1  23188  3836 pts/2    Ss   06:34   0:00 bash
root        100  0.0  0.1  39084  3408 pts/2    R+   06:34   0:00 ps aux

I co widzimy? Wewnątrz kontenera działa wiele procesów, min. Systemd czy Cron.

Dla porównania uruchomimy teraz kontener dockerowy, również na podstawie ubuntu, wskazując główny proces kontenera jako bash, po to, by następnie wyświetlić listę procesów.

$ docker run -it ubuntu /bin/bash
root@997e7a66b371:/$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.3  0.1  18504  3344 pts/0    Ss   06:38   0:00 /bin/bash
root         10  0.0  0.1  34400  2808 pts/0    R+   06:39   0:00 ps aux

Jak łatwo zauważyć w obrębie kontenera dockerowego działa tylko proces /bin/bash oraz proces, który służy do wyświetlania procesów (ps aux).


Docker vs LXC – zużycie zasobów przez kontener

Porównamy teraz, ile zasobów sprzętowych potrzebują oba typy kontenerów. Na początku sprawdźmy kontener LXC. Służy do tego polecenie lxc-info

$ lxc-info -n u1 -S
CPU use:        2.16 seconds
BlkIO use:      161.71 MiB
Memory use:     257.47 MiB
KMem use:       9.54 MiB
Link:           veth22ME4X
 TX bytes:      2.17 KiB
 RX bytes:      2.97 KiB
 Total bytes:   5.14 KiB

Sprawdźmy teraz jak to wygląda w przypadku kontenera dockerowego. W tym celu użyjemy polecenia docker stats.

$ docker stats ubuntu-docker --no-stream
CONTAINER ID   NAME            CPU %    MEM USAGE / LIMIT     MEM %   NET I/O    BLOCK I/O   PIDS
b1cb60de4b1a   ubuntu-docker   0.00%    1.234MiB / 1.861GiB   0.06%   906B / 0B  0B / 0B     1


Jak można łatwo zauważyć, kontener LXC potrzebuje o wiele więcej zasobów niż kontener dockerowy. Wynika to z jego “budowy” – czyli uruchomienia pełnoprawnego systemu operacyjnego. W kontenerze dockerowym, działa tylko jeden proces – /bin/bash. W związku z tym ilość potrzebnych zasobów do wykonania tego procesu jest znikoma.


LXD – czyli turbodoładowane LXC

Projekt pod nazwą LXD został uruchomiony w roku 2015, używając tych samych komponentów co LXC. Celem LXD było stworzenie przyjaźniejszego w użyciu ekosystemu. Bez wątpienia, narodziny Dockera w 2013 roku i rosnąca jego popularność skłoniła twórców LXD do działania, by móc “konkurować” z Dockerem.

Pomimo, że zarówno LXC jak i LXD są stale rozwijane, dla osób które nigdy nie korzystały z LXC, istnieje rekomendacja, aby od razu zacząć od LXD.

Jeżeli chciałbyś skorzystać z kontenerów LXD, możesz to zrobić w środowisku online TUTAJ. Jest to interaktywny tutorial przedstawiający podstawowe polecenia.


LXD – jak to działa?

LXD nieco skopiowało architekturę od Dockera. Głównie mowa tutaj o daemonie, z którym możemy się komunikować za pomocą REST API. Dzięki temu nie jesteśmy uzależnieni od terminala, lecz możemy nawet samemu tworzyć aplikacje komunikujące się z daemonem.

W przeciwieństwie do LXC, używając LXD wszystkie tworzone kontenery, są uruchamiane w trybie non-root. Aby uruchomić kontener w trybie root’a, musimy jasno to określic (-c security.privileged=true)


LXD – repozytorium obrazów

LXD posiada tez własne repozytorium obrazów dostępne TUTAJ.

Znajdziemy w nim niemalże wszystkie dystrybucje systemu Linux. W przeciwieństwie do Docker Hub’a, nie ma tutaj gotowych obrazów przeznaczonych dla konkretnej technologii, tak by wykorzystać go na potrzeby danej aplikacji.

Chcąc uruchomić aplikację przy użyciu kontenerów LXD, sami musimy stworzyć obraz bazując na wybranej przez nas dystrybucji. Przykład tworzenia obrazu LXD dla aplikacji NodeJS znajdziesz TUTAJ.


Docker vs LXD – polecenia

Poniżej znajdziesz porównanie poleceń. Jak łatwo zauważyć są one bardzo podobne.

Docker LXD
docker image ls lxc image list
docker container run –name first ubuntu:18.04 lxc launch images:ubuntu/18.04 first
docker container stop first lxc stop first
docker container rm firstlxc delete first
docker container lslxc list
docker container inspect firstlxc config show first
docker exec -it first /bin/bashlxc exec first — /bin/bash


Podsumowanie

Kontenery LXC mogą być swiętną alternatywą dla wirtualnej maszyny. Przede wszystkim są lżejsze, gdyż korzystają z kernela systemu hosta, jednocześnie zapewniając nam działania pełnoprawnego systemu operacyjnego.

Idea używania Dockera jest zupełnie inna. Od samego początku, jego głównym przeznaczeniem jest możliwość uruchomienia pojedynczej APLIKACJI w kontenerze, zamiast uruchamiania całego systemu operacyjnego.

Docker vs LXC – gdzie zatem stosować Dockera a gdzie LXC?

Jednym zdaniem – Docker do uruchamiania aplikacji, LXC (LXD) – tam, gdzie chcę wykorzystać zalety wirtualnej maszyny, ale aż tak bardzo nie potrzebuję pełnej wirtualizacji.



.

5 thoughts on “Docker vs LXC – czym to się różni?”

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *