[Из песочницы] Образы и контейнеры Docker в картинках
Перевод поста Visualizing Docker Containers and Images, от новичка к новичкам, автор на простых примерах объясняет базовые сущности и процессы в использовании docker.
Если вы не знаете, что такое Docker или не понимаете, как он соотносится с виртуальными машинами или с инструментами configuration management, то этот пост может показаться немного сложным.
Пост предназначен для тех, кто пытается освоить docker cli, понять, чем отличается контейнер и образ. В частности, будет объяснена разница между просто контейнером и запущенным контейнером.
В процессе освоения нужно представить себе некоторые лежащие в основе детали, например, слои файловой системы UnionFS. В течение последней пары недель я изучал технологию, я новичок в мире docker, и командная строка docker показалась мне довольно сложной для освоения.
По-моему, понимание того, как технология работает изнутри — лучший способ быстро освоить новый инструмент и правильно его использовать. Часто новая технология разрабатывает новые модели абстракций и привносит новые термины и метафоры, которые могут быть как будто бы понятны в начале, но без четкого понимания затрудняют последующее использование инструмента.
Хорошим примером является Git. Я не мог понять Git, пока не понял его базовую модель, включая trees, blobs, commits, tags, tree-ish и прочее. Я думаю, что люди, не понимающие внутренности Git, не могут мастерски использовать этот инструмент.
Определение образа (Image)
Визуализация образа представлена ниже в двух видах. Образ можно определить как «сущность» или «общий вид» (union view) стека слоев только для чтения.
Слева мы видим стек слоев для чтения. Они показаны только для понимания внутреннего устройства, они доступны вне запущенного контейнера на хост-системе. Важно то, что они доступны только для чтения (иммутабельны), а все изменения происходят в верхнем слое стека. Каждый слой может иметь одного родителя, родитель тоже имеет родителя и и.д. Слой верхнего уровня может быть использован как UnionFS (AUFS в моем случае с docker) и представлен в виде единой read-only файловой системы, в которой отражены все слои. Мы видим эту «сущность» образа на рисунке справа.
Если вы захотите посмотреть на эти слои в первозданном виде, вы можете найти их в файловой системе на хост-машине. Они не видны напрямую из запущенного контейнера. На моей хост-машине я могу найти образы в /var/lib/docker/aufs.
# sudo tree -L 1 /var/lib/docker/
/var/lib/docker/
├── aufs
├── containers
├── graph
├── init
├── linkgraph.db
├── repositories-aufs
├── tmp
├── trust
└── volumes
7 directories, 2 files
Определение контейнера (Container)
Контейнер можно назвать «сущностью» стека слоев с верхним слоем для записи.
На изображении выше показано примерно то же самое, что на изображении про образ, кроме того, что верхний слой доступен для записи. Вы могли заметить, что это определение ничего не говорит о том, запущен контейнер или нет и это неспроста. Разделение контейнеров на запущенные и не запущенные устранило путаницу в моем понимании.
Контейнер определяет лишь слой для записи наверху образа (стека слоев для чтения). Он не запущен.
Определение запущенного контейнера
Запущенный контейнер — это «общий вид» контейнера для чтения-записи и его изолированного пространства процессов. Ниже изображен контейнер в своем пространстве процессов.
Изоляция файловой системы обеспечивается технологиями уровня ядра, cgroups, namespaces и другие, позволяют докеру быть такой перспективной технологией. Процессы в пространстве контейнера могут изменять, удалять или создавать файлы, которые сохраняются в верхнем слое для записи. Смотрите изображение:
Чтобы проверить это, выполните команду на хост-машине:
docker run ubuntu touch happiness.txt
Вы можете найти новый файл в слое для записи на хост-машине, даже если контейнер не запущен.
# find / -name happiness.txt
/var/lib/docker/aufs/diff/860a7b...889/happiness.txt
Определение слоя образа (Image layer)
Наконец, мы определим слой образа. Изображение ниже представляет слой образа и дает нам понять, что слой — это не просто изменения в файловой системе.
Метаданные — дополнительная информация о слое, которая позволяет докеру сохранять информацию во время выполнения и во время сборки. Оба вида слоев (для чтения и для записи) содержат метаданные.
Кроме того, как мы уже упоминали раньше, каждый слой содержит указатель на родителя, используя id (на изображении родительские слои внизу). Если слой не указывает на родительский слой, значит он наверху стека.
Расположение метаданных
На данный момент (я понимаю, что разработчики docker могут позже сменить реализацию), метаданные слоев образов (для чтения) находятся в файле с именем «json» в папке /var/lib/docker/graph/id_слоя:
/var/lib/docker/graph/e809f156dc985.../json
где «e809f156dc985…» — урезанный id слоя.
Свяжем все вместе
Теперь, давайте посмотрим на команды, иллюстрированные понятными картинками.docker create
До:
После:
Команда 'docker create' добавляет слой для записи наверх стека слоев, найденного по
docker start
До:
После:
Команда 'docker start' создает пространство процессов вокруг слоев контейнера. Может быть только одно пространство процессов на один контейнер.
docker run
До:
После:
Один из первых вопросов, который задают люди (я тоже задавал): «В чем разница между 'docker start' и 'docker run'?» Одна из первоначальных целей этого поста — объяснить эту тонкость.
Как мы видим, команда 'docker run' находит образ, создает контейнер поверх него и запускает контейнер. Это сделано для удобства и скрывает детали двух команд.
Продолжая сравнение с освоением Git, я скажу, что 'docker run' очень похожа на 'git pull'. Так же, как и 'git pull' (который объединяет 'git fetch' и 'git merge'), команда 'docker run' объединяет две команды, которые могут использоваться и независимо. Это удобно, но поначалу может ввести в заблуждение.
docker ps
Команда 'docker ps' выводит список запущенных контейнеров на вашей хост-машине. Важно понимать, что в этот список входят только запущенные контейнеры, не запущенные контейнеры скрыты. Чтобы посмотреть список всех контейнеров, нужно использовать следующую команду.
docker ps -a
Команда 'docker ps -a', где 'a' — сокращение от 'all' выводит список всех контейнеров, независимо от их состояния.
docker images
Команда 'docker images' выводит список образов верхнего уровня (top-level images). Фактически, ничего особенного не отличает образ от слоя для чтения. Только те образы, которые имеют присоединенные контейнеры или те, что были получены с помощью pull, считаются образами верхнего уровня. Это различие нужно для удобства, так как за каждым образом верхнего уровня может быть множество слоев.
docker images -a
Команда 'docker images -a' выводит все образы на хост-машине. Это фактически список всех слоев для чтения в системе. Если вы хотите увидеть все слои одного образа, воспользуйтесь командой 'docker history'.
docker stop
До:
После:
Команда 'docker stop' посылает сигнал SIGTERM запущенному контейнеру, что мягко останавливает все процессы в пространстве процессов контейнера. В результате мы получаем не запущенный контейнер.
docker kill
До:
После:
Команда 'docker kill' посылает сигнал SIGKILL, что немедленно завершает все процессы в текущем контейнере. Это почти то же самое, что нажать Ctrl+\ в терминале.
docker pause
До:
После:
В отличие от 'docker stop' и 'docker kill', которые посылают настоящие UNIX сигналы процессам контейнера, команда 'docker pause' используют специальную возможность cgroups для заморозки запущенного пространства процессов. Подробности можно прочитать здесь, если вкратце, отправки сигнала Ctrl+Z (SIGTSTP) не достаточно, чтобы заморозить все процессы в пространстве контейнера.
docker rm
До:
После:
Команда 'docker rm' удаляет слой для записи, который определяет контейнер на хост-системе. Должна быть запущена на остановленном контейнерах. Удаляет файлы.
docker rmi
До:
После:
Команда 'docker rmi' удаляет слой для чтения, который определяет «сущность» образа. Она удаляет образ с хост-системы, но образ все еще может быть получен из репозитория через 'docker pull'. Вы можете использовать 'docker rmi' только для слоев верхнего уровня (или образов), для удаления промежуточных слоев нужно использовать 'docker rmi -f'.
docker commit
До:
или
После:
Команда 'docker commit' берет верхний уровень контейнера, тот, что для записи и превращает его в слой для чтения. Это фактически превращает контейнер (вне зависимости от того, запущен ли он) в неизменяемый образ.
docker build
До:
Dockerfile и
После:
Со многими другими слоями.
Команда 'docker build' интересна тем, что запускает целый ряд команд:
На изображении выше мы видим, как команда build использует значение инструкции FROM из файла Dockerfile как базовый образ после чего:
1) запускает контейнер (create и start)
2) изменяет слой для записи
3) делает commit
На каждой итерации создается новый слой. При исполнении 'docker build' может создаваться множество слоев.
docker exec
До:
После:
Команда 'docker exec' применяется к запущенному контейнеру, запускает новый процесс внутри пространства процессов контейнера.
docker inspect
До:
или
После:
Команда 'docker inspect' получает метаданные верхнего слоя контейнера или образа.
docker save
До:
После:
Команда 'docker save' создает один файл, который может быть использован для импорта образа на другую хост-систему. В отличие от команды 'export', она сохраняет все слои и их метаданные. Может быть применена только к образам.
docker export
До:
После:
Команда 'docker export' создает tar архив с содержимым файлов контейнера, в результате получается папка, пригодная для использования вне docker. Команда убирает слои и их метаданные. Может быть применена только для контейнеров.
docker history
До:
После:
Команда 'docker history' принимает
Итог
Я надеюсь, вам понравилась эта визуализация контейнеров и образов. Есть много других команд (pull, search, restart, attach и другие), которые могут или не могут быть объяснены моими сравнениями.