pl  |  en

LXC – kontener pingwinów

2-4 marca w Bielsku-Białej odbyły się już po raz piąty Dni Wolnego Oprogramowania. Ostatniego dnia konferencji prezentację o LXC wygłosił Grzesiek Nosek z naszego Teamu. Poniżej prezentacja oraz artykuł uzupełniający slajdy.

Wirtualizacja

Wirtualizacja to dość modne słowo w ostatnich latach. Co prawda sama koncepcja sięga lat 70. i ogromnych mainframe’ów, jednak dopiero względnie niedawno komputery w architekturze x86 osiągnęły wystarczającą moc obliczeniową.

Centralną ideą wirtualizacji jest podzielenie fizycznego sprzętu na odizolowane od siebie środowiska. Po co? Są zadania, które powinny być uruchamiane na dedykowanych maszynach, czasem ze względów technicznych (specyficzne wymagania co do środowiska), a czasem ze względów bezpieczeństwa lub organizacyjnych (tak jest wygodniej i przejrzyściej). Z drugiej strony, dzisiejsze serwery mają moc kilkuletniego superkomputera, więc takie rozdzielanie zadań prowadzi do znaczącego marnotrawstwa zasobów.

Wirtualizacja nie jest dziedziną wyłącznie serwerów — wkracza też na komputery osobiste i smartfony i zapowiada bardzo ciekawe perspektywy, zwłaszcza w kwestii bezpieczeństwa, jednak na pełne wykorzystanie jej potencjału musimy jeszcze trochę poczekać.

Cebula ma warstwy, serwer ma warstwy

Sprzęt, jądro systemu operacyjnego, biblioteki i usługi systemowe, a na tym wszystkim interesujące nas aplikacje. W zależności od tego, na której warstwie wprowadzimy wirtualizację, osiągniemy różne korzyści i różne zestawy problemów.

Pełna wirtualizacja pozwala na uruchomienie obok siebie niezależnych systemów operacyjnych, niekoniecznie nawet takich samych. Każdy taki system nie musi nawet wiedzieć, że działa na maszynie wirtualnej, ma wrażenie sprzętu przeznaczonego tylko dla siebie. Mimo to systemy świadome działania pod wirtualizacją mogą działać szybciej i dawać więcej możliwości, takich jak zmiana wielkości pamięci w locie (tzw. balooning). Minusem jest wydajność wyraźnie gorsza od fizycznego sprzętu, zwłaszcza przy wszelkiego rodzaju operacjach wejścia-wyjścia.

Inną opcją, której poświęcony jest ten artykuł, jest wirtualizacja na poziomie systemu operacyjnego. W tym wariancie jądro systemu operacyjnego jest jedno i to ono jest odpowiedzialne za to, żeby niezależne środowiska aplikacji były od siebie dobrze odizolowane. Plusem jest niewątpliwie wydajność, w zasadzie nieodróżnialna od zwykłego systemu. Lepiej też są wykorzystywane zasoby sprzętu (czas procesora, pamięć, dyski twarde) –- bardzo prosty jest np. overbooking, czyli przydział większej ilości zasobów niż jest rzeczywiście dostępna, oparty na założeniu, że średnie ich zużycie jest dużo mniejsze od maksymalnego. Przy pełnej wirtualizacji przydział zasobów jest o wiele sztywniejszy. Naturalnie takie rozwiązanie ma też minusy — wirtualne systemy muszą być na tyle zgodne, żeby działały na tym samym jądrze, a oferowana izolacja jest nieco słabsza. Wirtualne systemy muszą być odpowiednio przygotowane a ewentualne błędy w jądrze mogą mieć większe konsekwencje i dotyczyć wszystkich maszyn wirtualnych jednocześnie.

Linux Containers

LXC jako metoda wirtualizacji obejmuje niezależne komponenty -– namespace’y i cgroups w jądrze oraz narzędzia do zarządzania (lxc-tools lub po prostu lxc).

Namespace’y służą do właściwej wirtualizacji, tj. ukrycia różnych części systemu przed sobą nawzajem. Zadaniem cgroups jest przydzielanie zasobów zgodnie z polityką systemu, czyli przede wszystkim kontrola wydajności a nie dostępu.Na podstawie tych dwóch komponentów narzędzia LXC pozwalają zbudować kontenery, czyli wirtualne maszyny dość podobne do niezależnych systemów.

Namespace’ów jest kilka rodzajów, a każdy służy do wirtualizacji innego rodzaju zasobów. Każdy rodzaj namespace’ów tworzy niezależną hierarchię, która rozciąga się od inita (tego prawdziwego) i obejmuje wszystkie procesy w systemie. Dość istotny jest fakt, że system gospodarza, w który uruchamiane są kontenery, nie jest wyróżniony – po prostu wszystkie procesy w nim uruchomione mają domyślny zestaw namespace’ów. Co prawda miejscami główne namespace’y są traktowane szczególnie, ale dotyczy to głównie dostępu do sprzętu.

Istotny jest fakt, że manipulacje namespace’ami to operacja uprzywilejowana i wymaga uprawnień administratora, choć docelowe plany zawierają usunięcie tego ograniczenia. Inną ważną cechą namespace’ów jest to, że z poziomu działającego procesu można manipulować wyłącznie jego własnymi namespace’ami — nie można np. przenieść do innego namespace’u jakiegoś innego procesu.

Namespace’y

Filesystem namespace to najstarszy dostępny rodzaj namespace’ów. Pozwala grupie procesów utrzymywać własną wersję drzewa katalogów (a konkretnie punktów montowania). W wariancie mniej inwazyjnym można ich użyć np. do udostępnienia użytkownikom prywatnych wersji /tmp — każdy użytkownik może widzieć w tym miejscu inny katalog. Można w tym celu wykorzystać np. moduł pam_namespace. Można też iść na całość i podmienić katalog główny wywołaniem pivot_root (używanym, przynajmniej kiedyś, głównie przez initrd). Ma to tę przewagę nad typowym chroot(), że nie da się z takiego katalogu uciec nawet będąc rootem.

Żeby kontener działał poprawnie, musi mieć swój proces init z identyfikatorem równym 1. Niestety, jest on zajęty przez prawdziwego inita. Możemy ten problem rozwiązać tworząc nową przestrzeń identyfikatorów procesów. Powoduje to, że procesy wewnątrz niej mają po dwa pidy: z nowego namespace’u i z głównego. Po utworzeniu zagnieżdżonego namespace’u część procesów będzie miała trzy pidy itd. Nowy init sam siebie będzie widział jako pid 1, a procesy spoza kontenera będą go widzieć pod zupełnie innym numerem. Powoduje to problemy z narzędziami takimi jak strace -– podążanie za fork() nie działa poprawnie jeżeli są w innym namespace niż śledzony proces.

User namespace’y wirtualizują identyfikatory użytkowników, podobnie jak PID namespace identyfikatory procesów. Oznacza to, że dwa procesy z tym samym uidem (np. 500) w różnych namespace’ach nie będą traktowane jako procesy tego samego użytkownika. Tzn. nie będą mogły się nawzajem śledzić, wysyłać sobie sygnałów itp. W założeniach taki namespace może stworzyć każdy użytkownik. Wewnątrz namespace’u dostanie swój własny uid 0, który na zewnątrz będzie widziany dalej jako oryginalny użytkownik, co przyniesie efekt podobny do obecnego narzędzia fakeroot. Niestety user namespaces są niekompletne i póki co nie są używane przez LXC. Podstawowe ograniczenie polega na tym, że wszystkie odwołania do plików używają uprawnień dla „innych”, więc ciężko użyć tych namespace’ów w praktyce. Tym bardziej że taki namespace może obecnie stworzyć tylko administrator.

Kontenery mogą mieć całkowicie niezależną sieć. Domyślnie po utworzeniu net namespace’u znajduje się w nim tylko loopback. Żeby z takiego namespace’u zobaczyć resztę świata, trzeba do niego przenieść jakiś interfejs. Może to być fizyczne urządzenie (typu eth0 lub pojedynczy VLAN typu eth0.123), ale wtedy przestaje ono być widoczne poza kontenerem, z gospodarzem włącznie. Żeby współdzielić jedną kartę sieciową między kontenerami można użyć urządzenia macvlan, które funkcjonalnie przypomina switcha (każdy kontener ma swój unikalny adres MAC). Najprostsze w użyciu i najbardziej elastyczne, chociaż i z największym narzutem, są urządzenia veth. Są to w zasadzie pary interfejsów punkt-punkt, z których jeden przenosi się do kontenera, a drugi zostawia na gospodarzu i np. wpina do bridge’a. Wtedy gospodarz pełni funkcję switcha lub routera, zależnie od konfiguracji.

Dla dopełnienia wrażenia wydzielonego systemu Linux pozwala również na wirtualizację pamięci dzielonej SYSV i nazwy hosta. Nie są one specjalnie interesujące -– robią co do nich należy i raczej nie ma się w nich co zepsuć –- ale trzeba o nich wspomnieć zanim zajmiemy się CGroups.

Control Groups

Cgroups (Control Groups) to drugi fundamentalny składnik kontenerów, odpowiedzialny za ograniczanie zużycia zasobów (podczas gdy namespace’y ograniczają ich widoczność). W odróżnieniu od namespace’ów nie konfiguruje się ich dedykowanymi wywołaniami systemowymi, a za pomocą systemu plików, trochę podobnego do procfs i sysfs. Nową grupę tworzy się przez utworzenie katalogu a procesy (czy też konkretnie wątki) przenosi się między grupami zapisując ich pidy do pliku tasks w katalogu grupy. Same parametry kontrolerów cgroups również są dostępne w postaci plików. Zmiany nie muszą być dokonywane przez proces na samym sobie jak w przypadku namespace’ów, może je wykonać dowolny proces mający prawa zapisu do odpowiednich plików.

Poszczególne podsystemy cgroups mogą być zamontowane razem w jednym katalogu i wtedy mają wspólny podział procesów na grupy, albo osobno -– wtedy każdy podsystem ma własną hierarchię grup i powiązanych z nimi procesów. Każdy podsystem może być zamontowany dokładnie raz, tzn. nie można w jednym miejscu zamontować cpu i blkio, a w drugim samego cpu, ale dwa razy samo cpu już tak –- w obu miejscach będą widoczne te same grupy. Grupy mogą być zagnieżdżane jedna w drugiej na dowolną głębokość, ale takie
zagnieżdżone grupy nie mogą zdjąć ograniczeń nałożonych na grupę, w której się zawierają, dzięki czemu można wydelegować zarządzanie podgrupami do mniej zaufanych osób/procesów bez ryzykowania stabilności serwera.

Istnieje kilka mechanizmów związanych z czasem procesora. Kontroler cpuset ogranicza procesory, na których mogą działać procesy z danej grupy, choć nie oznacza to, że znikają z /proc/cpuinfo, jak pewnie zachowałby się cpuset namespace. Na przykład można stworzyć grupę, która może działać na wszystkich procesorach oprócz jednego i przenieść tam wszystkie procesy oprócz MySQLa –- wtedy MySQL będzie miał procesor dedykowany dla siebie

Kontroler cpuacct nic nie ogranicza, ale udostępnia dokładne informacje na temat zużycia czasu procesora. Ponieważ do grup przypisywane są poszczególne wątki a nie całe procesy, trzymając się MySQLa można pokusić się o rozliczanie CPU poszczególnym użytkownikom.

Zużycie czasu procesora można też ograniczać, i to na dwa sposoby. Pierwszy, dostępny od dawna, pozwala na ustalenie wagi poszczególnych grup („MySQL dostaje 4x tyle CPU co Apache”). Jądro pilnuje, żeby żadna grupa nie zmonopolizowała procesora i wszystkie procesy mogły się posuwać do przodu. Niedawny „magic patch”, który dawał cudowną poprawę czasu reakcji sprowadza się do tego, że każda sesja dostaje automatycznie osobną grupę typu CPU a standardowy scheduler załatwia resztę. Drugi sposób, który trafił do jądra w wersji 3.2, pozwala na ustawienie sztywnych limitów czasu procesora. Nie zostaną one przekroczone nawet jeżeli nie ma innych procesów, które tego czasu potrzebują.

Limity czasu procesora określa się podając dwie wielkości: okres i quotę (w mikrosekundach). W skrócie w czasie jednego okresu grupa może zużyć tyle czasu, ile ma ustawione jako quotę. Pozwala to na chwilowe przekroczenie ustalonych limitów ale administrator może ustawić długość tej chwili -– im krótsza, tym bardziej precyzyjne limito
wanie. Domyślny okres to 500ms, czyli np. grupa z przydziałem 50% CPU może użyć całego czasu przez 250ms, a przez kolejne 250ms czekać na koniec okresu. Po skróceniu okresu do 50ms skok może trwać maksymalnie 25ms itp.

Można też ograniczyć, albo przynajmniej dokładnie rozliczyć, pamięć fizyczną (samą lub razem z przestrzenią wymiany) zajętą przez procesy z danej grupy i wykorzystywany przez nie cache dyskowy. Przydatnym rozszerzeniem byłoby limitowanie pamięci jądra dla danej grupy (np. bufory pakietów wysyłanych w sieć lub innego I/O), ale łatki są dopiero rozwijane. Kontroler memcg to skomplikowany kawałek kodu w najczarniejszych zakątkach jądra i przez długi czas był mocno eksperymentalny, zwłaszcza przy limitowaniu pamięci wymiany. Do tego dość nisko ustawione limity pamięci mogą spowodować, że dużo częściej będzie uruchamiany OOM killer, więc mimo poprawnych ustawień system będzie sprawiał wrażenie niestabilnego.

Do limitowania ruchu sieciowego nie ma specjalnego mechanizmu pozwalającego ustawić przepustowość łącz np. w Mbps na grupę. Można natomiast za pomocą kontrolera net_cls otagować ruch z każdej grupy osobną klasą i użyć do kształtowania ruchu standardowego narzędzia tc. Niestety nie ma też widocznych w pliku statystyk dotyczących wygenerowanych transferów (analogicznych do cpuacct) i chcąc zmierzyć dokładnie ruch do/z grupy trzeba użyć np. również tc, albo mierzyć ruch dla całego net namespace’u.

Kontroler devices nie ma nic wspólnego z wydajnością, tylko z dostępem do poszczególnych plików urządzeń. Dzięki niemu można udostępnić danej grupie np. tylko /dev/null i nawet root nie będzie mógł stworzyć /dev/sda. Albo będzie mógł stworzyć, żeby różne narzędzia systemowe działały poprawnie, ale nie będzie mógł go przeczytać ani zapisać. Ten kontroler jest o tyle istotny, że nie ma czegoś takiego, jak device namespace, który mógłby zmienić rzeczywiste urządzenie pod danym numerem, więc jeżeli kontener dostanie np. plik urządzenia sda, to bez device cgroups nie można byłoby go powstrzymać przed nadpisaniem dysku.

Dostęp do dysków można rozliczać kontrolerem blkio na dowolnej warstwie, albo samych fizycznych urządzeń, albo macierzy czy innych woluminów LVM –- każde urządzenie może mieć swój limit i wszystkie są brane pod uwagę, czyli można np. dać po 10MB/s na pięciu woluminach na tym samym dysku a dodatkowo 30MB/s na sam dysk. Ten kontroler pozwala na ustalenie wag poszczególnych grup tak, że każda z nich może sama wykorzystać całą wydajność dysków, ale jeżeli obydwie chcą skorzystać w tym samym czasie, to jedna może obciążyć dyski dwukrotnie bardziej niż druga. Ten mechanizm działa podobnie do kontrolera CPU. Można też ustawić sztywne limity zarówno transferów sekwencyjnych (w bajtach na sekundę) i losowych (w operacjach odczytu/zapisu na sekundę).

Kontroler freezer umożliwia zatrzymanie wszystkich procesów wewnątrz grupy. Jego zastosowanie jest ograniczone, ale jest kluczowym elementem migracji działających kontenerów między fizycznymi maszynami. Niestety, tej funkcji w jądrze jeszcze nie ma, choć są co najmniej dwie równolegle rozwijane łaty. Jedna mieści całą logikę potrzebną do zapisania stanu kontenera w jądrze, druga wszystko co się da wykonuje w przestrzeni użytkownika. Żadna jeszcze nie zakwalifikowała się do głównej gałęzi jądra.

Perf to podsystem jądra, który pozwala na monitorowanie wydajności na różne niskopoziomowe sposoby –- np. jeżeli interesowało Was kiedyś, jaka funkcja z libc zajmuje najwięcej czasu przy kompilacji (bodajże memcpy), lub ile można zyskać na wydajności wymieniając pamięci na szybsze, to perfem możecie to sprawdzić. Kontroler perf_event pozwala na ograniczenie takiego monitoringu do procesów z konkretnej grupy.

Na tym w zasadzie kończy się wsparcie dla lekkiej wirtualizacji w jądrze. Zgodnie z zasadą że jądro dostarcza mechanizmy a nie politykę, sama konstrukcja kontenera dzieje się w przestrzeni użytkownika.

LXC-tools

LXC właściwe, czy też lxc-tools, to narzędzia, które na podstawie namespace’ów i cgroups tworzą działające kontenery.

Ponieważ emulacja osobnego sprzętu nie jest kompletna i pewnie nigdy nie będzie, LXC dostarcza skrypty, które pozwalają uruchomić jako kontenery popularne dystrybucje Linuksa -– czasem trzeba wyłączyć udeva, czasem przekonfigurować jakąś usługę itp. — w tych skryptach jest koło, którego nie trzeba na nowo wynajdywać.

Skrypty dostarczane przez lxc-tools pozwalają na stworzenie kontenera opartego na Debianie, Fedorze itp. Przy standardowym wywołaniu nowy kontener będzie miał sieć w formie pary urządzeń veth wpiętej do bridge’a na gospodarzu i adres z DHCP (lxc-tools uruchamia własny serwer), więc taki kontener powinien od razu mieć dostęp do sieci i ogólnie działać jak normalna instalacja. Dwie ostatnie pozycje są dość ciekawe, bo o ile busybox nie wymaga do działania absolutnie niczego z zewnątrz, o tyle kontener oparty o samo ssh brzmi dość dziwnie.

Co by się stało, gdyby współdzielić część systemu między kontenerami? W końcu jeżeli wszystkie działają na tej samej dystrybucji to binaria są dokładnie takie same, więc można oszczędzić trochę dysku a przede wszystkim cache’u. LXC jest na tyle elastyczne, że da się to zrobić bez problemu -– wystarczy trochę inaczej skonfigurować zawartość mount namespace’u przy starcie kontenera. Dokładnie coś takiego robi skrypt konfigurujący lxc-sshd – wybrane katalogi z gospodarza są montowane wewnątrz kontenera (bez możliwości zapisu), a cała reszta jest niedostępna. W ten sposób uzyskujemy kontener, który działa na tej samej dystrybucji co gospodarz, ale nie ma możliwości nic na nim zepsuć, a przynajmniej głęboko w to wierzymy.

Korzystając z kontenerów LXC musimy użyć wszystkich namespace’ów, ewentualnie z wyjątkiem wirtualizacji sieci i za wyjątkiem user namespace, które są jeszcze na tyle niekompletne, że nie da się ich użyć w warunkach bojowych. Jeżeli natomiast takie rozwiązanie jest za mało elastyczne, można użyć samych namespace’ów bez pomocy LXC i skonfigurować je do woli. W zależności od tego, co chcemy osiągnąć, można użyć różnych narzędzi do tworzenia namespace’ów. Najbardziej bezpośrednie jest lxc-unshare, które po prostu tworzy nowy proces z odpowiednim zestawem namespace’ów a całą ewentualną resztę trzeba zrobić ręcznie. Jeżeli wystarczy zmiana widocznego drzewa katalogów, można ją łatwo uzyskać za pomocą modułu do PAMa. Również systemd, wywołujący burzliwe dyskusje init nowej generacji, intensywnie korzysta z cgroups i w niektórych przypa
dkach automatyzuje tworzenie namespace’ów. Pozwala np. jedną linijką odizolować jakąś usługę od sieci albo poukrywać przed nią jakieś katalogi.

Pułapki na optymistów

Niestety, LXC jako zamiennik dla OpenVZ czy Linux-VServer jest jeszcze dość młody i nie wszystko działa jak w reklamie. Na przykład sysfs w ogóle nie wspiera wirtualizacji (podmontowany w kontenerze zachowuje się tak samo jak główny /sys), a LSMy (moduły obsługujące rozszerzone modele bezpieczeństwa, takie jak np. SELinux) mają jedną globalną konfigurację.

Dawanie podwyższonych uprawnień wewnątrz kontenerów też może się źle skończyć, np. z CAP_SYS_REBOOT, które rzeczywiście zrestartuje gospodarza (jest łatka w Ubuntu i w drodze do mainline). Nie są wspierane również osobne quoty dyskowe dla kontenerów, więc w praktyce najlepiej albo zakładać osobny wolumin na każdy kontener, albo zapewnić unikalne identyfikatory użytkowników. Nie ma też możliwości zwirtualizowania urządzeń, więc np. wszystkie terminale muszą być zastąpione pseudoterminalami. Nie jest to duży problem, ale trzeba o nim pamiętać np. przy przenoszeniu systemu z fizycznego sprzętu do kontenera.

Zresztą pułapki czyhają na każdym kroku. Stwórzmy sobie nowy pid namespace i uruchommy w środku shella.

root@precise1:~# lxc-unshare -s PID /bin/bash
root@precise1:~# echo $$
1

Coś się uruchomiło, pid 1 ma, ale sprawdźmy czy to na pewno shell.

root@precise1:~# readlink /proc/$$/exe
/sbin/init

Co się stało? Otóż korzystamy cały czas z oryginalnego /proc, stworzonego poza naszym pid namespacem. Wirtualizacja dalej działa, bo np. wysłać sygnałów się innym procesom nie da, ale i tak nasz nowy init widzi wszystkie procesy w systemie. W związku z tym pod pidem 1 widzi prawdziwego inita a sam siebie znalazłby gdzieś indziej -– ale nie ma możliwości ustalenia swojego identyfikatora w głównym namespace.

Podmontujmy mu poprawną wersję /proc i spróbujmy jeszcze raz.

root@precise1:~# lxc-unshare -s 'PID|MOUNT' /bin/bash
root@precise1:~# umount /proc && mount /proc
root@precise1:~# readlink /proc/$$/exe
/bin/bash

Teraz wszystko działa poprawnie.

Nie jest to błąd czy jakiś niedokończony kod, po prostu procfs pamięta, w jakim pid namespace był stworzony i przedstawia jego zawartość, a nie zawartość pidns procesu, który go przegląda.

Inny przykład -– spróbujmy ograniczyć zapis na dysk za pomocą blkio cgroup. (/sys/fs/cgroup/blkio zostało wcześniej stworzone przez skrypty startowe) Tworzymy grupę, dodajemy do niej swojego shella i ograniczamy do ok. 1MB/s zapisu na konkretne urządzenie.

root@precise1:~# mkdir /sys/fs/cgroup/blkio/slow
root@precise1:~# echo $$ > /sys/fs/cgroup/blkio/slow/tasks 
root@precise1:~# echo '253:0 1000000' > /sys/fs/cgroup/blkio/slow/blkio.throttle.write_bps_device 
root@precise1:~# dd if=/dev/zero of=zero bs=1M count=500 && rm zero
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 6.12093 s, 85.7 MB/s

Testujemy i wychodzi, że ograniczenie nie jest w ogóle brane pod uwagę. Niestety, limitowanie zapisów buforowanych, czyli w zasadzie „tych normalnych”, nie jest póki co obsługiwane, więc dd idzie z pełną prędkością. Swoją drogą widać tu narzut KVMa, bo testowany dysk bezpośrednio z maszyny fizycznej jest w stanie osiągnąć ok. 110 MB/s.

Co można z tym zrobić? W warunkach testowych możemy przełączyć dd na zapis synchroniczny, gdzie limity są ściśle przestrzegane, ale w praktyce nie ma takiej możliwości, bo zapis byłby bardzo wolny:

root@precise1:~# dd if=/dev/zero of=zero bs=1M count=500 oflag=sync && rm zero
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 528.348 s, 992 kB/s

Żeby wilk był syty i owca cała, możemy ustawić limit na to urządzenie w głównej grupie blkio. Daje to kolejny, bardzo dobry argument za tym, żeby na każdy kontener tworzyć nową partycję lub wolumin LVM.

root@precise1:~# echo '253:0 1000000' > /sys/fs/cgroup/blkio/blkio.throttle.write_bps_device 
root@precise1:~# dd if=/dev/zero of=zero bs=1M count=500 && rm zero
500+0 records in
500+0 records out
524288000 bytes (524 MB) copied, 377.854 s, 1.4 MB/s

Po nałożeniu limitu na główną grupę prędkość zapisu spada do oczekiwanej. ‚dd’ pokazuje wyższą prędkość bo nie czeka aż wszystkie dane znajdą się na dysku, ale dane trafiają na dysk dokładnie ze skonfigurowaną prędkością.

Kolejna pułapka pokazuje, jak łatwo przejechać się na braku wirtualizacji sysfs. Po podmontowaniu tego systemu plików wewnątrz kontenera można ustawić program wykonywany przez jądro po nadejściu zdarzenia związanego ze sprzętem. Problem w tym, że ten program jest wykonywany w kontekście gospodarza a odpowiednie zdarzenie jest bardzo łatwo wywołać. Ten przykład pochodzi ze strony https://blog.bofh.it/debian/id_413

lxc$ cat <<END > /tmp/evil-helper
#!/bin/sh
echo 'hi!' >> /tmp/evil-helper.log
END
lxc$ chmod +x /tmp/evil-helper

lxc# mkdir /sys
lxc# mount -t sysfs sysfs /sys
lxc# echo /var/lib/lxc/test/rootfs/tmp/evil-helper > /sys/kernel/uevent_helper
lxc# echo change > /sys/class/mem/null/uevent

Co prawda nie da się w ten sposób uzyskać uprawnień roota, ale będąc rootem można łatwo uciec z kontenera.

Podsumowując

Implementacja namespace’ów nie jest jeszcze na tyle kompletna, żeby bez strachu wpuścić do kontenera niezaufanego roota, ale i tak daje ciekawe możliwości. Zależnie od perspektywy, to samo środowisko możemy uważać za wirtualizację z kontenerami itd., jak i za pojedynczy system, ale z dobrze odizolowanymi użytkownikami czy usługami.

LXC nie jest rozwiązaniem całkowicie unikalnym. Istnieją przynajmniej dwa projekty, które mają podobny cel jak LXC – OpenVZ i Linux-VServer. Jak się one mają do LXC?

OpenVZ jest otwartym komponentem komercyjnego Virtuozzo, co ma swoje plusy i minusy. Z jednej strony –- oferuje duże możliwości (m.in. przenoszenie działających kontenerów między maszynami) i dobre wsparcie (pracują nad nim developerzy jądra). Z drugiej strony –- jako system na gospodarzu wspierany jest w zasadzie wyłącznie RedHat i pochodne, a oferowane kontenery są bardzo monolityczne (nie da się zwirtualizować np. tylko sieci).

Linux-VServer ma dużo słabsze wsparcie komercyjne, ale dużo szybciej nadąża za rozwojem jądra. Architektura też jest bardziej podobna do LXC i w coraz większej części korzysta z namespace’ów. Zgodnie z deklaracją głównego autora, niewykluczone że w przyszłości stanie się wyłącznie (lub w zdecydowanej większości) zestawem narzędzi do obsługi kontenerów.

  • 99

    świetna robota – gratulacje!

  • Grassoalvaro

    Używacie lub planujecie wdrożenie tego czy to tylko taki risercz rozwiązań?

    • Grzegorz Nosek

      Planujemy jak najbardziej. Produkcyjnie jeszcze nie używamy (prace w toku) ale mimo paru haczyków to jest najbardziej przyszłościowe rozwiązanie.

  • Muerte

    Fajny artykuł, przyjazne technicznie 😉

    • magdazarych

      Artykuł jest rozszerzeniem prelekcji z Dni Wolnego Oprogramowania i w tym roku Grzesiek będzie miał kolejną. Wciąż w temacie lxc – opowie o kontenerach per user. Postaramy się później zrobić z tego artykuł na blogaska 🙂