История контейнеризации

Контейнеры, микросервисы, Docker, Kubernetes и ещё +100500 непонятных слов и аббревиатур 🤯. Если вы считаете себя единственным, кто всего этого не понимает, не волнуйтесь — вы далеко не один!

В этой статье разберемся в основах контейнеризации, рассмотрим конкретные технологии контейнеризации приложений, а в следующих публикациях углубимся в тему и пройдемся по Docker и Kubernetes. Готовы? Отдать швартовы — отходим 🚢.

Основа контейнеризации — ядро Linux

Основы контейнеризации

Контейнер — это изолированная рабочая среда, содержащая все зависимости, конфигурационные и исполняемые файлы необходимые для работы программы или пользователя, находящегося в контейнере. Для работы контейнера ОС выделяет пул изолированных ресурсов: ядра ЦП, оперативная память, диск и сеть.

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

Пока в Linux существуют всего несколько инструментов ядра, которыми можно изолировать процессы и ограничить доступ к ресурсам. С помощью Namespaces (неймспейсы) процессы можно объединить в группы и изолировать, а с помощью Cgroups можно задать лимиты по ресурсам.

Namespaces и Cgroups мы разберём чуть позже, а пока взглянем на функцию изоляции процессов на уровне ядра ОС, которой пользовались задолго до появления Namespaces и Cgroups — chroot.

Первый механизм изоляции процессов — chroot

Механизм изоляции процессов — chroot

Про механизмы разделения процессов на изолированные окружения начали думать ещё в конце 70-х. Первым механизмом управления изоляцией процессов стал — chroot (чрут).

Chroot — это подмена корня файловой системы для группы процессов или временная смена корня и контекста для запуска выбранных процессов. Точнее сказать так: chroot добавляет в систему второй корневой каталог «/», который с точки зрения пользователя ничем не будет отличаться от первого.

После применения чрута файловая система начинает выглядеть как-то так:

Chroot

Теперь файловая система разделена на две не влияющие друг на друга части.

Сейчас chroot стандартная функция любой *nix-ОС. Появившись впервые в 1982, в BSD 4.2, он почти не изменился, отсюда такое обилие недостатков:

  • Общие пространство процессов — процесс запущенный в chroot видит все остальные процессы.
  • Сеть общая — невозможно запустить изолированные серверы в разных чрутах.
  • Нельзя назначить лимиты по ресурсам — в любой момент процесс из чрута может так же, как и любой другой процесс в системе аллоцировать все ресурсы.

В каких задачах chroot применяется до сих пор:

  • ограничение прав анонимных пользователей, подключившихся по ftp-протоколу
  • управление правами пользователей, подключившихся по ssh-протоколу.

Chroot служит для изоляции процессов. Например, bind во многих дистрибутивах Linux запускается в chroot, многие демоны перед понижением привилегий делают chroot в пустую директорию. Также chroot может изолировать пользователей — демоны ftp и ssh через chroot ограничивают права анонимных пользователей.

Если вам хочется попробовать chroot на практике и поупражняться в Linux-администрировании. Рекомендуем прочесть эту статью.

Chroot был самым простым и единственным способом изоляции процессов в течение 15 лет, пока не появился jail.

Первый контейнер — технология jail в FreeBSD 4.0

Jail

Jail появился в 1999-2000 годах, в FreeBSD 4.0. По факту jail — это тот же chroot только посложнее. В jail появилась изоляция сети. Однако во FreeBSD, это изоляция была половинчатой — сетевые интерфейсы были видны в любом окружении, но в зависимости от окружения были видны только определённые IP-адреса, которые были присвоены определенным интерфейсам. При этом loopback был общим.

Jail был уже намного продвинутее чрута, и на нём даже строили хостинги, однако ограничения ресурсов в нём по-прежнему не было. Назначать лимиты на ресурсы стало возможно лишь в FreeBSD версии 9.0, с появлением механизма управления ресурсами RCTL. Эта система позволяет ограничивать ресурсы как отдельным пользователям и процессам, так и целым jail.

Интересный факт

В мире кроме всем нам хорошо известных Linux и Windows есть много самописных ОС, которые, конечно, в качестве основы брали уже что-то готовое от других ОС, но при этом приносили и что-то неожиданное и оригинальное.

Такой ОС непохожей на все остальные была Plan9. Это ОС разработана в Bell labs. Её исходники опубликованы на рубеже 00-х.

Фишка Plan9 в отношении к идеологии *NIX. Постулат *NIX — «всё есть файл».

Plan9

Однако сокеты в *NIX-системах не вписываются в эту абстракцию, а вот в Plan9, сокет — это тоже файл.

Разработчики Plan9 возвели идеологию *NIX-систем в абсолют. В будущем Plan9 вдохновит немало других ОС, таких, как Harvey OS и Jehanne OS.

Примерно в то же время, в начале 2000 на свет появилась Virtuozzo в качестве коммерческой контейнеризации и openvz. Однако неудачные попытки коммерциализации продукта сильно затормозили приход новой системы контейнеризации в массы.

Немного о свободном ПО и коммерциализации

Если бы Parallels, Inc последовали бы сразу заветам Ричарда Столлмана, о коммерциализации open source-проектов, исходя из его заветов: зарабатывать open source может только на платном суппорте и платной доработке фич, возможно, судьба Virtuozzo сложилась бы по другому.

Кстати, Столлман ещё тот олдфаг. На своих выступлениях и лекциях он часто призывает отказаться от сотовых телефонов и передачи своих личных данных третьим сторонам.

Richard Stallman

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

Ознакомиться с биографией Ричарда Мэттью Столмана можно в Википедии.

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

Для более глубокого погружения в jail рекомендуем прочесть вот эту статью на Хабре. Внутри много кода и подробная инструкция по настройке jail.

Переломный момент — появление Namespaces

Namespaces

Свежим глотком воздуха стало появление в 2002 году, в ядре Linux версии 2.4.19 нового API для создания абстракции контроля над общими ресурсами — Namespaces API.

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

С появлением namespace в ядро было добавлено 3 новых функции, которые отвечают за управление атрибутами namespace:

  • clone() — аналог fork() с возможностью выделения частей общих ресурсов в отдельные namespaces.
  • setns() — подключает указанный процесс к заданному namespace.
  • unshare() — изменение контекста текущего процесса.

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

Существуют следующие пространства имён:

  • Mount — абстракция над пространством имен для файловых систем. Mount позволяет сразу монтировать новое устройство в несколько пространств имён файловой системы, вместо монтирования в каждом отдельном пространстве.
  • Network — абстракция над сетью: интерфейсы, таблицы маршрутизации и т.д. Пространство имен Network по сути выполняет роль туннеля между разными пространствами имён сети.
  • IPC — абстракция над межпроцессным взаимодействием. Процесс в пространстве имен IPC не может писать или читать IPC ресурсы, принадлежащие другому пространству имен. Так процессы в одном контейнере не могут вмешиваться в другие контейнеры.
  • PID — абстракция над пространством номеров процессов. PID изолирует пространство ID процессов. Процессы в различных пространствах могут иметь одинаковые ID.
  • User — абстракция над пространством пользователей. User изолирует ID пользователей и групп, корневой каталог, ключи и capabilities.
  • UTS (Unix Time Sharing) — абстракция над пространством hostname и NIS. UTS позволяет контейнерам иметь собственные доменные имена NIS domainname и имена контейнеров nodename.
  • Cgroup — используется как атрибут, корневой узел дерева cgroup.

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

Неймспейсы решили вопросы изоляции, однако вопрос ограничения ресурсов для изолированных процессов оставался открытым. Решение этого вопроса появилось с выходом ядра версии 2.6.20 в 2008 году — в нём появился механизм Cgroups.

Ограничение ресурсов — Cgroups

Ограничение ресурсов — Cgroups

Cgroups (control group) — группа процессов Linux, на которые наложена изоляция и установлены ограничения на вычислительные ресурсы (процессорные, сетевые, ресурсы памяти, ресурсы ввода-вывода) со стороны ядра Linux.

Cgroups — было отличное самодостаточное решение, которое сильно продвинуло технологии контейнеризации вперед. Первой попыткой создания подобного механизма лимитирования ресурсов для изолированных процессов был process accounting, появившийся в 1999 году.

Опять же не обошлось без Google

Google — компания, которая умеет идти своей дорогой, даже там, где дороги вроде бы и нет.

В 2006 году Google взялась за разработку собственного механизма управления ресурсами контейнеров — process containers. Разрабатывали механизм Пол Менедж и Рохит Сет.

Google

Замысел был скромен — усовершенствовать механизм cpuset, предназначенный для распределения процессорного времени и памяти между задачами. Но обыкновенная палка в руках опытных разработчиков выстрелила!

В итоге в конце 2007 года название process containers было заменено на control groups, а в 2008 году cgroups был официально добавлен в ядро Linux (версия 2.6.24).

Разработанные в Google и, включенные в ядро в 2008 году Cgroup стали новой абстракцией над управлением ресурсами. Cgroups состоит из двух частей: cgroup core (ядро cgroup) и подсистем (директории с управляющими файлами).

Количество подсистем может отличаться в зависимости от версии ядра. Так в ядре версии 4.4.0.21 таких подсистем 12, в ядре версии 5.10.16.3, которое содержится в Ubuntu 20 TLS, подсистем уже 13:

  1. blkio — управляет лимитами чтения и записи на блочных устройствах;
  2. cpu — управляет доступом к ресурсам процессора;
  3. cpuset — выделяет отдельных процессоров группе;
  4. cpuacct — управляет ресурсами cpu (работает совместно с cpu);
  5. devices — ограничивает доступ к устройствам;
  6. hugetlb — управляет работой групп с большими страницами памяти (huge pages);
  7. net_cls — размечает спец. тегами сетевые пакеты, что позволяет процессам генерирующим их определять какие процессы их сгенерировали;
  8. perf_event rdma — предоставляет интерфейс для инструмента анализа производительности в Linux (perf);
  9. freezer — управляет приостановкой и возобновлением процессов выполнения задач внутри контрольной группы;
  10. memory — управляет выделением памяти для групп процессов;
  11. net_prio — управляет динамическим назначением приоритета трафика;
  12. pids — ограничивает количество процессов в рамках контрольной группы;
  13. unified — автоматически монтирует файловую систему в каталог /sys/fs/cgroup/unified при запуске системы.

Вывести список подсистем в вашей версии ядра можно следующей командой:

$ ls /sys/fs/cgroup/
blkio cpuacct devices hugetlb net_cls perf_event rdma cpu cpuset freezer memory net_prio pids unified

Посмотреть к каким контрольным группам принадлежит процесс можно так:

  • вызовите утилиту htop
  • найдите нужный вам PID процесса
  • перейдите в каталог /proc и найдите там директорию с нужным вам PID
  • вызовите команду cat и передайте ей cgroup
$ cd /proc/PID/
$ cat cgroup
39:rdma:/
38:pids:/
37:hugetlb:/
36:net_prio:/
35:perf_event:/
34:net_cls:/
33:freezer:/
32:devices:/
31:memory:/
30:blkio:/
29:cpuacct:/
28:cpu:/
27:cpuset:/
0::/

Как работать с Cgroups

Работать с cgroups можно двумя способами:

  • Стандартным способ — работать с cgroups как с файлами. Вносить записи групп с помощью echo в нужный контроллер.
  • С помощью cgroup-tools — работать с cgroups через набор утилит, которые облегчают взаимодействие с cgroups.

Первый способ более предпочтительный, но более трудозатратный для пользователей, имеющих не богатый опыт администрирования Linux, поэтому мы пойдем вторым путем — будем использовать cgroup-tools.

Поместить процесс в контрольную группу просто. Достаточно выполнить команду cgcreate, передать ей следующие параметры:

  • -t uid:gid — пользователи, получающие права на перемещение заданий в (из) группы.
  • -a uid:gid — пользователи, получающие права на управление параметрами группы (опциональный параметр).
  • -g список подсистем:путь.

Пример создания контрольной группы по памяти и процессору:

$ sudo cgcreate -t root:root -g memory,cpu:/mycgroup

Теперь в нашу вновь созданную группу нужно переместить процессы. Для этого есть команда cgclassify, в которую с помощью ключа g передается группа, её название и пиды процессов:

$ sudo cgclassify -g cpu:/mycgroup 7

Убедится, что процесс попал в новую группу можно с помощью команды cat:

$ cat /proc/PID-номер/cgroup

Удалить процесс из группы можно также одной командой:

$ sudo cgdelete -g memory,cpu:/mycgroup

До этого мы умели лишь создавать группы и помещать в них процессы. Теперь научимся задавать ограничение ресурсов. Делается это командой cgset с параметром -r:

$ cgset -r cpu.shares=1 /mycgroup

Мы рассмотрели несколько основных инструментов управления контрольными группами из пакета cgroups-tools. Их там конечно же куда больше. Полное описание всех утилит пакета можно посмотреть тут.

Кстати, есть и альтернативный способ управления и настройки cgroups, о нём можно узнать из этой статьи:

Установка Cgroups

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

Что же такое контейнер в итоге?

Что такое контейнер?

По сути контейнер — это определенные пространства имён (Namespaces) и наборы контрольных групп (Control groups), удобно управляемые с помощью сторонних утилит. Например, с помощью Docker.

Namespace — механизм изоляции процессов. Мы можем создавать пространства имён процессов (группы процессов) помещать туда нужные нам процессы по их идентификаторам — пидам (PID) и эти процессы не могут обращаться к процессам вне своего пространства имён.

Control groups — механизм определения количества выдаваемых ресурсов процессам.

Контейнеры обладают следующими важными преимуществами:

  • Они легковесные и быстро выполняются, так как содержат только всё необходимое для работы контейнера: исполняемые и конфигурационные файлы, прочие зависимости.
  • Хостовая ОС может смотреть внутрь контейнера и видеть дерево процессов. Когда мы смотрим на процесс виртуальной машины с хоста, мы видим один процесс.
  • Появляется возможность использовать микросервисную архитектуру. Каждый сервис можно поместить в свой контейнер, назначить ему ресурсы, запускать и останавливать их независимо друг от друга.
  • Можно быстрее развертывать приложения, легче масштабировать их горизонтально, проще находить в них ошибки.
  • Выход из строя одного контейнера не влияет на дальнейшую работу других контейнеров.

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

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

Контейнеризация не подходит, если для работы приложения требуется другая ОС, а не та, что установлена на сервере.

И в конце, мы хотим оставить ссылку на интервью, того, без кого не было бы ни контейнеров, ни Линукса, ни системы контроля версий GIT, да и вообще, не было бы того технологического мира, в котором мы все с вами живём:

Интервью с Линусом Торвальдсом

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