Що таке Docker: короткий екскурс в історію та основні абстракції

10 серпня у Слерм стартував відеокурс з Docker, в якому ми розуміємо його повністю - від основних абстракцій до параметрів мережі.

У цій статті поговоримо про історію появи Docker та його основні абстракції: Image, Cli, Dockerfile. Лекція розрахована на новачків, тому навряд чи буде цікавою досвідченим користувачам. Тут не буде крові, апендикса та глибокого занурення. Найбільш основи.

Що таке Docker: короткий екскурс в історію та основні абстракції

Що таке Docker

Подивимося визначення Docker з Вікіпедії.

Docker — це програмне забезпечення для автоматизації розгортання та керування програмами в середовищах з підтримкою контейнеризації.

З цього визначення нічого не зрозуміло. Особливо незрозуміло, що означає «у середовищах із підтримкою контейнеризації». Щоб розібратися, повернемося до минулого. Почнемо з доби, яку я умовно називаю «Монолітною ерою».

Монолітна ера

Монолітна ера - це початок 2000-х, коли всі програми були монолітними, з купою залежностей. Розробка тривала довго. При цьому серверів було не так багато, ми всі їх знали за іменами та моніторили. Є таке кумедне порівняння:

Pets – це домашні тварини. У монолітній ері ми ставилися до своїх серверів, як до свійських тварин, пестили і плекали, порошинки здували. А для кращого управління ресурсами використовували віртуалізацію: брали сервер і пилили на кілька віртуальних машин, забезпечуючи тим самим ізоляцію оточення.

Системи віртуалізації з урахуванням гіпервізора

Про системи віртуалізації, напевно, всі чули: VMware, VirtualBox, Hyper-V, Qemu KVM і т. д. Вони забезпечують ізоляцію додатків та управління ресурсами, але у них є і мінуси. Щоб зробити віртуалізацію, потрібний гіпервізор. А гіпервізор — це оверхід ресурсів. Та й сама віртуальна машина зазвичай ціла махіна – важкий образ, на ньому операційна система, Nginx, Apache, можливо, і MySQL. Образ великою віртуальною машиною незручно оперувати. Як наслідок, робота з віртуалками може бути повільною. Щоб вирішити цю проблему, створили системи віртуалізації лише на рівні ядра.

Системи віртуалізації лише на рівні ядра

Віртуалізацію лише на рівні ядра підтримують системи OpenVZ, Systemd-nspawn, LXC. Яскравим прикладом такої віртуалізації є LXC (Linux Containers).

LXC - система віртуалізації на рівні операційної системи для запуску кількох ізольованих екземплярів операційної системи Linux на одному вузлі. LXC не використовує віртуальні машини, а створює віртуальне оточення з власним простором процесів та мережевим стеком.

Насправді LXC створює контейнери. У чому різниця між віртуальними машинами та контейнерами?

Що таке Docker: короткий екскурс в історію та основні абстракції

Контейнер не підходить для ізолювання процесів: у системах віртуалізації на рівні ядра знаходять уразливості, які дозволяють вилізти з контейнера на хост. Тому якщо вам потрібно щось ізолювати, краще використовувати віртуалку.

Відмінності між віртуалізацією та контейнеризацією можна побачити на схемі.
Бувають апаратні гіпервізори, гіпервізори поверх ОС та контейнери.

Що таке Docker: короткий екскурс в історію та основні абстракції

"Залізні" гіпервізори - це крута штука, якщо ви дійсно хочете щось ізолювати. Тому що там є можливість ізолювати лише на рівні сторінок пам'яті, процесорів.

Є гіпервізори як програма, і є контейнери, про них ми говоритимемо далі. У системах контейнеризації гіпервізора немає, але є Container Engine, який створює контейнери та керує ними. Штука це легковажніша, тому за рахунок роботи з ядром оверхед менше, або його немає зовсім.

Що використовується для контейнеризації на рівні ядра

Основні технології, які дозволяють створювати ізольований від інших процесів контейнер, це Namespaces і Control Groups.

Namespaces: PID, Networking, Mount та User. Є ще, але для простоти розуміння зупинимося на цих.

PID Namespace обмежує процеси. Коли ми, наприклад, створюємо PID Namespace, поміщаємо туди процес, він стає з PID 1. Зазвичай у системах PID 1 — це systemd чи init. Відповідно, коли ми поміщаємо процес у новий namespace, він також отримує PID 1.

Networking Namespace дозволяє обмежити/ізолювати мережу та всередині вже розміщувати свої інтерфейси. Mount — це обмеження файлової системи. User - обмеження за користувачами.

Control Groups: Memory, CPU, IOPS, Network - всього близько 12 налаштувань. Інакше їх ще називають Cgroups (Cі-групи).

Control Groups керують ресурсами для контейнера. Через Control Groups ми можемо сказати, що контейнер не повинен споживати більше кількості ресурсів.

Щоб контейнеризація повноцінно працювала, використовуються додаткові технології: Capabilities, Copy-on-write та інші.

Capabilities - це коли ми говоримо процесу, що він може робити, а чого не може. На рівні ядра це просто бітові карти з багатьма параметрами. Наприклад, користувач root має повні привілеї, може робити все. Сервер часу може змінювати системний час: у нього є можливості на Time Capsule, і все. За допомогою привілеїв можна гнучко налаштувати обмеження для процесів і тим самим убезпечити себе.

Система Copy-on-write дозволяє нам працювати з образами Docker, використовувати їх ефективніше.

Наразі Docker має проблеми із сумісністю Cgroups v2, тому у статті розглядаються саме Cgroups v1.

Але повернемося до історії.

Коли з'явилися системи віртуалізації лише на рівні ядра, їх почали активно застосовувати. Зверху на гіпервізор зник, але деякі проблеми залишилися:

  • великі образи: в ту ж OpenVZ штовхають операційну систему, бібліотеки, купу різного софту, і в результаті образ все одно виходить немаленьким;
  • немає нормального стандарту упаковки та доставки, тому залишається проблема залежностей. Бувають ситуації, коли два шматки коду використовують одну бібліотеку, але з різними версіями. Між ними можливий конфлікт.

Щоб усі ці проблеми вирішити, настала наступна ера.

Ера контейнерів

Коли настала Ера контейнерів, змінилася філософія роботи з ними:

  • Один процес – один контейнер.
  • Усі необхідні процесу залежності доставляємо у його контейнер. Це вимагає розпилювати моноліти на мікросервіси.
  • Чим менший образ, тим краще — менше можливих уразливостей, швидше розкочується і таке інше.
  • Інстанси стають ефемерними.

Пам'ятаєте, я говорив про pets vs cattle? Раніше інстанси були подібні до домашніх тварин, а тепер стали як cattle - худоба. Раніше був моноліт — одна програма. Наразі це 100 мікросервісів, 100 контейнерів. У якихось контейнерів може бути по 2-3 репліки. Нам стає не так важливо контролювати кожен контейнер. Нам швидше важлива доступність самого сервісу: того, що робить цей набір контейнерів. Це змінює підходи у моніторингу.

У 2014-2015 роках стався розквіт Docker - тієї технології, про яку ми зараз говоритимемо.

Docker змінив філософію та стандартизував упаковку програми. За допомогою Docker ми можемо запакувати програму, відправити її в репозиторій, завантажити звідти, розгорнути.

У Docker-контейнер ми закладаємо все необхідне, тому вирішується проблема залежностей. Docker гарантує відтворюваність. Я думаю, багато хто стикався з невоспроизводимостью: у тебе все працює, пушиш на продакшен, там це перестає працювати. З Docker ця проблема йде. Якщо твій Docker-контейнер запускається і робить те, що потрібно робити, то з великою ймовірністю він запуститься на продакшені і там зробить те саме.

Відступ про оверхід

З приводу оверхіду постійно точаться суперечки. Хтось вважає, що Docker не несе додаткове навантаження, оскільки використовує ядро ​​Linux та всі його процеси, необхідні для контейнеризації. Мовляв, «якщо ви кажете, що Docker — це оверхід, то тоді і ядро ​​Linux оверхід».

З іншого боку, якщо заглибитися, то в Docker і справді є кілька речей, про які з натяжкою можна сказати, що це оверхед.

Перше – це PID namespace. Коли ми в namespace поміщаємо якийсь процес, йому присвоюється PID 1. У той же час цей процес має ще один PID, який знаходиться на хостовому namespace, за межами контейнера. Наприклад, ми запустили у контейнері Nginx, він став PID 1 (майстер-процес). А на хості у нього PID 12623. І складно сказати, наскільки це оверхід.

Друга штука – це Cgroups. Візьмемо Cgroups по пам'яті, тобто можливість обмежувати контейнер пам'ять. При її включенні активуються лічильники, memory accounting: ядру треба розуміти, скільки сторінок виділено, а ще вільно для цього контейнера. Це можливо оверхед, але точних досліджень про те, як він впливає на продуктивність, я не зустрічав. І сам не помічав, що програма, запущена в Docker, раптом різко втрачала у продуктивності.

І ще одне зауваження щодо продуктивності. Деякі параметри ядра прокидаються з хоста у контейнер. Зокрема, деякі параметри мережі. Тому якщо ви хочете запустити в Docker щось високопродуктивне, наприклад те, що активно використовуватиме мережу, то вам, як мінімум, треба ці параметри підправити. Якийсь nf_conntrack, наприклад.

Про концепцію Docker

Docker складається з кількох компонентів:

  1. Docker Daemon - те саме Container Engine; запускає контейнери.
  2. Docker CII - утиліта з управління Docker.
  3. Dockerfile — інструкція щодо того, як збирати образ.
  4. Image – образ, з якого розкочується контейнер.
  5. Контейнер.
  6. Docker registry - сховище образів.

Схематично це виглядає приблизно так:

Що таке Docker: короткий екскурс в історію та основні абстракції

На Docker_host працює Docker Daemon, запускає контейнери. Є Client, який передає команди: збери образ, скачай образ, запусти контейнер. Docker daemon ходить у registre і виконує їх. Docker-клієнт може звертатися і локально (до юнікс-сокету), і TCP з віддаленого хоста.

Пройдемося по кожному компоненту.

Docker daemon (демон) - Це серверна частина, вона працює на хост-машині: завантажує образи і запускає з них контейнери, створює мережу між контейнерами, збирає логи. Коли ми говоримо «створи образ», цим теж займається демон.

Докер CLI - Клієнтська частина Docker, консольна утиліта для роботи з демоном. Повторю, вона може працювати не лише локально, а й по мережі.

Базові команди:

docker ps – показати контейнери, які зараз запущені на Docker-хості.
docker images - показати образи, завантажені локально.
docker search <> - пошук образу в registry.
docker pull <> - завантажити образ із registry на машину.
docker build < > - Зібрати образ.
docker run <> - запуск контейнера.
docker rm - видалити контейнер.
docker logs <> - логи контейнера
docker start/stop/restart <> — робота з контейнером

Якщо ви освоїте ці команди і впевнено ними користуватиметеся, то вважайте, що на 70% освоїли Docker на рівні користувача.

Докер-файл - Інструкція для створення образу. Майже кожна команда інструкції – новий шар. Подивимося на прикладі.

Що таке Docker: короткий екскурс в історію та основні абстракції

Приблизно так виглядає Dockerfile: ліворуч команди, праворуч аргументи. Кожна команда, що є (і взагалі пишеться в Dockerfile), створює новий шар в Image.

Навіть дивлячись на ліву частину можна приблизно зрозуміти, що відбувається. Ми говоримо: "Створи нам папку" - це один шар. "Зроби папку робочої" - це ще один шар, і так далі. Листковий пиріг спрощує життя. Якщо я створю ще один Dockerfile і в останньому рядку щось зміню - запущу не "python" "main.py", а щось інше, або встановлю залежність з іншого файлу - то попередні шари будуть перевикористані, як кеш.

зображення - Це упаковка контейнера, з образу запускаються контейнери. Якщо дивитися на Docker з точки зору пакетного менеджера (начебто ми працюємо з deb або rpm-пакетами), то image це по суті rpm-пакет. Через yum install ми можемо поставити програму, видалити її, знайти в репозиторії, завантажити. Тут приблизно те саме: з образу запускаються контейнери, вони зберігаються в Docker registry (за аналогією з yum, в репозиторії), і кожен image має хеш SHA-256, ім'я та тег.

Image збирається за інструкцією з Dockerfile. Кожна інструкція із Dockerfile створює новий шар. Шари можна використовувати повторно.

Docker registry - Це репозиторій образів Docker. За аналогією з ОС, Docker має загальнодоступний стандартний реєстр — dockerhub. Але можна зібрати власний репозиторій, власний Docker registry.

Контейнер - Те, що запускається з образу. За інструкцією з Dockerfile зібрали образ, потім ми його із цього образу запускаємо. Цей контейнер ізольований від інших контейнерів, він повинен містити все необхідне для роботи програми. При цьому один контейнер – один процес. Трапляється, що доводиться робити два процеси, але це дещо суперечить ідеології Docker.

Вимога "один контейнер - один процес" пов'язана з PID Namespace. Коли в Namespace запускається процес з PID 1, якщо він раптом помре, весь контейнер теж помирає. Якщо там запущено два процесу: один живе, а другий помер, то контейнер все одно продовжить жити. Але це питання Best Practices, ми про них поговоримо в інших матеріалах.

Більш детально вивчити особливості та повну програму курсу можна за посиланням: «Відеокурс з Docker».

Автор: Марсель Ібраєв, сертифікований адміністратор Kubernetes, практикуючий інженер у компанії Southbridge, спікер та розробник курсів Слерм.

Джерело: habr.com

Додати коментар або відгук