Zastosowanie KSM w maszynach wirtualnych QEMU/KVM

Spis treści

Użytkownicy linux'a, którzy korzystają z mechanizmu wirtualizacji QEMU/KVM, wiedzą, że takie maszyny wirtualne potrafią zjadać dość sporo pamięci operacyjnej. Im więcej takich maszyn zostanie uruchomionych w obrębie danego hosta, tym większe ryzyko, że nam tego RAM'u zwyczajnie zabraknie. Można oczywiście ratować się dokupieniem dodatkowych modułów pamięci ale też nie zawsze taki zabieg będzie możliwy, zwłaszcza w przypadku domowych stacji roboczych pokroju desktop/laptop. Szukając rozwiązania tego problemu natrafiłem na coś, co nazywa się Kernel Samepage Merging. W skrócie, KSM to mechanizm, który ma na celu współdzielenie takich samych stron pamięci operacyjnej przez kilka procesów. W ten sposób można (przynajmniej teoretycznie) dość znacznie obniżyć zużycie RAM, zwłaszcza w przypadku korzystania na maszynach wirtualnych z tych samych systemów operacyjnych. Przydałoby się zatem ocenić jak bardzo KSM wpłynie na wykorzystanie pamięci i czy będzie z niego jakiś większy użytek zarówno przy korzystaniu z maszyn wirtualnych, czy też w codziennym użytkowaniu komputera.

Jak działa KSM

Kernel Samepage Merging (KSM) to mechanizm kernela linux, mający na celu zoptymalizować wykorzystanie pamięci RAM przez hiperwizor (w tym przypadku QEMU/KVM). Działa on na zasadzie współdzielenia stron pamięci przez wiele maszyn wirtualnych działających w obrębie jednego hosta. Zwykle współdzieleniu podlegać będą biblioteki oraz inne często używane dane, których kontent pozostaje w miarę stały w czasie. Zatem KSM pozwala na uruchomienie większej liczby systemów gościa opartych o ten sam system operacyjny. W takiej sytuacji nie trzeba duplikować całej masy informacji, przez co mniej pamięci RAM jest potrzebne do obsługi maszyn wirtualnych.

Pierwotnie mechanizm KSM był stworzony głównie na potrzeby maszyn wirtualnych, by można było ich uruchomić więcej niż pozwalały na to ograniczenia związane z zainstalowaną pamięcią fizyczną. W obecnych czasach, KSM znajduje zastosowanie również poza zwirtualizowanym środowiskiem, tj. wszędzie tam gdzie zużycie pamięci RAM ma dla nas ogromne znaczenie (nawet w systemach wbudowanych).

Koncept współdzielenia pamięci nie jest niczym nowym w obecnych systemach operacyjnych. Dla przykładu, gdy uruchamiamy jakąś aplikację po raz pierwszy, współdzieli ona całą swoją pamięć z procesem swojego rodzica (parent proccess). Gdy któryś z tych dwóch procesów (rodzic albo dziecko) próbuje zmodyfikować tę pamięć, kernel alokuje nowy region pamięci, kopiuje tam oryginalny kontent i zezwala programowi na modyfikację tego nowego obszaru pamięci. Tego typu operacja jest znana pod nazwą Copy on Write (CoW), czyli kopiowanie przy zapisie.

Mechanizm KSM w linux'ie korzysta dokładnie z tego samego rozwiązania tyle, że w odwrotnej kolejności. KSM umożliwia kernelowi zbadanie dwóch (lub więcej) uruchomionych programów i porównanie ich pamięci. Jeśli którekolwiek strony pamięci operacyjnej są identyczne, to KSM redukuje je do jednej strony. Ta strona jest następnie oznaczana jako CoW. Jeśli w późniejszym czasie dany proces maszyny wirtualnej będzie chciał zmodyfikowana którąś z współdzielonych stron pamięci, to dla tego gościa zostanie utworzona nowa strona pamięci, w której te zmiany zostaną odzwierciedlone. Pozostałe maszyny wirtualne będą współdzielić w dalszym ciągu tę starą stronę pamięci.

Kiedy maszyna wirtualna jest uruchamiana, dziedziczy ona tylko pamięć z procesu QEMU/KVM. Po uruchomieniu systemu gościa, zawartość obrazu systemu operacyjnego gościa może być współdzielona przy założeniu, że inne systemy gościa korzystają dokładnie z tego samego systemu operacyjnego lub aplikacji. KSM umożliwia KVM zażądanie współdzielenia tych identycznych regionów pamięci gościa, przez co poprawia szybkość i wykorzystanie pamięci operacyjnej. W KSM wspólne dane procesu są przechowywane w pamięci podręcznej lub w pamięci głównej. Zmniejsza to liczbę błędów pamięci podręcznej (cache miss) dla systemów gościa, co może poprawić wydajność niektórych aplikacji i systemów operacyjnych.

Większe obciążenie procesora

Może i w pewnych sytuacjach będziemy w stanie "zwiększyć" nawet parokrotnie ilość dostępnej pamięci operacyjnej w systemie ale taki zabieg ma swoje konsekwencje. Obsługa mechanizmu KSM, dzięki któremu możemy uruchomić więcej maszyn wirtualnych, jest bardzo kosztowa jeśli chodzi o wykorzystanie procesora. Generalnie to im intensywniejsze będzie skanowanie stron pamięci w poszukiwaniu kandydatów do współdzielenia, to tym wyższy stopień deduplikacji uda nam się uzyskać ale też i procesor będzie wykorzystywany w większym stopniu. Dlatego KSM nie zawsze znajdzie zastosowanie i czasem lepszym wyjściem jest po prostu dokupienie większej ilości pamięci RAM.

Ryzyko wyczerpania się pamięci

Trzeba sobie także zdawać sprawię, że takie współdzielone strony pamięci mogą powodować zagrożenie związane z wyczerpaniem się pamięci operacyjnej. Załóżmy, że mamy dwa procesy i jeden z nich będzie chciał zmienić współdzieloną stronę pamięci. W takim przypadku system będzie musiał tę stronę pamięci skopiować. W efekcie, w pamięci RAM będą figurować dwie osobne strony (po jednej dla każdego z tych dwóch procesów), czyli dokładnie taka sama sytuacja, co w przypadku braku korzystania z KSM. Jeśli teraz uruchomimy wiele maszyn wirtualnych i zaistnieje jakieś zdarzenie, które sprawi, że spora ilość stron współdzielonych przestanie być współdzielona, to może się okazać, że nagle zabraknie nam pamięci operacyjnej, by te nowe strony pomieścić i system albo się powiesi, albo pozabija część maszyn wirtualnych.

Problemy z bezpieczeństwem KSM

W przeszłości, mechanizm KSM miewał poważne problemy z bezpieczeństwem, gdzie kolizje hash'ów mogły doprowadzić do wstrzyknięcia kodu w procesy należące do innych użytkowników, czy też maszyn wirtualnych. Później hash'e zostały zastąpione drzewami czerwono-czarnymi, przez co udało się zaadresować ten problem z kolizją hash'ów.

Wygląda jednak na to, że nie wszystkie obawy związane z bezpieczeństwem stosowania KSM zostały rozwiane, bo w systemach typu hardened, ten mechanizm jest wyłączony. Dla przykładu w CLIP OS, powód jaki został podany za tym, by KSM wyłączyć, to możliwość ułatwienia ataków typu side channel, takich jak FLUSH + RELOAD, które są wymierzone w pamięci cache L3 głównego procesora komputera. Generalnie chodzi o problem współdzielenia stron pamięci operacyjnej przez niezaufane procesy, a tych w maszynach wirtualnych nie brakuje. Wykorzystując mechanizm KSM jesteśmy zatem narażeni na wyciek informacji i kompromitację bezpieczeństwa systemu.

Jak włączyć/wyłączyć KSM na Debianie

Teoretycznie KSM wygląda na bardzo ciekawą opcję, która jest nam w stanie pozwolić dość znacznie zoptymalizować wykorzystanie pamięci w naszym linux'e. Jak można przeczytać na wiki, na hoście z 16G RAM udało się odpalić 52 maszyny wirtualne z przydziałem 1G wirtualnej pamięci operacyjnej. Zatem z 16G udało się zrobić 52G RAM. Przydałoby się sprawdzić jak ten mechanizm radzi sobie w realnych warunkach i czy faktycznie jest taki dobry jak to ludzie piszą na necie.

KSM jest zwykle obecny w dystrybucyjnych kernelach. Jeśli jednak trafi nam się takie jądro, które nie wspiera KSM, to trzeba będzie w nim włączyć opcję CONFIG_KSM . Do obsługi tego mechanizmu oddelegowane zostały pliki w katalogu /sys/kernel/mm/ksm/ i to przy ich pomocy możemy włączyć i wyłączyć KSM oraz kontrolować jego zachowanie podczas pracy systemu.

By włączyć KSM, wystarczy przesłać 1 do pliku /sys/kernel/mm/ksm/run :

# echo "1" > /sys/kernel/mm/ksm/run

KSM posiada predefiniowaną konfigurację i w zasadzie w sporej części przypadków, te domyślne ustawienia powinny nam wystarczyć. Rozruch KSM zajmie zwykle parę minut, także nie stresujmy się z początku, jeśli statystyki pamięci nie zmienią się w żaden sposób. Po prostu trzeba trochę poczekać, aż system dojdzie do wniosku, że pewne strony pamięci można skonsolidować.

Gdy w późniejszym czasie zajdzie potrzeba wyłączenia KSM, to można ten zabieg przeprowadzić na dwa sposoby. Pierwszym jest zwykłe zatrzymanie demona ksmd przez przesłanie 0 do pliku /sys/kernel/mm/ksm/run . W takim przypadku, aktualnie współdzielone strony pamięci w dalszym ciągu będą mogły być współdzielone między procesami, przynajmniej do momentu aż któryś z tych procesów nie zmieni tych stron. Jeśli jednak chcielibyśmy przy wyłączaniu demona ksmd usunąć przy okazji wszystkie współdzielone strony pamięci, to trzeba przesłać wartość 2 :

# echo "2" > /sys/kernel/mm/ksm/run

Włączanie KSM powyższym sposobem, czy też ogólnie operowanie na tym mechanizmie ma jedną wadę -- po restarcie maszyny trzeba będzie tę konfigurację zaaplikować jeszcze raz. Oczywiście by sobie ulżyć z tym zadaniem, możemy zamieścić stosowne wpisy w pliku /etc/sysfs.conf , przykładowo:

kernel/mm/ksm/run = 1

Usługi ksm.service oraz ksmtuned.service

W Debianie dostępny jest pakiet ksmtuned , który dostarcza usługi ksm.service oraz ksmtuned.service . Zadaniem usługi ksm.service jest jedynie uruchomienie mechanizmu KSM przez przesłanie 1 do pliku /sys/kernel/mm/ksm/run . Opcjonalnie też można przez plik /etc/default/ksm ustawić maksymalną ilość stron pamięci, których kernel nie będzie w stanie przenieść do SWAP i to te właśnie strony będą do wykorzystania przez KSM. Wygląda jednak na to, że ten ficzer jest jakimś reliktem przeszłości, bo już od dość dawna KSM może operować na stronach pamięci, które zostaną przeniesione do SWAP, choć współdzielenie tych stron zostanie usunięte po wydobyciu ich ze SWAP. Oczywiście w dalszym ciągu te strony będą mogły być współdzielone ale trzeba będzie chwilę poczekać aż demon ksmd na nowo je oznaczy. Jeśli zaś chodzi o usługę ksmtuned.service , to ma ona za zadanie uruchamiać i zatrzymywać usługę ksm.service w zależności od tego czy współdzielenie pamięci jest potrzebne. Ta usługa jest także w stanie dostosować konfigurację KSM. Demon ksmtuned jest także powiadamiany przez libvirt o tym czy maszyna wirtualna została uruchomiona lub zatrzymana.

Próbowałem te dwie usługi skonfigurować według informacji zawartych na stronie RedHat'a ale nie przyniosło to żadnego efektu. Wygląda na to, że te usługi nie działają poprawnie (albo też nie potrafię ich poprawnie skonfigurować). Zamiana jakiejkolwiek wartości w pliku konfiguracyjnym /etc/ksmtuned.conf nie przynosi żadnego efektu w kwestii dostosowania zachowania mechanizmu KSM. Brak rzeczowej dokumentacji sprawia, że nie chce mi się zbytnio zastanawiać co tutaj może być nie tak. Dlatego też lepiej odpuścić sobie te narzędzia i konfigurować KSM bezpośrednio przez katalog /sys/kernel/mm/ksm/ (via plik /etc/sysfs.conf ) lub też przy pomocy własnych skryptów shell'owych.

Mechanizm KSM w akcji

Zobaczmy zatem jak mechanizm KSM sprawuje się na naszym linux'ie. Żeby mieć jakieś porównanie, dobrze jest uruchomić parę maszyn wirtualnych przed włączeniem KSM, tj. przed przesłaniem 1 do pliku /sys/kernel/mm/ksm/run . W tym przypadku zostały uruchomione dwie maszyny wirtualne z Ubuntu na pokładzie. Jedna maszyna jest klonem drugiej. Powinniśmy mieć zatem dokładnie ten sam system na obu maszynach. Statystyki utylizacji pamięci RAM prezentują się mniej więcej tak:

# ps_mem
 Private  +   Shared  =  RAM used       Program

  3.9 GiB +  11.0 MiB =   3.9 GiB       qemu-system-x86_64 (2)

Mamy zatem 3.9 GiB zajętego RAM'u przez procesy maszyn wirtualnych. Warto zwrócić uwagę, że ilość danych w kolumnie Shared jest niewielka. Co ciekawe pewne dane będą współdzielone zawsze bez względu na to, czy korzystamy z KSM czy też nie. Dzieje się tak dlatego, że współdzieleniu zawsze podlega segment .text programu.

Po przesłaniu 1 do pliku /sys/kernel/mm/ksm/run , otrzymujemy coś takiego:

# ps_mem
 Private  +   Shared  =  RAM used       Program

  3.7 GiB +  56.4 MiB =   3.7 GiB       qemu-system-x86_64 (2)

No nie widać jakoś specjalnie różnicy. Trochę więcej danych jest w kolumnie Shared . Jeśli jednak poczekamy kilka minut, to będziemy mieć poniższy wynik:

# ps_mem
 Private  +   Shared  =  RAM used       Program

  3.2 GiB + 155.6 MiB =   3.4 GiB       qemu-system-x86_64 (2)

Jak widać, wartość w kolumnie Shared urosła przy jednoczesnym obniżeniu się pozostałych wartości. Gdybyśmy jeszcze trochę poczekali, to uzyskamy znacznie lepszy wynik:

# ps_mem
 Private  +   Shared  =  RAM used       Program

  2.6 GiB + 479.6 MiB =   3.0 GiB       qemu-system-x86_64 (2)

Wartość w kolumnie Shared dość mocno wzrosła, przez co każda z tych dwóch maszyn wirtualnych zjada trochę mniej pamięci operacyjnej, bo przecież część danych jest współdzielona. Im dłużej te maszyny wirtualne pochodzą, tym więcej danych będzie współdzielonych, bo system ostatecznie przyjmie, że pewne dane w przewidywalnej przyszłości raczej nie zostaną w żaden sposób zmienione, a że dodatkowo te dane są takie same, to można je współdzielić.

Czasami ps_mem może niedokładnie prezentować wartości zużycia RAM'u. Zawsze możemy podejrzeć pliki w katalogu /sys/kernel/mm/ksm/ , a konkretnie pages_sharing oraz pages_shared, by zobaczyć ile udało nam się tej pamięci zaoszczędzić. Parametr pages_shared odpowiada za ilość stron pamięci, które podlegają współdzieleniu, a parametr pages_sharing mówi ile pamięci RAM udało się faktycznie odzyskać. W tym przypadku mamy wartości 137077 i 278260 . Obie te wartości są w stronach pamięci, a każda strona to 4096 bajtów. Zatem mamy odpowiednio (137077×4096)/1024/1024=535.46 MiB oraz (278260×4096)/1024/1024=1086.95 MiB, czyli udało się zaoszczędzić około 1,1 GiB RAM.

Prawdę mówiąc oczekiwałem trochę lepszego wyniku. Te dwie maszyny wirtualne mają przydział po 2 GiB RAM. Bez KSM utylizacja pamięci operacyjnej przez tych gości powinna być na poziomie 4 GiB. Z KSM mamy około 3 GiB, czyli zysk około 25%. W tym przypadku mamy do czynienia ze sklonowanymi systemami, na których w zasadzie nic nie było robione. Jest to bardzo rzadko spotykana sytuacja i w realnych warunkach raczej trzeba by się spodziewać, że te maszyny wirtualne będą się nieco różnić, np. przez inny stopień aktualizacji czy uruchomione aplikacje. Zatem faktyczna oszczędność pamięci będzie jeszcze niższa.

Dlaczego zatem w przypadku windows'ów można zaoszczędzić tak dużo pamięci, a na linux tak niewiele? Wygląda na to, że windows zeruje nieużywaną pamięć, przez co ta niewykorzystana pamięć maszyny wirtualnej zawiera same zera. Strony pamięci zawierające same zera są takie same, więc można je współdzielić i to dlatego mamy tak ogromny boost w przypadku systemów gości mających uruchomionego windows'a.

Jeśli zaś chodzi o zużycie procesora przez proces ksmd , to mamy tutaj do czynienia z dodatkowym narzutem na poziomie około 5% (wahania w zakresie 3% - 7%). Nie wydaje się to być dużą wartością, no ale mało to też nie jest, zwłaszcza gdy pod uwagę weźmiemy odzyskanie jedynie 1 GiB RAM. Trzeba mieć na uwadze też fakt, że im bardziej będziemy wykorzystywać maszyny wirtualne, tj. zmieniać ich dane w RAM podczas ich pracy (uruchamiać/zamykać aplikacje), tym mniej danych będzie współdzielonych, a zużycie procesora na potrzeby ksmd będzie mniej więcej stałe. Może się zdarzyć zatem tak, że procesor będzie utylizowany, a my przy tym nie zauważymy praktycznie różnicy w wykorzystywaniu pamięci RAM lub też ta różnica będzie niewielka.

Tuning parametrów pages_to_scan i sleep_millisecs

Domyślna konfiguracja demona ksmd działa raczej w porządku. Możemy jednak nieco podbić lub obniżyć wartości parametrów przechowywanych w /sys/kernel/mm/ksm/ , tak by dobrać konfigurację KSM pod nasz system indywidualnie. Możemy zwiększyć ilość stron pamięci ( pages_to_scan ) z domyślnych 100 do 1000, które ksmd będzie skanował w jednym podejściu. Dodatkowo możemy zmniejszyć interwał między kolejnymi skanami ( sleep_millisecs ) z domyślnych 20 ms na 10 ms. W ten sposób więcej stron będzie skanowanych i ten proces będzie przebiegał częściej. Ostatecznie udało mi się uzyskać ten poniższy wynik:

# ps_mem
 Private  +   Shared  =  RAM used       Program

  2.2 GiB + 765.5 MiB =   2.9 GiB       qemu-system-x86_64 (2)

Jak widać, wartość w kolumnie Shared skoczyła w górę jeszcze bardziej. Podobnie z wykorzystaniem procesora, tj. z ~5% mamy wzrost na około 15%. Zatem bardziej intensywne skanowanie jest w stanie nam zwrócić trochę więcej pamięci ale dość sporym kosztem procesora. Warto też zaznaczyć tutaj, że system będzie nam tę pamięć szybciej odzyskiwał niż w przypadku wolniejszych skanów.

Efektywność mechanizmu KSM

Efektywność KSM możemy sprawdzić korelując dane parametrów pages_shared , pages_sharing , pages_unshared oraz pages_volatile . Poniżej są wartości, które udało mi się wyłapać:

               jedna maszyna | dwie maszyny | dwie maszyny po 20 minutach
pages_shared        7284           12434                168067
pages_sharing      28539           90630                299539
pages_unshared    100180          173383                482783
pages_volatile    312701          600636                 23884

Wysokie ratio pages_sharing/pages_shared oznacza, że KSM działa bardzo dobrze, bo wiele stron jest współdzielonych między procesami. W tym przypadku mamy odpowiednio 3.92/7.29/1.72. Zatem widać, że po uruchomieniu drugiej maszyny wirtualnej sporo tych stron jest współdzielonych ale po chwili ten współczynnik dość mocno spada. Wysokie ratio pages_unshared/pages_sharing oznacza marnotrawstwo zasobów i tutaj mamy 3.51/1,91/1.61 , czyli im dłużej te maszyny działają, tym lepiej. Wartości w pages_volatile oznaczają strony, które się zmieniają zbyt szybko, by system je rozważał jako nadające się do współdzielenia. Jak widać, po inicjacji maszyn wirtualnych, ta wartość spadła dość znacznie.

Podsumowanie

Jakby nie patrzeć trochę pamięci RAM można odzyskać stosując mechanizm KSM na linux. W moim systemie, jedyną aplikacją, która jest w stanie robić użytek z KSM, to QEMU/KVM, zatem w sporej części przypadków ten mechanizm będzie miał zastosowanie jedynie przy wirtualizacji lub też będzie kompletnie nieużywany. Na szczęście jeśli nie korzystamy z KSM (nawet jeśli włączony jest on w kernelu), to system nie zużywa praktycznie żadnych dodatkowych zasobów. Gdy jednak już z KSM zaczniemy korzystać, to sporo mocy procesora trzeba będzie przeznaczyć na obsługę tego mechanizmu. Im więcej RAM'u będziemy chcieli odzyskać, tym więcej mocy procesora trzeba będzie poświęcić. Odzysk pamięci RAM w postaci 25% może dla jednych być sporą wartością ale dla mnie nie jest on wart utraty cykli procesora rzędu 10-15%. Do tego dochodzą jeszcze problemy z bezpieczeństwem, przez co ogólny całokształt nie renderuje się zbyt pozytywnie. Na moich systemach, gdzie bezpieczeństwo jest priorytetem, KSM nie znajdzie zastosowania. Lepiej jest dokupić ten 1 GiB RAM na każde 4 GiB zainstalowanej pamięci fizycznej, tak by nie katować procesora niepotrzebnie i nie martwić się o sprawy związane z bezpieczeństwem systemu i danych w nim przechowywanych.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.