CGroups (Control Groups)

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 16:11, 20 декабря 2015.
CGroups (Control Groups)
Веб-сайт {{#property:P856}}

cgroups (англ. control group) — механизм ядра Linux, который ограничивает и изолирует вычислительные ресурсы (процессорные, сетевые, ресурсы памяти, ресурсы ввода-вывода) для групп процессов. Механизм позволяет образовывать иерархические группы процессов с заданными ресурсными свойствами и обеспечивает программное управление ими.

История

Разработка была начата инженерами Google Полом Менэджем (Paul Menage) и Рохитом Сетом (Rohit Seth) в 2006 году и первоначально называлась «контейнеры процессов» (англ. process containers). В 2007 году проект был переименован в сgroups (от англ. control groups) по причине неоднозначности значения термина «контейнер» в ядре Linux.

Начиная с версии 2.6.24 ядра Linux технология включена в официальные версии ядра. С этого момента разработка значительно активизировалась, в механизм добавлено много дополнительных возможностей, механизм существенным образом используется в технологии инициализации systemd, а также является ключевым элементом в реализации системы виртуализации на уровне операционной системы LXC.

Возможности

Механизм предоставляет следующие возможности:

  • ограничение ресурсов (англ. resource limiting): использование памяти, в том числе виртуальной;
  • приоритезацию: разным группам можно выделить разное количество процессорного ресурса и пропускной способности подсистемы ввода-вывода;
  • учёт: подсчёт затрат тех либо иных ресурсов группой;
  • изоляцию: разделение пространств имён для групп таким образом, что одной группе недоступны процессы, сетевые соединения и файлы другой;
  • управление: приостановку (freezing) групп, создание контрольных точек (checkpointing) и их перезагрузку.

Подключение

Для управления cgroups используется «виртуальная» файловая система (ФС) с идентификатором типа cgroup, подключаемая, как правило, к директориям иерархии /sys/fs/cgroup. Каждое такое подключение соответствует отдельной и независимой иерархии групп управления.

Проверить наличие активных иерархий можно следующей командой:

$ grep -E -- \\\<cgroup\\\> < /proc/mounts
 cgroup /sys/fs/cgroup tmpfs rw,relatime 0 0
 rg42 /sys/fs/cgroup/rg42 cgroup rw,relatime,memory 0 0
$

В данном примере, в системе активна единственная иерархия, управляемая через файлы директории /sys/fs/cgroup/rg42, и позволяющая ограничивать использование памяти процессами, — на что указывает параметр memory.

Создать такую иерархию можно командами mkdir и mount, подобно:

# mkdir -- /sys/fs/cgroup/rg42
# mount -t cgroup -o memory -- rg42 /sys/fs/cgroup/rg42
#

(При необходимости управлять или учитывать использование также и других ресурсов в рамках данной иерархии, после -o следует перечислить все соответствующие подсистемы, например: -o blkio,cpuacct,memory.)

Перед этим, однако, следует удостовериться в наличии директории /sys/fs/cgroup и, при необходимости, создать ее, подобно:

# mount -t tmpfs -o size=64M -- cgroup /sys/fs/cgroup
#

Отметим, что использование Cgroups для ограничения используемой памяти предполагает некоторые накладные расходы независимо от фактического использования данной функции. Чтобы их избежать, поддержка подсистемы memory по-умолчанию отключена; ее включение требует явного указания параметра cgroup_enable=memory в командной строке ядра. (Для изменения последней, в свою очередь, необходима перезагрузка системы.)

Информацию о поддерживаемых используемой сборкой ядра и доступных непосредственно в текущий момент параметрах Cgroups можно найти в файле /proc/cgroups. Для решаемой задачи, в поле enabled для подсистемы memory должно присутствовать ненулевое значение.

Напомним также, что копия действующей командной строки ядра отражается в файл /proc/cmdline (должен присутствовать параметр cgroup_enable=memory); поддерживаемые типы ФС перечислены в файле /proc/filesystems (должен присутствовать тип cgroup.)

Примеры

Заморозка процессов

В cgroups существует возможность создания "специального изолятора" для процессов выполнение которых требуется немедленно приостановить. Подсистема, отвечающая за это, называется freezer. Пользоваться ей очень просто:

# cgcreate -t roman:roman -g freezer:/perl

Запускаем процесс съедающий все ядра процессора в группе perl подсистемы freezer:

$ cgexec -g freezer:/perl perl -Mthreads -we 'print "My PID: $$\n";
for(1..4){threads->create(sub { while(1){} })}  while(1){};'
My PID: 16604

Результат:

%Cpu0  :100.0 us
%Cpu1  : 93.5 us
%Cpu2  : 96.8 us
%Cpu3  :100.0 us

Останавливаем группу:

# cgset -r freezer.state=FROZEN /perl

Использование процессора сразу пришло в норму:

%Cpu0  :  6.5 us
%Cpu1  :  6.7 us
%Cpu2  :  9.7 us
%Cpu3  : 20.0 us

Проверяем состояние процесса:

$ ps -o pid,state,time,comm -p 16604
  PID S     TIME COMMAND
16604 D 00:11:03 perl

Процесс остановлен. Для "разморозки" нужно выполнить:

$ cgset -r freezer.state=THAWED /perl

Подсистема freezer выгодно отличается от обычного SIGSTOP следующими моментами:

  • Поддерживается наследование на уровне процессов. Все дочерние процессы порождённые процессом находящимся в групее при необходимости будут заморожены одновременно с ним.
  • Поддерживается иерархичность структуры. При заморозке родительской группы - одновременно будут "заморожены" все дочерние группы.
  • Сигнал STOP не может быть перехвачен процессом которому он послан, но может быть замечен его родителем, либо процессом который ведёт его отладку (ptrace). Это может привести к неожиданным и негативным последствиям. Заморозка процесса в отличии от посылки SIGSTOP не может быть отслежена родителем/отладчиком, потому не приводит к таким последствиям.

Привязка к ядрам процессора

Создадим тестовую контрольную группу в иерархии cpu_control:

# mkdir perl
# cd perl/

Удостоверимся, что в ней нет ни одной задачи:

# cat tasks

Проводим предварительну настройку группы, указываем доступные ядра процессора и узлы памяти:

# echo 3 > cpuset.cpus
# echo 0 > cpuset.mems

Настройка задаёт выполнение всех задач в группе на 3м (счёт с нуля) ядре процессора и на нулевом узле памяти.

Смотрим текущую загрузку всех ядер процессора (кусочек вывода top):

  %Cpu0  :  3,3 us
  %Cpu1  :  3,2 us
  %Cpu2  :  6,7 us
  %Cpu3  :  9,7 us

Запускаем тестовый однострочник гарантированно загружающий все ядра:

$ perl -Mthreads -e 'print "My PID: $$\n";
for(1..3){threads->create(sub{while(1){}})}; while(1){};'
My PID: 384

Все 4 ядра процессора ожидаемо загружены:

  %Cpu0  :100,0 us
  %Cpu1  : 96,8 us
  %Cpu2  :100,0 us
  %Cpu3  : 96,9 us

Помещаем тестовый процесс в созданную нами группу:

# echo 384 > cgroup.procs

Это действие автоматически добавляет процесс с PID 384 и все его потоки в контрольную группу.

Проверяем полученный результат:

# cat tasks
384
385
386
387

Все порождённые процессом потоки действительно добавились в файл tasks автоматически. Загрузка CPU:

  %Cpu0  :  3,1 us
  %Cpu1  :  6,5 us
  %Cpu2  :  0,0 us
  %Cpu3  :100,0 us

Ограничение ресурсов

Ограничение процессорного времени. Создадим контрольную группу perl утилитой cgcreate:

# cgcreate -t roman:roman -g 'cpu:/perl'

Параметр -t задаёт пользователя и группу, имеющих доступ к файлу tasks, а значит способного управлять составом данной группы (см. man cgcreate). По сути эта команда эквивалентна mkdir <root>/perl && chown roman:roman <root>/perl/tasks. Важно отметить, что на права доступа к файлу cgroup.procs эта команда не влияет. Зададим выполнение тестовой контрольной группе на 3-ем ядре процессора:

# cgset -r cpuset.mems=0 /perl
# cgset -r cpuset.cpus=3 /perl

Опять запустим тестовый многопоточный однострочник, но сделаем это через утилиту cgexec, тем самым сразу поместив его в нужную группу:

$ cgexec -g cpu:/perl perl -Mthreads -e 'print "My PID: $$\n";
for(1..3){threads->create(sub { while(1){} })} while(1){};'
My PID: 8209

Процесс с PID 8209 запустившись сразу попал в контрольную группу perl, после чего породил 3 потока унаследовавших родительскую группу. Потому все все 4 потока (включая родительский) оказались в нашей контрольной группе, о чём свидетельствует загрузка только одного ядра CPU:

%Cpu0  :  3,3 us
%Cpu1  :  6,7 us
%Cpu2  :  0,0 us
%Cpu3  :100,0 us

Введём ограничение использования процессорного времени для группы perl. Сперва просмотрим текущие лимиты:

# cgget -r cpu.cfs_period_us -r cpu.cfs_quota_us /perl
/perl:
cpu.cfs_period_us: 100000
cpu.cfs_quota_us: -1

Что бы разрешить задачам из нашей контрольной группы выполняться не более 60% времени в течение контрольного периода, нужно соответственно установить значение cpu.cfs_quota_us в 60 мс:

# cgset -r cpu.cfs_quota_us=60000 /perl

Проверяем результат:

%Cpu0  :  4,1 us
%Cpu1  :  2,7 us
%Cpu2  :  5,0 us
%Cpu3  : 60,7 us

Ограничение по памяти. Создаём контрольную группу:

# cgcreate -t roman:roman -g memory:/perl

Задаём жёсткий лимит по потреблению физической памяти в 5 Мб:

# cgset -r memory.limit_in_bytes=$(( 1024 * 1024 * 5 )) /perl

Такой же лимит потреблению физической памяти + swap (у меня swap просто отключен):

# cgset -r memory.memsw.limit_in_bytes=$(( 1024 * 1024 * 5 )) /perl

Проверяем:

# cgget -r memory.limit_in_bytes -r memory.memsw.limit_in_bytes /perl
/perl:
memory.limit_in_bytes: 5242880
memory.memsw.limit_in_bytes: 5242880

Запускаем тестового "пожирателя памяти" через cgexec, сразу же помещая его в контрольную группу и смотрим что будет (скрипт каждую секунду выделяет примерно 1 Мб памяти):

$ cgexec -g memory:/perl perl -e 'print "My PID: $$\n";
while(1){$x.="x"x(1024**2); print "Len: ",length($x),"\n"; sleep 1}'
My PID: 8882
Len: 1048576
Len: 2097152
Len: 3145728
Killed

Смотрим что в /var/log/messages:

kernel: [347004.116334] perl invoked oom-killer: gfp_mask=0xd0, order=0, oom_score_adj=0
kernel: [347004.116345] perl cpuset=/ mems_allowed=0
kernel: [347004.116354] CPU: 0 PID: 8882 Comm: perl Not tainted 3.13.3 #5
/* Описание Hardware и большой Call Trace пропущены */
kernel: [347004.116519] Task in /perl killed as a result of limit of /perl
kernel: [347004.116525] memory: usage 5120kB, limit 5120kB, failcnt 79
kernel: [347004.116529] memory+swap: usage 5120kB, limit 5120kB, failcnt 0
/* Продолжение служебной информации относительно действий OOM Killer'а */

Достигнут ожидаемый результат, о чём во всех подробностях ядро рассказало в логе. Для проверки можно запустить два процесса параллельно в разных терминалах. В таком случае первый приблизившийся к суммарному пределу (т.е. примерно половине от доступной памяти) будет убит, после чего второй процесс продолжит выполняться пока в одиночку не использует всю доступную память после чего так же наступит неминуемая гибель.

Ссылки по теме