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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заметьте, на следующем изображении 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, чтобы увидеть, какие системные вызовы были добавлены или вырезаны в прошлой версии ядра.

Пример мануала по системным вызовамПример мануала по системным вызовам

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

Заключение

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

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

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