Содержание урока · 10 разделов
- Reaper docker-уборщик: как он устроен
- Что попадает под метки: контейнеры, сети и тома
- TESTCONTAINERS_RYUK_DISABLED: когда выключение оправдано
- Утечки после падения теста и ручная гигиена
- Политика уборки: окружение решает
- Окружение и политика уборки
- Типовые ошибки уборки
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Уборка в testcontainers — двухслойная, и путать слои дорого. Первый — ваш собственный: Terminate, зарегистрированный в t.Cleanup, исполняется при провале, панике и штатном выходе — механика разобрана в уроке о жизненном цикле. Этот слой быстрый и предсказуемый: ресурс освобождается мгновенно, не дожидаясь конца сессии. Второй слой — страховочный: он не знает про тесты и Cleanup, он знает про процесс. Пока go test жив, страховка молчит; умрёт процесс вместе со всеми незакрытыми Cleanup — страховка сработает и выметет всё, что процесс насоздавал. Архитектура закрывает дыру, которую не закрыть изнутри: ни один defer не выполнится в процессе, убитом SIGKILL'ом. Отсюда правило чтения урока: первый слой — дисциплина, второй — гарантия. Убрать первый — второй не даст утонуть окружению, но потянет до конца сессии с лишними пожирателями памяти и портов. Убрать второй — останется одна дисциплина, и единственная забытая уборка превращается в зомби, который живёт до ручной зачистки. Коварство утечек в том, что на быстрой машине они невидимы: пара лишних контейнеров ничего не ломает, тесты зелёные, и кажется, что всё под контролем. Рокировка происходит на перегруженном CI: там же исчерпаются свободные порты, кончится место под образы, а соседний прогон начнёт ловить странные отказы в подключении. Урок 18 — о том, кому доверена уборка, когда вы закрываете ноутбук посреди прогона.
Reaper docker-уборщик: как он устроен#
Связка «testcontainers ryuk» всплывает в багтрекерах не зря: без понимания механики половина поведения уборщика выглядит мистикой. При первом же контейнере библиотека заводит sidecar — контейнер из образа testcontainers/ryuk с именем reaper_<sessionID>, где sessionID — случайный идентификатор сессии. Ему монтируют docker.sock — без него убирать чужие ресурсы нечем, — ставят AutoRemove, чтобы он вычистил и сам себя, и ждут старта по журналу и порту: комбинированная стратегия ждёт строчку Started в логах и открытый порт 8080/tcp, который при желании сдвигается переменной RYUK_PORT. Дальше рукопожатие: тестовый процесс подключается к уборщику по TCP и передаёт набор фильтров по меткам сессии, тот отвечает ACK, и соединение считается рабочим. Уборщик — синглтон: второй контейнер не плодит второго уборщика, библиотека находит живого по меткам и имени и переиспользует. Старт не мгновенный: если уборщика нет, его находят или создают с ретраями и растущими паузами — на всё про всё около двадцати секунд, поэтому первый тест в прогоне просыпается чуть медленнее остальных. Пока тесты идут, создание каждого ресурса держит собственное соединение с уборщиком. Стоит процессу умереть — обрываются все разом. Рукопожатие — не формальность: ошибка подключения не глотается тихо, а после исчерпания попыток всплывает из создания контейнера на руки тесту, так что сломанный уборщик невозможно перепутать с медленным демоном.
Два слоя защиты в одном тесте
GOfunc TestPayments(t *testing.T) {
ctx := context.Background()
ctr, err := redis.Run(ctx, "redis:7-alpine")
if err != nil {
t.Fatalf("redis: %v", err)
}
// слой 1: сработает при провале, панике и штатном выходе
t.Cleanup(func() { _ = ctr.Terminate(ctx) })
// слой 2 уже дежурит: reaper_<sessionID> держит соединение
// и видит смерть процесса там, где Cleanup бессилен
addr, err := ctr.Endpoint(ctx, "")
if err != nil {
t.Fatalf("endpoint: %v", err)
}
_ = addr // ... работа с сервисом
}Слои не конкурируют: Cleanup отвечает за штатные и полуштатные исходы, уборщик — за смерть процесса. Endpoint забирают после Run: модуль сам дождался готовности. Закомментируйте Cleanup — тест всё равно оставит чистую машину, но ресурс будет жечь память до конца сессии. Выключите уборщика — останется одна дисциплина: единственная забытая уборка станет зомби.
Что попадает под метки: контейнеры, сети и тома#
Уборщик не гадает, что удалять: он сверяется с метками, а библиотека снабжает ими всё, что создаёт. Служебный набор скромен: org.testcontainers=true, язык go, версия библиотеки, идентификатор сессии и — главный — org.testcontainers.reap, разрешение на зачистку. Разрешение ставится только при включённом уборщике: выключите Ryuk — меток reap на ресурсах не будет вовсе, и автозачистка теряет право голоса. Маркировка достаётся и сетям: при создании библиотека дописывает тот же набор, поэтому сеть-сирота не переживёт сессию. Меченым оказывается вообще всё, что выходит из провайдера, — включая вспомогательные контейнеры вроде sshd-проброса портов, который библиотека заводит для доступа к локальным службам изнутри контейнера. Пространство имён org.testcontainers — служебное: метку с таким префиксом библиотека отклонит с ошибкой, для своих пометок выбирайте собственный префикс. Что стирается по меткам: контейнеры, сети, тома. С томами тонкость: force-remove при Terminate сносит анонимные тома контейнера, а именованные переживают удаление — их перечисляют опцией RemoveVolumes или подметают prune'ом с фильтром по меткам. Наконец, уборщик прибирает и себя: AutoRemove срабатывает, когда зачищать больше нечего, — от sidecar не остаётся даже имени.
Проверка: кто приберёт после SIGKILL
Процесс go test убит kill -9. t.Cleanup не выполнился. Кто и когда приберёт контейнеры теста?
TESTCONTAINERS_RYUK_DISABLED: когда выключение оправдано#
Выключатель один, сценариев два. Первый — переменная TESTCONTAINERS_RYUK_DISABLED=true: окружение сильнее файла, поэтому в CI дёргают именно её, и сравнение здесь строгое — выключит только литеральное true, единица или tRuE уборщика не остановят. Второй — свойство ryuk.disabled=true в ~/.testcontainers.properties: удобно включать флаг локально и надолго. Что происходит после: reaper-контейнер не стартует вообще, рукопожатий нет, метки reap не раздаются, автозачистки нет, старт первого теста чуть ускоряется — минус один контейнер в пачке. Оправданий ровно два. Первое — CI, где окружение одноразовое и уборку ведёт сам раннер: гонять уборщика в среде, которую снесут через минуту, — работа без клиента. Второе — отладка: выключенный уборщик оставляет контейнер живым после упавшего теста, и docker logs показывает всё, что нужно. Отдельная история — окружения, где уборщику не дают подняться: если демон отказывает в запуске, сначала проверьте флаг TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true и только потом думайте о глушении. Тонкие настройки живут в соседних переменных: RYUK_CONNECTION_TIMEOUT — таймаут на подключение, минута по умолчанию; RYUK_RECONNECTION_TIMEOUT — сколько ждать пропавшего клиента, десять секунд; RYUK_VERBOSE — болтливые логи уборщика.
Выключение и флаг привилегий
GO# CI без права на привилегированные контейнеры:
# уборку берёт на себя одноразовый раннер
export TESTCONTAINERS_RYUK_DISABLED=true
go test ./...
# отладка упавшего теста: контейнер останется живым для docker logs
TESTCONTAINERS_RYUK_DISABLED=true go test -run TestBroken -count=1
# если окружение ругается на запуск уборщика:
export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=trueПервые два вызова — про выключение: env-переменная перекрывает properties-файл, поэтому в пайплайнах ставят её, а локально удобнее флаг в файле. Третий вызов — про окружения, где уборщику не хватает прав: privileged-режим решает, глушить Ryuk не требуется. Отладочный прогон с выключенным уборщиком оставляет труп теста нетронутым — идеальный материал для docker logs.
Что именно случится, если процесс теста умрёт насильственно? Хронология честная. SIGKILL не оставляет процессу шансов: ни defer, ни Cleanup — всё замерло на месте. Обрываются TCP-соединения с уборщиком: рукопожатие с ACK давно пройдено, соединения держались всю сессию. Уборщик замечает тишину и начинает отсчёт reconnection timeout — десяти секунд по умолчанию; клиент, вернувшийся в окно, спасает сессию. Не вернулся — сессия объявляется мёртвой. При этом штатная уборка одиночного контейнера соседей не трогает: Terminate закрывает лишь собственное соединение этого контейнера, и уборщик продолжает дежурить для остальных. Дальше механика зачистки: по меткам собирается всё, что носит отпечаток сессии, контейнеры останавливаются и удаляются, сети и тома стираются следом, под конец уборщик прибирает самого себя. Задержка — не баг, а предохранитель: короткий сетевой шторм или пауза уснувшего ноутбука не должны стоить целого прогона, клиент успевает переподключиться. Но и не вечность: через десяток секунд после смерти процесса docker ps опустеет. Загадочные «зависшие контейнеры после kill» — вопрос двадцати секунд терпения.
Что покажет docker ps через минуту
Прочитайте код и выберите, что он выведет:
// Прогон интеграционных тестов убит: kill -9 <pid go test>
// Ryuk включён, RYUK_RECONNECTION_TIMEOUT не задан.
// docker ps выполнен сразу после убийства — контейнеры на месте.
// Повторный docker ps — через минуту.Частая паника: убили зависший прогон, docker ps показывает те же контейнеры, рука тянется чистить — а через десять секунд список пуст. Это не сбой, это reconnection timeout: уборщик даёт клиенту шанс вернуться, и торопливый ручной docker rm здесь только мешает — вы удаляете то, что уборщик уже взял в работу. Подождите его, прежде чем хвататься за prune. Зеркальный случай — зомби висят дольше: проверьте RYUK_RECONNECTION_TIMEOUT и жив ли reaper-контейнер, прежде чем подозревать библиотеку.
Утечки после падения теста и ручная гигиена#
Утечка после падения — не только контейнер. За один упавший прогон остаются контейнеры со своими лимитами памяти, занятые маппнутые порты, сети-сироты, на которые больше никто не ссылается, и тома, переживают удаление контейнера. При включённом уборщике всё это сходит за десятки секунд; при выключенном — копится до ручной зачистки, и на длинном CI-дне копится быстро: порты кончаются раньше места. Диагностика начинается с фильтра по меткам: docker ps -a --filter label=org.testcontainers показывает всё, что создала библиотека, — по меткам видно и язык, и сессию. Дальше точечная зачистка тем же фильтром: удалить контейнеры, подметать сети и тома. Тома отдельная глава: анонимные уходят вместе с контейнером, а именованные — например, pgdata-it из уроков про базы — остаются на диске после любого удаления и ждут либо RemoveVolumes в Terminate, либо volume prune по меткам. Сколько места съедено, говорит docker system df: его стоит смотреть и до, и после уборки. Вслепую пущенный docker system prune -a на общем демоне — оружие массового поражения: с ним уходят и чужие образы, поэтому фильтр по меткам — не паранойя, а вежливость. Для локального docker правило одно: чистка по меткам безопасна, чистка по вдохновению — нет.
Ручная гигиена по меткам
GO# что осталось от тестовых прогонов
docker ps -a --filter label=org.testcontainers
# точечная зачистка по меткам: чужое не трогаем
docker rm -f $(docker ps -aq --filter label=org.testcontainers)
docker network prune -f --filter label=org.testcontainers
docker volume prune -f --filter label=org.testcontainers
# сколько места занимает docker в целом
docker system dfФильтр label=org.testcontainers выдёргивает только тестовые ресурсы: чужие коллекции демона не страдают. Порядок команд отражает зависимости — сперва контейнеры, затем сети, затем тома. docker system df — не уборка, а рентген: покажет, сколько съедено образами, контейнерами, томами и кэшем сборки, до и после зачистки.
Соберите хронологию смерти процесса
Расставьте события от убийства процесса до самозачистки уборщика.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Политика уборки: окружение решает#
Единой политики не существует: уборка — решение окружения, и таблица ниже собирает типовые конфигурации в одну карту. Между строк таблицы живёт и настройка сессии. По умолчанию каждый процесс тестов получает собственный случайный sessionID и собственный уборщика — для go test ./... с параллельными пакетами это целая пачка reaper'ов, каждый со своими метками. Переменная TESTCONTAINERS_SESSION_ID фиксирует идентификатор: все процессы пользуются одним уборщиком, где бы ни стартовали. Значение проверяется на допустимые символы — буквы, цифры, точки, дефисы, подчёркивания; слэш в нём уронит прогон паникой ещё до первого теста, и это правильно: лучше громкий старт, чем тихая путаница меток в середине. Оборотная сторона фиксации — общие метки: на общем демоне один sessionID означает, что kill одного воркера вынесет ресурсы всех, кто делит ту же сессию, поэтому на командном демоне у каждого своя сессия. Какая бы политика ни выпала окружению, рассуждать о разделяемых ресурсах и жизненных циклах процессов качает продвинутый курс по Go: там контексты и смерть процесса разбираются глубже, чем позволяет формат урока.
Окружение и политика уборки#
| Окружение | Политика уборки | Почему так |
|---|---|---|
| Локальная разработка | Ryuk включён по умолчанию, плюс Terminate в t.Cleanup | страховка на случай kill, паники и уснувшего ноутбука |
| CI с одноразовым раннером и полными правами | Ryuk включён, без настроек | одноразовый раннер и так чист, уборщик — подстраховка |
| CI без права на privileged-контейнеры | TESTCONTAINERS_RYUK_DISABLED=true | уборку ведёт сам раннер после прогона |
| Отладка упавшего теста | TESTCONTAINERS_RYUK_DISABLED=true локально | контейнер живёт для docker logs, дальше docker rm руками |
| Параллельные go-пакеты | TESTCONTAINERS_SESSION_ID фиксированный | один reaper на все процессы вместо пачки |
| Общий Docker-демон команды | Ryuk включён, чистка только по меткам | prune вслепую снесёт чужие ресурсы |
Проверка: что меняет RYUK_DISABLED
В CI выставлена переменная TESTCONTAINERS_RYUK_DISABLED=true. Что изменится в прогоне?
Соберите опции уборки
Восстановите вызов Terminate: таймаут останова и именованный том, который переживёт обычное удаление.
Типовые ошибки уборки#
- глушить Ryuk «ради скорости старта» на постоянной основе — мусор копится, место и порты кончаются
- отключать уборщика в CI без собственного механизма уборки — зомби переживают прогон
- ставить свои метки с префиксом org.testcontainers — библиотека отклонит их: это служебное пространство
- ждать мгновенной уборки после kill: reaper выжидает reconnection timeout — десять секунд по умолчанию
- жить по принципу «уборщик всё приберёт»: между тестами контейнеры жгут память и порты зря
- чистить общий демон вслепую через docker system prune -a — вместе с мусором уходят чужие образы
- раздавать один TESTCONTAINERS_SESSION_ID всей команде на общем демоне: kill одного воркера вынесет чужие сессии
- забывать про именованные тома: обычное удаление сносит анонимные, именованные переживают его без RemoveVolumes
Честная оговорка: в песочнице сайта этот урок не исполняется — уборщику нужен живой демон Docker, а браузерная среда не поднимает контейнеров. Читать это не мешает: вся механика разобрана по шагам. Эксперименты — локальные: поднимите любой тест из урока об установке, найдите в docker ps контейнер, чьё имя начинается с reaper_, убейте go test посреди теста и замерьте таймер уборки; затем выставьте TESTCONTAINERS_RYUK_DISABLED=true и найдите зомби руками.
Карточки: Ryuk и ресурсы
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Уборка двухслойная: t.Cleanup с Terminate — быстрая штатная зачистка, Ryuk — страховка на случай смерти процесса; первый слой можно опускать лишь осознанно.
- Reaper — sidecar testcontainers/ryuk с именем reaper_<sessionID>: смонтированный docker.sock, TCP-рукопожатие с ACK, переиспользование в рамках сессии.
- Под метки попадают контейнеры, сети и тома: метка org.testcontainers.reap ставится только при включённом уборщике, именованные тома удаляются опцией RemoveVolumes.
- TESTCONTAINERS_RYUK_DISABLED=true выключает уборщика — env сильнее properties-файла; ручная гигиена — чистка по меткам, а не prune вслепую.
Домашнее задание
Проведите ревизию: docker ps -a --filter label=org.testcontainers и docker system df до и после зачистки по меткам. Прогоните любой тест из курса с TESTCONTAINERS_RYUK_DISABLED=true, найдите зомби через docker ps, уберите руками. Финал: верните уборщика, убейте go test посреди теста и замерьте секундомером, через сколько секунд docker ps опустеет.
Личные заметкиПусто
Вопросы и ответы по уроку
Ryuk же всё приберёт — можно ли не писать t.Cleanup?
Технически мусор исчезнет, но позже и дороже. Уборщик срабатывает по смерти процесса, а не по окончании теста: до конца сессии контейнеры жгут память и держат порты, параллельные тесты наступают друг другу на ноги, задержки растут. На длинном прогоне одна брошенная база может съесть то, чего не хватит пяти живым тестам. t.Cleanup освобождает ресурс сразу и детерминированно. Слои работают в связке: дисциплина даёт скорость, страховка — гарантию, и отменять любую из них дорого.
На CI reaper не запускается — тесты падают. Что делать?
Сначала выясните, что именно запретил демон: чаще всего окружение не даёт уборщику нужных привилегий. Если политика позволяет — включите TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true. Если раннер одноразовый и сносит окружение сам, честнее выключить уборщика TESTCONTAINERS_RYUK_DISABLED=true и положиться на механизм раннера: держать контейнер-уборщик в среде, которая умрёт через минуту, бессмысленно.
Чем reaper лучше docker prune по расписанию в cron?
Точностью и моментом. Prune не различает, чей мусор: фильтры по возрасту или имени рано или поздно сотрут нужное чужое, а отладить такой инцидент почти невозможно — улик не останется. Уборщик зачищает строго по меткам сессии — только то, что создал этот прогон, включая сети и тома, — и делает это сразу после смерти клиента, а не в полночь по расписанию. Cron на ноутбуке разработчика и вовсе не живёт: ноутбук выключают.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Go с нуля: базовый синтаксис и конструкции
Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.
- Писать, компилировать и запускать программы на Go
- Свободно работать с переменными, типами, операторами
- Управлять потоком выполнения: if, switch, for, range
- Хранить данные в массивах, срезах и картах