Запуск процессов в linux с ограничением ресурсов

Иногда хочется ограничить максимальное количество ресурсов доступных процессу. Последней пинком стали юнит-тесты из текущего проекта — из-за ошибок они несколько раз съели все 8Gb ОЗУ и отправили систему в глубокий своп, откуда она со скрипом доставалась минут 15. Полная виртуализация таких случаях избыточна — нужно что-то по легче. В linux для этого есть cgroups (control groups) — они позволяют поместить процесс (вместе со всеми его потомками) в контейнер, имеющий доступ только к части ресурсов системы. На самом деле cgroups умеют больше, чем просто ограничение ресурсов — тут и счетчики производительности и другая статистика.

cgroups можно манипулировать вручную или с помощью libcgroup. Последний способ значительно удобнее и по нему есть отличная документация от redhat. Она обязательна к прочтению — есть несколько не очевидных моментов (для пользователей ubuntu — в этом дистрибутиве по умолчанию cgroups монтируются в /sys/fs/cgroups).

Краткий пересказ документации на примере ubuntu/libcgroup.

сgroups основываются на контроллерах и иерархии групп. Процессы входят в группы, группы организовываются в деревья, где подгруппы получают часть ресурсов родительских групп, а контроллеры управляют ресурсами. Контроллер может быть привязан или только к одной иерархии или к нескольким если он в каждой единственный контроллер, а каждый процесс может быть привязан только к одной группе в каждой иерархии, но может быть привязан к другим группам в других иерархиях. Деревья групп отображаются на файловую систему и все работа с ними идет через специальные файлы.

Основные контроллеры позволяют управлять привязкой процессов к ядрам, ограничивать максимальную долю процессорного времени, объем используемой памяти и свопа и использование пропускной способности сети, блочных устройств и другое.

Без подсветки синтаксиса

# apt-get install cgroup-bin

# apt-get install cgroup-bin

После установки в системе появится сервис cgconfig, который автоматически создает иерархию групп и контроллеров по описанию из файла /etc/cgconfig.cfg. После правки файла сервис необходимо перезапустить. Иногда он не может корректно перезапуститься из-за не отмонтированных при останове контроллеров. Лечится это ручным отмонтированием контроллеров:

Без подсветки синтаксиса

$ sudo service cgconfig restart
[sudo] password for koder:
stop: Job failed while stopping
$ lscgroups
cpuset:/
cpuset:/sysdefault
$ rm -rf /sys/fs/cgroup/cpuset/sysdefault
$ umount /sys/fs/cgroup/cpuset
$ umount /sys/fs/cgroup
# # теперь можно перезапускать

$ sudo service cgconfig restart
[sudo] password for koder:
stop: Job failed while stopping
$ lscgroups
cpuset:/
cpuset:/sysdefault
$ rm -rf /sys/fs/cgroup/cpuset/sysdefault
$ umount /sys/fs/cgroup/cpuset
$ umount /sys/fs/cgroup
# # теперь можно перезапускать

Второй интересный сервис — cgred. Он умеет автоматически распределять процессы по группам в зависимости от правил, описанных в /etc/cgrules.conf. По умолчанию не запущен.

После установки cgroup-bin в /sys/fs/cgroup появится группы из стартового конфига:

Без подсветки синтаксиса

$ ls /sys/fs/cgroup
cpu cpuacct device s freezer memory

$ ls /sys/fs/cgroup
cpu cpuacct devices freezer memory

К каждой иерархии привязан один контроллер с соответствующим именем. Что-бы поместаить процесс под управление одного из контроллеров нужно выполнить 3 шага:

  • Сделать группу. sudo mkdir /sys/fs/cgroup/memory/test создаст группу test под управлением контроллера memory. В папке /sys/fs/cgroup/memory/test тут же появятся специальные файлы для управления и мониторинга группы.
  • Настроить контроллер. Некоторые контроллеры готовы к работе сразу, а некотрые требуют предварительной настройки. Правила настройки контроллеров не очень очевидны и описаны в документации. Для memory минимально необходимо записать ограничение в файл limit_in_bytes.

Без подсветки синтаксиса

$ sudo echo 4M > /sys/fs/cgroup/memory/test/limit_in_bytes

$ sudo echo 4M > /sys/fs/cgroup/memory/test/limit_in_bytes

Теперь процессы в группе могут использовать не больше 4M ОЗУ на всех.

  • Перенести процесс в группу — для этого нужно записать его pid в файл /sys/fs/cgroup/memory/test/tasks.

Когда группа станет не нужна ее можно удалить через rm -rf. Что-бы создать подгруппу нужно сделать новую папку в папке группы.

cgroups-bin предлагает чуть более удобный интерфейс для управления группами:

Без подсветки синтаксиса

# cgcreate -g memory:/test # создать группу tt под управлением контроллера memory

# lscgroup memory:/
memory:///
memory:///test
memory:///sysdefault


# cgset -r memory.limit_in_bytes=4M memory:test # устанавливаем limit
# cgexec -g memory:test python -c "range(10000000000)" # пытаемся сделать всем плохо
Killed

# cgdelete memory:test # удалить группу

# lscgroup memory:/
memory:///
memory:///sysdefault

# cgcreate -g memory:/test # создать группу tt под управлением контроллера memory

# lscgroup memory:/
memory:///
memory:///test
memory:///sysdefault


# cgset -r memory.limit_in_bytes=4M memory:test # устанавливаем limit
# cgexec -g memory:test python -c "range(10000000000)" # пытаемся сделать всем плохо
Killed

# cgdelete memory:test # удалить группу

# lscgroup memory:/
memory:///
memory:///sysdefault

Чтобы не делать это каждый раз руками можно настроить cgred или написать автоматизирующий скрипт; я сделал себе такой:

Показать код

#!/bin/bash
#set -x
USAGE="Usage: `basename $0` [-h] [-m MEM_LIMIT_IN_MEGS] [-c CPUS] [-i IOLIMIT] [-u USER] CMD"

cgroup_mount=/sys/fs/cgroup

GROUP=app_cell_1
while [ true ] ; do

OK=1
for cgroup in `lscgroup | awk -F: '{print $1}' | uniq`; do
if [ -d $cgroup_mount/$cgr oup/$GROUP ] ; then
OK=0
break
fi
done

if (( $OK==1 )) ; then
break
fi

GROUP=app_cell_$RANDOM
done

MEMLIMIT=
CPUS=
USER=
IOLIMIT=

while getopts hm:c:i:u: OPT; do
case "$OPT" in
h)
echo $USAGE
exit 0
;;
c)
CPUS=$OPTARG
echo "CPUS = $CPUS"
;;
i)
IOLIMIT=$OPTARG
;;
m)
MEMLIMIT=$OPTARG
;;
u)
USER=$OPTARG
;;
?)
echo $USAGE >&2
exit 1
;;
esac
done

shift $(($OPTIND - 1))

if [ $# -eq 0 ]; then
echo $USAGE >&2
exit 1
fi

CMD=$@

#cgdelete memory:/$GROUP 2>/dev/null
#cgdelete cpuset:/$GROUP 2>/dev/null
#cgdelete blkio:/$GROUP 2>/dev/null

CGEXEC_OPT=
LIMITS=0

if [ -n "$MEMLIMIT" ] ; then
LIMITS=1
cgcreate -g memory:/$GROUP
cgset -r memory.limit_in_bytes=$MEMLIMIT $GROUP
cgset -r memory.memsw.limit_in_bytes=$MEMLIMIT $GROUP
CGEXEC_OPT="$CGEXEC_OPT -g memory:$GROUP"
fi

if [ -n "$CPUS" ] ; then
LIMITS=1
cgcreate -g cpuset:/$GROUP
cgset -r cpuset.cpus=$CPUS $GROUP
cgset -r cpuset.mems=0 $GROUP
CGEXEC_OPT="$CGEXEC_OPT -g cpuset:$GROUP"
fi

if [ -n "$IOLIMIT" ] ; then
echo "IO limits not supported yet" >&2
fi

if (( $LIMITS==0 )) ; then
echo "At least one limit should be set" >&2
echo $USAGE >&2
exit 1
fi

if [ -e "$USER" ] ; then
cgexec $CGEXEC_OPT $CMD
else
cgexec $CGEXEC_OPT su "$USER" -c "$CMD"
fi

#!/bin/bash
#set -x
USAGE="Usage: `basename $0` [-h] [-m MEM_LIMIT_IN_MEGS] [-c CPUS] [-i IOLIMIT] [-u USER] CMD"

cgroup_mount=/sys/fs/cgroup

GROUP=app_cell_1
while [ true ] ; do

OK=1
for cgroup in `lscgroup | awk -F: '{print $1}' | uniq`; do
if [ -d $cgroup_mount/$cgroup/$GROUP ] ; then
OK=0
break
fi
done

if (( $OK==1 )) ; then
break
fi

GROUP=app_cell_$RANDOM
done

MEMLIMIT=
CPUS=
USER=
IOLIMIT=

while getopts hm:c:i:u: OPT; do
case "$OPT" in
h)
echo $USAGE
exit 0
;;
c)
CPUS=$OPTARG
echo "CPUS = $CPUS"
;;
i)
IOLIMIT=$OPTARG
;;
m)
MEMLIMIT=$OPTARG
;;
u)
USER=$OPTARG
;;
?)
echo $USAGE >&2
exit 1
;;
esac
done

shift $(($OPTIND - 1))

if [ $# -eq 0 ]; then
echo $USAGE >&2
exit 1
fi

CMD=$@

#cgdelete memory:/$GROUP 2>/dev/null
#cgdelete cpuset:/$GROUP 2>/dev/null
#cgdelete blkio:/$GROUP 2>/dev/null

CGEXEC_OPT=
LIMITS=0

if [ -n "$MEMLIMIT" ] ; then
LIMITS=1
cgcreate -g memory:/$GROUP
cgset -r memory.limit_in_bytes=$MEMLIMIT $GROUP
cgset -r memory.memsw.limit_in_bytes=$MEMLIMIT $GROUP
CGEXEC_OPT="$CGEXEC_OPT -g memory:$GROUP"
fi

if [ -n "$CPUS" ] ; then
LIMITS=1
cgcreate -g cpuset:/$GROUP
cgset -r cpuset.cpus=$CPUS $GROUP
cgset -r cpuset.mems=0 $GROUP
CGEXEC_OPT="$CGEXEC_OPT -g cpuset:$GROUP"
fi

if [ -n "$IOLIMIT" ] ; then
echo "IO limits not supported yet" >&2
fi

if (( $LIMITS==0 )) ; then
echo "At least one limit should be set" >&2
echo $USAGE >&2
exit 1
fi

if [ -e "$USER" ] ; then
cgexec $CGEXEC_OPT $CMD
else
cgexec $CGEXEC_OPT su "$USER" -c "$CMD"
fi

Используется так:

Без подсветки синтаксиса

$ sudo ./incontainer -u koder -m 1G -c 0-1 nosetests
# #nosetests ограничен 1 гигабайтом ОЗУ и только двумя ядрами.

$ sudo ./incontainer -u koder -m 1G -c 0-1 nosetests
# #nosetests ограничен 1 гигабайтом ОЗУ и только двумя ядрами.

Скрипт каждый раз создает новую группу вида app_cell_случайное_число, которые нужно периодически удалять, когда в них не останется ни одного процесса. Например, так:

Показать код

#!/bin/bash
cgroup_mount=/sys/fs/cgroup

for cgroup in `lscgroup | awk -F: '{print $1}' | uniq` ; do
for group in `ls -1d $cgroup_mount/$cgroup/app_cell_* 2>/dev/null` ; do
#group=$cgroup_mount/$cgroup/$_group
if [ -d $group ] ; then
TC=`cat $group/tasks | wc -l`
if (( $TC==0 )) ; then
gname=$cgroup:/`basename $group`
echo "Group $gname is empty - clear it"
cgdelete -r $gname
fi
fi
done
done

#!/bin/bash
cgroup_mount=/sys/fs/cgroup

for cgroup in `lscgroup | awk -F: '{print $1}' | uniq` ; do
for group in `ls -1d $cgroup_mount/$cgroup/app_cell_* 2>/dev/null` ; do
#group=$cgroup_mount/$cgroup/$_group
if [ -d $group ] ; then
TC=`cat $group/tasks | wc -l`
if (( $TC==0 )) ; then
gname=$cgroup:/`basename $group`
echo "Group $gname is empty - clear it"
cgdelete -r $gname
fi
fi
done
done

P.S. Ковыряясь в cgroup наткнулся на очень интересный системный вызов

Без подсветки синтаксиса

prctl(PR_SET_SECCOMP, 0, 0, 0, 0)

prctl(PR_SET_SECCOMP, 0, 0, 0, 0)

После него текущий процесс не может делать никакие системные вызовы, кроме записи/чтения в уже открытые файлы, _exit и sigreturn. Появился в 2.6.33, вместе с cgroups отличная идея для интерпретации/валидации потенциально опасных данных, например для реализации интерпретаторов, получающих скрипты из не доверенных источников.

Ссылки:
          www.mjmwired.net/kernel/Documentation/cgroups.txt
          libcg.sourceforge.net
          docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide
          www.kernel.org/doc/man-pages/online/pages/man2/prctl.2.html

Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

Автор: konstantin danilov