company_banner

Хранение данных в Docker


    Важная характеристика Docker-контейнеров — эфемерность. В любой момент контейнер может рестартовать: завершиться и вновь запуститься из образа. При этом все накопленные в нём данные будут потеряны. Но как в таком случае запускать в Docker приложения, которые должны сохранять информацию о своём состоянии? Для этого есть несколько инструментов.


    В этой статье рассмотрим docker volumes, bind mount и tmpfs, дадим советы по их использованию, проведём небольшую практику.


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


    Прежде чем перейти к способам хранения данных, вспомним устройство контейнеров. Это поможет лучше понять основную тему.


    Контейнер создаётся из образа, в котором есть всё для начала его работы. Но там не хранится и тем более не изменяется ничего важного. В любой момент приложение в контейнере может быть завершено, а контейнер уничтожен, и это нормально. Контейнер отработал — выкидываем его и собираем новый. Если пользователь загрузил в приложение картинку, то при замене контейнера она удалится.


    На схеме показано устройство контейнера, запущенного из образа Ubuntu 15.04. Контейнер состоит из пяти слоёв: четыре из них принадлежат образу, и лишь один — самому контейнеру. Слои образа доступны только для чтения, слой контейнера — для чтения и для записи. Если при работе приложения какие-то данные будут изменяться, они попадут в слой контейнера. Но при уничтожении контейнера слой будет безвозвратно потерян, и все данные вместе с ним.



    В идеальном мире Docker используют только для запуска stateless-приложений, которые не читают и не сохраняют данные о своём состоянии и готовы в любой момент завершиться. Однако в реальности большинство программ относятся к категории stateful, то есть требуют сохранения данных между перезапусками.


    Поэтому нужны способы сделать так, чтобы важные изменяемые данные не зависели от эфемерности контейнеров и, как бонус, были доступными сразу из нескольких мест.


    В Docker есть несколько способов хранения данных. Наиболее распространенные:


    • тома хранения данных (docker volumes),
    • монтирование каталогов с хоста (bind mount).

    Особые типы хранения:


    • именованные каналы (named pipes, только в Windows),
    • монтирование tmpfs (только в Linux).


    На схеме показаны самые популярные типы хранения данных для Linux: в памяти (tmpfs), в файловой системе хоста (bind mount), в томе Docker (docker volumes). Разберём каждый вариант.


    Тома (docker volumes)


    Тома — рекомендуемый разработчиками Docker способ хранения данных. В Linux тома находятся по умолчанию в /var/lib/docker/volumes/. Другие программы не должны получать к ним доступ напрямую, только через контейнер.


    Тома создаются и управляются средствами Docker: командой docker volume create, через указание тома при создании контейнера в Dockerfile или docker-compose.yml.


    В контейнере том видно как обычный каталог, который мы определяем в Dockerfile. Тома могут быть с именами или без — безымянным томам Docker сам присвоит имя.


    Один том может быть примонтирован одновременно в несколько контейнеров. Когда никто не использует том, он не удаляется, а продолжает существовать. Команда для удаления томов: docker volume prune.


    Можно выбрать специальный драйвер для тома и хранить данные не на хосте, а на удалённом сервере или в облаке.


    Для чего стоит использовать тома в Docker:


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

    Монтирование каталога с хоста (bind mount)


    Это более простая концепция: файл или каталог с хоста просто монтируется в контейнер.


    Используется, когда нужно пробросить в контейнер конфигурационные файлы с хоста. Например, именно так в контейнерах реализуется DNS: с хоста монтируется файл /etc/resolv.conf.


    Другое очевидное применение — в разработке. Код находится на хосте (вашем ноутбуке), но исполняется в контейнере. Вы меняете код и сразу видите результат. Это возможно, так как процессы хоста и контейнера одновременно имеют доступ к одним и тем же данным.


    Особенности bind mount:


    1. Запись в примонтированный каталог могут вести программы как в контейнере, так и на хосте. Это значит, есть риск случайно затереть данные, не понимая, что с ними работает контейнер.
    2. Лучше не использовать в продакшене. Для продакшена убедитесь, что код копируется в контейнер, а не монтируется с хоста.
    3. Для успешного монтирования указывайте полный путь к файлу или каталогу на хосте.
    4. Если приложение в контейнере запущено от root, а совместно используется каталог с ограниченными правами, то в какой-то момент может возникнуть проблема с правами на файлы и невозможность что-то удалить без использования sudo.

    Когда использовать тома, а когда монтирование с хоста


    Volume Bind mount
    Просто расшарить данные между контейнерами. Пробросить конфигурацию с хоста в контейнер.
    У хоста нет нужной структуры каталогов. Расшарить исходники и/или уже собранные приложения.
    Данные лучше хранить не локально (а в облаке, например). Есть стабильная структура каталогов и файлов, которую нужно расшарить между контейнерами.

    Монтирование tmpfs


    Tmpfs — временное файловое хранилище. Это некая специально отведённая область в оперативной памяти компьютера. Из определения выходит, что tmpfs — не лучшее хранилище для важных данных. Так оно и есть: при остановке или перезапуске контейнера сохранённые в tmpfs данные будут навсегда потеряны.


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


    Например, приложение в контейнере тормозит из-за того, что в ходе работы активно идут операции чтения-записи, а диски на хосте не очень быстрые. Если вы не уверены, в какой каталог идёт эта нагрузка, можно применить к запущенному контейнеру команду docker diff. И вот этот каталог смонтировать как tmpfs, таким образом перенеся ввод-вывод с диска в оперативную память.


    Такое хранилище может одновременно работать только с одним контейнером и доступно только в Linux.


    Общие советы по использованию томов


    Монтирование в непустые директории


    Если вы монтируете пустой том в каталог контейнера, где уже есть файлы, то эти файлы не удалятся, а будут скопированы в том. Этим можно пользоваться, когда нужно скопировать данные из одного контейнера в другой.


    Если вы монтируете непустой том или каталог с хоста в контейнер, где уже есть файлы, то эти файлы тоже не удалятся, а просто будут скрыты. Видно будет только то, что есть в томе или каталоге на хосте. Похоже на простое монтирование в Linux.


    Монтирование служебных файлов


    С хоста можно монтировать любые файлы, в том числе служебные. Например, сокет docker. В результате получится docker-in-docker: один контейнер запустится внутри другого. UPD: (*это не совсем так. mwizard в комментариях пояснил, что в таком случае родительский docker запустит sibling-контейнер). Выглядит как бред, но в некоторых случаях бывает оправдано. Например, при настройке CI/CD.


    Монтирование /var/lib/docker


    Разработчики Docker говорят, что не стоит монтировать с хоста каталог /var/lib/docker, так как могут возникнуть проблемы. Однако есть некоторые программы, для запуска которых это необходимо.


    Практика: создадим тестовый том

    Ключ командной строки для Docker при работе с томами.


    Для volume или bind mount:


    --volume | -v

    Для tmpfs:


    --tmpfs

    Команды для управления томами в интерфейсе CLI Docker:


    $ docker volume
    
    Commands:
      create   Create a volume (Создать том)
      inspect  Display detailed information on one or more
            volumes (Отобразить детальную информацию)
      ls    List volumes (Вывести список томов)
      prune Remove all unused volumes (Удалить все неиспользуемые тома)
      rm    Remove one or more volumes (Удалить один или несколько томов)

    Создадим тестовый том:


    $ docker volume create slurm-storage
    slurm-storage

    Вот он появился в списке:


    $ docker volume ls
    DRIVER  VOLUME NAME
    local   slurm-storage

    Команда inspect выдаст примерно такой список информации в json:


    $ docker inspect slurm-storage
    [
        {
            "CreatedAt": "2020-12-14T15:00:37Z",
            "Driver": "local",
            "Labels": {},
            "Mountpoint": "/var/lib/docker/volumes/slurm-storage/_data",
            "Name": "slurm-storage",
            "Options": {},
            "Scope": "local"
        }
    ]

    Попробуем использовать созданный том, запустим с ним контейнер:


    $ docker run --rm -v slurm-storage:/data -it ubuntu:20.10 /bin/bash
    # echo $RANDOM > /data/file
    # cat /data/file
    13279
    # exit

    После самоуничтожения контейнера запустим другой и подключим к нему тот же том. Проверяем, что в нашем файле:


    $ docker run --rm -v slurm-storage:/data -it centos:8 /bin/bash -c "cat /data/file"
    13279

    То же самое, отлично.


    Теперь примонтируем каталог с хоста:


    $ docker run -v /srv:/host/srv --name slurm --rm -it ubuntu:20.10 /bin/bash

    Docker не любит относительные пути, лучше указывайте абсолютные!


    Теперь попробуем совместить оба типа томов сразу:


    $ docker run -v /srv:/host/srv -v slurm-storage:/data --name slurm --rm -it ubuntu:20.10 /bin/bash

    Отлично! А если нам нужно передать ровно те же тома другому контейнеру?


    $ docker run --volumes-from slurm --name backup --rm -it centos:8 /bin/bash

    Вы можете заметить некий лаг в обновлении данных между контейнерами, это зависит от используемого Docker драйвера файловой системы.


    Создавать том заранее необязательно, всё сработает в момент запуска docker run:


    $ docker run -v newslurm:/newdata -v /srv:/host/srv -v slurm-storage:/data --name slurm --rm -it ubuntu:20.10 /bin/bash

    Посмотрим теперь на список томов:


    $ docker volume ls
    DRIVER  VOLUME NAME
    local   slurm-storage
    local   newslurm

    Ещё немного усложним команду запуска, создадим анонимный том:


    $ docker run -v /anonymous -v newslurm:/newdata -v /srv:/host/srv -v slurm-storage:/data --name slurm --rm -it ubuntu:20.10 /bin/bash

    Такой том самоуничтожится после выхода из контейнера, так как мы указали ключ –rm.


    Если этого не сделать, давайте проверим что будет:


    $ docker run -v /anonymous -v newslurm:/newdata -v /srv:/host/srv -v slurm-storage:/data --name slurm -it ubuntu:20.10 /bin/bash
    $ docker volume ls
    DRIVER  VOLUME NAME
    local     04c490b16184bf71015f7714b423a517ce9599e9360af07421ceb54ab96bd333
    local   newslurm
    local   slurm-storage

    Хозяйке на заметку: тома (как образы и контейнеры) ограничены значением настройки dm.basesize, которая устанавливается на уровне настроек демона Docker. Как правило, что-то около 10Gb. Это значение можно изменить вручную, но потребуется перезапуск демона Docker.


    При запуске демона с ключом это выглядит так:


    $ sudo dockerd --storage-opt dm.basesize=40G

    Однажды увеличив значение, его уже нельзя просто так уменьшить. При запуске Docker выдаст ошибку.


    Если вам нужно вручную очистить содержимое всех томов, придётся удалять каталог, предварительно остановив демон:


    $ sudo service docker stop
    $ sudo rm -rf /var/lib/docker

    Если вам интересно узнать подробнее о работе с данными в Docker и других возможностях технологии, приглашаем на двухдневный онлайн-интенсив в феврале. Будет много практики.

    Автор статьи: Александр Швалов, практикующий инженер Southbridge, Certified Kubernetes Administrator, автор и разработчик курсов Слёрм.

    Southbridge
    Обеспечиваем стабильную работу highload-проектов

    Комментарии 13

      +1

      Нет, если пробросить сокет docker внутрь контейнера, это не даст docker-in-docker, потому что создаваемые контейнеры будут не child, а sibling. Чтобы контейнеры были вложенными, нужно сам dockerd запустить внутри контейнера, тогда сокет внутри будет свой собственный.

        +2
        Спасибо за ценное уточнение! Да, монтирование сокета родительского docker внутри контейнера даёт очень похожий эффект, но это не полный docker-in-docker. Обязательно учтём этот момент в наших учебных материалах.
        +3

        Не раскрыт вопрос директивы VOLUME для чего она нужна, когда надо использовать, а когда нет

          0
          Это отличное замечание, спасибо. Обязательно раскроем подробнее на онлайн-интенсиве.
          +2

          Интересно было бы узнать про объем volume и как им управлять, столкнулся с тем, что большие файлы больше нескольких гиг не записываются в докер (snapdrop.io) в общем не знаю, как исследовать эту проблему.

            +1
            Увы, сам с такими проблемами не сталкивался. Могу только посоветовать посмотреть в сторону параметра запуска docker-демона
            dm.basesize
            , по умолчанию это 10Гб и его можно увеличить, например отредактировав systemd-юнит. docs.docker.com/engine/reference/commandline/dockerd
              0

              Вроде этот параметр про размеры конейнера, томов не касается. Но могу ошибаться.

                0

                Так точно. И только при использовании хранения devicemapper

            0

            Подскажите, как правильно использовать NFS-хранилища.


            Есть NFS-шара nas:/share/docker, которая примонтирована в папку /mnt/nas на хосте. В этой шаре созданы следующие папки: nas:/share/docker/node01/conf и nas:/share/docker/node01/data.


            В docker-compose.yml эти папки примонтированы таким образом:


            services:
              node01:
                image: ...
                container_name: ...
                volumes:
                  - /mnt/nas/node01/conf:/app/conf
                  - /mnt/nas/node01/data:/app/data

            Хочется убрать NFS с хоста и перенести подключение в сам контейнер. Прописываем в .yml описание тома:


            volumes:
              nfs-nas-docker:
                driver: local
                driver_opts:
                  type: "nfs"
                  o: "addr=nas,rw,vers=4.1,noatime,rsize=65536,wsize=65536,tcp,timeo=14"
                  device: ":/share/docker/"

            Но я так понимаю, что отдельные папки из этого тома мы не сможем монтировать в /app/conf и /app/data?
            Как правильно поступать в таких ситуациях?

              +1
              В вашем случае можно сделать два тома, просто подключить в них разные каталоги с одной NFS-шары. Должно работать примерно так у вас:
              volumes:
                nfs-nas-docker-conf:
                  driver: local
                  driver_opts:
                    type: "nfs"
                    o: "addr=nas,rw,vers=4.1,noatime,rsize=65536,wsize=65536,tcp,timeo=14"
                    device: ":/share/docker/conf"
              

              А ещё есть вот такой плагин для Docker: github.com/ContainX/docker-volume-netshare, правда видно что он не в активной разработке, поэтому я бы не использовал его для критичных задач.
              +3
              >запущенного из образа Ubuntu 15.04.
              Статья 2015 года?
                +3

                Обожаю эту схему из документации.



                Идеальный пример поясняющей картинки, которая вообще ничего не пояснит новичку и в то же время иллюстрирует очевидное для чуть более опытного специалиста. Абсолютно непонятно на кого она ориентирована.

                  0

                  Вынужден согласиться, потому что без пояснений (я проверял на днях на одном студенте) эта диаграмма «ни о чем»

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое