Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~16 мин +50 XP

Ryuk и уборка мусора

Половина уроков курса заканчивалась одной фразой: Terminate в t.Cleanup — и мусора не будет. Пора спросить, кто уберёт, если Cleanup не исполнится: процесс теста убит, пайплайн снят посреди прогона, ноутбук уснул вместе с Docker. Сценарий «testcontainers ryuk» — про этот случай: reaper docker-уборщик, которого библиотека заводит сама, кормит метками и оставляет дежурить даже после смерти процесса. Разберём, как он устроен и почему у него два таймаута, что именно попадает под метки, когда его выключают через RYUK_DISABLED и какая гигиена остаётся на вас, чтобы локальная машина и CI не зарастали зомби.

Содержание урока · 10 разделов

Уборка в 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, и соединение считается рабочим. Уборщик — синглтон: второй контейнер не плодит второго уборщика, библиотека находит живого по меткам и имени и переиспользует. Старт не мгновенный: если уборщика нет, его находят или создают с ретраями и растущими паузами — на всё про всё около двадцати секунд, поэтому первый тест в прогоне просыпается чуть медленнее остальных. Пока тесты идут, создание каждого ресурса держит собственное соединение с уборщиком. Стоит процессу умереть — обрываются все разом. Рукопожатие — не формальность: ошибка подключения не глотается тихо, а после исчерпания попыток всплывает из создания контейнера на руки тесту, так что сломанный уборщик невозможно перепутать с медленным демоном.

Два слоя защиты в одном тесте

GO
func 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 — через минуту.

Утечки после падения теста и ручная гигиена#

Утечка после падения — не только контейнер. За один упавший прогон остаются контейнеры со своими лимитами памяти, занятые маппнутые порты, сети-сироты, на которые больше никто не ссылается, и тома, переживают удаление контейнера. При включённом уборщике всё это сходит за десятки секунд; при выключенном — копится до ручной зачистки, и на длинном 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: таймаут останова и именованный том, который переживёт обычное удаление.

t.Cleanup(func() { _ = ctr.Terminate(context.Background(), testcontainers._(30*time.Second), testcontainers._("pgdata-it"), ) })

Типовые ошибки уборки#

  • глушить Ryuk «ради скорости старта» на постоянной основе — мусор копится, место и порты кончаются
  • отключать уборщика в CI без собственного механизма уборки — зомби переживают прогон
  • ставить свои метки с префиксом org.testcontainers — библиотека отклонит их: это служебное пространство
  • ждать мгновенной уборки после kill: reaper выжидает reconnection timeout — десять секунд по умолчанию
  • жить по принципу «уборщик всё приберёт»: между тестами контейнеры жгут память и порты зря
  • чистить общий демон вслепую через docker system prune -a — вместе с мусором уходят чужие образы
  • раздавать один TESTCONTAINERS_SESSION_ID всей команде на общем демоне: kill одного воркера вынесет чужие сессии
  • забывать про именованные тома: обычное удаление сносит анонимные, именованные переживают его без RemoveVolumes

Карточки: Ryuk и ресурсы

Термин

Ryuk / Reaper

нажмите, чтобы перевернуть
Определение

Контейнер-супервизор из образа testcontainers/ryuk с именем reaper_<sessionID>: смонтированный docker.sock, TCP-рукопожатие с ACK и зачистка контейнеров, сетей и томов по меткам сессии после смерти клиента.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 опустеет.

Личные заметкиПусто
Хранятся только в вашем браузере (Testcontainers · Ryuk и уборка мусора)

Вопросы и ответы по уроку

Ryuk же всё приберёт — можно ли не писать t.Cleanup?

Технически мусор исчезнет, но позже и дороже. Уборщик срабатывает по смерти процесса, а не по окончании теста: до конца сессии контейнеры жгут память и держат порты, параллельные тесты наступают друг другу на ноги, задержки растут. На длинном прогоне одна брошенная база может съесть то, чего не хватит пяти живым тестам. t.Cleanup освобождает ресурс сразу и детерминированно. Слои работают в связке: дисциплина даёт скорость, страховка — гарантию, и отменять любую из них дорого.

На CI reaper не запускается — тесты падают. Что делать?

Сначала выясните, что именно запретил демон: чаще всего окружение не даёт уборщику нужных привилегий. Если политика позволяет — включите TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true. Если раннер одноразовый и сносит окружение сам, честнее выключить уборщика TESTCONTAINERS_RYUK_DISABLED=true и положиться на механизм раннера: держать контейнер-уборщик в среде, которая умрёт через минуту, бессмысленно.

Чем reaper лучше docker prune по расписанию в cron?

Точностью и моментом. Prune не различает, чей мусор: фильтры по возрасту или имени рано или поздно сотрут нужное чужое, а отладить такой инцидент почти невозможно — улик не останется. Уборщик зачищает строго по меткам сессии — только то, что создал этот прогон, включая сети и тома, — и делает это сразу после смерти клиента, а не в полночь по расписанию. Cron на ноутбуке разработчика и вовсе не живёт: ноутбук выключают.

Термины урока

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

Следующая ступень учебного пути

Go Base25 уроков~12 чновичок25 уроков уже доступны

Go с нуля: базовый синтаксис и конструкции

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

  • Писать, компилировать и запускать программы на Go
  • Свободно работать с переменными, типами, операторами
  • Управлять потоком выполнения: if, switch, for, range
  • Хранить данные в массивах, срезах и картах