Skip to content

Latest commit

 

History

History
812 lines (307 loc) · 80.5 KB

linux_resource_contention.md

File metadata and controls

812 lines (307 loc) · 80.5 KB

Борьба за ресурсы

Источник: Борьба за ресурсы

Борьба за ресурсы, часть 1: Основы Cgroups

Хорошо, а что это вообще за cgroups, и причем здесь управление ресурсами или производительностью?

Контроль на уровне ядра

Начиная с вышедшей в январе 2008 года версии 2.6.24, в ядре Linux появилось то, что изначально было придумано и создано в Google под именем «process containers», а в Linux стало называться «control groups», сокращенно cgroups. Вкратце, cgroups – это механизм ядра, позволяющий ограничивать использование, вести учет и изолировать потребление системных ресурсов (ЦП, память, дисковый ввод/вывод, сеть и т. п.) на уровне коллекций процессов. Cgroups также могут замораживать процессы для проверки и перезапуска. Контроллеры cgroups впервые появились в 6-й версии Red Hat Enterprise Linux, но там их надо было настраивать вручную. А вот с приходом Red Hat Enterprise Linux 7 и systemd преднастроенный набор cgroups идет уже в комплекте с ОС.

Все это работает на уровне ядра ОС и поэтому гарантирует строгий контроль над каждым процессом. Так что теперь какому-нибудь зловреду крайне сложно нагрузить систему так, чтобы она перестала реагировать и зависла. Хотя, конечно, багованный код с прямым доступом к «железу» (например, драйверы), все еще на такое способен. При этом, Red Hat Enterprise Linux 7 предоставляет интерфейс для взаимодействия с cgroups, и вся работа с ними в основном ведется через команду systemd.

Свой кусок пирога

На диаграмме ниже, напоминающей нарезанный пирог, представлены три cgroups, которые по умолчанию есть на сервере Red Hat Enterprise Linux 7 – System, User и Machine. Каждая из этих групп называется «слайс» (slice – сектор). Как видно на рисунке, каждый слайс может иметь дочерние секторы-слайсы. И, как и в случае с тортом, в сумме все слайсы дают 100% соответствующего ресурса.

Теперь рассмотрим несколько концепций cgroups на примере процессорных ресурсов.

На рисунке выше видно, что процессорное время поровну делится между тремя слайсами верхнего уровня (System, User и Machine). Но так происходит только под нагрузкой. Если же какой-то процесс из слайса User попросит 100% процессорных ресурсов, и никому больше эти ресурсы в данный момент не нужны, то он получит все 100% процессорного времени.

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

  • System– демоны и сервисы.
  • User– пользовательские сеансы. Каждый пользователь получает свой дочерний слайс, причем все сеансы с одинаковым UID «живут» в одном и том же слайсе, чтобы особо ушлые умники не могли получить ресурсов больше положенного.
  • Machine– виртуальные машины, типа KVM-гостей.

Кроме того, для управления использованием ресурсов применяется концепция так называемых «шар» (share – доля). Шара – это относительный числовой параметр; его значение имеет смысл только в сравнении со значениями других шар, входящих в ту же cgroup. По умолчанию все слайсы имеют шару, равную 1024. В слайсе System на рисунке выше для httpd, sshd, crond и gdm заданы CPU-шары, равные 1024. Значения шар для слайсов System, User и Machine тоже равны 1024. Немного запутанно? На самом деле это можно представить в виде дерева:

  • System — 1024
    • httpd — 1024
    • sshd — 1024
    • crond — 1024
    • gdm — 1024
  • User — 1024
    • bash (mrichter) — 1024
    • bash (dorf) — 1024
  • Machine — 1024
    • testvm — 1024

В этом списке у нас есть несколько запущенных демонов, пара пользователей и одна виртуальная машина. Теперь представим, что все они одновременно запрашивают всё процессорное время, какое только можно получить.

В итоге:

  • Слайс System получает 33,333% процессорного времени и поровну делит его между четырьмя демонами, что дает каждому из них по 8,25% ресурсов ЦП.
  • Слайс User получает 33,333% процессорного времени и делит его между двумя пользователями, каждый из которых имеет по 16,5% ресурсов ЦП. Если пользователь mrichter выйдет из системы или остановит все свои запущенные процессы, то пользователю dorf станет доступно 33% ресурсов ЦП.
  • Слайс Machine получает 33,333% процессорного времени. Если выключить ВМ или перевести ее в холостой режим, то слайсы System и User будут получать примерно по 50 % ресурсов ЦП, которые затем поделятся между их дочерними слайсами.

Кроме того, для любого демона, пользователя или виртуальной машины можно вводить не только относительные, но и абсолютные ограничения на потребление процессорного времени, причем не только одного, но и нескольких процессоров. Например, у слайса пользователя mrichter есть свойство CPUQuota. Если выставить его на 20 %, то mrichter ни при каких обстоятельствах не получит больше 20 % ресурсов одного ЦП. На многоядерных серверах CPUQuota может быть больше 100 %, чтобы слайс мог пользоваться ресурсами более чем одного процессора. Например, при CPUQuota = 200 % слайс может полностью задействовать два процессорных ядра. При этом важно понимать, что CPUQuota не резервирует, иначе говоря, не гарантирует заданный процент процессорного времени при любой загруженности системы – это лишь максимум, который может быть выделен слайсу с учетом всех прочих слайсов и настроек.

Выкручиваем на полную!

Как можно поменять настройки слайсов?

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

Во втором случае используется команда systemctl set-property. Вот что будет на экране, если набрать эту команду, добавить в конце имя слайса (в нашем случае User) и затем нажать клавишу Tab для отображения опций:

Не все свойства на этом скриншоте являются настройками cgroup. Нас в основном интересуют те, что начинаются на Block, CPU и Memory.

Если вы предпочитаете не командную строку, а config-файлы (например, для автоматизированного развертывания на нескольких хостах), то тогда придется заняться файлами в папке /etc/systemd/system. Эти файлы автоматически создаются при установке свойств с помощью команды systemctl, но их также можно создавать в текстовом редакторе, штамповать через Puppet или даже генерировать скриптами на лету.

Итак, с базовыми понятиями cgroups все должно быть ясно. В следующий раз пройдем по некоторым сценариям и посмотрим, как изменения тех или иных свойств влияют на производительность.

Борьба за ресурсы, часть 2: Играемся с настройками Cgroups

Есть два инструмента, с помощью которых можно увидеть состояние активных cgroups в системе. Во-первых, это systemd-cgls – команда, выдающая древовидный список cgroups и запущенных процессов. Ее вывод выглядит примерно так:

Здесь мы видим cgroups верхнего уровня: user.slice и system.slice. Виртуальных машин у нас нет, поэтому под нагрузкой эти группы верхнего уровня получают по 50 % ресурсов ЦП (поскольку слайс machine не активен). В user.slice есть два дочерних слайса: user-1000.slice и user-0.slice. Пользовательские слайсы идентифицируются по User ID (UID), поэтому определить владельца может быть непросто, разве что по запущенным процессам. В нашем случае по ssh-сеансам видно, что user 1000 – это mrichter, а user 0 – соответственно, root.

Вторая команда, которую мы будем использовать – это systemd-cgtop. Она показывает картину использования ресурсов в реальном времени (вывод systemd-cgls, кстати, тоже обновляется в реальном времени). На экране это выглядит примерно так:

С systemd-cgtop есть одна проблема – она показывает статистику только по тем службам и слайсам, для которых включен учет использования ресурсов. Учет включается путем создания conf-файлов drop-in в соответствующих подкаталогах в /etc/systemd/system. Например, drop-in на скриншоте ниже включает учет ресурсов ЦП и памяти для службы sshd. Чтобы сделать так у себя, просто создайте такой же drop-in в текстовом редакторе. Кроме того, учет можно включить и командой systemctl set-property sshd.service CPUAccounting=true MemoryAccounting=true.

После создания drop-in’а надо обязательно ввести команду systemctl daemon-reload, а также команду systemctl restart <имя_службы> для соответствующей службы. В результате вы будете видеть статистику использования ресурсов, но это создаст дополнительную нагрузку, поскольку на ведение учета тоже будут расходоваться ресурсы. Поэтому учет стоит включать осмотрительно и лишь для тех служб и cgroups, которые нужно мониторить подобным образом. Впрочем, часто вместо systemd-cgtop можно обойтись командами top или iotop.

Изменяем CPU-шары по приколу и с пользой

Теперь посмотрим, как изменение процессорных шар (CPU Shares) отражается на производительности. Для примера у нас будет два непривилегированных пользователя и одна системная служба. Пользователь с логином mrichter имеет UID 1000, что можно проверить по файлу /etc/passwd.

Это важно, поскольку пользовательские слайсы именуются по UID, а не по имени учетной записи.

Теперь пробежимся по каталогу drop-in’ов и посмотрим, если там уже что-нибудь для его слайса.

Нет, ничего нет. Хотя есть кое-что другое – взгляните на вещи, относящиеся к foo.service:

Если вы знакомы с юнит-файлами systemd, то увидите здесь вполне обычный юнит-файл, который запускает команду /usr/bin/sha1sum /dev/zero в качестве службы (иначе говоря, демона), Для нас важно то, что foo будет брать буквально все процессорные ресурсы, которые система разрешит ему использовать. Кроме того, здесь у нас есть drop-in, устанавливающий для службы foo значение CPU-шары, равное 2048. По умолчанию, как вы помните, используется значением 1024, поэтому под нагрузкой foo будет получать двойную долю CPU-ресурсов в рамках system.slice, своего родительского слайса верхнего уровня (так как foo – это служба).

Теперь запустим foo через systemctl и посмотрим, что нам покажет команда top:

Поскольку в системе практически нет других работающих вещей, служба foo (pid 2848) потребляет почти все процессорное время одного CPU.

Теперь введем в уравнение пользователя mrichter. Сначала урежем ему CPU-шару до 256, затем он войдет в систему и запустит foo.exe, иначе говоря, ту же самую программу, но как пользовательский процесс.

Итак, mrichter запустил foo. И вот что теперь показывает команда top:

Странно, да? Пользователь mrichter вроде как должен получить процентов 10 процессорного времени, поскольку у него шара = 256, а у foo.service – целых 2048, нет?

Теперь введем в уравнение dorf’а. Это еще один обычный пользователь со стандартной CPU-шарой, равной 1024. Он тоже запустит foo, а мы опять посмотрим, как изменится распределение процессорного времени.

dorf – пользователь старой школы, он просто запускает процесс, без всяких умных сценариев и прочего. А мы опять смотрим вывод top:

Так… давайте-ка глянем дерево cgroups и попробуем разобраться, что к чему:

Если помните, обычно в системе есть три cgroups верхнего уровня: System, User и Machine. Поскольку виртуальных машин в нашем примере нет, остаются только слайсы System и User. Каждый из них имеет CPU-шару по 1024, и поэтому под нагрузкой получает половину процессорного времени. Так как foo.service живет в System, и других претендентов на процессорное время в этом слайсе нет, то foo.service получает 50% ресурсов CPU.

Далее, в слайсе User живут пользователи dorf и mrichter. У первого шара равна 1024, у второго – 256. Поэтому dorf получает в четыре раза больше процессорного времени, чем mrichter. Теперь смотрим, что показывает top: foo.service – 50%, dorf – 40%, mrichter – 10%.

Переводя это на язык сценариев использования, можно сказать, что dorf имеет больший приоритет. Соответственно, cgroups настроены так, что пользователю mrichter урезают ресурсы на то время, пока они нужны dorf'у. Действительно, ведь пока mrichter был в системе один, он получал 50% процессорного времени, поскольку в слайсе User больше никто не конкурировал на ресурсы CPU.

По сути, CPU-шары – это способ обеспечить некий «гарантированный минимум» процессорного времени, даже для пользователей и служб с пониженным приоритетом.

Кроме того, у нас есть способ установить жесткую квоту на ресурсы CPU, некий лимит в абсолютных цифрах. Сделаем это для пользователя mrichter и посмотрим, как изменится картина распределение ресурсов.

А теперь убьем задачи пользователя dorf, и вот что получится:

Для mrichter’прописан абсолютный CPU-лимит в 5%, поэтому foo.service получает все остальное процессорное время.

Продолжим издевательства и остановим foo.service:

Что мы здесь видим: mrichter имеет 5% процессорного времени, а оставшиеся 95 % система простаивает. Форменное издевательство, да.

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

Итак, мы узнали, как контролировать текущую ситуацию с cgroups. Теперь копнем чуть глубже и посмотрим, как cgroup реализуются на уровне виртуальной файловой системы.

Корневой каталог для всех работающих cgroups располагается по адресу /sys/fs/cgroup. При загрузке системы он заполняется по мере запуска сервисов и прочих задач. При запуске и остановке служб, их подкаталоги появляются и исчезают.

На скриншоте ниже мы перешли в подкаталог для CPU-контроллера, а именно в слайсе System. Как видим, подкаталога для foo здесь пока нет. Запустим foo и проверим пару вещей, а именно, его PID и его текущую CPU-шару:

Важное предостережение: здесь можно менять значения на лету. Да, в теории это выглядит круто (и в реальности тоже), но может обернуться большим бардаком. Поэтому прежде чем что-то менять, тщательно все взвесьте и никогда не играйтесь на боевых серверах. Но в любом случае, виртуальная файловая система – это то, в чем стоит поковыряться по мере изучения того, как работают cgroups.

Борьба за ресурсы, часть 3: Памяти мало не бывает

Что касается оперативной памяти, то systemd предлагает только один способ регулировки, а именно…

Объем памяти, который может быть выделен пользователю или службе. Допустим, мы хотим ограничить пользователя mrichter 200 МБ ОЗУ. Если помните, его UID равен 1000, поэтому мы вводим следующую команду:

systemctl set-property user-1000.slice MemoryLimit=200M

Теперь mrichter хочет проверить свои границы и запускает утилиту нагрузочного тестирования stress, которая начинает усиленно потреблять память. И stress очень быстро выдает ошибку:

По системному журналу видно, что stress был попросту прерван OOM (Out Of Memory) Killer.

Здесь важно обратить внимание вот на что: по умолчанию ограничение на ОЗУ распространяется только на резидентную память. То есть, если процесс может уходить в файл подкачки («своп»), то он обойдет установленное ограничение. В нашем примере stress вылетел потому, что превысил ограничение на резидентную память.

А если мы не хотим, чтобы программа сливалась в своп?

Это, в общем-то, легко запретить. Ну или относительно легко… В общем, придется кое-куда залезть.

Есть такие настройки cgroup, до которых не добраться ни через команду systemctl, ни через юнит-файлы. Однако эти настройки можно менять на лету через файлы в папке /sys/fs/cgroup/. Вот как, к примеру, выглядит cgroup пользователя mrichter в части памяти:

Файл, отвечающий за то, сколько памяти может уходить в своп, вполне очевидно называется memory.swappiness. Посмотрим, что у него внутри:

Если вам случалось играться с настройками ядра и подсистемой свопинга, то вы сразу увидите здесь стандартное значение параметра swappiness по умолчанию. Если поменять его на ноль, то ОЗУ-регулятор для пользователя mrichter вообще запретит ему использовать своп.

Кстати, здесь же можно глянуть статистику памяти для пользователя mrichter:

Значение параметра hierarchical_memory_limit – это тот самый MemoryLimit, который мы задали командой systemctl. Параметр hierarchical_memsw_limit представляет собой суммарный лимит (резидентная память и память в файле подачки). Мы запретили пользователю mrichter использовать файл подкачки, поэтому значение этого параметра такое странное.

Теперь о проблемах только что описанного подхода:

  • Вносить изменения в эти файлы можно только тогда, когда пользователь mrichter залогинился в систему. Пока он не войдет, его cgroup будет неактивна.
  • Эти настройки не сохраняются после перезагрузки. Более того, они потеряются, если mrichter перелогинится.

Справиться с этими проблемами поможет сценарий pam_exec (подробнее см.access.redhat.com/solutions/46199).

Вот какой сценарий мы создадим в папке /usr/local/bin:

А затем добавим его вызов в последнюю строку /etc/pam.d/sshd. В результате, этот сценарий будет запускаться при каждом входе пользователя через ssh. Именно поэтому мы и проверяем в сценарии, что это пользователь mrichter, прежде чем менять настройки.

Итак, мы отрезали пользователя mrichter от файла подкачки.

Можно конечно пойти еще дальше и менять конфигурационные файлы активной cgroup на лету, но мы пока отложим это рисковое дело. Тем не менее, общий метод, как менять настройки пользователя, вы уловили.

А со службами все еще проще. В юнит-файле службы можно использовать директиву ExecStartPost=, чтобы запускать сценарий, меняющий настройки. Например, вот как надо изменить юнит-файл службы foo, чтобы выключить свопинг:

Запускаем foo – и никакого свопинга:

Ладно, на сегодня, пожалуй, хватит с нас этого шаманства.

Но прежде чем закончить, давайте остановимся на документации по cgroup, в которой можно найти информацию обо всех этих скрытых настройках регуляторов. Вы можете установить пакет kernel-doc на свой компьютер, как это сделал я, загрузив его из репозитория «rhel-7-server-rpms».

После установки откройте папку /usr/share/docs, соответствующую вашему ядру, и перейдите в папку cgroups, где и содержится последняя информация по всем регуляторам.

В следующем раз мы поговорим о вводе-выводе. И, кстати, мы уже почти подошли к тому, чтобы узнать, как cgroups привели к появлению контейнеров (на самом деле cgroups – это ключевой компонент контейнеров в Red Hat Enterprise Linux и Red Hat OpenShift Container Platform).

Борьба за ресурсы, часть 4: Замечательно выходит

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

Взгляните на рисунок ниже.

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

Производительность подсистемы хранения данных сводится к производительности используемых в ней технологий хранения: жесткие диски, SSD, SAN, NAS – они могут сильно варьироваться по скорости доступа и пропускной способности. И мощный процессор и куча памяти не спасут ситуацию, если устройства хранения не отвечают требованиям решаемых задач.

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

А теперь давайте посмотрим, что можно сделать с помощью регуляторов ввода-вывода (I/O).

Все дело в устройствах хранения

Официально регулятор ввода-вывода называется blkio, но в хорошем настроении он отзывается на Blocky. Как и у CPU-регулятора, у Blocky есть два режима работы:

  • Регулировка с помощью относительных I/O-шар (shares), которые позволяют управлять производительностью на уровне всех или выбранных устройств блочного хранения путем установки значений в интервале от 10 до 1000. По умолчанию используется 1000, поэтому любые изменения лишь уменьшают I/O-шары выбранного пользователя или службы. Почему 1000, а не 1024, как в случае CPU? Хороший вопрос. Видимо, это тот случай, когда открытая природа Linux не идет ему на пользу.
  • Регулировка абсолютной пропускной способности, позволяющая ограничить скорость чтения и/или записи для заданного пользователя или службы. По умолчанию этот режим отключен.

На скрине ниже показаны параметры, которые можно регулировать с помощью команды systemctl. Здесь мы воспользовались магией автоподсказок по клавише Tab, чтобы вывести на экран список параметров. Это называется bash-completion, и если вы все еще не пользуетесь этой функцией, самое время установить соответствующий PRM.

Относительные I/O-шары регулируются параметрами BlockIODeviceWeight и BlockIOWeight. Прежде чем играться с этими регуляторами, нужно уяснить вот что: они работают, только если для устройства хранения включен планировщик ввода-вывода CFQ.

Что такое планировщик ввода-вывода? Давайте начнем издалека и вспомним, что ядро Linux отвечает за то, чтобы все аппаратные компоненты компьютера общались друг с другом правильно. А поскольку все эти компоненты одновременно хотят разного, тут не обойтись без упорядочивания. Ну, как мы, люди, к примеру, упорядочиваем свою жизнь, структурируя ее на работу, отдых, сон и прочее.

Если говорить об устройствах хранения, то за упорядочивание их работы в рамках ядра отвечает планировщике ввода-вывода. Это просто программный код, который задает способ управления потоком данных для блочных устройств, начиная от USB-флешек и жестких дисков, и заканчивая виртуальными дисками, которые на самом деле представляют собой файлы где-то на устройствах ISCI в сети SAN.

И поверх всех этих устройств, которые можно использовать в Linux, есть различные задачи, которые должен выполнять компьютер. Кроме того, в реальной жизни существует то, что мы в Red Hat называем «сценарии использования». Именно поэтому и существуют разные планировщики, ориентированные на разные сценарии. Называются эти планировщики noop, deadline и cfq. В двух словах каждый из них можно охарактеризовать следующим образом:

  • Noop – хорошо подходит для блочных устройств хранения, у которых нет вращающихся частей (флеш, ssd и прочее).
  • Deadline – легковесный планировщик, ориентированный на минимизацию задержек. По умолчанию отдает приоритет чтению в ущерб записи, поскольку большинство приложений спотыкаются именно на чтении.
  • Cfq – ориентирован на справедливое распределение пропускной способности ввода-вывода на общесистемном уровне. И как мы уже сказали выше, это единственный планировщик, который поддерживает относительные параметры ввода-вывода для cgroups.

Дополнительные сведения о планировщикахможно найти в руководстве по настройке производительности Red Hat Enterprise Linux 7.

К чему были все эти рассуждения про планировщики? К тому, что на большинстве компьютеров cfq по умолчанию НЕ ИСПОЛЬЗУЕТСЯ, если у них нет накопителей SATA. Не зная этого, вы можете до посинения менять BlockIOWeight без какого бы то ни было эффекта. К сожалению, systemd не скажет вам: «Извините, вы зря пытаетесь поменять данный параметр. Это не сработает, поскольку на устройстве используется не тот планировщик».

Так откуда же можно узнать об этой «интересной» особенности? Как обычно, из документации по cgroups, о которой мы писали в прошлом посте. С ней всегда полезно ознакомиться, прежде чем пользоваться теми или иными регуляторами.

Переходим к сценарию использования

Снова переходим от общих слов к конкретике: позвольте представить вам господина Крякина.

Он занимается общепитом, и у него на сервере приложений есть две базы данных для отслеживания заказов. Г-н Крякин настаивает, что БД заказов на блюда из утки гораздо важнее базы для блюд из гуся, поскольку гуси – это самозванцы на троне водоплавающих.

Обе БД настроены в качестве сервисов, а их юнит-файлы выглядят следующим образом:

На самом деле вызываемые в них сценарии (duck.sh и goose.sh) не выполняют никакой реальной работы в БД, а лишь имитируют чтение и запись с помощью циклов команды dd. Оба сценария используют файловую систему /database, которая лежит на своем виртуальном диске.

Давайте запустим duck и goose и посмотрим, где они приземлятся в иерархии cgroup:

А теперь, поскольку мы знаем PID’ы процессов dd, обратимся к команде iotop, чтобы посмотреть, что происходит в подсистеме хранения:

М-да, 12-14 Мбайт/с… не быстро. Похоже, г-н Крякин не очень-то вкладывался в систему хранения данных. Хотя у нас уже были вопросы насчет его адекватности, так что удивляться особо нечему.

Теперь смотрим на две наших задачи: PID 3301 (goose) и PID 3300 (duck). Каждая использует ввод-вывод где-то на 6 Мбайт/с. На скрине выше немного другие цифры, но в реальности они постоянно скачут, и в среднем эти две задачи поровну делят пропускную способность устройства хранения.

Г-н Крякин хочет, чтобы duck имел по меньшей мере в 5 раз больше пропускной способности ввода-вывода, чем goose, чтобы заказы на утку всегда обрабатывались в первую очередь. Попробуем использовать для этого параметр BlockIOWeight с помощью следующих команд:

Смотрим iotop и видим, что не сработало:

Давайте-ка проверим планировщик ввода-вывода для устройства /dev/vdb:

Интересно… Мы пробуем сменить планировщик на cfq и ничего не выходит. Почему?

Дело в том, что наша система работает на KVM-виртуалке, и, оказывается, начиная с версии 7.1 в Red Hat Enterprise Linux большенельзя поменять планировщик. И это не баг, а фича, связанная с улучшением механизмов работы с виртуализованными устройствами ввода-вывода.

Но не будем отчаиваться. У нас есть еще два параметра, которые можно поменять: BlockIOReadBandwidth и BlockIOWriteBandwidth работают на уровне блочного устройства и игнорируют планировщик ввода-вывода. Поскольку мы знаем пропускную способность устройства /dev/vdb (где-то 14 Мбайт/с на прием и на отдачу), то ограничив goose на уровне 2 Мбайт/с, мы, похоже, сможем реализовать пожелание г-на Крякина. Давайте попробуем:

Смотрим: PID 3426, он же goose, теперь использует ввод-вывод где-то на 2 Мбайт/с, а PID 3425, то бишь duck, – почти на все 14!

Борьба за ресурсы, часть 5: Начиная с нуля

Однако cgroups в Red Hat Enterprise Linux 6 и сегодня на многое способны, что мы сегодня и проиллюстрируем.

Разберем возможности cgroups в Red Hat Enterprise Linux 6 на одном чисто гипотетическом примере, целиком и полностью основанном на реальных событиях. Но для начала, по традиции, маленькое отступление.

С безопасностью в ИТ еще никогда не было столько проблем как сейчас. Неудивительно, ведь сегодня к сети подключены не только все компьютеры и телефоны, но и холодильники, пылесосы и куча разных других вещей – простор для сетевых угроз просто необъятный. И борьба с этими угрозами, как правило, начинается сразу по всем фронтам. Оперативная установка исправлений безопасности? Да, обязательно! Усиление защиты системы – брандмауэры, SELinux, грамотная аутентификация, вот это вот все? Безусловно! Антивирусные сканеры на Linux-машинах? Ну-у, как сказать…

На Linux-машинах от антивирусных сканеров иногда бывает больше вреда, чем пользы. Однако у безопасников свои резоны, и они зачастую требуют регулярно запускать антивирусные проверки, не особо задумываясь об их обоснованности с технической точки зрения. И это реальность, с которой приходится мириться, и с которой, рано или поздно, сталкивается практический любой ИТ-шник.

Второй момент заключатся в том, что Red Hat Enterprise Linux 7 – это конечно модно, продвинуто и круто, но многие всё еще используют Red Hat Enterprise Linux 6 и не думают от нее отказываться. Вообще-то, люди поэтому и выбирают Red Hat – можно годами сидеть на одной и той же версии и при этом иметь все последние патчи, обновления и поддержку.

Возвращаемся к нашему примеру… Представьте, что есть парень по имени Джерри. Джерри работает в большой конторе и отвечает за сервера Red Hat Enterprise Linux 6. Его полностью устраивает, как они работают, и новые проблемы и головняки ему не нужны.

Но тут ребята из отдела безопасности решают, что на все его сервера надо поставить одну штуку под названием ScanIT. И поскольку эта штука будет периодически проверять диски и память на вирусы и прочие зловреды, ей нужен полный root-доступ.

Джерри вздыхает, откладывает гитару и идет ставить ScanIT на тестовую машину. Довольно быстро выясняется вот что:

  • При выполнении антивирусного сканирования scanit (это скрипт для запуска процесса) отъедает все процессорное время, до которого только может дотянуться. И это о-очень плохо отражается на работе тестовой машины – один раз Джерри даже не мог до нее достучаться по ssh.
  • Кроме того, процесс scanit время от времени ест память как не в себя. В результате, просыпаетсяOOM Killerи начинает убивать какие угодно процессы, кроме самого scanit.

В общем, с этим надо что-то делать.

Джерри берет гитару и, наигрывая Grateful Dead, начинает думать. Довольно быстро ему в голову приходит мысль, что здесь наверно могут помочь те самые cgroups из Red Hat Enterprise Linux 7, про которые ему прожужжал все уши приятель по имени Алекс. Джерри опять откладывает гитару и берется читать присланные Алексомдоки по Red Hat Enterprise Linux 6. Выясняется, что первым делом ему понадобится libcgroup.

На тестовой машине libcgroup нет, поэтому Джерри начинает ее ставить:

Кроме того, Джерри включает две службы, которые нужны для работы постоянных (персистентных) cgroups:

  • cgconfig – предоставляет более-менее простой интерфейс для работы с деревьями cgroup. Джерри конечно мог бы монтировать и конфигурировать cgroups вручную, но зачем, если можно сэкономить время?
  • cgred – эта штука представляет собой движок правил cgroup: при запуске какого-либо процесса эта служба кладет его в ту или иную cgroup согласно заданным правилам.

Установив и настроив все это, Джерри наконец-то может приступать непосредственно к самой проблеме. Хорошенько все обдумав, он принимает следующее решение:

  • scanit и его дочерние процессы должны потреблять не более 20 % CPU-ресурсов. На самом деле даже меньше – не более 20 % ресурсов одного процессорного ядра, даже на многоядерной машине. В cgroups это делается с помощью CPU-квот.
  • Что касается памяти, то scanit и его дочерние процессы должны потреблять не более 512 Мб системной памяти. Если они переходя эту черту, система должны убивать именно их, а не какие-либо другие процессы.

Не надо говорить мне, что мне делать!

Джерри придется иметь дело с двумя наборами конфигурационных файлов:

  • /etc/cgconfig.conf – автоматически генерируется при установке libcgroup.
  • /etc/cgrules.conf – содержит набор правил ruleset, согласно которым cgred сортирует запускающиеся процессы по группам cgroups.

Вот как по умолчанию выглядит файл cgconfig.conf:

Джерри мог бы внести необходимые правки прямо в него, но лучше использовать для этого conf-файлы drop-in. Как это работает? Если положить (англ. drop-in – вбросить) в папку /etc/cgconfig.d любой файл с расширением .conf, система обработает его и внесет соответствующие изменения в конфигурацию. Это удобно тем, что можно создавать drop-in’ы под разные задачи и добавлять или удалять их из конфигурации с помощью тех, инструментов, которые вам больше нравятся (допустим, Ansible, ну, это же все-таки блог Red Hat).

Вначале Джерри создает файл drop-in для CPU:

Смотрим, что тут у нас и как это работает.

Ключевое слово group просто задает имя новой группы cgroup, в нашем случае – scanit. Внутри фигурных скобок мы указываем регуляторы cgroup, которые хотим использовать. Здесь это cpu.cfs_period_us и cpu.cfs_quota_us, они позволяют задавать соответствующие лимиты в Completely Fair Scheduler, планировщике ядра, который по умолчанию используется в Red Hat Enterprise Linux 6. Давайте посмотрим, что про них написано вРуководстве по управлению ресурсами Red Hat Enterprise Linux 6:

Иначе говоря, Джерри написал в своем drop-in вот что: «Для каждого процесса, относящегося к cgroup по имени scanit, раз в секунду проверять объем выделенных ему CPU-ресурсов. Если суммарное процессорное время по всем процессам в этой группе больше 200 000 миллисекунд, то полностью перестать выдавать процессорное время этим процессам». Ну, то есть выделять всем процессам в cgroup-группе scanit, а также их дочерним процессам, суммарно не более 20% процессорного времени.

После перезапуска cgconfig сервер обновит конфигурацию, и если залезть в файловую систему, мы увидим, что scanit теперь располагается в каталоге контроллера CPU:

Это, конечно, хорошо, но нам еще надо как-то засунуть в эту cgroup собственно сам scanit. Тут пригодится crged, по умолчанию он выглядит примерно так:

Пользоваться этим файлом более-менее легко. Правда, для этого нам придется напрямую редактировать файл cgrules.conf, поскольку механизм drop-in’ов здесь не поддерживается. Мы указываем пользователя или группу, которые являются владельцами процесса, а также имя конкретного – если хочется – процесса, а также настраиваемый регулятор и группу назначения cgroup.

В нашем примере мы вместо реального антивирусного сканера scanit используем сценарий, который тоже называется scanit, но на самом деле просто эмулирует нагрузку. Без cgroup все это выглядит вот так:

CPU полностью занят, в основном user space’ом и немного system.

Джерри чешет бороду. Он запускает vi и, пользуясь строго одним указательным пальцем, вносит кое-какие изменения и перезапускает демон cgred:

Потом он вручную запускает scanit…:

И – ура! Победа.

Как видите, наши процессы-эмуляторы нагрузки (дочерние процессы scanitс) теперь суммарно потребляют 20 % ресурсов CPU, в основном в user space и немного в system. Значит, этот чертов антивирус больше не будет грузить машину до полной невменяемости.

Помните, что дальше?

Обрадованный успехом, Джерри чуть было не забыл про память. Но потом все-таки вспоминает и снова запускает vi, чтобы поправить свой config-файл.

Теперь он добавляет туда две настройки, касающиеся памяти:

  • Memory.limit_in_bytes – макс. объем RAM, который могут использовать все процессы в cgroup-группе scanit, суммарно. И без учета места в свопе. Джери ограничивает его 256 Мб
  • Memory.memsw.limit_in_bytes – макс. объем RAM, плюс место в свопе-файле, которые могут выделяться всем процессам в cgroup-группе scanit, суммарно. При превышении этого порога процессы будут убиваться OOM killer’ом. Джерри устанавливает его равным 512 Мб.

О-о, нет! Да что не так-то?

Джерри смотрит топ и видит, что дочерние процессы scanit по-прежнему работают. Поскольку эта cgroup сейчас используется, Джерри не может запустить службу. Поэтому он убивает дочерние процессы вручную и такие перезапускает службы.

Теперь немного правки в cgred.conf:

Для проверки Джерри запускает сразу несколько задач scanit, чтобы OOM killer сработал наверняка.

Потом Джерри смотрит системный лог и удовлетворенно кивает – scanit больше не может безнаказанно отъедет память в любых количествах.

Борьба за ресурсы, часть 6: cpuset или Делиться не всегда правильно

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

В детстве нам часто говорили, что делиться – это хорошо и правильно. По большому счету, так оно и есть. Но бывают исключения.

Как мы писали впервом посте этой серии, по умолчанию Red Hat Enterprise Linux 7 ведет себя как сферическая добрая бабушка. В том смысле, что она старается справедливо распределять системные ресурсы между всеми, кто их просит. Однако в реальной жизни у бабушек бывают любимчики, которым достается больше. В переводе на сисадминский это означает, что бывают ситуации, когда одни приложения или сервисы важнее других, поэтому им надо уделять всё возможное внимание, чтобы они были максимально отзывчивы.

В Red Hat Enterprise Linux 7 это делается в два этапа:

  1. Изолируем часть процессорных ядер, чтобы передать их в эксклюзивное пользование такому приложению.
  2. Создаем группы cgroups и юнит-файлы, привязывающие это приложение к изолированными ядрам.

Небольшое отступление касательно примеров из этих постов

В Hat Enterprise Linux 7.4 изменился механизм работы с недолговечными слайсами, типа пользовательских сеансов. В результате для них больше нельзя менять настройки cgroup на лету, вносить постоянные изменения в конфигурацию, а также создавать файлы drop-in с помощью команды systemctl set-property. Да, это обидно, но так уж решило сообщество разработки Linux. Хорошая новость в том, что эти изменения не затронули службы. То есть, если приложения запускаются и останавливаются через юнит-файлы (работают в качестве демонов), то все наши примеры работают. Кроме того, остается возможность создавать собственные cgroups с помощью таких древних инструментов, как cgcreate и cgset, и затем помещать в эти группы пользовательские сеансы и процессы, чтобы задействовать CPU-шары и прочие регуляторы. В жизни все меняется, поэтому нам остается только приспосабливаться и изобретать новые техники. А теперь переходим к сегодняшней теме.

Устраиваем сепаратизм с помощью isolcpus

Одна из самых важных составляющих в ядре Linux – это планировщик (диспетчер) процессов. Если чуть глубже, то процесс – это исполняемый код, являющийся часть приложения или сервиса. По сути, процесс состоит из серии инструкций, которые компьютер выполняет, делая ту или иную работу, будь просмотр котиков или что-то посерьезнее.

Выполнением этих инструкций занимается центральный процессор, он же CPU. На современных компьютерах CPU, как правило, состоит из нескольких процессоров, которые называются ядрами.

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

Поэтому нам надо как-то сказать планировщику, чтобы он не трогал часть процессорных ядер, то есть не отдавал им какие ни попадя процессы. А затем мы сами (или с помощью какого-то другого процесса) будем принудительно сажать на эти изолированные от планировщика ядра те процессы, которые посчитаем нужными. Это можно сделать с помощью параметра isolcpus в строке загрузки ядра в конфигурационном файле grub. В примере ниже у нас машина с четырьмя ядрам, на которой есть два файла grub: один лежит в /etc/default и называется grub.noiso (это резервная копия конфигурации по умолчанию), а второй лежит там же и называется просто grub, чтобы его подхватывал grub2-mkconfig. Этот второй файл подредактирован так, чтобы изолировать ядра 1-3 от планировщика процессов.

ВНИМАНИЕ: в Red Hat Enterprise Linux 7 никогда не надо вручную модифицировать файл grub.conf в папке /boot. Вместо этого внесите необходимые изменения в /etc/default/grub и затем пересоберите grub.conf file с помощью соответствующей утилиты, например, так:

При использовании параметра isolcpus надо через запятую перечислить высвобождаемые процессорные ядра, нумерация начинается с 0. После перезагрузки системы планировщик процессов не будет использовать эти ядра ни для чего, за исключением определенных процессов системного уровня, которые ДОЛЖНЫ БЫТЬ на каждом ядре. Чтобы проверить, сработал ли наш метод, запустим несколько нагрузочных процессов и затем посмотрим загрузку каждого ядра с помощью команды top.

Как видим, все нагрузочные процессы сели на CPU 0, вместо того, чтобы равномерно распределиться по всем четырем ядрам. Значит, мы прописали загрузочный параметр правильно.

Привязываем процессы к ядрам с помощью cpuset

Теперь переходим к вещам,которые лучше не делать, если вы не понимаете, зачем это делаете, а также которые лучше развертывать в продакшне только после тщательного тестирования.

К чему эти предостережения? К тому, что мы будем делать, в общем-то, простые вещи с помощью инструментария libcgroup, о котором писали в прошлом посте. Если помните, это просто набор команд для создания, модифицирования и уничтожения групп cgroups. Вообще-то они являются частью Red Hat Enterprise Linux 6, но их можно установить и на Red Hat Enterprise Linux 7, хотя не исключено, что в будущем эта возможность исчезнет. Вкратце напомним основные рекомендации по использованию libcgroup:

  1. Используйте systemd для управления теми контроллерами-регуляторами cgroup, которые находятся под управлением самого systemd (это CPU, память и блочный ввод-вывод).
  2. Используйте инструменты libcgroup для управления всеми остальными контроллерами-регуляторами cgroup.
  3. Будьте очень осторожны в плане незапланированных последствий ваших действий.

С концепцией cpuset все просто – это список процессорных ядер (нумерация, напомним, начинается с 0), принимающий задачи, которые будут исполняться ТОЛЬКО на этих ядрах. Это самые обычные процессорные ядра, они могут находиться либо под управлением планировщика процессов (именно так система настроена по умолчанию), либо, наоборот, могут быть изолированы от планировщика (как мы сделали в примере выше).

Давайте проверим каталог /sys/fs/cgroup filesystem на системе из нашего примера. Как видим, каталог cpuset уже существует, поскольку этот контроллер – это часть ядра (хотя он и не находится под управлением systemd). Однако в нем пока нет cgroups, поэтому мы видим в этом каталоге только настройки по умолчанию.

Проверим, что на нашей машине установлен инструментарий libcgroup:

Если не установлен, то это легко исправить командой yum install libcgroup, даже перезагрузка не понадобится.

Теперь создадим cpuset. Для этого мы будем использовать следующие команды, чтобы создать новую cgroup для cpuset и прописать ее свойства:

Команда Cgcreate создает cgroup с именем testset и размещает ее внутри контроллера cpuset. Затем мы назначаем третье ядро нашей ВМ этому новому cpuset’у и выделяем ему же NUMA-зону 0. Даже если ваша система не использует NUMA (а наша как раз не использует), зону все равно надо прописывать, иначе не получится назначать задачи группе cgroup. Теперь проверим, что в файловой системе создался каталог testset, и посмотрим, что у него внутри.

Как видим, наши изменения на месте, но на этом cpuset’е пока что не выполняется ни один процесс. Как посадить сюда какой-нибудь процесс?

Это можно сделать несколькими способами:

  • Можно вбить PID существующего процесса в файл tasks. Это работает, но не очень красиво.
  • Можно воспользоваться cgexec и указать группу при запуске процесса. Это работает, если приложение не является демоном; к тому же, все это можно красиво прописать в скрипт запуска приложения.
  • Для приложения, которое запускаются в качестве демона под управлением systemd, можно создать service-файл.

Давайте посмотрим вариант с cgexec.

Мы запустили foo.exe, он в свою очередь запустил дочерний процесс, который только и делает, что активно грузит процессор. Опция --sticky в команде cgexec говорит, что «любой дочерний процесс должен оставаться в той же cgroup, что и родительский процесс». Так что это важная опция, и ее надо запомнить. Теперь мы видим, что в нашей cgroup крутятся два процесса, и мы знаем их PID’ы. Глянем top:

Как видим, CPU 3 теперь загружен под завязку, а остальные прохлаждаются.

А вот как выглядит юнит-файл для запуска того же приложения в качестве сервиса systemd:

В юнит-файле есть три команды ExecStartPre, которые выполняют настройки, которые мы уже успели сделать руками. Затем идет команда ExecStart, которая запускает приложение. А при остановке приложения, команда ExecStopPost подчищает за собой, удаляя cgroup.

Как видите, в последнем примере мы создали новую cgroup по имени set1. Мы сделали это, чтобы показать, что можно иметь несколько активных cgroups, которые делят одни и те же CPU. Кому это может показаться полезным, а кого-то наоборот запутать.

Ну что, все работает? Похоже, да!

А теперь завершим работу нашего сервиса и проверим, что cgroup уничтожилась:

ВНИМАНИЕ: Группы cgroup, создаваемые с помощью cgcreate, не сохраняются после перезагрузки. Поэтому создание таких групп надо прописывать в сценариях запуска и юнит-файлах.


cgroups