Docker

Общее потребление

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

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

Если мы не очищали локальную машину в течение некоторого времени, то результаты этой команды могут удивить:

$ docker system df
Пример потребления памяти Docker в файловой системе хоста

Эта команда показывает использование диска Docker в нескольких категориях:

  • Образы (Images): размер образов, извлеченных из реестра и созданных локально.
  • Контейнеры (Containers): дисковое пространство, занимаемое слоями чтения-записи каждого из контейнеров, работающих в системе.
  • Локальные тома (Local Volumes): в случае, если хранение осуществляется на хосте, но вне файловой системы контейнера.
  • Кэш сборки (Build Cache): кэш, сгенерированный процессом сборки образа (касается BuildKit в Docker 18.09).

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

Использование диска контейнерами

Каждый раз, когда создается контейнер, в папке /var/lib/docker на хост-машине появляется несколько папок и файлов. Среди них:

  • Папка /var/lib/docker/containers/ID (ID — уникальный идентификатор контейнера). Если контейнер использует драйвер логгирования по умолчанию, все логи будут сохранены в файле JSON внутри нее. Создание слишком большого количества логов может повлиять на файловую систему хост-машины.
  • Папка в /var/lib/docker/overlay2, содержащая слой чтения-записи контейнера (overlay2 является предпочтительным драйвером хранилища в большинстве дистрибутивов Linux). Если контейнер сохраняет данные в своей собственной файловой системе, они будут храниться в /var/lib/docker/overlay2 на хост-машине.

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

$ docker system df
TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         0          0          0B         0B
Containers     0          0          0B         0B
Local Volumes  0          0          0B         0B
Build Cache    0          0          0B         0B

Во-первых, запустим контейнер NGINX:

$ docker container run --name www -d -p 8000:80 nginx:1.16

Снова запустив команду df, мы увидим:

  • один образ размером 126 Мбайт. Его загрузил NGINX: 1.16, когда мы запустили контейнер;
  • контейнер www, запускающийся из образа NGINX.
$ docker system df
TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         1          1          126M       0B (0%)
Containers     1          1          2B         0B (0%)
Local Volumes  0          0          0B         0B
Build Cache    0          0          0B         0B

Поскольку контейнер запущен, а образ в данный момент используется, освобождаемого пространства пока нет. Так как его размер (2B) незначителен, и поэтому его нелегко отследить в файловой системе, создадим пустой файл размером 100 Мбайт в файловой системе контейнера. Для этого мы используем удобную команду dd из контейнера www.

$ docker exec -ti www \
  dd if=/dev/zero of=test.img bs=1024 count=0 seek=$[1024*100]

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

$ docker system df
TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         1          1          126M       0B (0%)
Containers     1          1          104.9MB    0B (0%)
Local Volumes  0          0          0B         0B
Build Cache    0          0          0B         0B

Где находится этот файл на хосте? Проверим:

$ find /var/lib/docker -type f -name test.img
/var/lib/docker/overlay2/83f177...630078/merged/test.img
/var/lib/docker/overlay2/83f177...630078/diff/test.img

Не вдаваясь глубоко в детали — этот файл был создан на слое чтения-записи контейнера, который управляется драйвером overlay2. Если мы остановим контейнер, используемое им дисковое пространство станет пригодным для восстановления. Посмотрим:

# Остановка контейнера www
$ docker stop www

# Влияние на использование диска
$ docker system df
TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         1          1          126M       0B (0%)
Containers     1          0          104.9MB    104.9MB (100%)
Local Volumes  0          0          0B         0B
Build Cache    0          0          0B         0B

Как можно восстановить это пространство? Путем удаления контейнера, что приведет к удалению связанного с ним слоя чтения-записи.

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

$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
5e7f8e5097ace9ef5518ebf0c6fc2062ff024efb495f11ccc89df21ec9b4dcc2

Total reclaimed space: 104.9MB
Total reclaimed space: 104.9MB

Из вывода видно, что больше нет места, используемого контейнерами, и, поскольку образ больше не используется (контейнер не работает), пространство, которое он использует в файловой системе хоста, может быть восстановлено:

$ docker system df

TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         1          0          126M       126M (100%)
Containers     0          0          0B         0B
Local Volumes  0          0          0B         0B
Build Cache    0          0          0B         0B

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

Подкоманда prune, которую мы применяли выше, удаляет остановленные контейнеры. Если нам нужно удалить все — и запущенные, и остановленные, мы можем использовать одну из следующих команд (обе эквивалентны):

# Старая команда
$ docker rm -f $(docker ps -aq)

# Более современная
$ docker container rm -f $(docker container ls -aq)

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

Использование диска образами

Пару лет назад несколько сотен Мбайт на образ было нормой. Ubuntu была около 600 Мбайт, а образы Microsoft .Net весили несколько гигабайт (это правда). В то время загрузка только пары таких образов могла сразу повлиять на дисковое пространство хост-машины, даже если слои между ними разделялись. Сегодня это не совсем так — базовые образы намного легче, но через некоторое время их накопление определенно окажет влияние, если не быть осторожным.

Существует несколько видов образов, которые не видны напрямую конечному пользователю:

  • Промежуточные — те, что ссылаются на другие (дочерние) и не могут быть удалены.
  • Висящие — это те, на которые больше нет ссылок. Они занимают некоторое место на диске и могут быть удалены.

Следующие команды отображают висящие образы в системе:

$ docker image ls -f dangling=true
REPOSITORY  TAG      IMAGE ID         CREATED             SIZE
<none>      <none>   21e658fe5351     12 minutes ago      71.3MB

Чтобы удалить их, можно пойти долгим путем:

$ docker image rm $(docker image ls -f dangling=true -q)

Или использовать подкоманду prune:

$ docker image prune
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Deleted Images:
deleted: 
sha256:143407a3cb7efa6e95761b8cd6cea25e3f41455be6d5e7cda
deleted: 
sha256:738010bda9dd34896bac9bbc77b2d60addd7738ad1a95e5cc
deleted: 
sha256:fa4f0194a1eb829523ecf3bad04b4a7bdce089c8361e2c347
deleted: 
sha256:c5041938bcb46f78bf2f2a7f0a0df0eea74c4555097cc9197
deleted: 
sha256:5945bb6e12888cf320828e0fd00728947104da82e3eb4452f

Total reclaimed space: 12.9kB

Или использовать подкоманду prune:

Total reclaimed space: 12.9kB

В случае, если нужно удалить все образы сразу (а не только висящие), можно запустить следующую команду. Однако это не позволит удалить те, которые используются контейнером в данный момент:

$ docker image rm $(docker image ls -q)

Использование диска томами

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

Допустим, мы запускаем контейнер на основе MongoDB, а затем используем его для тестирования бэкапа, который мы сделали ранее (он доступен локально в файле bck.json):

# Запуск контейнера mongo
$ docker run --name db -v $PWD:/tmp -p 27017:27017 -d mongo:4.0

# Импорт существующего бэкапа (из огромного файла bck.json)
$ docker exec -ti db mongoimport \
  --db 'test' \
  --collection 'demo' \
  --file /tmp/bck.json \
  --jsonArray

Данные в файле бэкапа будут храниться на хосте в папке /var/lib/docker/volumes. Почему эти данные не сохраняются в слое контейнера? Причина в том, что в Dockerfile образа mongo расположение /data/db (где mongo хранит свои данные по умолчанию) определяется как том.

Извлечение Dockerfile, используемого для сборки образа контейнера MongoDB

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

Окончив тестирование бэкапа, мы останавливаем или удаляем контейнер. Однако том не удаляется, если мы не сделаем этого явно — он остается, потребляя дисковое пространство. Тогда мы можем пойти долгим путем:

$ docker volume rm $(docker volume ls -q)

Или использовать prune:

$ docker volume prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
d50b6402eb75d09ec17a5f57df4ed7b520c448429f70725fc5707334e5ded4d5
8f7a16e1cf117cdfddb6a38d1f4f02b18d21a485b49037e2670753fa34d115fc
599c3dd48d529b2e105eec38537cd16dac1ae6f899a123e2a62ffac6168b2f5f
...
732e610e435c24f6acae827cd340a60ce4132387cfc512452994bc0728dd66df
9a3f39cc8bd0f9ce54dea3421193f752bda4b8846841b6d36f8ee24358a85bae
045a9b534259ec6c0318cb162b7b4fca75b553d4e86fc93faafd0e7c77c79799
c6283fe9f8d2ca105d30ecaad31868410e809aba0909b3e60d68a26e92a094da

Total reclaimed space: 25.82GB
[email protected]:~$

Использование диска кэшем сборки

В релизе Docker 18.09 представлены усовершенствования процесса сборки с помощью BuildKit. Использование этого инструмента позволяет повысить производительность, управление хранилищем, функциональность и безопасность. Мы не будем подробно его описывать, а просто посмотрим, как его включить и как он влияет на использование диска.

Рассмотрим следующее фиктивное приложение Node.Js и связанный с ним Dockerfile:

Файл index.js определяет простой HTTP-сервер, который предоставляет конечную точку ‘/’ и отвечает строкой на каждый полученный запрос:

var express = require('express');
var util    = require('util');
var app = express();
app.get('/', function(req, res) {
  res.setHeader('Content-Type', 'text/plain');
  res.end(util.format("%s - %s", new Date(), 'Got Request'));
});
app.listen(process.env.PORT || 80);

package.json определяет зависимости — используем expressjs, чтобы настроить HTTP-сервер:

{
  "name": "testnode",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.14.0"
  }
}

Dockerfile определяет, как построить образ из приведенного выше кода:

FROM node:13-alpine
COPY package.json /app/package.json
RUN cd /app && npm install
COPY . /app/
WORKDIR /app
EXPOSE 80
CMD ["npm", "start"]

Создадим образ как обычно и без включенного BuildKit:

$ docker build -t app:1.0 .

При проверке использования диска мы увидим только базовый (node:13-alpine был загружен в начале сборки) и конечный образ сборки (app: 1.0):

$ docker system df
TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         2          0          109.3MB    109.3MB (100%)
Containers     0          0          0B         0B
Local Volumes  0          0          0B         0B
Build Cache    0          0          0B         0B

Теперь соберем версию образа 2.0 с BuildKit. Нужно просто установить DOCKER_BUILDKIT в 1:

$ DOCKER_BUILDKIT=1 docker build -t app:2.0 .

При повторной проверке использования диска видим, что был создан кэш сборки (Build Cache):

$ docker system df
TYPE           TOTAL      ACTIVE     SIZE       RECLAIMABLE
Images         2          0          109.3MB    109.3MB (100%)
Containers     0          0          0B         0B
Local Volumes  0          0          0B         0B
Build Cache    11         0          8.949kB    8.949kB

Для его удаления можно выполнить:

$ docker builder prune

WARNING! This will remove all dangling build cache.
Are you sure you want to continue? [y/N] y
Deleted build cache objects:
rffq7b06h9t09xe584rn4f91e
ztexgsz949ci8mx8p5tzgdzhe
3z9jeoqbbmj3eftltawvkiayi
Total reclaimed space: 8.949kB
Total reclaimed space: 8.949kB

Очистка всего и сразу

В приведенных выше примерах каждая из команд контейнера, образа и тома предоставляет подкоманду prune для освобождения дискового пространства. Она доступна на системном уровне Docker, поэтому удаляет все сразу:

$ docker system prune
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - all dangling build cache

Are you sure you want to continue? [y/N]

Выполнение этой команды время от времени для очистки диска — хорошая привычка.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Luc Juggery: Docker Tips: Clean Up Your Local Machine