Ograniczanie zasobów zbyt żarłocznym procesom
Kategoria: Artykuły, etykiety: nice, ionice, cgroups, renice, taskset, trickle
Dodany: 2014-02-11 22:19
(zmodyfikowany: 2014-10-15 18:31)
Przez: morfik
Wyświetleń: 12507
Niektóre procesy czasem zjadają trochę więcej zasobów niż powinny, co może przyczynić się w pewnych sytuacjach do powieszenia systemu i bez twardego resetu się nie obejdzie. Oczywiście, najlepszym rozwiązaniem jest rozbudowanie maszyny ale co w przypadku gdy chcemy dać jakiemuś procesowi pierwszeństwo w korzystaniu z zasobów pc? Istnieje kilka rozwiązań, które mogą nam pomóc w ogarnięciu zasobożernych procesów.
1. Cgroups
Cgroups ma postać wirtualnego systemu plików, w którego skład wchodzą: blkio, cpu, cpuacct, cpusets, devices, freezer, memory, net_cls, net_prio oraz perf_event. Na stronie red hata jest to trochę bardziej przyjaźnie opisane niż na stronie kernela.
Z grubsza moduły odpowiadają kolejno kontrolę I/O dysków, za przydział procesora, za statystyki cpu, za przydział konkretnego rdzenia i nodów pamięci, za dostęp do urządzeń, za zatrzymywanie i wznawianie procesów, za przydział pamięci i staty pamięci, za przypisywanie pakietom sieciowym odpowiednich klas dla traffic control, za ustawianie priorytetu pakietom sieciowym generowanym przez określone aplikacje w oparciu o soket SO_PRIORITY i ostatnia pozycja za monitorowanie grup przy pomocy narzędzia pref .
W przeszłości debian miał pewne problemy z obsługą cgroups. Na szczęście, chyba większość z nich została wyeliminowana i by wdrożyć w naszym systemie ten mechanizm, musimy doinstalować poniższe pakiety:
# aptitude install cgmanager cgroup-bin cgroupfs-mount cgroup-tools
Ja nie korzystam z systemd dlatego też by u mnie cgroups działał jak należy, muszę dodatkowo doinstalować systemd-shim .
Trzeba się także upewnić czy aby na pewno mamy odpowiednie moduły w kernelu:
# grep -i cgroup /boot/config-3.16-2-amd64
CONFIG_CGROUPS=y
# CONFIG_CGROUP_DEBUG is not set
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_CPUACCT=y
# CONFIG_CGROUP_HUGETLB is not set
CONFIG_CGROUP_PERF=y
CONFIG_CGROUP_SCHED=y
CONFIG_BLK_CGROUP=y
# CONFIG_DEBUG_BLK_CGROUP is not set
CONFIG_NETFILTER_XT_MATCH_CGROUP=m
CONFIG_NET_CLS_CGROUP=m
CONFIG_CGROUP_NET_PRIO=y
CONFIG_CGROUP_NET_CLASSID=y
W tym przypadku, by móc skorzystać w pełni z możliwości oferowanych przez cgroups, trzeba będzie załadować dodatkowo dwa moduły -- xt_cgroup oraz cls_cgroup , oczywiście jeśli ich potrzebujemy. Jeśli tak, dopisujemy je do pliku /etc/modules :
xt_cgroup
cls_cgroup
Co sprawi, że te moduły będą ładowane na starcie systemu i nie będziemy musieli sobie nimi głowy zawracać.
W debianie dodatkowo trzeba dopisać do linijki kernela (extlinux, grub) poniższy parametr umożliwiający włączenie zarządzania pamięcią w cgroups:
cgroup_enable=memory
By sprawdzić poprawność konfiguracji cgroups, posłużymy się narzędziem lxc-checkconfig dostępnym w pakiecie lxc . Po weryfikacji, pakiet lxc możemy zwyczajnie usunąć, chyba, że interesują nas kontenery LXC . Sprawdzamy zatem, czy wszystko jest w porządku:
root:/sys/fs/cgroup/net_cls# lxc-checkconfig
Kernel configuration not found at /proc/config.gz; searching...
Kernel configuration found at /boot/config-3.16-2-amd64
--- Namespaces ---
Namespaces: enabled
Utsname namespace: enabled
Ipc namespace: enabled
Pid namespace: enabled
User namespace: enabled
Network namespace: enabled
Multiple /dev/pts instances: enabled
--- Control groups ---
Cgroup: enabled
Cgroup clone_children flag: enabled
Cgroup device: enabled
Cgroup sched: enabled
Cgroup cpu account: enabled
Cgroup memory controller: enabled
Cgroup cpuset: enabled
--- Misc ---
Veth pair device: enabled
Macvlan: enabled
Vlan: enabled
File capabilities: enabled
Note : Before booting a new kernel, you can check its configuration
usage : CONFIG=/path/to/config /usr/bin/lxc-checkconfig
Poszczególne części systemu cgroups, montowane są pod /sys/fs/cgroup/ :
# ls -al /sys/fs/cgroup/
total 0
drwxrwxrwt 14 root root 280 Oct 14 20:27 ./
drwxr-xr-x 8 root root 0 Oct 14 20:27 ../
dr-xr-xr-x 2 root root 0 Oct 14 20:27 blkio/
drwxr-xr-x 2 root root 60 Oct 14 20:25 cgmanager/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 cpu/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 cpuacct/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 cpuset/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 devices/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 freezer/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 memory/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 net_cls/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 net_prio/
dr-xr-xr-x 2 root root 0 Oct 14 20:27 perf_event/
Operowanie na cgroups odbywa się przez identyfikację procesu i aplikowanie reguł, co do tego ile zasobów ten proces może wykorzystać. Tylko RAM można ustawić na sztywno. Pozostałe parametry nie będą limitować zasobów w taki sposób jak człowiek może przypuszczać -- jeśli ustawimy max 50% procesora pod jakiś proces, to ten proces będzie zjadał dostępne zasoby jak gdyby nigdy nic ale w przypadku obciążenia maszyny jeśli by ten proces chciał zjadać więcej niż 50%, to mu zostanie to zabronione, dzięki czemu inny proces będzie mógł wykorzystać swój przydział i nie zostanie zduszony przez żarłoczną aplikację.
Potrzebujemy zatem dwóch rzeczy -- pliku konfiguracyjnego z regułami, na których to podstawie będą ograniczane zasoby oraz demona, który będzie identyfikował procesy jak tylko te zostaną zainicjowane i uzupełniał plik tasks o odpowiednie pidy.
Tworzymy zatem dwa skrypty startowe. Pierwszy z nich, plik /etc/init.d/cgrulesengd , będzie uruchamiał demona cgrulesengd:
#! /bin/sh
### BEGIN INIT INFO
# Provides: cgrulesengd
# Required-Start: $local_fs $syslog
# Required-Stop: $local_fs $syslog
# Should-Start: cgmanager cgroupfs-mount cgproxy cgconfig
# Should-Stop: cgmanager cgroupfs-mount cgproxy cgconfig
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop the cgroups rules engine daemon
# Description: CGroup Rules Engine is a tool for automatically using \
# cgroups to classify processes
### END INIT INFO
# Author: Mikhail Morfikov
# Do NOT "set -e"
PATH=/sbin:/usr/sbin:/bin:/usr/bin
NAME="cgrulesengd"
DESC="CGroup Rules Engine Daemon"
SCRIPTNAME="/etc/init.d/$NAME"
PIDFILE="/var/run/$NAME.pid"
LOCKFILE="/run/lock/$NAME.lock"
DAEMON="/usr/sbin/$NAME"
DAEMON_ARGS="-n -Q"
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 1
. /lib/init/vars.sh
. /lib/lsb/init-functions
do_start()
{
if [ -f "$LOCKFILE" ]; then
log_warning_msg "$NAME is already running with PID `cat ${PIDFILE}`"
exit 0
fi
log_daemon_msg "Starting $DESC" "$NAME"
start-stop-daemon --start --quiet --make-pidfile --pidfile $PIDFILE --background --name $NAME --exec $DAEMON -- $DAEMON_ARGS
RETVAL="$?"
if [ "$RETVAL" -eq "0" ]; then
log_end_msg 0
touch "$LOCKFILE"
else
log_end_msg 1
fi
}
do_stop()
{
if [ ! -f "$LOCKFILE" ]; then
log_warning_msg "$NAME is not running, so it can't be stopped"
exit 0
fi
log_daemon_msg "Stopping $DESC" "$NAME"
start-stop-daemon --stop --quiet --pidfile $PIDFILE --name $NAME
RETVAL="$?"
if [ "$RETVAL" -eq "0" ]; then
log_end_msg 0
rm -f $PIDFILE $LOCKFILE
else
log_end_msg 1
fi
}
case "$1" in
start)
do_start
;;
stop)
do_stop
;;
restart)
do_stop
do_start
;;
status)
status_of_proc "$DAEMON" "$NAME"
exit 0
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2
exit 0
;;
esac
exit 0
Nic innego mi nie przyszło do głowy przy przerabianiu tego niedziałającego skryptu dołączonego w pakiecie cgroup-bin . W każdym razie trzeba zwrócić uwagę na nagłówek -- ten skrypt musi się odpalić po skrypcie z regułami.
Drugi ze skryptów, /etc/init.d/cgconfig :
#!/bin/bash
### BEGIN INIT INFO
# Provides: cgconfig
# Required-Start: $local_fs $syslog
# Required-Stop: $local_fs $syslog
# Should-Start: cgmanager cgroupfs-mount cgproxy
# Should-Stop: cgmanager cgroupfs-mount cgproxy
# Default-Start: 2 3 4 5
# Default-Stop:
# Short-Description: Create and setup control group filesystem(s)
# Description: Create and setup control group filesystem(s)
### END INIT INFO
PATH=/sbin:/usr/sbin:/bin:/usr/bin
NAME="cgconfig"
DESC="Cgroups filesystem settings"
SCRIPTNAME="/etc/init.d/$NAME"
CONFIG_FILE="/etc/cgconfig.conf"
CGDIR='/sys/fs/cgroup'
. /lib/init/vars.sh
. /lib/lsb/init-functions
#if [ ! -d $CGDIR ]; then
# mkdir -p $CGDIR/{cpuset,cpu,cpuacct,memory,devices,freezer,net_cls,blkio,net_prio,perf_event}
#fi
do_mount() {
NUM=`grep "cgroup" /proc/mounts | awk '$3=="cgroup"' | wc -l`
if [ "$NUM" -lt "1" ]; then
log_warning_msg "Cgroups filesystem wasn't mounted"
# mount $CGDIR/cpuset
# mount $CGDIR/cpu
# mount $CGDIR/cpuacct
# mount $CGDIR/memory
# mount $CGDIR/devices
# mount $CGDIR/freezer
# mount $CGDIR/net_cls
# mount $CGDIR/blkio
# mount $CGDIR/net_prio
# mount $CGDIR/perf_event
mount -t cgroup -oblkio none $CGDIR/blkio
mount -t cgroup -ocpu none $CGDIR/cpu
mount -t cgroup -ocpuacct none $CGDIR/cpuacct
mount -t cgroup -ocpuset none $CGDIR/cpuset
mount -t cgroup -odevices none $CGDIR/devices
mount -t cgroup -ofreezer none $CGDIR/freezer
mount -t cgroup -omemory none $CGDIR/memory
mount -t cgroup -onet_cls none $CGDIR/net_cls
mount -t cgroup -onet_prio none $CGDIR/net_prio
mount -t cgroup -operf_event none $CGDIR/perf_event
fi
}
do_start() {
# for release in `ls $CGDIR/*/notify_on_release`;
# do echo 1 >$release; done;
for clone in `ls $CGDIR/*/cgroup.clone_children`;
do echo 1 > $clone; done;
echo 1 > $CGDIR/memory/memory.use_hierarchy
### FIREFOX ###
mkdir -p $CGDIR/cpu/users/firefox
echo '600' > $CGDIR/cpu/users/firefox/cpu.shares
mkdir -p $CGDIR/cpuacct/users/firefox
mkdir -p $CGDIR/cpuset/users/firefox
mkdir -p $CGDIR/memory/users/firefox
echo '500m' > $CGDIR/memory/users/firefox/memory.limit_in_bytes
echo '700m' > $CGDIR/memory/users/firefox/memory.soft_limit_in_bytes
# mkdir -p $CGDIR/net_cls/users/firefox
# echo '0x00010005' > $CGDIR/net_cls/users/firefox/net_cls.classid
# echo "eth0 3" > $CGDIR/net_prio/net_prio.ifpriomap
# mkdir -p $CGDIR/net_prio/users/firefox
# echo "eth0 5" > $CGDIR/net_prio/users/firefox/net_prio.ifpriomap
### QBITTORRENT ###
mkdir -p $CGDIR/cpu/users/qbittorrent
echo '300' > $CGDIR/cpu/users/qbittorrent/cpu.shares
# mkdir -p $CGDIR/net_cls/users/qbittorrent
# echo '0x00010002' > $CGDIR/net_cls/users/qbittorrent/net_cls.classid
# mkdir -p $CGDIR/net_prio/users/qbittorrent
# echo "eth0 1" > $CGDIR/net_prio/users/qbittorrent/net_prio.ifpriomap
### MINITUBE ###
mkdir -p $CGDIR/cpu/users/minitube
echo '300' > $CGDIR/cpu/users/minitube/cpu.shares
# mkdir -p $CGDIR/net_cls/users/minitube
# echo '0x00010003' > $CGDIR/net_cls/users/minitube/net_cls.classid
chmod 660 $CGDIR/*/*/*/tasks
chown root:cgroups $CGDIR/*/users/*/tasks
}
do_stop() {
cgclear
}
do_restart() {
do_stop && sleep 1
do_mount
do_start
}
usage() {
echo "Usage: $SCRIPTNAME <start|stop|restart>"
exit 0
}
RETVAL=0
case $1 in
'stop')
log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
RETVAL="$?"
if [ "$RETVAL" -eq "0" ]; then
log_end_msg 0
else
log_end_msg 1
fi
;;
'start')
do_mount
log_daemon_msg "Starting $DESC" "$NAME"
do_start
RETVAL="$?"
if [ "$RETVAL" -eq "0" ]; then
log_end_msg 0
else
log_end_msg 1
fi
;;
'restart'|'reload')
log_daemon_msg "Restarting $DESC" "$NAME"
do_restart
RETVAL="$?"
if [ "$RETVAL" -eq "0" ]; then
log_end_msg 0
else
log_end_msg 1
fi
;;
*)
usage
;;
esac
exit 0
Każdy podsystem zawiera swoje pliki konfiguracyjne. Powyższy skrypt przesyła odpowiednie wartości do tych plików przy pomocy polecenia echo. Nie będę opisywał tutaj wszystkich możliwych wartości we wszystkich plikach każdego modułu cgroups, bo z połowy nigdy nie korzystałem, a drugiej połowy jeszcze do końca nie poznałem. xD Wszystko jest dość obszernie opisane zarówno na stronie kernela jak i red hata.
Niemniej jednak opcje użyte przeze mnie w powyższym skrypcie trzeba opisać, poza tym nie jest ich tam aż tak dużo. Weźmy sobie przykład firefoxa, bo ma tych linijek najwięcej. Pozycje z mkdir tworzą odpowiednie grupy w drzewie katalogów cgroups. W tym przypadku, w sekcjach cpuacct, cpu, cpuset, memory i net_cls, zostanie utworzona grupa główna users i podgrupa firefox, nazwy są dowolne. Przy pomocy echo są przesyłane odpowiednie wartości do plików. W przypadku pliku cpu.shares, wartość 300 oznacza 300/1024, czyli około 1/3 czasu procesora. Domyślną wartością dla wszystkich procesów jest 1024 i tą wartością można się dowolnie bawić. Jeśli ustawimy więcej niż 1024, np. 2048, proces dostałby 2/3 czasu procesora w przypadku wysokiego obciążenia maszyny, bo 2048+1024=3072 i 2048/3072=2/3 . Oczywiście to wszystko przy założeniu, że tylko dwa procesy by wykorzystywały procesor w 100% ale życie jest trochę bardziej skompilowane.
Trzeba uważać trochę w przypadku podgrup, bo grupa główna może mieć własny przydział procesora i podgrupy również, w takim przypadku czas procesora będzie liczony trochę inaczej. Dla uproszczenia, załóżmy, że mamy 2 grupy główne -- A oraz B, które mają cpu.shares odpowiednio 1024 i 2048. Grupa A ma dwie podgrupy A1 i A2, a grupa B ma tylko jedną podgrupę B1. Te podgrupy zaś mają następujące wartości w cpu.shares : 512, 1024 i 2048 . Pytanie jest takie -- jak się rozłoży czas procesora przy maksymalnym obciążeniu? Najpierw analizujemy grupy główne, czas procka na te grupy rozłoży się w stosunku 1/3 i 2/3, odpowiednio dla grup A i B (1024+2048=3072, 1024/3072 oraz 2048/3072).
Teraz podgrupy. Jeśli w grupie A nie będzie żadnych procesów (mogą być ale załóżmy, że nie ma), to procesor przypadnie w 1/3 na grupę A1 i w 2/3 na grupę A2 (512/(512+1024) oraz 1024/(512+1024) . Jeśli by były jakieś procesy w grupie A, to rozkład mocy procesora by przybrał następującą postać: dla grupy A 2/5 , dla A1 1/5 i dla A2 2/5 . Czemu tak? trzeba wziąć pod uwagę przydziały wszystkich trzech grup (grupy głównej i dwóch podgrup), co daje nam 1024 (A) + 512 (A1) + 1024 (A2) = 2560 i odpowiednio 1024/2560 , 512/2560 , 1024/2560, co daje 2/5, 1/5 i 2/5 . To tyle jeśli chodzi o grupę A, została jeszcze gruba B.
W grupie B jest tylko jedna podgrupa B1 i w przypadku gdy procesy będą tylko w podgrupie B1, cały przydział procka jej przypadnie. Gdyby procesy także były w grupie B, podział procka rozłoży się 1/2 dla B i 1/2 dla B1, bo obie mają takie same wartości cpu.shares (2048).
I chyba najbardziej złożony model, który można by rozpisać, biorąc pod uwagę powyższy przykład, to gdy procesy trafiają do grup A, A1, A2, B oraz B1. Jak w takim przypadku rozłoży się czas procesora? Jak już wiemy, grupy A i B podzielą procesor w stosunku 1/3 i 2/3. Rozkład w grupie A wynosi 2/5, 1/5 i 2/5 , mnożymy to przez ratio wyższej grupy -- 1/3 -- i dostajemy wartości 2/15, 1/15 i 2/15. Łącznie daje nam to 5/15, czyli 1/3. Podobnie postępujemy z grupami B i B1 -- mają współczynniki przydziału 1/2 i 1/2, mnożymy przez ratio 2/3, co daje 2/6 i 2/6, rzem 4/6=2/3 , a 1/3+2/3=1, czyli wszystko się zgadza. Jeszcze dajemy to na wspólny mianownik 80 (6*15) i mamy współczynniki 12/90, 6/90, 12/90, 30/90 i 30/90, odpowiednio dla A, A1, A2, B i B1. Łącznie daje 1, więc chyba wszystko się zgadza. xD Przykład zaczerpnięty z tej strony ale odrobinę został zmieniony.
Trzeba jeszcze tylko pamiętać, że cpu.shares odnosi się do całego procesora, czyli wszystkich rdzeni i jeśli będzie ustawimy przydział, powiedzmy, 512, to na dwu rdzeniowym procku, proces mógłby zjeść jeden rdzeń w pełni.
Dość tego liczenia. Następna linijka w sekcji z firefoxem to memory.limit_in_bytes. Ustawia ona limit pamięci dla grupy i w tym przypadku jest to 340MiB . Po przekroczeniu tej wartości, dane będą zrzucane do swap. Kolejna linijka to memory.soft_limit_in_bytes. Parametr jest podobny do tego powyżej i ma znaczenie głównie przy zbyt dużym wykorzystaniu pamięci RAM, czyli gdy brakuje zasobów. W takim przypadku trzeba będzie zwolnić zasoby pamięci, które? To zależy od tego parametru właśnie. W tym przypadku, na pierwszy ogień pójdzie 100MiB z przydziału firefoxa. Przez zwolnienie, ma się rozumieć, że dane trafią do swap, a nie, że wylecą permanentnie z pamięci.
Jeśli ktoś jest ciekaw jak prezentują się statystyki pamięci, może je podejrzeć w dwóch poniższych plikach:
# cat /proc/`pidof firefox`/status
Name: firefox
State: S (sleeping)
Tgid: 30859
Pid: 30859
PPid: 30858
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
FDSize: 128
Groups: 24 25 27 29 30 44 46 100 115 1000 1004 5001 5004
VmPeak: 977992 kB
VmSize: 944208 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 270780 kB
VmRSS: 124944 kB
VmData: 562404 kB
VmStk: 152 kB
VmExe: 124 kB
VmLib: 66632 kB
VmPTE: 1500 kB
VmSwap: 113628 kB
Threads: 36
SigQ: 0/7866
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000001004
SigCgt: 0000000f800044eb
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
Seccomp: 0
Cpus_allowed: 3
Cpus_allowed_list: 0-1
Mems_allowed: 00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 79553
nonvoluntary_ctxt_switches: 181247
oraz:
# cat /sys/fs/cgroup/memory/users/firefox/memory.stat
cache 9502720
rss 122515456
rss_huge 0
mapped_file 9482240
writeback 0
pgpgin 3718856
pgpgout 3686625
pgfault 3737370
pgmajfault 19419
inactive_anon 61382656
active_anon 61132800
inactive_file 4710400
active_file 4792320
unevictable 0
hierarchical_memory_limit 18446744073709551615
total_cache 9502720
total_rss 122515456
total_rss_huge 0
total_mapped_file 9482240
total_writeback 0
total_pgpgin 3718856
total_pgpgout 3686625
total_pgfault 3737370
total_pgmajfault 19419
total_inactive_anon 61382656
total_active_anon 61132800
total_inactive_file 4710400
total_active_file 4792320
total_unevictable 0
W tych plikach są również zawarte informacje na temat tego ile dany proces zajmuje miejsca w swapie.
Ostatnią pozycją tyczącą się firefoxa jest net_cls.classid. Moduł net_cls odpowiada za nadawanie pakietom sieciowym odpowiednich klas, które są używane przy traffic control. Jeśli chodzi o firefoxa, ta aplikacja korzysta z internetu i przydałoby się odrobinę kontrolować jej ruch. W tym celu nadawany jest id grupy pakietom tworzonym przez procesy firefoxa. Oznaczenie 0x00010003 jest zapisem hexalnym i w jego skład wchodzą dwie liczby -- 0001 oraz 0003. Po przeliczeniu tego na system decymalny otrzymujemy 1 i 3, które narzędzie tc traktuje jako grupę 1:3 i tam właśnie wysyła pakiety, którym można nadać wyższy priorytet, określić pasmo i tego podobne rzeczy. Jeśli nie mamy pojęcia co to takiego to całe tc, to raczej nie potrzebujemy tej linijki.
Przy pomocy chown i chmod nadajemy odpowiednie uprawnienia plikom tasks, za pośrednictwem których to możemy wskazywać pidy procesów. Chodzi głównie o ustawienie odpowiednich praw dostępu do tych plików, by użytkownicy w grupie cgroup mogli swobodnie operować na nich.
Linijka z cgroup.clone_children sprawi, że konfiguracja katalogów nadrzędnych będzie kopiowana do tych podrzędnych przy ich inicjacji, czyli przy zakładaniu kolejnych grup i podgrup.
Musimy jeszcze stworzyć plik konfiguracyjny /etc/cgrules.conf , bo to w nim są definiowane reguły dla cgrulesengd:
*:firefox cpu,memory users/firefox/
*:firefox* cpu,memory users/firefox/
*:qbittorrent cpu users/qbittorrent/
*:qbittorrent* cpu users/qbittorrent/
*:qbittorrent-nox cpu users/qbittorrent/
*:qbittorrent-nox* cpu users/qbittorrent/
*:minitube cpu users/minitube/
*:minitube* cpu users/minitube/
Pierwsza kolumna może się składać z samego użytkownika, w tym przypadku * odnosi się do wszystkich użytkowników w systemie. W tym miejscu może być także określona grupa za pomocą @morfik zamiast użytkownika. Proces nie jest wymagany ale można go sprecyzować po :. Przyjmuje on wartość, albo pełnej ścieżki do programu, albo nazwy procesu widocznego np. w ps . Następna kolumna odpowiada za moduły cgroups, do których proces będzie przypisywany. Jeśli proces ma być przypisany do wszystkich podsystemów cgroups, można posłużyć się * . Ostatnia kolumna to położenie w drzewie katalogów cgroups, czyli tam gdzie plik tasks się powinien znajdować, do którego to cgrulesengd będzie przesyłał pidy procesów.
Dodajemy skrypty do autostartu:
# update-rc.d cgrulesengd defaults
# update-rc.d cgconfig defaults
I to w zasadzie tyle jeśli chodzi o implementację cgroups w debianie. Wszystkie procesy, które chcemy kontrolować definiujemy w /etc/cgrules.conf , a linijki z wartościami dla określonych plików cgroups wpisujemy do /etc/init.d/cgconfig .
W przypadku gdyby coś nie działało można użyć poniższych poleceń w celu sprawdzenia czy cgroups jest poprawnie montowany, w których miejscach i czy procesy są przypisane do odpowiednich grup. Można w tym celu użyć albo narzędzi dostarczonych przez cgroup-bin albo też i ręcznie odpytywać określone pliki. I tak np. sprawdźmy czy jest coś w ogóle łapane przez cgroups:
# lscgroup
blkio:/
cpu:/
cpu:/users
cpu:/users/minitube
cpu:/users/qbittorrent
cpu:/users/firefox
cpuacct:/
cpuacct:/users
cpuacct:/users/firefox
cpuset:/
cpuset:/users
cpuset:/users/firefox
devices:/
freezer:/
memory:/
memory:/users
memory:/users/firefox
net_cls:/users
net_cls:/users/minitube
net_cls:/users/qbittorrent
net_cls:/users/firefox
net_prio:/
perf_event:/
Jak widać powyżej, grupy są utworzone prawidłowo i cgroups je widzi w strukturze katalogów. Jeśli jednak byśmy mieli więcej aplikacji kontrolowanych przez cgroups, możemy ograniczyć się do wyszukania konkretnego procesu i sprawdzenia czy z nim jest wszystko w porządku. W tym celu trzeba odszukać pid w /proc/ :
# cat /proc/`pidof firefox`/cgroup
11:perf_event:/
10:net_prio:/
9:net_cls:/users/firefox
8:memory:/users/firefox
7:freezer:/
6:devices:/
5:cpuset:/
4:cpuacct:/
3:cpu:/users/firefox
2:blkio:/
Jeśli nie wiemy czy wirtualny system plików cgroups jest w ogóle montowany, możemy to sprawdzić przez:
# lssubsys -am
cpuset /sys/fs/cgroup/cpuset
cpu /sys/fs/cgroup/cpu
cpuacct /sys/fs/cgroup/cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls /sys/fs/cgroup/net_cls
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
net_prio /sys/fs/cgroup/net_prio
Jeśli chcemy mieć trochę więcej info możemy odpytać /proc/mounts :
# grep 'cgroup' /proc/mounts
cgroup /sys/fs/cgroup tmpfs rw,relatime,size=12k 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,relatime,cpuset,release_agent=/run/cgmanager/agents/cgm-release-agent.cpuset,clone_children 0 0
cgroup /sys/fs/cgroup/cpu cgroup rw,relatime,cpu,release_agent=/run/cgmanager/agents/cgm-release-agent.cpu,clone_children 0 0
cgroup /sys/fs/cgroup/cpuacct cgroup rw,relatime,cpuacct,release_agent=/run/cgmanager/agents/cgm-release-agent.cpuacct,clone_children 0 0
cgroup /sys/fs/cgroup/memory cgroup rw,relatime,memory,release_agent=/run/cgmanager/agents/cgm-release-agent.memory,clone_children 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,relatime,devices,release_agent=/run/cgmanager/agents/cgm-release-agent.devices,clone_children 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,relatime,freezer,release_agent=/run/cgmanager/agents/cgm-release-agent.freezer,clone_children 0 0
cgroup /sys/fs/cgroup/net_cls cgroup rw,relatime,net_cls,release_agent=/run/cgmanager/agents/cgm-release-agent.net_cls,clone_children 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,relatime,blkio,release_agent=/run/cgmanager/agents/cgm-release-agent.blkio,clone_children 0 0
cgroup /sys/fs/cgroup/perf_event cgroup rw,relatime,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event,clone_children 0 0
cgroup /sys/fs/cgroup/net_prio cgroup rw,relatime,net_prio,release_agent=/run/cgmanager/agents/cgm-release-agent.net_prio,clone_children 0 0
Informacje na temat dostępnych podsystemów cgroups i ich hierarchii można odnaleźć pod:
# cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 5 3 1
cpu 3 5 1
cpuacct 4 3 1
memory 8 3 1
devices 6 1 1
freezer 7 1 1
net_cls 9 1 1
blkio 2 1 1
perf_event 11 1 1
net_prio 10 1 1
Jeśli problem leży gdzieś indziej i powyższe polecenia zwracają pożądane wartości, być może problem tkwi w demonie cgrulesengd . Najlepszym wyjściem jest sprawdzenie czy aby na pewno pidy procesów trafiają do plików tasks:
morfik:~$ cat /sys/fs/cgroups/cpu/users/firefox/tasks
30859
30921
30922
30923
30924
30925
30926
30927
30928
30929
30931
30932
30933
30934
30935
30973
30974
30975
30976
30978
30979
30980
30989
30990
30991
30992
31003
31005
31024
31025
31081
31082
31101
W przypadku braku pidów w pliku tasks, oznacza to, że demon, albo nie działa, albo jest źle skonfigurowany. W takiej sytuacji zostaną również wyrzucone informacje w syslogu ilekroć tylko odpalimy aplikację, której pid powinien powędrować do pliku tasks. Watro zajrzeć w logi systemowe w przypadku problemów.
2. NICE, IONICE, TASKSET oraz TRICKLE
Jeśli cgroups nas przerasta albo z jakiegoś powodu nie możemy z niego korzystać, jest inne wyjście, które bardzo dobrze sprawdza się przy skryptach czy programach. Mowa tutaj o nice, ionice, taskset oraz trickle. Ten pierwszy limituje wykorzystanie procesora, drugi zaś dysku twardego. Z kolei pozostałe dwa odpowiadają za przypisanie procesu konkretnemu rdzeniowi oraz za limitowanie sieci.
Jeśli chodzi o nice i ionice, to bardzo często wykorzystuje się te dwa razem, konfigurując przy ich pomocy procesy, które mają za zadanie działać w tle i nie sprawiać pozorów swojego istnienia, np. przy backupie danych. Każdy chyba wie, że w takich sytuacjach dysk ostro ryje i czasem to zachowanie uniemożliwia normalną pracę na kompie.
Jeśli chcemy ograniczyć zasoby jakiemuś procesowi, przy jego wywoływaniu dodajemy nice, ionice, taskset albo trickle z odpowiednimi opcjami, w zależności od tego do jakich zasobów chcemy mu ograniczyć dostęp. Możliwe jest sprecyzowanie kilku z nich jeden po drugim, co stworzy drzewo procesów, na końcu którego będzie nasza aplikacja, a działa to na zasadzie dziedziczenia parametrów procesów -- jeśli proces z nice -n 19 wywoła inny proces, ten drugi również dostanie nice -n 19. Przykładowe wywołanie skryptu od backupu:
$ nice -n 19 ionice -c 3 backup.sh
Priorytety dla nice są z zakresu -20 do 19. Zwykły użytkownik może nadawać tylko priorytety od 0 do 19. Im wyższy numer tym niższy priorytet, czyli procesy z numerem -20 mają najwyższy priorytet, a te z numerem 19 najniższy.
W przypadku ionice, przy pomocy -c 3 ustalamy zapis/odczyt dysku na tryb idle, czyli gdy inne procesy go nie wykorzystują. Możliwe jest także sprecyzowanie mniej agresywnego rozwiązani czyli -c 2 -n 7. Przy czym opcja -n w tym przypadku jest z zakresu 0-7 i również im mniejsza wartość, tym wyższy priorytet, a im wyższy priorytet, tym lepszy dostęp do dysku.
Warto jeszcze wiedzieć jak zmieniać priorytety uruchomionych programów. Jeśli chcemy zmienić wcześniej nadany priorytet przy pomocy nice, używamy renice, z tym, że definiujemy proces za pomocą opcji -p:
$ renice -n 10 -p 1110
Podobnie z ionice:
$ ionice -c 2 -n 5 -p 1110
Jest tylko jeden problem. Co w przypadku drzewa procesów? Raczej nikomu się nie będzie chciało zmieniać 20 procesów potomnych. Można to zrobić szybciej przez uzyskanie id sesji procesów:
# ps -j
PID PGID SID TTY TIME CMD
8077 8077 9924 pts/8 00:00:00 build
8252 8077 9924 pts/8 00:00:00 build
8253 8077 9924 pts/8 00:00:00 build
8254 8077 9924 pts/8 00:00:00 tee
8351 8077 9924 pts/8 00:00:00 bootstrap
9443 8077 9924 pts/8 00:00:00 bootstrap_archi
9561 8077 9924 pts/8 00:00:00 aptitude
9567 8077 9924 pts/8 00:00:00 http
9572 8077 9924 pts/8 00:00:00 gpgv
9595 8077 9924 pts/8 00:00:00 bzip2
9970 9970 9924 pts/8 00:00:00 su
10009 10009 9924 pts/8 00:00:00 ps
10051 10051 9924 pts/8 00:00:00 bash
Id procesów można także wyciągnąć z pgrep , o ile znamy id sesji:
# pgrep -s 9924
8077
8252
8253
8254
8351
9443
9561
9567
9572
9595
9924
9970
10051
lub też przez:
# ps -eo sess:1=,pid:1=,nice:1= | grep 9924
9924 8077 0
9924 8252 0
9924 8253 0
9924 8254 0
9924 8351 0
9924 9443 0
9924 9561 0
9924 9567 0
9924 9572 0
9924 9595 0
9924 9924 0
9924 9970 0
9924 10051 0
Ten ostatni pokazuje także aktualny nice. By zmienić nice wszystkim procesom podpiętym do jednej sesji, wydajemy poniższe polecenie :
# renice -n 19 $(pgrep -s 9924)
8252 (process ID) old priority 0, new priority 19
8253 (process ID) old priority 0, new priority 19
8254 (process ID) old priority 0, new priority 19
8351 (process ID) old priority 0, new priority 19
9443 (process ID) old priority 0, new priority 19
9561 (process ID) old priority 0, new priority 19
9567 (process ID) old priority 0, new priority 19
9572 (process ID) old priority 0, new priority 19
9595 (process ID) old priority 0, new priority 19
9924 (process ID) old priority 0, new priority 19
9970 (process ID) old priority 0, new priority 19
10051 (process ID) old priority 0, new priority 19
Jak widać priorytety uległy zmianie. Podobnie postępujemy z ionice:
# ionice -c 3 -p $(pgrep -s 9924)
Jeśli interesuje nas by danym procesem zajmował się określony rdzeń, możemy dodatkowo dopisać do linijki wywoławczej taskset i numer rdzenia, a te są numerowane od 0. I tak dla przykładu jeśli chcemy by amarok działał na pierwszym rdzeniu (numer zero), wydajemy poniższe polecenie:
$ taskset -c 0 amarok
Jeśli chcemy zmienić przydział rdzeni procesora dla uruchomionego już procesu:
$ taskset -p -c 0 15538
pid 15538's current affinity list: 0,1
pid 15538's new affinity list: 0
I na dwa rdzenie:
$ taskset -p -c 0,1 15538
pid 15538's current affinity list: 0
pid 15538's new affinity list: 0,1
Z kolei trickle zajmuje się ograniczaniem pasma sieciowego dla danego procesu. Jednak trzeba uważać by nie ustawić zbyt niskiej wartości, bo wtedy odpalany program zacznie sypać błędami typu segfault. Jeśli chcemy nadać ograniczenie jakiemuś programu, który komunikuje się z siecią, powiedzmy 500KB/s down i 100KB/s up, robimy to w ten sposób:
$ trickle -s -d 500 -u 100 qbittorrent
Więcej informacji na temat nice, renice, ionice, taskset i trickle możn znaleźć tu i tu.