open atlas
↑ К треку
Docker: контейнеры как система DOCK · 04 · 01

Записываемый слой: copy-on-write, upperdir и почему состояние контейнера испаряется

У каждого контейнера есть тонкий записываемый слой поверх read-only образа через overlay2. Запись делает copy-up целого файла из lowerdir в upperdir, слой умирает вместе с контейнером, а неограниченная запись (логи, временные файлы) тихо забивает диск хоста.

DOCK Senior ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Контейнер с Postgres работал восемь месяцев, когда узел дал алерт: диск забит на 100%, любая запись падает. Первая мысль DBA — том, но том с данными был заполнен на 40%. Виновником оказался /var/lib/docker/overlay2, и одна директория внутри разрослась до 180 ГБ. Когда-то кто-то выставил контейнеру уровень логирования debug и направил лог приложения в файл внутри контейнера, а не в смонтированный том. Восемь месяцев debug-логов копились в записываемом слое со скоростью примерно 900 МБ в день — невидимые для любого df, который команда запускала внутри тома, невидимые для бэкапов и в одном docker rm от полной потери. Починка заняла тридцать секунд — перенаправить логи в stdout, — но урок был структурным: записываемый слой — это место, где данные тихо умирают, и единственное, что держало диск живым, было везение.

Стек: lowerdir, upperdir, merged

Понимание этого стека — то, что позволит тебе объяснить, почему диск заполняется, пока дашборды томов горят зелёным, и как это остановить. При старте контейнера Docker не копирует образ. С драйвером по умолчанию overlay2 он собирает union-монтирование: read-only слои образа становятся lowerdir (стек, нативно до 128 слоёв), свежая пустая директория — upperdir контейнера (записываемый слой), а ядро показывает единое представление в merged — корневую ФС, которую процесс видит как /. Четвёртая директория, workdir, — внутренняя промежуточная зона OverlayFS для атомарных операций. Всё это лежит под /var/lib/docker/overlay2/<id>/, с директорией l/ коротких симлинков, чтобы команда монтирования укладывалась в лимит длины аргументов ядра.

$ docker inspect --format '{{ .GraphDriver.Data.MergedDir }}' web
/var/lib/docker/overlay2/3f9a.../merged

# Четыре overlay-компонента для запущенного контейнера:
#   lowerdir  -> слои образа (read-only, общие между контейнерами)
#   upperdir  -> записи этого контейнера (записываемый слой)
#   workdir   -> рабочая зона ядра (не трогать)
#   merged    -> единый корень, который видит процесс

Чтение проходит сквозь стек: ядро сначала смотрит в upperdir, затем в каждый lowerdir сверху вниз и отдаёт первое попадание. Поскольку lowerdir общий и read-only, десять контейнеров из одного образа делят одну копию на диске и одну запись page cache на файл — overlay2 поддерживает разделяемый page cache, поэтому на узел можно набить сотни одинаковых контейнеров без десятикратного расхода памяти. Записываемый слой — единственное хранилище на контейнер, и он стартует пустым: upperdir только что запущенного контейнера часто весит лишь пару сотен килобайт.

Copy-up: цена первой записи

Когда встретишь контейнер, который тормозит или зависает на самой первой записи в большой файл — copy-up почти наверняка и есть причина. Дорогая операция — изменение файла, лежащего в нижнем слое. OverlayFS работает на уровне файла, а не блока, поэтому первая запись в любой файл нижнего слоя запускает copy_up: ядро копирует весь файл из lowerdir в upperdir, прежде чем применить ваше изменение. Допишите один байт к 2-гигабайтной базе SQLite, запечённой в образ, — и ядро сперва скопирует все 2 ГБ: многосекундный затык, 2 ГБ съеденного диска и скачок задержки, выглядящий как зависание процесса. Последующие записи в этот уже скопированный файл — обычные прямые записи; налог платится один раз, на первом касании.

Поэтому файлы данных базы никогда не должны жить в образе или записываемом слое. Нагруженная таблица с блочными апдейтами поверх скопированного многогигабайтного файла означает, что OverlayFS уже скопировал его целиком, а файловая гранулярность означает, что мелкие случайные записи не получают никакого выигрыша от абстракции union-ФС — каждая запись идёт через лишний слой, который ядру нужно пройти. Удаление файла нижнего слоя тоже не бесплатно и не настоящее: OverlayFS пишет в upperdir маркер-whiteout, чтобы скрыть файл, так что байты по-прежнему занимают lowerdir, а записываемый слой растёт, когда вы удаляете.

Викторина

Образ содержит read-only файл данных на 2 ГБ. Контейнер дописывает к нему одну строку лога. Что произойдёт на диске?

Эфемерность и тихий рост

Определяющее свойство записываемого слоя в том, что он удаляется при удалении контейнера. docker rm, docker compose down, перепланировка пода в Kubernetes, флаг --rm, OOM-kill с последующим свежим контейнером — всё это выбрасывает upperdir. Состояние, записанное туда, не переживает жизненный цикл контейнера, и ровно ради этого существуют тома. docker stop с последующим docker start сохраняет слой (тот же контейнер), но пересоздание контейнера из образа — а это делает любой деплой или восстановление после краха — стартует новенький пустой upperdir.

Вторая опасность — невидимость. Всё, что пишет в несмонтированный путь внутри контейнера, — логи приложения, временные файлы загрузок, директория кэша, разбухший core dump — попадает в записываемый слой и засчитывается в Docker-хранилище хоста, а не в том, который мониторит команда. Болтливый сервис, пишущий в файл хотя бы 20 МБ/час, добавляет полгигабайта в день к слою, за которым никто не следит; умножьте на пару сотен контейнеров на узел — и /var/lib/docker забивается, пока каждый дашборд тома горит зелёным. docker ps -s это вскрывает: в колонке SIZE цифра virtual — общий образ, а ведущее число — фактический рост записываемого слоя.

Викторина

Сервис пишет 30 МБ/час логов в /app/logs/app.log внутри контейнера, том туда не смонтирован. После пересоздания контейнера на деплое что верно?

Почему это работает

Зачем вообще делать записываемый слой эфемерным, а не просто сохранять его? Потому что вся модель контейнеров обменивает устойчивое состояние на экземпляр на одноразовость: контейнер, который можно выбросить и пересоздать идентично из образа, — это то, что делает горизонтальное масштабирование, rolling-деплои и восстановление после краха тривиальными. Если бы записываемый слой сохранялся, каждый контейнер накапливал бы уникальное «снежиночное» состояние, и свойство «скот, а не питомцы» было бы потеряно. Эфемерность — не ограничение, которое надо обходить, а контракт, и правильная реакция — вытолкнуть каждый байт устойчивого состояния из контейнера в том.

Вспомните перед уходом
  1. 01
    Пройди по шагам, что делает overlay2, когда контейнер изменяет 2-гигабайтный файл из образа, и объясни, почему это проблема для баз данных.
  2. 02
    Перечисли способы, которыми записываемый слой контейнера может забить диск хоста или потерять данные, и как каждого избежать.
Итог

С драйвером по умолчанию overlay2 корневая ФС контейнера — это union-монтирование, а не копия образа: слои образа стопкой read-only как lowerdir, свежий пустой upperdir держит записи контейнера, а ядро показывает их объединёнными в merged — всё под /var/lib/docker/overlay2. Чтение проходит upperdir, затем lowerdir-ы, и поскольку lowerdir общий, множество контейнеров из одного образа делят одну копию на диске и одну запись page cache на файл, что и делает высокую плотность контейнеров дешёвой по памяти. Цена живёт на первой записи в файл нижнего слоя: OverlayFS копирует с файловой гранулярностью, так что изменение 2-гигабайтного файла из образа копирует все 2 ГБ в upperdir до того, как ляжет правка, — затык и скачок диска, делающие большие изменяемые файлы в образах ловушкой, особенно для баз, где union-абстракция вдобавок облагает налогом каждую мелкую запись. Два свойства делают записываемый слой опасным домом для состояния. Он эфемерен: удаление или пересоздание контейнера удаляет upperdir, так что деплой или крах выбрасывают то, что туда записали. И он невидим: запись в любой несмонтированный путь — логи, временные файлы, кэши — копится в записываемом слое, засчитывается в Docker-хранилище хоста, а не в том, который команда мониторит, и забивает /var/lib/docker, пока каждый дашборд тома горит зелёным. docker ps -s вскрывает рост. Дисциплина одна: ничего устойчивого не живёт в контейнере — логи в stdout, черновик в tmpfs, и каждый байт состояния в том. Теперь, когда получишь алерт на диск узла, а дашборды томов горят зелёным, первый шаг — docker ps -s: ответ в записываемом слое.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

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

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.