В продолжение второй части…

Распараллеливание заданий файловой системы


Если вы посмотрите график загрузки текущих дистрибутивов, увидите больше точек синхронизации чем просто запуски демонов: больше всего времени отнимают задания, связанные с ФС: монтирование, проверка ФС на ошибки (fsck), квотирование. Сейчас, во время загрузки много времени тратится в ожидании, пока все диски, указанные в /etc/fstab не появятся в дереве устройств, а далее проверены на ошибки, примонтированы и применены квоты (если конечно они включены). Только после всего этого мы сможем пойти дальше и в действительности начнется загрузка служб.

Можем ли мы улучшить этот процесс? Выходит, что можем. Гарольд Хойер пришел с идеей использования достопочтенной autofs для улучшения процесса.

Просто как вызов connect() «заявляет», что он заинтересован в другом сервисе, также вызов open() (или другой похожий вызов) «заявляет», что он заинтересован в некотором файле или некоторой файловой системе. Итак, чтобы услучшить распараллеливание нам надо сделать так чтобы эти приложения ждали, только тогда когда ФС, которую они ищут еще не примонтирована, но очень скоро будет. Для этого мы подключаем точку монтирования autofs (фейковая точка монтирования), а когда наша реальная ФС пройдет проверку целостности утилитой fsck во время нормальной загрузки ОС, мы заменим ее настоящей точкой монитирования. В то время как настоящая ФС еще не примонтирована, попытка доступа к ФС будет поставлена ядром в очередь и попытка доступа будет заблокирована, но только для единственного этого демона, который обратился. Таким образом, мы можем запускать наши демоны задолго до того, как все ФС станут доступными и при этом без потери каких-либо обращений к файлам и максимально распараллеливая процесс загрузки ОС.

Распараллеливание заданий ФС не имеет никакого смысла для точки монтирования — / (корень ФС), где находятся все службы (демоны) и все бинарники. Тем не менее, за счет точки монтирования /home, которая как правило больше и даже может быть зашифрована, а возможно даже монтируемая с удаленной машины и к которой редко обращаются демоны во время загрузки ОС, мы можем значительно увеличить скорость загрузки ОС. Вероятно, нет необходимости напоминать, что виртуальные ФС такие как procfs или sysfs никогда не должны быть примонтированы через autofs.

Для меня не будет сюрпризом если некоторые читатели посчитают решение интеграции autofs в процесс init слегка «хрупким» или даже странным, и может быть отнесут к более «хакерской» стороне вещей. Тем не менее, в значительной степени поигравшись с ним, я могу сказать, что autofs на своем нынешнем месте чувствует себя вполне даже хорошо. Использование autofs означает, что мы можем создать точку монитирования без немедленного предоставления реальной ФС. В действительности мы получаем отложенный доступ. Если приложение пытается получить доступ к ФС autofs и у нас уходит очень много времени чтобы заменить ее реальной ФС, тогда приложение зависнет в прерываемом сне, означающем что вы можете с легкостью отменить его, для примера, с помощью Ctrl+C. Также замечу, что в любой точке исполнения приложения, если реальная ФС не сможет корректно примонтироваться (в следствие провала fsck), то мы можем просто попросить autofs вернуть реальной код ошибки (скажем ENOENT). Итак, я думаю, что… Я хочу сказать, что даже если интеграция autofs в систему инициализации сперва может показаться безрассудным, наш экспериментальный код показал, что идея на практике, как не удивительно, ведет себя очень даже хорошо — если конечно идея реализована правильно и ради правильных причин.

Также замечу, что точки монитирования autofs должны быть так называемыми — прямыми отображениями (прим. переводчика: информация по типам отображения тут), означающим, что с точки зрения приложения есть только незначительные различия между классической (реальной) точкой монтирования и базированными на autofs.

Сохраняем первый пользовательский PID маленьким


Другая хорошая вещь которому мы можем поучиться у логики загрузки MacOS — это то, что скрипты оболочки (прим. переводчика: командный процессор, shell) — зло. Оболочка как «быстра» так оболочка и медлительна. В скриптах оболочки можно быстро разобраться, что к чему, но скорость выполнения оставляет желать лучшего! Классическая система загрузки sysvinit смоделирована вокруг скриптов оболочки. Будь то /bin/bash или любая другая оболочка (которая, скорее всего, была написана чтобы ускорить выполнения скриптов оболочки) в конечном итоге обречены быть медленными. На моей машине скрипты в /etc/init.d вызывают grep 77 раз. awk вызывается 92 раза, cut — 23 и sed — 74. Каждый раз когда эти команды (или другие) вызываются, порождается новый процесс, далее ищутся какие-либо связанные библиотеки, также настраивается такие вещи как i18n и так далее. Даже если редко выполнять операции, чуть сложнее, чем тривиальные операции с строками, процесс загрузки все равно прерывается. Конечно, это все выполняется невероятно медленно. Но никакой другой язык кроме оболочки, не мог бы делать что-то подобное. Более того, скрипты оболочки очень «хрупкие», как я говорил выше: они меняют свое поведение от переменных окружения и других подобных вещей, которые трудно отследить и как-то контролировать.

Так давайте же избавимся от бремени скриптов оболочки в процессе загрузки ОС. Перед тем как мы сможем это сделать, мы должны в начале разобраться для чего они на самом деле используются: такс, если смотреть с высока, большую часть времени они выполняют очень скучные вещи. Большинство скриптов тратят время на тривиальный запуск и остановку служб, и должны быть переписаны на C, и либо виде отдельных исполняемых файлов, либо перенесены внутрь самих служб (демонов) или же реализованы в самой системе загрузки.

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

Хорошей метрикой для измерения инвазивности скриптов оболочки в процесс загрузки ОС — это PID номер первого процесса, который вы можете запустить после полной загрузки ОС. Загрузитесь и залогиньтесь, откройте терминал и наберите echo $$. Попробуйте это на вашей Linux машине, а затем сравните результат с MacOS! (Подсказка, результат может быть примерно следующим: Linux PID — 1823; MacOS PID — 154. Измерено на наших тестовых машинах.)

Отслеживание процессов


Главной составляющей системы, которая запускает и управляет службами, должен быть процесс «сиделка»: он должен следить за службами. Перезапускать службы когда, они остановятся. Когда служба «упадет» он должен собрать всю необходимую информацию о крахе и держать их где-то поблизости, чтобы администратор мог их в дальнейшем посмотреть, а также построить перекрестные связи с информацией, которая присутствует в системах сбора отчетов о крахе (crash dump) таких как abrt, а также в службе логов такой как syslog или в системе аудита.

Процесс наблюдатель («сиделка») также должен иметь возможность полностью остановить службу (прим. переводчика: имеется ввиду процесс службы и все его дочерние процессы). Это может показаться тривиальной задачей, но на самом деле это даже сложнее чем кажется. Традиционно в Unix некий процесс, который форкает себя два раза, может избавиться от наблюдения за собой его родительским процессом, и самый первый родитель не будет знать о связи с новым процессом, который на самом деле создал его потомок. Для примера: в текущем положении вещей, «шалящий» (плохо себя ведущий) CGI скрипт, который два раза форкнул себя не будет остановлен, когда будет остановлена служба Apache. Более того, у вас даже нету возможности установить связь между CGI скриптом «шалуном» и Apache, пока вы не будете знать его имя и назначение.

Так как же мы можем отслеживать процессы, чтобы они не смогли избавиться от «сиделки» и так чтобы мы могли контролировать их как одну сущность если они форкнулись газиллион раз?

Разные люди предлагают различные решения для этой проблемы. Я не собираюсь вдаваться в детали в рамках этой статьи, но позвольте по крайне мере сказать, что решения основанные на ptrace или netlink коннекторе (интерфейс ядра который позволяет получить сообщение netlink каждый раз когда какой-либо процесс в системе вызывает fork() или exit), которым некоторые люди посвятили время и реализовали, критикуются многими как уродливые и не масштабируемые решения.

Итак, что мы можем сделать с этим? Что ж, с недавних пор в ядре появились Control Groups (aka «cgroups»). В общем, они позволяют создавать иерархию групп процессов. Иерархия прямо спроецирована в виртуальную файловую систему, и следовательно легко доступна. Именами групп являются имена директорий в виртуальной ФС. Если процесс принадлежит какой-либо из групп, все его потомки (дочерние процессы) созданные через вызов fork() также будут принадлежать этой же группе. Если процесс не имеет на то привилегий (скажем root), но имеет доступ к ФС cgroup, он не может покинуть свою группу. Изначально, cgroups были добавлены в ядро для целей контейнеров: определенные подсистемы ядра могут поставить ограничения на ресурсы или на определенные группы, такие как лимиты на использования CPU или памяти. Традиционно ограничения на лимиты (как реализовано в setrlimit()) устанавливаются на каждый процесс (в основном). cgroups, с другой стороны, позволяют устанавливать лимиты на всю группу процессов. cgroups также используется для установления лимитов, вне зоны своей прямой ответственности — контейнеры. Например, вы можете использовать cgroups чтобы установить максимальное количество памяти или CPU, используемое Apache, а также всеми ее дочерними процессами. Тогда, плохо себя ведущий CGI скрипт, не сможет более покинуть свою группу, выставленную через setrlimit(), просто вызвав еще раз fork().

В дополнение к контейнерам и установлению лимитов на ресурсы, cgroups также очень полезны в качестве инструмента отслеживания процессов (служб): членство в cgroup безопасно наследуется всеми дочерними процессами, из которого они не смогут убежать. Также присутствует система оповещений и родительский процесс будет оповещен если запущенная cgroup пуста. Вы можете узнать cgroups процесса считав файл /proc/$PID/cgroup. Следовательно, cgroups очень хорошо подходит для роли «сиделки» за процессами, т.е. их непосредственного отслеживания.

Контролируем среду выполнения процесса


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

Под обеспечением серды понимается установка очевидных параметров процесса таких как лимиты ресурсов — setrlimit, идентификатор пользователя/группы или блок переменных окружения, и это еще не все. Ядро Linux предоставляет пользователям и администраторам хороший уровень контроля над процессами. Для каждого процесса вы можете установить параметры планировщика CPU и IO, притяжение к CPU и конечно же окружение cgroup с дополнительными лимитами и многое другое.

Как пример, вызов ioprio_set() с IOPRIO_CLASS_IDLE — отличный способ минимизировать влияние функции updatedb утилиты locate на интерактивность системы.

На вершине сего, определенные высокоуровневые инструменты контроля могут быть очень кстати, такие как примонтирование дополнительного слоя ФС только для чтения, основанная на точках монитирования только для чтения (bind mounts). Таким образом мы можем запустить определенные демоны (службы) так, что все (или некоторые) ФС буду казаться им в режиме только для чтения и, следовательно, будет возвращаться ошибка EROFS на каждую попытку записи. Таким способом мы можем изолировать демоны в том, что они могут делать, в той же самой манере как это делает бедняга SELinux (но определенно этот метод не является заменой SELinux, и пожалуйста, не используйте плохие идей типа SELi....).

И на последок. Логгирование является важной частью выполнения служб: в идеале каждый бит вывода, сгенерированной службой, должен быть залоггирован. Следовательно, система загрузки, должна предоставить службы логгирования демонам, которые он запускает, с самого начала загрузки и присоединить стандартный вывод и вывод ошибок к syslog или в некоторых случаях к /dev/kmsg, которая во многих случаях служит очень хорошей заменой syslog (парни, занимающиеся встроенными системами, прислушайтесь!), особенно в тех случаях, когда из коробки буфер лога ядра сконфигурирован ужасно большим.

Продолжение следует…

Комментарии (0)