Jakiś czas temu, ktoś na forum napisał posta o kompresji danych w pamięci RAM, dodatkowo była też wzmianka o SWAPie. Jako, że wszyscy zachwalają ten sposób z kompresją danych, zwłaszcza na słabych maszynach, tych z małą ilością RAM, to postanowiłem sprawdzić na ile są wiarygodne te wierzenia w rzekomy lepszy performance, no jakby nie patrzeć ciężko znaleźć kogoś kto ma mniej RAMu ode mnie.

Na sam początek trochę słów o moim sprzęcie, a konkretnie o RAMie i procesorze, bo głównie od tych dwóch czynników będzie zależał wynik końcowy. Zgodnie z tym co pokazuje inxi, RAM i procek prezentuje się następująco:

# inxi -F -c 21
...
CPU:       Dual core Intel Pentium D CPU (-MCP-) cache: 2048 KB flags: (lm nx sse sse2 sse3)
           Clock Speeds: 1: 3000.00 MHz 2: 3000.00 MHz
...
Info:      Processes: 227 Uptime: 10:24 Memory: 829.6/1001.2MB Client: Shell (bash) inxi: 1.9.17

Czyli mam dwurdzeniowy procesor i 1GiB pamięci RAM, czyli skompresowany RAM i SWAP powinny przypaść mi do gustu. Spróbujmy zatem wdrożyć tę technologię.

Kernel musi posiadać odpowiedni moduł, by w ogóle była możliwa zabawa ze ZRAMem:

# cat /boot/config-3.12-1-amd64 | grep -i zram
CONFIG_ZRAM=m
# CONFIG_ZRAM_DEBUG is not set

Bez tego ani rusz i jeśli nasz kernel nie posiada tego modułu, konieczna będzie rekompilacja kernela. Kolejna sprawa to załadowanie tego modułu na starcie systemu, zatem dopisujemy w /etc/modules poniższą linijkę:

zram num_devices=2

Trzeba tylko dobrać odpowiednią liczbę urządzeń, a zależy ona od ilości rdzeni procesora, mój procek ma 2 rdzenie, więc parametr num_devices przyjął wartość 2. Ale samo załadowanie modułu nic nam nie da, trzeba jeszcze stworzyć odpowiednie RAM dyski. Te RAM dyski to nic innego jak sformatowane na SWAP urządzenia zamontowane w RAMie. Mamy dwie opcje -- albo stworzyć skrypt startowy, albo zaprzęgnąć do tego celu udeva. Jeśli chcemy skrypt, to wygląda on mniej więcej tak:

#!/bin/bash
### BEGIN INIT INFO
# Provides:          zram
# Required-Start:    $syslog $remote_fs
# Required-Stop:     $syslog $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: zram devices
# Description:       zram devices
### END INIT INFO

# Author: Mikhail Morfikov morfikov[at]gmail.com

PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin

SCRIPTNAME="zram"

zram_start () {
modprobe zram num_devices=2

SIZE="250"
echo $(($SIZE*1024*1024)) > /sys/block/zram0/disksize
echo $(($SIZE*1024*1024)) > /sys/block/zram1/disksize

mkswap -L zram0 /dev/zram0
mkswap -L zram1 /dev/zram1

swapon /dev/zram0 -p 70
swapon /dev/zram1 -p 70
}

zram_stop () {
swapoff /dev/zram0
swapoff /dev/zram1

echo 1 > /sys/block/zram0/reset
echo 1 > /sys/block/zram1/reset

modprobe -r zram
}

case "$1" in
    start)
        zram_start || exit 1
    ;;
    stop)
        zram_stop || exit 1
    ;;
    force-reload|restart)
        zram_stop && sleep 1
        zram_start || exit 1
    ;;
        *)
        echo "Usage: $SCRIPTNAME {start|stop|restart}"
        exit 1
    ;;       
esac

exit 0

Można również skorzystać z udeva. To wymaga stworzenia pliku z regułami /etc/udev/rules.d/70-zram.rules o treści:

KERNEL=="zram0", SUBSYSTEM=="block", DRIVER=="", ACTION=="add", ATTR{disksize}=="0", ATTR{disksize}="150M", RUN+="/sbin/mkswap $env{DEVNAME}"
KERNEL=="zram1", SUBSYSTEM=="block", DRIVER=="", ACTION=="add", ATTR{disksize}=="0", ATTR{disksize}="150M", RUN+="/sbin/mkswap $env{DEVNAME}"

Kluczowe jest tutaj ustalenie wartości ATTR{disksize}, która to określa rozmiar jednego RAM dysku. Teoretycznie nie powinno się stosować wartości większej niż 50% dostępnego RAMu, czyli jeśli ja mam 1GiB, to maksymalna wartość jaką mógłbym użyć na ZRAM to 512MiB, po 256MiB na każdy z rdzeni procesora. Ale przy takiej wartości, komp trochę zaczynał mulić i zadziałało to zjadliwie przy wartości 300MiB, czyli 150MiB na każdy rdzeń.

Dodatkowo trzeba dodać do /etc/fstab dwa wpisy, które zamontują nam te urządzenia ZRAM przy starcie systemu:

/dev/zram0       swap    swap    defaults,pri=70     0 0
/dev/zram1      swap    swap    defaults,pri=70     0 0
UUID=3f9f24d7-86d1-4e21-93e9-f3c181d05cf0   swap    swap    defaults,pri=10     0 0

Powyżej jest również pokazany mój zwykły SWAP. Różni się on jedynie priorytetem -- trzeba je nadać tak by wyższy priorytet przypadł urządzeniom ZRAM, bo w takim przypadku dane pierw będą zrzucane do tych RAM dysków, a gdy zabranie w nich miejsca, dane polecą do tradycyjnego SWAPa.

Pozostała jeszcze kwestia skompresowanego SWAPu, tego zwykłego. Tutaj sprawa ma się dość prosto, trzeba dopisać jeden parametr do linijki kernela -- zswap.enabled=1 . Edytujemy zatem konfigurację extlinuxa (lub gruba), plik /boot/extlinux/extlinux.conf i dopisujemy ten parametr:

APPEND root=/dev/mapper/debian_crypt-root cgroup_enable=memory zswap.enabled=1 fbcon=scrollback:124k ro

Po resecie maszyny sprawdzamy czy aby wszytko zostało utworzone i zamontowane prawidłowo:

# swapon -s
Filename                                Type            Size    Used    Priority
/dev/mapper/debian_crypt-swap           partition       2097148 398368  10
/dev/zram0                              partition       153596  153480  70
/dev/zram1                              partition       153596  153500  70

Jak widać powyżej, mam zapchane oba urządzenia ZRAM, łącznie prawie 700MiB siedzi w tych SWAPach. Samo zachowanie się urządzeń ZRAM można monitorować przez pliki w katalogu /sys/block/zram0 oraz /sys/block/zram1 . Dodatkowo znalazłem skrypt, który pokazuje stopień kompresji danych:

#!/bin/sh -eu
 
TOTAL_DATA=0
TOTAL_COMP=0
HAS_ZRAM=
 
# Iterate through swap devices searching for compressed ones
while read NAME _; do
# Filter zram swaps and let's hope your ordinary swap doesn't have
# "zram" in its name :D
case $NAME in
*zram*) ;;
*) continue
esac
 
DIR=/sys`udevadm info --query=path --name=$NAME`
read DATA <$DIR/orig_data_size
read COMP <$DIR/mem_used_total
TOTAL_DATA=$((TOTAL_DATA + DATA))
TOTAL_COMP=$((TOTAL_COMP + COMP))
HAS_ZRAM=1
done </proc/swaps
 
# Extract physical memory in kibibytes and scale back to bytes
{
read _ MEM_KIB _
read _ FREE_KIB _
read _ BUF_KIB _
read _ CACHE_KIB _
} </proc/meminfo
 
/usr/bin/printf "\
Physical memory: %'17d bytes
Buffers and cache: %'17d bytes / %4.1f%% of total memory
Unallocated: %'17d bytes / %4.1f%% of total memory
" `echo "scale=6; 1024*$MEM_KIB; 1024*($BUF_KIB+$CACHE_KIB); 100*($BUF_KIB+$CACHE_KIB)/$MEM_KIB; 1024*$FREE_KIB; 100*$FREE_KIB/$MEM_KIB"|bc`
 
if test "$HAS_ZRAM"; then
/usr/bin/printf "\
Compressed: %'17d bytes / %4.1f%% of total memory
Uncompressed size: %'17d bytes / %.3f compression ratio
" `echo "scale=6; $TOTAL_COMP; 100*$TOTAL_COMP/$MEM_KIB/1024; scale=6; $TOTAL_DATA; $TOTAL_DATA/$TOTAL_COMP"|bc`
else
echo "Compressed: 0 bytes / zram not in use"
fi

Po wywołaniu tego skryptu dostałem poniższy wynik:

# ./zraminfo.sh
Physical memory:      1,049,821,184 bytes
Buffers and cache:      116,097,024 bytes / 11.1% of total memory
Unallocated:             81,043,456 bytes /  7.7% of total memory
Compressed:             121,118,720 bytes / 11.5% of total memory
Uncompressed size:      346,472,448 bytes / 2.861 compression ratio

Jeśli dobrze odczytuję te wartości, to kompresja jest na poziomie prawie 300% tylko co nam po tych 300% skoro większość danych to cache? Przynajmniej tak wynika z przeprowadzonego przeze mnie testu (dostępny pod tym linkiem [1]). Ten cache zamiast rezydować sobie normalnie w RAMie, gdy ten nie jest używany, zapycha urządzenia co po paru minutach sprawia, że z tego 1 GiB ramu robi się 700, bo ten cache nie jest uwalniany...

Póki co nie udało mi się znaleźć informacji na temat tego czy takie zachowanie jest normalne czy też nie, bo jakby nie patrzeć wyrzucanie danych z RAMu na dysk by zrobić miejsce dla cache niezbyt pomaga słabym maszynom. Niemniej jednak, warto wiedzieć, że tego typu mechanizm jak ZRAM istnieje, a mi może kiedyś uda się rozwiązać tę zagadkę.


Przypisy:

  1. http://forum.dug.net.pl/viewtopic.php?id=25239