kolyavolkov 05 июля 2021

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

Возможно вам приходилось разрабатывать инфраструктуру приложений на основе контейнеров. Если так, вы почти наверняка понимаете достоинства, которые контейнеры обеспечивают разработчикам, архитекторам и команде эксплуатации.
3
🐧 Проектирование контейнеров (часть 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. Как безопасно обновить инфраструктуру под этим контейнером? Будет ли это приложение также хорошо работать на новейших доступных хостах?

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

Источники

Комментарии

 
 

ВАКАНСИИ

Добавить вакансию
Senior Software Engineer (Java)
от 4500 USD до 6000 USD
Senior MLE (SE)
от 5000 USD до 9000 USD

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

LIVE >

Подпишись

на push-уведомления