Первая запись в файл Docker-образа медленная: разбираем OverlayFS copy-up

В образ приложения положили подготовленный файл индекса. При старте сервис меняет в нём несколько байтов, и первый запуск каждого контейнера внезапно даёт всплеск записи на диске. Следующие обновления того же файла проходят тише. Если каталог остался в файловой системе контейнера, а движок использует OverlayFS, стоит проверить, не создаёт ли одна маленькая правка отдельную копию большого файла.

Откуда берётся лишняя запись

Образ состоит из слоёв, которые контейнер использует для чтения. Поверх них есть собственный слой контейнера для изменений. Пока приложение только читает файл из образа, достаточно нижнего слоя. При первой записи OverlayFS поднимает файл в верхний слой: операция называется copy_up. Устройство слоёв позволяет нескольким контейнерам читать один исходный файл и изменять каждый свою копию. При этом одна запись в начало файла может добавить в writable layer файл его полного логического размера. Следующая запись в тот же файл работает уже с верхней копией.

Точное количество I/O и время операции зависят от ядра, файловой системы и реализации копирования. Если рост начался после обновления окружения, сначала посмотрите docker info и проверьте, какой storage driver или snapshotter используется. Пример рассчитан на Docker с OverlayFS для слоёв Linux-контейнеров. У других механизмов хранения единица копирования может отличаться.

Сравните размер до и после записи

В отдельной учебной папке соберите образ со специально созданным файлом размером 16 МиБ. Имя образа и контейнера должны быть свободны. sleep держит контейнер в работе, чтобы можно было измерить один и тот же экземпляр до и после записи. Для пробы не нужны база данных, подключение к сети контейнера и доступ к каталогам хоста.

FROM alpine:3.22
RUN mkdir -p /data && dd if=/dev/zero of=/data/index.bin bs=1048576 count=16
CMD ["sleep", "300"]
docker build -t overlay-copyup:demo .
docker run -d --pull=never --network none --name overlay-copyup-demo overlay-copyup:demo
docker inspect --size --format '{{.SizeRw}}' overlay-copyup-demo
docker exec overlay-copyup-demo sh -ec 'printf X | dd of=/data/index.bin bs=1 seek=0 conv=notrunc'
docker inspect --size --format '{{.SizeRw}}' overlay-copyup-demo
docker exec overlay-copyup-demo sh -ec 'printf Y | dd of=/data/index.bin bs=1 seek=1 conv=notrunc'
docker inspect --size --format '{{.SizeRw}}' overlay-copyup-demo

Смотрите на приращение SizeRw после первой правки и на результат второй. Для описанного драйвера ожидается рост примерно на размер файла при первой записи и отсутствие сопоставимого роста при второй. conv=notrunc сохраняет длину файла, поэтому эффект нельзя объяснить обычным увеличением размера индекса. SizeRw показывает размер изменённых файлов слоя и сам по себе не измеряет физические записи или задержку на диске. Если расследуете именно задержку, отдельно снимите время первой и последующих записей при сопоставимой нагрузке и сравните с вариантом хранения в томе.

Где размещать обновляемый индекс

Оставьте начальный шаблон по пути вроде /opt/defaults/index.bin. Отдельно подключите том в /data и создавайте /data/index.bin из шаблона только при инициализации нового тома. Последующие запуски должны открывать существующий индекс в томе. Такой порядок делает копирование при создании данных отдельным, управляемым шагом и позволяет переиспользовать результат при пересоздании контейнера. Если индексы создают несколько экземпляров, назначьте владельца шага инициализации и дождитесь его завершения до первого открытия индекса.

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

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

Продолжить на практике

  • Docker и Docker Compose: контейнеризация приложений ↗

    Практический курс Docker для разработчиков: упакуйте своё приложение, подключите PostgreSQL и запускайте окружение через Compose. Научитесь находить поломки, обновлять сервис и восстанавливать данные. 6 Dockerfile-задач с автопроверкой и 42 локальные лабораторные.