Какая файловая система docker
By default all files created inside a container are stored on a writable container layer. This means that:
- The data doesn’t persist when that container no longer exists, and it can be difficult to get the data out of the container if another process needs it.
- A container’s writable layer is tightly coupled to the host machine where the container is running. You can’t easily move the data somewhere else.
- Writing into a container’s writable layer requires a storage driver to manage the filesystem. The storage driver provides a union filesystem, using the Linux kernel. This extra abstraction reduces performance as compared to using data volumes, which write directly to the host filesystem.
Docker has two options for containers to store files on the host machine, so that the files are persisted even after the container stops: volumes, and bind mounts.
Docker also supports containers storing files in-memory on the the host machine. Such files are not persisted. If you’re running Docker on Linux, tmpfs mount is used to store files in the host’s system memory. If you’re running Docker on Windows, named pipe is used to store files in the host’s system memory.
Keep reading for more information about persisting data or taking advantage of in-memory files.
Good use cases for bind mounts
In general, you should use volumes where possible. Bind mounts are appropriate for the following types of use case:
Sharing configuration files from the host machine to containers. This is how Docker provides DNS resolution to containers by default, by mounting /etc/resolv.conf from the host machine into each container.
Sharing source code or build artifacts between a development environment on the Docker host and a container. For instance, you may mount a Maven target/ directory into a container, and each time you build the Maven project on the Docker host, the container gets access to the rebuilt artifacts.
If you use Docker for development this way, your production Dockerfile would copy the production-ready artifacts directly into the image, rather than relying on a bind mount.
When the file or directory structure of the Docker host is guaranteed to be consistent with the bind mounts the containers require.
Tips for using bind mounts or volumes
If you use either bind mounts or volumes, keep the following in mind:
If you mount an empty volume into a directory in the container in which files or directories exist, these files or directories are propagated (copied) into the volume. Similarly, if you start a container and specify a volume which does not already exist, an empty volume is created for you. This is a good way to pre-populate data that another container needs.
If you mount a bind mount or non-empty volume into a directory in the container in which some files or directories exist, these files or directories are obscured by the mount, just as if you saved files into /mnt on a Linux host and then mounted a USB drive into /mnt . The contents of /mnt would be obscured by the contents of the USB drive until the USB drive were unmounted. The obscured files are not removed or altered, but are not accessible while the bind mount or volume is mounted.
Докер — это открытая платформа для разработки, доставки и эксплуатации приложений. Docker разработан для более быстрого выкладывания ваших приложений. С помощью docker вы можете отделить ваше приложение от вашей инфраструктуры и обращаться с инфраструктурой как управляемым приложением. Docker помогает выкладывать ваш код быстрее, быстрее тестировать, быстрее выкладывать приложения и уменьшить время между написанием кода и запуска кода. Docker делает это с помощью легковесной платформы контейнерной виртуализации, используя процессы и утилиты, которые помогают управлять и выкладывать ваши приложения.
В своем ядре docker позволяет запускать практически любое приложение, безопасно изолированное в контейнере. Безопасная изоляция позволяет вам запускать на одном хосте много контейнеров одновременно. Легковесная природа контейнера, который запускается без дополнительной нагрузки гипервизора, позволяет вам добиваться больше от вашего железа.
- упаковывание вашего приложения (и так же используемых компонент) в docker контейнеры;
- раздача и доставка этих контейнеров вашим командам для разработки и тестирования;
- выкладывания этих контейнеров на ваши продакшены, как в дата центры так и в облака.
Общие советы по использованию томов
Монтирование в непустые директории
Если вы монтируете пустой том в каталог контейнера, где уже есть файлы, то эти файлы не удалятся, а будут скопированы в том. Этим можно пользоваться, когда нужно скопировать данные из одного контейнера в другой.
Если вы монтируете непустой том или каталог с хоста в контейнер, где уже есть файлы, то эти файлы тоже не удалятся, а просто будут скрыты. Видно будет только то, что есть в томе или каталоге на хосте. Похоже на простое монтирование в Linux.
Монтирование служебных файлов
С хоста можно монтировать любые файлы, в том числе служебные. Например, сокет docker. В результате получится docker-in-docker: один контейнер запустится внутри другого. UPD: (*это не совсем так. mwizard в комментариях пояснил, что в таком случае родительский docker запустит sibling-контейнер). Выглядит как бред, но в некоторых случаях бывает оправдано. Например, при настройке CI/CD.
Монтирование /var/lib/docker
Разработчики Docker говорят, что не стоит монтировать с хоста каталог /var/lib/docker, так как могут возникнуть проблемы. Однако есть некоторые программы, для запуска которых это необходимо.
Ключ командной строки для Docker при работе с томами.
Для volume или bind mount:
Команды для управления томами в интерфейсе CLI Docker:
Создадим тестовый том:
Вот он появился в списке:
Команда inspect выдаст примерно такой список информации в json:
Попробуем использовать созданный том, запустим с ним контейнер:
После самоуничтожения контейнера запустим другой и подключим к нему тот же том. Проверяем, что в нашем файле:
То же самое, отлично.
Теперь примонтируем каталог с хоста:
Docker не любит относительные пути, лучше указывайте абсолютные!
Теперь попробуем совместить оба типа томов сразу:
Отлично! А если нам нужно передать ровно те же тома другому контейнеру?
Вы можете заметить некий лаг в обновлении данных между контейнерами, это зависит от используемого Docker драйвера файловой системы.
Создавать том заранее необязательно, всё сработает в момент запуска docker run:
Посмотрим теперь на список томов:
Ещё немного усложним команду запуска, создадим анонимный том:
Такой том самоуничтожится после выхода из контейнера, так как мы указали ключ –rm.
Если этого не сделать, давайте проверим что будет:
Хозяйке на заметку: тома (как образы и контейнеры) ограничены значением настройки dm.basesize, которая устанавливается на уровне настроек демона Docker. Как правило, что-то около 10Gb. Это значение можно изменить вручную, но потребуется перезапуск демона Docker.
При запуске демона с ключом это выглядит так:
Однажды увеличив значение, его уже нельзя просто так уменьшить. При запуске Docker выдаст ошибку.
Если вам нужно вручную очистить содержимое всех томов, придётся удалять каталог, предварительно остановив демон:
Если вам интересно узнать подробнее о работе с данными в Docker и других возможностях технологии, приглашаем на двухдневный онлайн-интенсив в феврале. Будет много практики.
Автор статьи: Александр Швалов, практикующий инженер Southbridge, Certified Kubernetes Administrator, автор и разработчик курсов Слёрм.
To use storage drivers effectively, it’s important to know how Docker builds and stores images, and how these images are used by containers. You can use this information to make informed choices about the best way to persist data from your applications and avoid performance problems along the way.
Images and layers
A Docker image is built up from a series of layers. Each layer represents an instruction in the image’s Dockerfile. Each layer except the very last one is read-only. Consider the following Dockerfile:
This Dockerfile contains four commands. Commands that modify the filesystem create a layer. The FROM statement starts out by creating a layer from the ubuntu:18.04 image. The LABEL command only modifies the image’s metadata, and does not produce a new layer. The COPY command adds some files from your Docker client’s current directory. The first RUN command builds your application using the make command, and writes the result to a new layer. The second RUN command removes a cache directory, and writes the result to a new layer. Finally, the CMD instruction specifies what command to run within the container, which only modifies the image’s metadata, which does not produce an image layer.
Each layer is only a set of differences from the layer before it. Note that both adding, and removing files will result in a new layer. In the example above, the $HOME/.cache directory is removed, but will still be available in the previous layer and add up to the image’s total size. Refer to the Best practices for writing Dockerfiles and use multi-stage builds sections to learn how to optimize your Dockerfiles for efficient images.
The layers are stacked on top of each other. When you create a new container, you add a new writable layer on top of the underlying layers. This layer is often called the “container layer”. All changes made to the running container, such as writing new files, modifying existing files, and deleting files, are written to this thin writable container layer. The diagram below shows a container based on an ubuntu:15.04 image.
A storage driver handles the details about the way these layers interact with each other. Different storage drivers are available, which have advantages and disadvantages in different situations.
Как это работает?
Теперь вы вправе задать мне важный вопрос: как же это всё работает на практике? Из сказанного выше может создаться впечатление, что объединённая файловая система работает с применением некой чёрной магии, но на самом деле это не так. Сейчас я попытаюсь объяснить, как это работает в общем (неконтейнерном) случае. Предположим, нам нужно объединить два каталога (верхний и нижний) в одной точке монтирования и чтобы такие каталоги были представлены унифицированно:
В терминологии объединённого монтирования такие каталоги называются ветвями. Каждой из таких ветвей присваивается свой приоритет. Приоритет используется для того, чтобы решить, какой именно файл будет отображаться в объединённом представлении, если в нескольких исходных ветках присутствуют файлы с одним и тем же именем. Если проанализировать представленные выше файлы и каталоги, станет понятно, что такой конфликт может возникнуть, если мы попытаемся использовать их в режиме наложения (файл code.py). Давайте попробуем и посмотрим, что у нас получится:
В приведённом выше примере мы использовали команду mount с type overlay, чтобы объединить нижний каталог (только для чтения; более низкий приоритет) и верхний каталог (чтение-запись; более высокий приоритет) в объединённое представление в каталоге /mnt/merged. Мы также включили опцию workdir=./workdir. Этот каталог служит местом для подготовки объединённого представления нижнего каталога (lowerdir) и верхнего каталога (upperdir) перед их перемещением в каталог /mnt/merged.
Если посмотреть на выходные данные команды cat, можно заметить, что в объединённом представлении приоритет получили файлы верхнего каталога.
Теперь мы знаем, как объединить два каталога и что произойдёт при возникновении конфликта. Но что произойдёт, если попытаться изменить определенные файлы в объединённом представлении? Здесь в игру вступает функция копирования при записи (CoW). Что именно делает эта функция? CoW — это способ оптимизации, при котором, если две вызывающих программы обращаются к одному и тому же ресурсу, можно дать им указатель на один и тот же ресурс, не копируя его. Копирование необходимо только тогда, когда одна из вызывающих программ пытается осуществить запись собственной "копии" — отсюда в названии способа появилось слово "копия", то есть копирование осуществляется при (первой попытке) записи.
В случае объединённого монтирования это означает, что, если мы пытаемся изменить совместно используемый файл (или файл только для чтения), он вначале копируется в верхнюю ветвь, доступную для записи (upperdir), имеющую более высокий приоритет, чем нижние ветви (lowerdir), доступные только для чтения. Когда файл попадает в ветвь, доступную для записи, его можно безопасно изменить, и его новое содержимое отобразится в объединённом представлении, так как верхний слой имеет более высокий приоритет.
Последняя операция, которую мы, возможно, захотим выполнить, — это удаление файлов. Чтобы "удалить" файл, в ветви, доступной для записи, создается файл whiteout для очистки "удаляемого" файла. На самом деле файл не будет удалён физически. Вернее сказать, что он будет скрыт в объединённом представлении.
Мы много говорили о принципах объединённого монтирования, но как все эти принципы работают на платформе Docker и её контейнерах? Рассмотрим многоуровневую архитектуру Docker. Песочница контейнера состоит из нескольких ветвей образа, или, как мы их называем, слоёв. Такими слоями являются часть объединённого представления, доступная только для чтения (lowerdir), и слой контейнера — тонкая верхняя часть, доступная для записи (upperdir).
Не считая терминологических различий, речь фактически идёт об одном и том же — слои образа, извлекаемые из реестра, представляют собой lowerdir, и, если запускается контейнер, upperdir прикрепляется поверх слоев образа, обеспечивая рабочую область, доступную для записи в контейнер. Звучит довольно просто, не так ли? Давайте проверим, как всё работает!
Проверяем
Чтобы показать, как Docker использует файловую систему OverlayFS, попробуем смоделировать процесс монтирования Docker контейнеров и слоев образа. Прежде чем мы приступим, нужно вначале очистить рабочее пространство и получить образ, с которым можно работать:
Итак, у нас имеется образ (nginx), с которым можно работать, далее нужно проверить его слои. Проверить слои образа можно, либо запустив проверку образа в Docker и изучив поля GraphDriver, либо перейдя в каталог /var/lib/docker/overlay2, в котором хранятся все слои образа. Выполним обе эти операции и посмотрим, что получится:
Если внимательно посмотреть на полученные результаты, можно заметить, что они весьма похожи на те, что мы уже наблюдали после применения команды mount, не находите?
LowerDir: это каталог, в котором слои образа, доступные только для чтения, разделены двоеточиями.
MergedDir: объединённое представление всех слоев образа и контейнера.
UpperDir: слой для чтения и записи, на котором записываются изменения.
WorkDir: рабочий каталог, используемый Linux OverlayFS для подготовки объединённого представления.
Сделаем ещё один шаг — запустим контейнер и изучим его слои:
Из представленных выше выходных данных следует, что те же каталоги, которые были перечислены в выводе команды docker inspect nginx ранее как MergedDir, UpperDir и WorkDir (с id 3d963d191b2101b3406348217f4257d7374aa4b4a73b4a6dd4ab0f365d38dfbd), теперь являются частью LowerDir контейнера. В нашем случае LowerDir составляется из всех слоев образа nginx, размещённых друг на друге. Поверх них размещается слой в UpperDir, доступный для записи, содержащий каталоги /etc, /run и /var. Также, раз уж мы выше упомянули MergedDir, можно видеть всю доступную для контейнера файловую систему, в том числе всё содержимое каталогов UpperDir и LowerDir.
И, наконец, чтобы эмулировать поведение Docker, мы можем использовать эти же каталоги для ручного создания собственного объединённого представления:
В нашем случае мы просто взяли значения из предыдущего фрагмента кода и передали их в качестве соответствующих аргументов в команду mount. Разница лишь в том, что для объединённого представления вместо /var/lib/docker/overlay2/. /merged мы использовали /mnt/merged.
Именно к этому сводится действие файловой системы OverlayFS в Docker — на множестве уложенных друг на друга слоев может использоваться одна команда монтирования. Ниже приводится отвечающая за это часть кода Docker — заменяются значения lowerdir=. upperdir=. workdir=. после чего следует команда unix.Mount.
Good use cases for volumes
Volumes are the preferred way to persist data in Docker containers and services. Some use cases for volumes include:
Sharing data among multiple running containers. If you don’t explicitly create it, a volume is created the first time it is mounted into a container. When that container stops or is removed, the volume still exists. Multiple containers can mount the same volume simultaneously, either read-write or read-only. Volumes are only removed when you explicitly remove them.
When the Docker host is not guaranteed to have a given directory or file structure. Volumes help you decouple the configuration of the Docker host from the container runtime.
When you want to store your container’s data on a remote host or a cloud provider, rather than locally.
When you need to back up, restore, or migrate data from one Docker host to another, volumes are a better choice. You can stop containers using the volume, then back up the volume’s directory (such as /var/lib/docker/volumes/ ).
When your application requires high-performance I/O on Docker Desktop. Volumes are stored in the Linux VM rather than the host, which means that the reads and writes have much lower latency and higher throughput.
When your application requires fully native file system behavior on Docker Desktop. For example, a database engine requires precise control over disk flushing to guarantee transaction durability. Volumes are stored in the Linux VM and can make these guarantees, whereas bind mounts are remoted to macOS or Windows, where the file systems behave slightly differently.
More details about mount types
Volumes: Created and managed by Docker. You can create a volume explicitly using the docker volume create command, or Docker can create a volume during container or service creation.
When you create a volume, it is stored within a directory on the Docker host. When you mount the volume into a container, this directory is what is mounted into the container. This is similar to the way that bind mounts work, except that volumes are managed by Docker and are isolated from the core functionality of the host machine.
A given volume can be mounted into multiple containers simultaneously. When no running container is using a volume, the volume is still available to Docker and is not removed automatically. You can remove unused volumes using docker volume prune .
When you mount a volume, it may be named or anonymous. Anonymous volumes are not given an explicit name when they are first mounted into a container, so Docker gives them a random name that is guaranteed to be unique within a given Docker host. Besides the name, named and anonymous volumes behave in the same ways.
Volumes also support the use of volume drivers, which allow you to store your data on remote hosts or cloud providers, among other possibilities.
Bind mounts: Available since the early days of Docker. Bind mounts have limited functionality compared to volumes. When you use a bind mount, a file or directory on the host machine is mounted into a container. The file or directory is referenced by its full path on the host machine. The file or directory does not need to exist on the Docker host already. It is created on demand if it does not yet exist. Bind mounts are very performant, but they rely on the host machine’s filesystem having a specific directory structure available. If you are developing new Docker applications, consider using named volumes instead. You can’t use Docker CLI commands to directly manage bind mounts.
Bind mounts allow access to sensitive files
One side effect of using bind mounts, for better or for worse, is that you can change the host filesystem via processes running in a container, including creating, modifying, or deleting important system files or directories. This is a powerful ability which can have security implications, including impacting non-Docker processes on the host system.
tmpfs mounts: A tmpfs mount is not persisted on disk, either on the Docker host or within a container. It can be used by a container during the lifetime of the container, to store non-persistent state or sensitive information. For instance, internally, swarm services use tmpfs mounts to mount secrets into a service’s containers.
named pipes: An npipe mount can be used for communication between the Docker host and a container. Common use case is to run a third-party tool inside of a container and connect to the Docker Engine API using a named pipe.
Bind mounts and volumes can both be mounted into containers using the -v or --volume flag, but the syntax for each is slightly different. For tmpfs mounts, you can use the --tmpfs flag. We recommend using the --mount flag for both containers and services, for bind mounts, volumes, or tmpfs mounts, as the syntax is more clear.
Монтирование tmpfs
Tmpfs — временное файловое хранилище. Это некая специально отведённая область в оперативной памяти компьютера. Из определения выходит, что tmpfs — не лучшее хранилище для важных данных. Так оно и есть: при остановке или перезапуске контейнера сохранённые в tmpfs данные будут навсегда потеряны.
На самом деле tmpfs нужно не для сохранения данных, а для безопасности, полученные в ходе работы приложения чувствительные данные безвозвратно исчезнут после завершения работы контейнера. Бонусом использования будет высокая скорость доступа к информации.
Например, приложение в контейнере тормозит из-за того, что в ходе работы активно идут операции чтения-записи, а диски на хосте не очень быстрые. Если вы не уверены, в какой каталог идёт эта нагрузка, можно применить к запущенному контейнеру команду docker diff . И вот этот каталог смонтировать как tmpfs, таким образом перенеся ввод-вывод с диска в оперативную память.
Такое хранилище может одновременно работать только с одним контейнером и доступно только в Linux.
Что такое объединённая файловая система?
Каскадно-объединённое монтирование — это тип файловой системы, в которой создается иллюзия объединения содержимого нескольких каталогов в один без изменения исходных (физических) источников. Такой подход может оказаться полезным, если имеются связанные наборы файлов, хранящиеся в разных местах или на разных носителях, но отображать их надо как единое и совокупное целое. Например, набор пользовательских/корневых каталогов, расположенных на удалённых NFS-серверах, можно свести в один каталог или можно объединить разбитый на части ISO-образ в один целый образ.
Однако технологию объединённого монтирования (или объединённой файловой системы), по сути, нельзя считать отдельным типом файловой системы. Это, скорее, особая концепция с множеством реализаций. Некоторые реализации работают быстрее, некоторые медленнее, некоторые проще в использовании, некоторые сложнее — проще говоря, у разных реализаций разные цели и разные уровни зрелости. Поэтому, прежде чем углубиться в детали, ознакомимся с некоторыми наиболее популярными реализациями объединённой файловой системы:
aufs — альтернативная версия исходной файловой системы UnionFS с добавлением множества новых функций. Данную файловую систему нельзя использовать в составе ванильного ядра Linux. Aufs использовалась в качестве файловой системы по умолчанию для Docker на Ubuntu/Debian, однако со временем она была заменена на OverlayFS (для ядра Linux >4.0). По сравнению с другими объединёнными файловыми системами эта система имеет ряд преимуществ, описанных в Docker Docs.
Следующая система — OverlayFS — была включена в ядро Linux Kernel, начиная с версии 3.18 (26 октября 2014 года). Данная файловая система используется по умолчанию драйвером overlay2 Docker (это можно проверить, запустив команду docker system info | grep Storage). Данная файловая система в целом обеспечивает лучшую, чем aufs, производительность и имеет ряд интересных функциональных особенностей, например функцию разделения страничного кэша.
ZFS — объединённая файловая система, разработанная Sun Microsystems (в настоящее время эта компания называется Oracle). В этой системе реализован ряд полезных функций, таких как функция иерархического контрольного суммирования, функция обработки снимков, функция резервного копирования/репликации или архивирования и дедупликации (исключения избыточности) внутренних данных. Однако, поскольку автором этой файловой системы является Oracle, её выпуск осуществлялся под общей лицензией на разработку и распространение (CDDL), не распространяемой на программное обеспечение с открытым исходным кодом, поэтому данная файловая система не может поставляться как часть ядра Linux. Тем не менее можно воспользоваться проектом ZFS on Linux (ZoL), который в документации Docker описывается как работоспособный и хорошо проработанный. но, увы, непригодный к промышленной эксплуатации. Если вам захочется поработать с этой файловой системой, её можно найти здесь.
Btrfs — ещё один вариант файловой системы, представляющий собой совместный проект множества компаний, в том числе SUSE, WD и Facebook. Данная файловая система выпущена под лицензией GPL и является частью ядра Linux. Btrfs — файловая система по умолчанию дистрибутива Fedora 33. В ней также реализованы некоторые полезные функции, такие как операции на уровне блоков, дефрагментация, доступные для записи снимки и множество других. Если вас не пугают трудности, связанные с переходом на специализированный драйвер устройств памяти для Docker, файловая система Btrfs с её функциональными и производительными возможностями может стать лучшим вариантом.
Если вы хотите более глубоко изучить характеристики драйверов, используемых в Docker, в Docker Docs можно ознакомиться с таблицей сравнения разных драйверов. Если вы затрудняетесь с выбором файловой системы (не сомневаюсь, есть и такие программисты, которые знают все тонкости файловых систем, но эта статья предназначена не для них), возьмите за основу файловую систему по умолчанию overlay2 — именно её я буду использовать в примерах в оставшейся части данной статьи.
Sharing promotes smaller images
When you use docker pull to pull down an image from a repository, or when you create a container from an image that does not yet exist locally, each layer is pulled down separately, and stored in Docker’s local storage area, which is usually /var/lib/docker/ on Linux hosts. You can see these layers being pulled in this example:
Each of these layers is stored in its own directory inside the Docker host’s local storage area. To examine the layers on the filesystem, list the contents of /var/lib/docker/ . This example uses the overlay2 storage driver:
The directory names do not correspond to the layer IDs.
Now imagine that you have two different Dockerfiles. You use the first one to create an image called acme/my-base-image:1.0 .
The second one is based on acme/my-base-image:1.0 , but has some additional layers:
The second image contains all the layers from the first image, plus new layers created by the COPY and RUN instructions, and a read-write container layer. Docker already has all the layers from the first image, so it does not need to pull them again. The two images share any layers they have in common.
If you build images from the two Dockerfiles, you can use docker image ls and docker image history commands to verify that the cryptographic IDs of the shared layers are the same.
Make a new directory cow-test/ and change into it.
Within cow-test/ , create a new file called hello.sh with the following contents:
Copy the contents of the first Dockerfile above into a new file called Dockerfile.base .
Copy the contents of the second Dockerfile above into a new file called Dockerfile .
Within the cow-test/ directory, build the first image. Don’t forget to include the final . in the command. That sets the PATH , which tells Docker where to look for any files that need to be added to the image.
Build the second image.
Check out the sizes of the images:
Check out the history of each image:
Some steps do not have a size ( 0B ), and are metadata-only changes, which do not produce an image layer and do not take up any size, other than the metadata itself. The output above shows that this image consists of 2 image layers.
Notice that all steps of the first image are also included in the final image. The final image includes the two layers from the first image, and two layers that were added in the second image.
What are the steps?
The lines in the docker history output indicate that those steps were either built on another system and part of the alpine image that was pulled from Docker Hub, or were built with BuildKit as builder. Before BuildKit, the “classic” builder would produce a new “intermediate” image for each step for caching purposes, and the IMAGE column would show the ID of that image. BuildKit uses its own caching mechanism, and no longer requires intermediate images for caching. Refer to build images with BuildKit to learn more about other enhancements made in BuildKit.
Check out the layers for each image
Use the docker image inspect command to view the cryptographic IDs of the layers in each image:
Notice that the first two layers are identical in both images. The second image adds two additional layers. Shared image layers are only stored once in /var/lib/docker/ and are also shared when pushing and pulling and image to an image registry. Shared image layers can therefore reduce network bandwidth and storage.
Tip: format output of Docker commands with the --format option
The examples above use the docker image inspect command with the --format option to view the layer IDs, formatted as a JSON array. The --format option on Docker commands can be a powerful feature that allows you to extract and format specific information from the output, without requiring additional tools such as awk or sed . To learn more about formatting the output of docker commands using the --format flag, refer to the format command and log output section. We also pretty-printed the JSON output using the jq utility for readability.
Тома (docker volumes)
Тома — рекомендуемый разработчиками Docker способ хранения данных. В Linux тома находятся по умолчанию в /var/lib/docker/volumes/. Другие программы не должны получать к ним доступ напрямую, только через контейнер.
Тома создаются и управляются средствами Docker: командой docker volume create, через указание тома при создании контейнера в Dockerfile или docker-compose.yml.
В контейнере том видно как обычный каталог, который мы определяем в Dockerfile. Тома могут быть с именами или без — безымянным томам Docker сам присвоит имя.
Один том может быть примонтирован одновременно в несколько контейнеров. Когда никто не использует том, он не удаляется, а продолжает существовать. Команда для удаления томов: docker volume prune.
Можно выбрать специальный драйвер для тома и хранить данные не на хосте, а на удалённом сервере или в облаке.
Для чего стоит использовать тома в Docker:
- шаринг данных между несколькими запущенными контейнерами,
- решение проблемы привязки к ОС хоста,
- удалённое хранение данных,
- бэкап или миграция данных на другой хост с Docker (для этого надо остановить все контейнеры и скопировать содержимое из каталога тома в нужное место).
Container size on disk
To view the approximate size of a running container, you can use the docker ps -s command. Two different columns relate to size.
- size : the amount of data (on disk) that is used for the writable layer of each container.
- virtual size : the amount of data used for the read-only image data used by the container plus the container’s writable layer size . Multiple containers may share some or all read-only image data. Two containers started from the same image share 100% of the read-only data, while two containers with different images which have layers in common share those common layers. Therefore, you can’t just total the virtual sizes. This over-estimates the total disk usage by a potentially non-trivial amount.
The total disk space used by all of the running containers on disk is some combination of each container’s size and the virtual size values. If multiple containers started from the same exact image, the total size on disk for these containers would be SUM ( size of containers) plus one image size ( virtual size - size ).
This also does not count the following additional ways a container can take up disk space:
- Disk space used for log files stored by the logging-driver. This can be non-trivial if your container generates a large amount of logging data and log rotation is not configured.
- Volumes and bind mounts used by the container.
- Disk space used for the container’s configuration files, which are typically small.
- Memory written to disk (if swapping is enabled).
- Checkpoints, if you’re using the experimental checkpoint/restore feature.
Архитектура Docker
Docker использует архитектуру клиент-сервер. Docker клиент общается с демоном Docker, который берет на себя тяжесть создания, запуска, распределения ваших контейнеров. Оба, клиент и сервер могут работать на одной системе, вы можете подключить клиент к удаленному демону docker. Клиент и сервер общаются через сокет или через RESTful API.
Docker-демон
Как показано на диаграмме, демон за пускается на хост-машине. Пользователь не взаимодействует с сервером на прямую, а использует для этого клиент.
Docker-клиент
Docker-клиент, программа docker — главный интерфейс к Docker. Она получает команды от пользователя и взаимодействует с docker-демоном.
Внутри docker-а
- образы (images)
- реестр (registries)
- контейнеры
Образы
Docker-образ — это read-only шаблон. Например, образ может содержать операционку Ubuntu c Apache и приложением на ней. Образы используются для создания контейнеров. Docker позволяет легко создавать новые образы, обновлять существующие, или вы можете скачать образы созданные другими людьми. Образы — это компонента сборки docker-а.
Реестр
Docker-реестр хранит образы. Есть публичные и приватные реестры, из которых можно скачать либо загрузить образы. Публичный Docker-реестр — это Docker Hub. Там хранится огромная коллекция образов. Как вы знаете, образы могут быть созданы вами или вы можете использовать образы созданные другими. Реестры — это компонента распространения.
Контейнеры
Контейнеры похожи на директории. В контейнерах содержится все, что нужно для работы приложения. Каждый контейнер создается из образа. Контейнеры могут быть созданы, запущены, остановлены, перенесены или удалены. Каждый контейнер изолирован и является безопасной платформой для приложения. Контейнеры — это компонента работы.
Так как же работает Docker?
- можем создавать образы, в которых находятся наши приложения;
- можем создавать контейнеры из образов, для запуска приложений;
- можем распространять образы через Docker Hub или другой реестр образов.
Как работает образ?
Мы уже знаем, что образ — это read-only шаблон, из которого создается контейнер. Каждый образ состоит из набора уровней. Docker использует union file system для сочетания этих уровней в один образ. Union file system позволяет файлам и директориями из разных файловых систем (разным ветвям) прозрачно накладываться, создавая когерентную файловую систему.
Одна из причин, по которой docker легковесен — это использование таких уровней. Когда вы изменяете образ, например, обновляете приложение, создается новый уровень. Так, без замены всего образа или его пересборки, как вам возможно придётся сделать с виртуальной машиной, только уровень добавляется или обновляется. И вам не нужно раздавать весь новый образ, раздается только обновление, что позволяет распространять образы проще и быстрее.
В основе каждого образа находится базовый образ. Например, ubuntu, базовый образ Ubuntu, или fedora, базовый образ дистрибутива Fedora. Так же вы можете использовать образы как базу для создания новых образов. Например, если у вас есть образ apache, вы можете использовать его как базовый образ для ваших веб-приложений.
Примечание! Docker обычно берет образы из реестра Docker Hub.
Docker образы могут создаться из этих базовых образов, шаги описания для создания этих образов мы называем инструкциями. Каждая инструкция создает новый образ или уровень. Инструкциями будут следующие действия:
- запуск команды
- добавление файла или директории
- создание переменной окружения
- указания что запускать когда запускается контейнер этого образа
Эти инструкции хранятся в файле Dockerfile . Docker считывает это Dockerfile , когда вы собираете образ, выполняет эти инструкции, и возвращает конечный образ.
Как работает docker реестр?
Реестр — это хранилище docker образов. После создания образа вы можете опубликовать его на публичном реестре Docker Hub или на вашем личном реестре.
С помощью docker клиента вы можете искать уже опубликованные образы и скачивать их на вашу машину с docker для создания контейнеров.
Docker Hub предоставляет публичные и приватные хранилища образов. Поиск и скачивание образов из публичных хранилищ доступно для всех. Содержимое приватных хранилищ не попадает в результат поиска. И только вы и ваши пользователи могут получать эти образы и создавать из них контейнеры.
Как работает контейнер?
Контейнер состоит из операционной системы, пользовательских файлов и метаданных. Как мы знаем, каждый контейнер создается из образа. Этот образ говорит docker-у, что находится в контейнере, какой процесс запустить, когда запускается контейнер и другие конфигурационные данные. Docker образ доступен только для чтения. Когда docker запускает контейнер, он создает уровень для чтения/записи сверху образа (используя union file system, как было указано раньше), в котором может быть запущено приложение.
Что происходит, когда запускается контейнер?
Или с помощью программы docker , или с помощью RESTful API, docker клиент говорит docker демону запустить контейнер.
- какой образ использовать для создания контейнера. В нашем случае ubuntu
- команду которую вы хотите запустить когда контейнер будет запущен. В нашем случае /bin/bash
Что же происходит под капотом, когда мы запускаем эту команду?
- скачивает образ ubuntu: docker проверяет наличие образа ubuntu на локальной машине, и если его нет — то скачивает его с Docker Hub. Если же образ есть, то использует его для создания контейнера;
- создает контейнер: когда образ получен, docker использует его для создания контейнера;
- инициализирует файловую систему и монтирует read-only уровень: контейнер создан в файловой системе и read-only уровень добавлен образ;
- инициализирует сеть/мост: создает сетевой интерфейс, который позволяет docker-у общаться хост машиной;
- Установка IP адреса: находит и задает адрес;
- Запускает указанный процесс: запускает ваше приложение;
- Обрабатывает и выдает вывод вашего приложения: подключается и логирует стандартный вход, вывод и поток ошибок вашего приложения, что бы вы могли отслеживать как работает ваше приложение.
Good use cases for tmpfs mounts
tmpfs mounts are best used for cases when you do not want the data to persist either on the host machine or within the container. This may be for security reasons or to protect the performance of the container when your application needs to write a large volume of non-persistent state data.
Заключение
Интерфейс Docker может показаться чёрным ящиком с большим количеством скрытых внутри непонятных технологий. Эти технологии — пусть непонятные — довольно интересны и полезны. Я вовсе не хочу сказать, что для эффективного использования Docker нужно досконально знать все их тонкости, но, на мой взгляд, если вы потратите немного времени и поймёте, как они работают, это пойдёт вам только на пользу. Ясное понимание принципов работы инструмента облегчает принятие правильных решений — в нашем случае речь идёт о повышении производительности и возможных аспектах, связанных с безопасностью. Кроме того, вы сможете ознакомиться с некоторыми продвинутыми технологиями, и кто знает, в каких областях знаний они могут вам пригодится в будущем!
В этой статье мы рассмотрели только часть архитектуры Docker — файловую систему. Есть и другие части, с которыми стоит ознакомиться более внимательно, например контрольные группы (cgroups) или пространства имен Linux. Если вы их освоите — можно уже задуматься о переходе в востребованный DevOps. А с остальными знаниями, необходимыми для данной профессии мы поможем на курсе по профессии DevOps-инженер.
Важная характеристика Docker-контейнеров — эфемерность. В любой момент контейнер может рестартовать: завершиться и вновь запуститься из образа. При этом все накопленные в нём данные будут потеряны. Но как в таком случае запускать в Docker приложения, которые должны сохранять информацию о своём состоянии? Для этого есть несколько инструментов.
В этой статье рассмотрим docker volumes, bind mount и tmpfs, дадим советы по их использованию, проведём небольшую практику.
Почему именно она?
В предыдущем разделе мы рассказали о некоторых полезных возможностях файловой системы такого типа, но почему именно она является оптимальным выбором для работы Docker и контейнеров в целом?
Многие образы, используемые для запуска контейнеров, занимают довольно большой объём, например, ubuntu занимает 72 Мб, а nginx — 133 Мб. Было бы довольно разорительно выделять столько места всякий раз, когда потребуется из этих образов создать контейнер. При использовании объединённой файловой системы Docker создаёт поверх образа тонкий слой, а остальная часть образа может быть распределена между всеми контейнерами. Мы также получаем дополнительное преимущество за счёт сокращения времени запуска, так как отпадает необходимость в копировании файлов образа и данных.
Объединённая файловая система также обеспечивает изоляцию, поскольку контейнеры имеют доступ к общим слоям образа только для чтения. Если контейнерам когда-нибудь понадобится внести изменения в любой файл, доступный только для чтения, они используют стратегию копирования при записи copy-on-write (её мы обсудим чуть позже), позволяющую копировать содержимое на верхний слой, доступный для записи, где такое содержимое может быть безопасно изменено.
Используемые технологии
Докер написан на Go и использует некоторые возможности ядра Linux, чтобы реализовать приведенный выше функционал.
Пространство имен(namespaces)
Docker использует технологию namespaces для организации изолированных рабочих пространств, которые мы называем контейнерами. Когда мы запускаем контейнер, docker создает набор пространств имен для данного контейнера.
Это создает изолированный уровень, каждый аспект контейнера запущен в своем простанстве имен, и не имеет доступ к внешней системе.
- pid: для изоляции процесса;
- net: для управления сетевыми интерфейсами;
- ipc: для управления IPC ресурсами. (ICP: InterProccess Communication);
- mnt: для управления точками монтирования;
- utc: для изолирования ядра и контроля генерации версий(UTC: Unix timesharing system).
Control groups (контрольные группы)
Docker также использует технологию cgroups или контрольные группы. Ключ к работе приложения в изоляции, предоставление приложению только тех ресурсов, которые вы хотите предоставить. Это гарантирует, что контейнеры будут хорошими соседями. Контрольные группы позволяют разделять доступные ресурсы железа и если необходимо, устанавливать пределы и ограничения. Например, ограничить возможное количество памяти контейнеру.
Union File System
Union File Sysem или UnionFS — это файловая система, которая работает создавая уровни, делая ее очень легковесной и быстрой. Docker использует UnionFS для создания блоков, из которых строится контейнер. Docker может использовать несколько вариантов UnionFS включая: AUFS, btrfs, vfs и DeviceMapper.
Форматы контейнеров
Docker сочетает эти компоненты в обертку, которую мы называем форматом контейнера. Формат, используемый по умолчанию, называется libcontainer . Так же docker поддерживает традиционный формат контейнеров в Linux c помощью LXC. В будущем Docker возможно будет поддерживать другие форматы контейнеров. Например, интегрируясь с BSD Jails или Solaris Zones.
Создавать, запускать, просматривать, перемещать контейнеры и образы с помощью интерфейса командной строки Docker (Docker CLI) проще простого, но задумывались ли вы когда-нибудь, как на самом деле работают внутренние компоненты, обеспечивающие работу интерфейса Docker? За этим простым интерфейсом скрывается множество продвинутых технологий, и специально к старту нового потока курса по DevOps в этой статье мы рассмотрим одну из них — объединённую файловую систему, используемую во всех слоях контейнеров и образов. Маститым знатокам контейнеризации и оркестрации данный материал навряд ли откроет что-то новое, зато будет полезен тем, кто делает первые шаги в DevOps.
The copy-on-write (CoW) strategy
Copy-on-write is a strategy of sharing and copying files for maximum efficiency. If a file or directory exists in a lower layer within the image, and another layer (including the writable layer) needs read access to it, it just uses the existing file. The first time another layer needs to modify the file (when building the image or running the container), the file is copied into that layer and modified. This minimizes I/O and the size of each of the subsequent layers. These advantages are explained in more depth below.
Особенности работы контейнеров
Прежде чем перейти к способам хранения данных, вспомним устройство контейнеров. Это поможет лучше понять основную тему.
Контейнер создаётся из образа, в котором есть всё для начала его работы. Но там не хранится и тем более не изменяется ничего важного. В любой момент приложение в контейнере может быть завершено, а контейнер уничтожен, и это нормально. Контейнер отработал — выкидываем его и собираем новый. Если пользователь загрузил в приложение картинку, то при замене контейнера она удалится.
На схеме показано устройство контейнера, запущенного из образа Ubuntu 15.04. Контейнер состоит из пяти слоёв: четыре из них принадлежат образу, и лишь один — самому контейнеру. Слои образа доступны только для чтения, слой контейнера — для чтения и для записи. Если при работе приложения какие-то данные будут изменяться, они попадут в слой контейнера. Но при уничтожении контейнера слой будет безвозвратно потерян, и все данные вместе с ним.
В идеальном мире Docker используют только для запуска stateless-приложений, которые не читают и не сохраняют данные о своём состоянии и готовы в любой момент завершиться. Однако в реальности большинство программ относятся к категории stateful, то есть требуют сохранения данных между перезапусками.
Поэтому нужны способы сделать так, чтобы важные изменяемые данные не зависели от эфемерности контейнеров и, как бонус, были доступными сразу из нескольких мест.
В Docker есть несколько способов хранения данных. Наиболее распространенные:
- тома хранения данных (docker volumes),
- монтирование каталогов с хоста (bind mount).
Особые типы хранения:
- именованные каналы (named pipes, только в Windows),
- монтирование tmpfs (только в Linux).
На схеме показаны самые популярные типы хранения данных для Linux: в памяти (tmpfs), в файловой системе хоста (bind mount), в томе Docker (docker volumes). Разберём каждый вариант.
Choose the right type of mount
No matter which type of mount you choose to use, the data looks the same from within the container. It is exposed as either a directory or an individual file in the container’s filesystem.
An easy way to visualize the difference among volumes, bind mounts, and tmpfs mounts is to think about where the data lives on the Docker host.
Volumes are stored in a part of the host filesystem which is managed by Docker ( /var/lib/docker/volumes/ on Linux). Non-Docker processes should not modify this part of the filesystem. Volumes are the best way to persist data in Docker.
Bind mounts may be stored anywhere on the host system. They may even be important system files or directories. Non-Docker processes on the Docker host or a Docker container can modify them at any time.
tmpfs mounts are stored in the host system’s memory only, and are never written to the host system’s filesystem.
Copying makes containers efficient
When you start a container, a thin writable container layer is added on top of the other layers. Any changes the container makes to the filesystem are stored here. Any files the container does not change do not get copied to this writable layer. This means that the writable layer is as small as possible.
When an existing file in a container is modified, the storage driver performs a copy-on-write operation. The specifics steps involved depend on the specific storage driver. For the overlay2 , overlay , and aufs drivers, the copy-on-write operation follows this rough sequence:
- Search through the image layers for the file to update. The process starts at the newest layer and works down to the base layer one layer at a time. When results are found, they are added to a cache to speed future operations.
- Perform a copy_up operation on the first copy of the file that is found, to copy the file to the container’s writable layer.
- Any modifications are made to this copy of the file, and the container cannot see the read-only copy of the file that exists in the lower layer.
Btrfs, ZFS, and other drivers handle the copy-on-write differently. You can read more about the methods of these drivers later in their detailed descriptions.
Containers that write a lot of data consume more space than containers that do not. This is because most write operations consume new space in the container’s thin writable top layer. Note that changing the metadata of files, for example, changing file permissions or ownership of a file, can also result in a copy_up operation, therefore duplicating the file to the writable layer.
Tip: Use volumes for write-heavy applications
For write-heavy applications, you should not store the data in the container. Applications, such as write-intensive database storage, are known to be problematic particularly when pre-existing data exists in the read-only layer.
Instead, use Docker volumes, which are independent of the running container, and designed to be efficient for I/O. In addition, volumes can be shared among containers and do not increase the size of your container’s writable layer. Refer to the use volumes section to learn about volumes.
A copy_up operation can incur a noticeable performance overhead. This overhead is different depending on which storage driver is in use. Large files, lots of layers, and deep directory trees can make the impact more noticeable. This is mitigated by the fact that each copy_up operation only occurs the first time a given file is modified.
To verify the way that copy-on-write works, the following procedures spins up 5 containers based on the acme/my-final-image:1.0 image we built earlier and examines how much room they take up.
From a terminal on your Docker host, run the following docker run commands. The strings at the end are the IDs of each container.
Run the docker ps command with the --size option to verify the 5 containers are running, and to see each container’s size.
The output above shows that all containers share the image’s read-only layers (7.75MB), but no data was written to the container’s filesystem, so no additional storage is used for the containers.
Advanced: metadata and logs storage used for containers
Note: This step requires a Linux machine, and does not work on Docker Desktop for Mac or Docker Desktop for Windows, as it requires access to the Docker Daemon’s file storage.
While the output of docker ps provides you information about disk space consumed by a container’s writable layer, it does not include information about metadata and log-files stored for each container.
More details can be obtained by exploring the Docker Daemon’s storage location ( /var/lib/docker by default).
Each of these containers only takes up 36k of space on the filesystem.
To demonstrate this, run the following command to write the word ‘hello’ to a file on the container’s writable layer in containers my_container_1 , my_container_2 , and my_container_3 :
Running the docker ps command again afterward shows that those containers now consume 5 bytes each. This data is unique to each container, and not shared. The read-only layers of the containers are not affected, and are still shared by all containers.
The examples above illustrate how copy-on-write filesystems help making containers efficient. Not only does copy-on-write save space, but it also reduces container start-up time. When you create a container (or multiple containers from the same image), Docker only needs to create the thin writable container layer.
If Docker had to make an entire copy of the underlying image stack each time it created a new container, container create times and disk space used would be significantly increased. This would be similar to the way that virtual machines work, with one or more virtual disks per virtual machine. The vfs storage does not provide a CoW filesystem or other optimizations. When using this storage driver, a full copy of the image’s data is created for each container.
Главные компоненты Docker
- Docker: платформа виртуализации с открытым кодом;
- Docker Hub: наша платформа-как-сервис для распространения и управления docker контейнерами.
Storage drivers versus Docker volumes
Docker uses storage drivers to store image layers, and to store data in the writable layer of a container. The container’s writable layer does not persist after the container is deleted, but is suitable for storing ephemeral data that is generated at runtime. Storage drivers are optimized for space efficiency, but (depending on the storage driver) write speeds are lower than native file system performance, especially for storage drivers that use a copy-on-write filesystem. Write-intensive applications, such as database storage, are impacted by a performance overhead, particularly if pre-existing data exists in the read-only layer.
Use Docker volumes for write-intensive data, data that must persist beyond the container’s lifespan, and data that must be shared between containers. Refer to the volumes section to learn how to use volumes to persist data and improve performance.
Container and layers
The major difference between a container and an image is the top writable layer. All writes to the container that add new or modify existing data are stored in this writable layer. When the container is deleted, the writable layer is also deleted. The underlying image remains unchanged.
Because each container has its own writable container layer, and all changes are stored in this container layer, multiple containers can share access to the same underlying image and yet have their own data state. The diagram below shows multiple containers sharing the same Ubuntu 15.04 image.
Docker uses storage drivers to manage the contents of the image layers and the writable container layer. Each storage driver handles the implementation differently, but all drivers use stackable image layers and the copy-on-write (CoW) strategy.
Note
Use Docker volumes if you need multiple containers to have shared access to the exact same data. Refer to the volumes section to learn about volumes.
Монтирование каталога с хоста (bind mount)
Это более простая концепция: файл или каталог с хоста просто монтируется в контейнер.
Используется, когда нужно пробросить в контейнер конфигурационные файлы с хоста. Например, именно так в контейнерах реализуется DNS: с хоста монтируется файл /etc/resolv.conf.
Другое очевидное применение — в разработке. Код находится на хосте (вашем ноутбуке), но исполняется в контейнере. Вы меняете код и сразу видите результат. Это возможно, так как процессы хоста и контейнера одновременно имеют доступ к одним и тем же данным.
Особенности bind mount:
- Запись в примонтированный каталог могут вести программы как в контейнере, так и на хосте. Это значит, есть риск случайно затереть данные, не понимая, что с ними работает контейнер.
- Лучше не использовать в продакшене. Для продакшена убедитесь, что код копируется в контейнер, а не монтируется с хоста.
- Для успешного монтирования указывайте полный путь к файлу или каталогу на хосте.
- Если приложение в контейнере запущено от root, а совместно используется каталог с ограниченными правами, то в какой-то момент может возникнуть проблема с правами на файлы и невозможность что-то удалить без использования sudo.
Когда использовать тома, а когда монтирование с хоста
Volume | Bind mount |
---|---|
Просто расшарить данные между контейнерами. | Пробросить конфигурацию с хоста в контейнер. |
У хоста нет нужной структуры каталогов. | Расшарить исходники и/или уже собранные приложения. |
Данные лучше хранить не локально (а в облаке, например). | Есть стабильная структура каталогов и файлов, которую нужно расшарить между контейнерами. |
Для чего я могу использовать docker?
Быстрое выкладывание ваших приложений
Docker прекрасно подходит для организации цикла разработки. Docker позволяет разработчикам использовать локальные контейнеры с приложениями и сервисами. Что в последствии позволяет интегрироваться с процессом постоянной интеграции и выкладывания (continuous integration and deployment workflow).
Более простое выкладывание и разворачивание
Основанная на контейнерах docker платформа позволят легко портировать вашу полезную нагрузку. Docker контейнеры могут работать на вашей локальной машине, как реальной так и на виртуальной машине в дата центре, так и в облаке.
Портируемость и легковесная природа docker позволяет легко динамически управлять вашей нагрузкой. Вы можете использовать docker, чтобы развернуть или погасить ваше приложение или сервисы. Скорость docker позволяет делать это почти в режиме реального времени.
Высокие нагрузки и больше полезных нагрузок
Docker легковесен и быстр. Он предоставляет устойчивую, рентабельную альтернативу виртуальным машинам на основе гипервизора. Он особенно полезен в условиях высоких нагрузок, например, при создания собственного облака или платформа-как-сервис (platform-as-service). Но он так же полезен для маленьких и средних приложений, когда вам хочется получать больше из имеющихся ресурсов.
Читайте также: