Основное

» Linux Docker 0.9 - краткое практическое руководство
Установка, настройка, запуск, основные моменты работы.

» Изучаем CGroups
Практический подход к изучению подсистемы ядра Linux

» db2dhcp - DHCP сервер на SQL СУБД
Сборка из исходников, настройка БД, запуск.

Последние записи

» Отличия синтаксиса C++ & Java и некоторые особенности Java
В рамках "заметок на полях" - краткий, очень поверхностный и слабо структурированный набор различий в...

» Восстановление старых разработок
Старые проекты по которым планируется восстановить работы

» Linux Docker 0.9 - краткое практическое руководство
Установка, настройка, запуск, основные моменты работы.

CGroups - вводная. Базовые практические примеры

11 марта 2014

В данной статье будут рассмотрены базовые принципы работы с cgroups на примере подсистем управления ресурсами вычислительных узлов (cpuset), процессорного времени (cpu), памяти (memory) и подсистемы "заморозки" задач (freezer).

Содержание

Подготовка

Для использования Linux cgroups возможно потребуется выполнить несколько простых подготовительных действий.

Требуемый инструмент

Минимально необходимый комплект инструментов для проводимых опытов:

  • Ядро Linux с поддержкой cgroups
  • Системные утилиты: mount, mkdir, echo, cat, rmdir.
  • Пакет cgroup-bin

Все практические действия изначально будут выполняться "вручную", средствами системных утилит, далее - будут использоваться утилиты пакета cgroup-bin.

Пара слов о ядре

CGroups включены в основной код ядра начиная с версии 2.6.24, система постоянно дорабатывается, потому чем старше ваше ядро, тем меньше в нём функционала cgroups относительно современных ядер.

В моём случае используется "ванильное" ядро 3.13.3. На правах "отказа от ответственности" напоминаю: лучше воздежраться от использования очень свежих ядер на Production без весомых на то оснований.

Kernel 3.13.2 config for using cgroups:

 Location:                                    
  -> General setup                          
    -> Control Group support (CGROUPS [=y]) 
       [ ]   Example debug cgroup subsystem                                          
       [*]   Freezer cgroup subsystem                                                
       [*]   Device controller for cgroups                                           
       [*]   Cpuset support                                                          
       [*]     Include legacy /proc/<pid>/cpuset file                                
       [*]   Simple CPU accounting cgroup subsystem                                  
       [*]   Resource counters                                                       
       [*]     Memory Resource Controller for Control Groups                         
       [*]       Memory Resource Controller Swap Extension                           
       [*]         Memory Resource Controller Swap Extension enabled by default (NEW)
       [*]       Memory Resource Controller Kernel Memory accounting (NEW)           
       [ ]     HugeTLB Resource Controller for Control Groups (NEW)                  
       [*]   Enable perf_event per-cpu per-container group (cgroup) monitoring       
       -*-   Group CPU scheduler  --->                                               
       [*]   Block IO controller                                                     
       [ ]     Enable Block IO controller debugging                                  
 Location:                                                     
   -> Enable the block layer (BLOCK [=y])                      
     -> IO Schedulers                                          
       -> CFQ I/O scheduler (IOSCHED_CFQ [=y])                 
 Location:                                
   -> Enable the block layer (BLOCK [=y]) 

То же самое, но в виде названий параметров для .config:

    CONFIG_CGROUPS=y
    CONFIG_CGROUP_FREEZER=y
    CONFIG_CGROUP_DEVICE=y
    CONFIG_CGROUP_CPUACCT=y
    CONFIG_MEMCG=y
    CONFIG_MEMCG_SWAP=y
    CONFIG_MEMCG_SWAP_ENABLED=y
    CONFIG_MEMCG_KMEM=y
    CONFIG_CGROUP_PERF=y
    CONFIG_CGROUP_SCHED=y
    CONFIG_BLK_CGROUP=y
    CONFIG_NET_CLS_CGROUP=y
    CONFIG_CFQ_GROUP_IOSCHED=y; 
    CONFIG_BLK_DEV_THROTTLING=y.                                        

Важные замечания для пользователей Debian

  1. При использовании дистрибутивного ядра, для включения подсистемы контроля за памятью (memory), необходимо добавить в загрузочные параметры ядра "cgroup_enable=memory" и "swapaccount=1" (если желаем контролировать swap). И конечно же перезагрузиться. Разработчики Debian заботливо экономят вашу память, потому они отключили memory accounting по умолчанию, даже если ядро собрано с его поддержкой. Ведь на каждую страницу физической памяти memory accounting тратит по 8 или 16 (для 32/64 битной архитектуры соответственно) байт памяти. На 4 Гб памяти при размере страницы 4 Кб (см. вывод getconf PAGESIZE) накладные расходы составляют менее 20 Мб, что на мой взгляд совсем не существенно на фоне открывающихся возможностей.
  2. Пакет cgroup-bin для администрирования cgroups в составе дистрибутива (7.* версий) пребывает в очень печальном состоянии, по сути это пересобранный в deb-формат пакет от RH. Следствием этого является полное отсутствие работоспособных (в Debian) скриптов инициализации и файлов конфигурации по умолчанию в составе пакета. Я на скору руку пересобрал пакет добавив в него работоспособные скрипты.

Активация интерфейса cgroups

Код и логика работы "контрольных групп" разбиты на несколько независимых подсистем (так же могут называться "контроллерами ресурсов/контроллерами") предоставляющих различный функционал. Сконфирурированное по примеру выше ядро поддерживает: blkio (ввод/вывод блочных устройств), cpu (приоритеты/ограничение быстродействия CPU), cpuacct (статистика использования CPU), cpuset (контроль за использованием отдельных узлов CPU/памяти), devices (доступ к устройствам), freezer ("заморозка" задач), memory (контроль использования и статистика памяти), net_cls ("метит" исходящие сетевые пакеты), perf_event (позволяет perf работать с иерархией cgroup).

Список подсистем доступных в работающем ядре можно получить выполнив:

$ lssubsys -a

Либо:

$ cat /proc/cgroups

Интерфейс подсистем cgroups представляет из себя псевдо файловую систему. Для использования любой из подсистем - нужно её предварительно смонтировать как обычную ФС. Для этого при запуске mount опции -o аргументом передаётся имя активируемой подсистемы. В одну точку монтирования одновременно можно подключить сразу несколько подсистем, указав их имена среди опций через запятую.

Примечание: Я подключаю cgroups к tmpfs файловой системе по стандартному (для Debian) пути /sys/fs/cgroup, для чего монтируется отдельная tmpfs в этом каталоге (непосредственно в sysfs запрещено создавать файлы и каталоги):
# mount -t tmpfs -o rw,nosuid,nodev,noexec,relatime,size=0k cgroup_root /sys/fs/cgroup/
Но ничего не мешает монтировать cgroups к совершенно любой точке системы. В случае использования tmpfs не забудьте добавить соответствующую строчку в /etc/fstab, для восстановления корня cgroups после перезагрузки:
# echo 'cgroup_root /sys/fs/cgroup tmpfs rw,nosuid,nodev,noexec,relatime,size=0k 0 0' >> /etc/fstab

Подключим несколько наиболее интересных подсистем.

# cd /sys/fs/cgroup

Контроль использования ресурсов CPU(s) (cpu, cpuset, cpuacct):

# mkdir cpu_control
# mount -t cgroup -o cpu,cpuset,cpuacct cgroup_cpu /sys/fs/cgroup/cpu_control/

Примечание: в качестве параметра "device" в данном случае использовано "cgroup_cpu". На самом деле имя "устройства" которое здесь указывается, системе совершенно безразлично, и нужно только более удобного разбора вывода mount. То же касается и остальных случаев монтирования cgroups подсистем.

Контроль использования памяти (memory):

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

Группа для "заморозки" процессов:

 # mkdir hold                                                  
 # mount -t cgroup -o freezer cgroup_hold /sys/fs/cgroup/hold/ 

Если всё прошло хорошо, то все команды завершились без вывода вывода => без ошибок. В противном случае:

  • Монтирование подсистемы memory завершилось ошибкой:

    # mount -t cgroup -o memory cgroup /sys/fs/cgroup/memory
    mount: special device cgroup does not exist
    

    Если ядро собрано верно и в выводе lssubsys -a присутствует подсистема memory, то вероятно вы счастливый пользователь Debian, или его клона :-) А значит - см. выше "Важные замечания для пользователей Debian"

  • Попытка создать каталог для любой подсистемы:

    # mkdir memory                                  
    mkdir: cannot create directory `memory`: No such file or directory 
    

    Вероятно вы были невнимательны и пытаетесь создать каталог в псевдо-ФС (например в /sys/fs/cgroup) не примонтировав tmpfs.

  • Попытка монтирования любой подсистемы или набора:

    # mount -t cgroup -o cpu,cpuset,cpuacct cgroup_cpu /sys/fs/cgroup/cpu_control/ 
    mount: cgroup_cpu already mounted or /sys/fs/cgroup/cpu_control/ busy
    

    Скорее всего одна из подсистем передаваемых в опцию -o уже примонтирована. Вами, либо какой-нибудь системной автоматикой (systemd например). Найти кто/куда примонтирован поможет findmnt:

    # findmnt -t cgroup                             
       TARGET                SOURCE      FSTYPE OPTIONS            
       /tmp/test             test_group  cgroup rw,relatime,cpu    
    

    Что дальше с этим делать - зависит от обстоятельств в вашей системе. В моём случае выполнить umount /tmp/test

В результате произведённых действий мы получили три каталога (/sys/fs/cgroup/{cpu_control,memory,hold}) являющихся интерфейсами к cgroups. Они называются иерархиями.

С точки зрения пользователя, иерархический механизм управления правилами cgroups реализован очень просто: каждый из каталогов (далее - иерархий) содержит набор файлов через которые задаются настройки для подсистемы смонтированной в данном каталоге. В общем случае имена имена файлов (т.е. настроек) выглядят как <имя-подсистемы>.<имя-параметра>, исключая несколько файлов общих для всех подсистем объединённых в одну иерархию (т.е. примонтированных к одному каталогу). В нашем случае самыми интересными являются файлы tasks содержащий список задач включённых в данную иерархию, и cgroup.procs содержащий список процессов включённых в иерархию. Об остальных файлах будет написано чуть позже. Разделение задач по подгруппам данной иерархии выполняется через создание подкаталогов в текущей иерархии командой mkdir или соответствующим системным вызовом (man 2 mkdir). При этом, файлы для управления всеми необходимыми настройками создаются в новых подкаталогах автоматически. Настройки дочерних иерархий могут отличаться друг от друга и могут наследовать ряд параметров от родительской иерархии.

Примечания относительно иерархий и задач в cgroups:

  • Под "задачами" в cgroups - подразумеваются потоки выполнения программного кода, вернее то что под ними подразумевается в Linux (LWP - light weight process (thread)). Просмотреть номер LWP можно например выполнив ps -eLf или запустив top -H. Таким образом понятие "задача" не эквивалентно понятию "процесс", т.к. процесс может содержать в себе множество задач.
  • По умолчанию в корне каждой иерархии, в файле tasks, присутствуют все выполняющиеся в системе задачи. "Удалить" задачу из корневого файла "tasks" можно только перенеся её в дочернюю иерархию по отношению к корневой.
  • В корне иерархии большинство параметров предназначены только для чтения, настройка параметров cgroups ресурсов производиться в дочерних уровнях иерархии (подкаталогах созданных mkdir).

Есть несколько правил и особенностей работы с иерархиями и подсистемами cgroups, достаточно хорошо описанных в руководстве RH.

Общие настройки в иерархии

Есть несколько файлов-настроек общих для различных иерархий:

  • cgroup.clone_children - имеет смысл только для cpuset подсистемы (см. ниже), если он установлен, то дочерние группы будут автоматически наследовать конфигурацию родителя.
  • cgroup.event_control - даёт возможность программно получать уведомления о превышении пороговых значений использования памяти в подсистеме memory.
  • cgroup.procs - список процессов состоящих в группе.
  • tasks - список задач (см. выше) состоящих в группе.
  • cgroup.sane_behavior - на сколько я понимаю, пока ещё весьма экспериментальная опция, почитать про неё можно например здесь и здесь.
  • notify_on_release - выполнять-ли release_agent когда в группе не осталось ни одной задачи. Если да, то выполняется программа/скрипт указанная в параметре release_agent. В качестве параметра передаётся имя пустой группы (относительно корня иерархии). Можно применять для автоматического удаления пустых групп.
  • release_agent - может быть только в корне иерархии. Чаще всего применяется для автоматического удаления пустых групп.

Примеры настройки cgroups

Несколько простых тестовых примеров, что бы немного разобраться как всё это работает. Примечание: в примерах производятся "тренировки на кошках", т.е. на обычном рабочем компьютере, но опыт свидетельствует о успешном применении cgroups на Production.

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

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

# mkdir perl
# cd perl/

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

# cat tasks

Проводим предварительну настройку группы, указываем доступные ядра процессора и узлы памяти ("memory nodes", см. man 7 numa):

# 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 и все его потоки в контрольную группу.
Если что-то пошло не так: см. ниже "Возможные проблемы" и "примечание о cgroup.procs".

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

# cat tasks
384
385
386
387

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

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

В целях эксперимента можно "пересадить" группу на другое ядро или их набор, задавая в качестве аргументов echo соответствующий номер. При этом, поддерживается задание списка и диапазона одновременно, например: echo 0-1,3 > cpuset.cpus разрешит выполнение задач на 0, 1 и 3м ядрах процессора.

Возможные проблемы:

  1. # echo $PID > cgroup.procs
    echo: write error: No space left on device
    
    Несмотря на столь странное описание ошибки, скорее всего это значит что вы не провели предварительную настройку группы, т.е. не задали значения для параметров cpuset.{cpus,mems}, потому ядро не может решить на каких узлах процесора/памяти выполнять вашу задачу. Лечение - надо задать значения параметров.
  2. # echo $PID > cgroup.procs 
    echo: write error: Invalid argument
    
    Поддержка записи в параметр cgroup.procs появилась только в относительно свежих версиях ядер, потому вероятно ваше ядро не умеет этот финт. В таком случае придётся добавлять потоки вручну, в файл tasks.

Примечание о cgroup.procs: Важно понимать разницу между добавлением ID задачи в cgroup.procs и tasks - в случае добавления задачи в cgroup.procs, все другие задачи (потоки) того же процесса автоматически добавляются в tasks выбранной группы. Если же вы добавляете задачу (пусть даже идентифицируемую PID) в tasks, то в выбранную группу добавляется только эта задача. Контрольные группы остальных задач этого процесса не меяются. Таким образом, если желаете что бы все задачи процесса попадали в группу автоматически - добавляйте любую из них в cgroup.procs.

Удаление процесса из группы осуществляется просто - его нужно перенести в какую-либо другую группу. Если планируется просто снять все ограничения, то достаточно перенести его в корневую группу иерархии, т.е. в нашем случае нужно сделать:

# echo 384 > /sys/fs/cgroup/cpu_control/cgroup.procs

При этом из файлов /sys/fs/cgroup/cpu_control/perl/{cgroup.procs,tasks} ID потоков удалятся автоматически. Удалим созданную группу:

# rmdir perl/

Если вместо удаления вы получили сообщение:

rmdir: failed to remove 'perl/': Device or resource busy

То вероятней всего не все процессы удалены из этой контрольной группы, см. файл perl/cgroup.procs. Такую же ошибку можно получить если группа содержит подгруппы (подкаталоги), пусть даже без задач в них.

Настройка средствами утилит из пакета cgroup-bin

Создание контрольной группы в выбранной иерархии:

# cgcreate -g 'cpu,cpuset,cpuacct:/perl'

В принципе не обязательно указывать полный список подсистем в которых нужно создать группу, достаточно указать одну из подсистем в выбранной иерархии (т.е. например эквивалентно было бы указать -g cpuacct:/perl).

Настройка группы:

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

Проверка настроек:

# cgget -r cpuset.cpus /perl   
/perl:                                          
cpuset.cpus: 3                                  
# cgget -r cpuset.mems /perl
/perl:                                          
cpuset.mems: 0                                  

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

# cgclassify -g cpu:/perl 1566 1567 1568 1569

cgclassify на данном этапе не умеет помещать процесс в cgroup.procs файл (видимо из соображений совместимости со старыми ядрами), потому приходится вручную указывать список LWP для всех потоков.

Перемещение процесса в корневую группу для снятия ограничений:

# cgclassify -g cpu:/ 1566 1567 1568 1569

Удаление группы:

# cgdelete -g 'cpuacct:/perl'

Обращаю внимание - если указать все подсистемы использованные в данной иерархии (т.е. cpu, cpuset, cpuacct), то программа выдаст ошибку вида: cgdelete: cannot remove group '/perl': No such file or directory, т.к. будет пытаться удалить каталог /perl для каждой подсистемы в отдельности, и соответственно сработает это только один раз. Что мягко говоря не совсем разумно. Но, в качестве компенсации за это, она умеет рекурсивное удаление групп. Например, подобный набор команд работает без ошибок создавая и удаляя группы на всю затребованную глубину:

# cgcreate -g 'cpu:/perl/1/2/3/4'
# cgdelete -r -g 'cpu:/perl/'

Кроме того - срабатывает удаление групп в которых имеются задачи, задачи автоматически перемещаются в родительскую группу.

Ограничение процессорного времени

Не менее интересным и полезным выглядит возможность ограничивать процессорное время выделяемое контрольной группе. Создадим ещё раз контрольную группу 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

Пояснения:

  • cpu.cfs_period_us - "контрольный период" подсчёта процессорного времени для данной группы, в микросекундах.
  • cpu.cfs_quota_us - квант времени на который данной контрольной группе предоставляется ресурс процессора в течение контрольного периода. В микросекундах. Значение -1 обозначает не лимитированное использование процессора.

На первый взгляд можно предположить, что cpu.cfs_quota_us не может быть больше cpu.cfs_period_us. Но в общем случае это не так.

Значение cpu.cfs_period_us задаёт период времени на который выделяется ресурс процессора контрольной группе. При этом ресурс процессора - это процессорное время которое может быть использовано на всех ядрах процессора доступных данной контрольной группе. Например, если группе доступно 2 ядра процессора, и cpu.cfs_period_us равен 100 мс (100000 микросекунд), то для 100% использования обеих ядер значение cpu.cfs_quota_us необходимо установить равным 200 мс.

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

Что бы разрешить задачам из нашей контрольной группы выполняться не более 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

Успех!
Теперь разрешим задачам контрольной группы выполняться на 2х ядрах. Исходя из алгоритма распределения ресурса процессорного времени мы получим равномерное распределение выделенных 60 мс на оба ядра в течение периода 100 мс (заданного cpu.cfs_period_us), следовательно - 30% загрузку каждого из ядер:

%Cpu0  :  4,6 us
%Cpu1  :  6,2 us
%Cpu2  : 30,8 us
%Cpu3  : 28,8 us

Из этого следует очевидный факт - что бы загрузить оба ядра на 100% нужно задать значение cpu.cfs_quota_us вдвое большим нежели значение cpu.cfs_period_us. К слову - значение cpu.cfs_quota_us можно задать сколь угодно большим, не учитывая реально доступное количество ядер. В таком случае, ограничение начнёт срабатывать только если группе будет доступно ядер CPU больше чем cpu.cfs_quota_us / cpu.cfs_period_us.

Ограничение по памяти

Это одна из важнейших подсистем cgroups. Если при недостатке CPU у нас просто всё "тормозит", но до определённых пределов можно терпеть, то при недостатке памяти в обычной ситуации приходит OOM-Killer, после чего происходят совсем уж неприятные вещи (например крах БД или Hi-Load сервиса).

Создаём контрольную группу:

# 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'а */

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

Отлично! Но что делать если нам не хочется "убивать" прожорливые процессы, как минимум до выяснения обстоятельств их прожорливости и возможных попыток "мягкого" разрешения проблемы? Для этого в подсистеме memory существует параметр memory.oom_control. Посмотрим его исходное значение:

# cgget -r memory.oom_control /perl
/perl:
memory.oom_control: oom_kill_disable 0
   under_oom 0
  • oom_kill_disable - смысл очевиден из названия. Процесс убивается при достижении лимита если параметр равен нулю, что мы наблюдали выше.
  • under_oom - если значение oom_kill_disable не 0, то в случае достижения лимита использования памяти в группе, процесс запрашивающий "ещё кусочек" замораживается (аналогично действию подсистемы "freezer"), после чего значение under_oom устанавливается в 1. Внимание - это показатель-флаг, но не счётчик.

Для отключения "убийств" процессов нужно memory.oom_control установить в 1:

# cgset -r memory.oom_control=1 /perl

Ещё раз запустим процесс-пожиратель:

$ 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: 8963
Len: 1048576
Len: 2097152
Len: 3145728

После чего процесс остановил своё выполнение. Посмотрим в каком состоянии он находится:

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

Колонка S (state - состояние процесса) сообщает нам что процесс находится в состоянии "uninterruptible sleep (usually IO)" (см. man ps). Обычно в таких ситуациях поцесс игнорирует любые попытки взаимодействия с ним, включая kill -SIGKILL, но в случае с cgroups его можно завершить обычным kill -TERM. Если же процесс не трогать, то он будет пребывать в замороженном состоянии пока в группе не появится свободная память либо через изменение параметров memory.memsw.limit_in_bytes и memory.limit_in_bytes, либо в результате освобождения памяти другим процессом из группы.

Посмотрим на показания memory.oom_control:
# cgget -r memory.oom_control /perl
/perl:
memory.oom_control: oom_kill_disable 1
        under_oom 1

under_oom свидетельствует что память в группе закончилась, и теперь у нас есть время на размышление о том что делать дальше. Вариантов тут не так уж и много:

  • Убить какой-нибудь процесс из данной группы.
  • Увеличить лимиты доступной памяти для группы.
  • запросить сокращение расхода памяти у других процессов группы (если это поддерживается приложением).
  • перенести процесс в другую группу командой cgclassify, например:
    # cgclassify -g memory:/ 8963
    Перенесёт процесс в корневую группу иерархии, не имеющую лимитов потребления.
  • Официальная документация Linux содержит ещё один пункт: "remove some files (on tmpfs?)" - вероятно имеется ввиду, что при достижении общесистемного лимита физического памяти приложения могут получить отказ даже если формально в группе ещё есть свободный ресурс.

Важные особенности

  • Невозможно задать жёсткий лимит памяти (memory.limit_in_bytes) меньшим чем объём уже используемой памяти.
  • При добавлении процесса в группу не учитывается сколько памяти он уже использует. Таким образом если лимит в группе 10 Мб, и вы добавляете в него процесс уже использующий 100 Мб, то отсчёт выделения памяти для него начинается "с нуля".
  • Если в системе используется swap, то необходимо обязательно задавать значение memory.memsw.limit_in_bytes. В противном случае может получиться что прожорливый до памяти процесс захватит весь swap, имея при этом ограничение по RAM. Значение параметра memory.memsw.limit_in_bytes - сумма из доступной процессу физической памяти и swap'а (=> его значение может быть больше или равно memory.limit_in_bytes).
  • Не удаётся установить значение memory.limit_in_bytes. Ошибки: cgset: the group can't be modified (или echo: write error: Invalid argument при установке через echo). Такое возможно если вы пытаетесь задать значение memory.limit_in_bytes большим чем memory.memsw.limit_in_bytes (см. предыдущий пункт). Нужно сперва увеличить memory.memsw.limit_in_bytes, после чего можно увеличивать memory.limit_in_bytes.

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

В 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

(Или: echo FROZEN > /sys/fs/cgroup/hold/perl/freezer.state )

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

%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

(Или: echo THAWED > /sys/fs/cgroup/hold/perl/freezer.state )

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

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

На пока всё, продолжение следует!


comments powered by Disqus