🐧 Проектирование контейнеров (часть 1): почему важно понимать разницу между пространствами ядра и пользователя?

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

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

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

  • Все приложения, включая контейнеры, опираются на ядро операционной системы.
  • Ядро обеспечивает API (интерфейс прикладного программирования) для этих приложений через системные вызовы (system calls).
  • Управление версиями API важно, так как они обеспечивают прямую связь между пространствами ядра и пользователя.

Все процессы осуществляют системные вызовы:

Обращение процесса к ядру

Так как контейнеры являются процессами, они тоже совершают системные вызовы:

Обращение контейнера к ядру

Хорошо, мы разобрались с процессами и поняли, что контейнер – тоже процесс. Но что насчет файлов и программ внутри образа контейнера? Эти файлы и программы составляют так называемое пространство пользователя. Когда запускается контейнер, программа загружается в память из образа контейнера. После запуска программы в контейнере, она выполняет системные вызовы в пространство ядра. Возможность пространства пользователя взаимодействовать с ядром имеет критическое значение.

Пространство пользователя

Пространство пользователя относится ко всему коду в операционной системе, находящемуся вне ядра. Большинство UNIX-подобных систем (включая Linux) поставляются с разными видами предустановленных утилит, языков программирования и графических инструментов – все это приложения пространства пользователя.
  • Пользовательские приложения могут включать программы, написанные на C, Java, Python, Ruby и других языках. В мире контейнеров эти программы обычно доставляются в формате образа докер (docker).

Когда вы извлекаете образ контейнера RHEL7 (Red Hat Enterprise Linux 7) из официального докер-репозитория Red Hat, вы используете минимальное предустановленное пространство пользователя. Туда входят базовые утилиты, такие как bash, awk, grep и yum (чтобы вы могли установить остальной софт).

docker run -i -t rhel7 bash

Все пользовательские программы (в контейнерах или нет) работают, управляя данными, но где эти данные находятся? Они могут поступать из регистров центрального процессора и внешних устройств, но чаще всего они хранятся в памяти и на диске. Пользовательские программы получают доступ к данным, через специальные запросы к ядру, которые называются системными вызовами. Например аллоцирование памяти или открытие файла. В памяти и файлах часто содержится конфиденциальная информация, принадлежащая разным пользователям. Так что доступ должен запрашиваться у ядра через «syscall».

Пространство ядра

Ядро обеспечивает уровень абстракции для безопасности, оборудования и внутренних структур данных. Системный вызов open() обычно используется для получения файлового дескриптора в Python, C, Ruby и других языках. Вам не понравится, если программы смогут вносить изменения в файловую систему XFS, поэтому ядро предоставляет системный вызов и обрабатывает драйвера. Этот системный вызов настолько распространен, что включен в библиотеку POSIX.

Заметьте, на следующем изображении bash совершает вызов getpid(), который запрашивает свой идентификатор процесса. Также, обратите внимание, что команда cat() запрашивает доступ к /etc/hosts через вызов open(). В следующей статье мы подробно разберемся, как это работает в контейнерной среде, но отметим, что часть кода находится в пространстве пользователя, а часть в ядре.

Различные системные вызовы от программ пространства пользователя
  • Обычные пользовательские программы постоянно создают системные вызовы, чтобы выполнить свою задачу, например, ls, ps, top и bash.
  • А вот некоторые пользовательские программы, которые почти напрямую соответствуют системным вызовам: chroot, sync, mount/umount, swapon/swapoff.

Если копнуть глубже, то вот еще примеры системных вызовов, которые совершают перечисленные программы: open, getpid, socket. Обычно эти функции вызываются через библиотеки, такие как glibc, или интерпретатор Ruby, Python, Java Virtual Machine.

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

Пример взаимодействия программы с ядром

Чтобы понять, какие системные вызовы есть в ядре Linux, советуем посмотреть справку man syscalls. Мы выполняем эту команду на RHEL7, но используем образ контейнера RHEL6, чтобы увидеть, какие системные вызовы были добавлены или вырезаны в прошлой версии ядра.

docker run -t -i rhel6-base man syscalls
Пример мануала по системным вызовам

Обратите внимание, что некоторые системные вызовы были добавлены или удалены в разных версиях ядра. Линус Торвальдс и другие разработчики позаботились, чтобы их поведение было стабильным и прозрачно понятным. В RHEL7 (версия ядра 3.10) доступно 382 системных вызова. Время от времени появляются новые, другие устаревают – это стоит учитывать при планировании вашей контейнерной инфраструктуры и приложений в ней.

Заключение

Ключевые выводы для понимания разницы между пространством ядра и пользователя:

  • Приложения содержат бизнес-логику, но зависят от системных вызовов.
  • После компиляции приложения набор используемых вызовов встраивается в бинарный файл (в языках более высокого уровня в интерпретатор или виртуальную машину).
  • Контейнер не абстрагируется от необходимости пространств пользователя и ядра в использовании общего набора системных вызовов.
  • В мире контейнеров, пользовательское пространство упаковывается и доставляется до разных хостов, от ноутбуков до промышленных серверов.
  • В ближайшие годы это вызовет проблемы.
Спустя время трудно будет гарантировать, что созданный сегодня контейнер запустится на хостах в будущем. Представьте, что в 2024 году у вас на промышленном сервере все еще будет контейнерное приложение, работающее в окружении RHEL7. Как безопасно обновить инфраструктуру под этим контейнером? Будет ли это приложение также хорошо работать на новейших доступных хостах?

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

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

Библиотека программиста
12 июля 2017

Что такое Docker, и как его использовать? Подробно рассказываем

Разберем по косточкам, ведь Docker – это мощный инструмент, и огромное коли...
Библиотека программиста
30 декабря 2016

10 лучших ресурсов для изучения хакинга с помощью Kali Linux

Подборка 10 отличных ресурсов для изучения хакинга с помощью Kali Linux.<em...