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).

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 first | lxc delete first |
docker container ls | lxc list |
docker container inspect first | lxc config show first |
docker exec -it first /bin/bash | lxc 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.
Bardzo przydatny artykuł.
Dziękuje Kamilu!
Dzięki, takiej informacji szukałem, porównanie i konkrety. Super
Bardzo się cieszę, że artykuł się przydał. Pozdrawiam!
Bardzo dobrze napisane. Łapka w górę!