Глава 30 / 51
Том 4 · Глубокое погружение в перезагрузку

GC и ноль утечек: погребальный обряд старого поколения

Что происходит после обнуления lease? Выделенный поток в фиксированном порядке разбирает старое поколение — глава о том, как убедиться, что разобрано оно действительно начисто.

Обзор

Позиционирование: верификационная глава тома 4 — механизмы разобраны ранее, здесь даются средства наблюдения (логи finalize, утверждения force-free, отчёты памяти) и реальный исторический кейс (как ранняя стратегия «старое поколение течёт целиком» эволюционировала в GC с нулём утечек).

30.1 Введение

У сборки мусора горячей перезагрузки есть природная трудность: судьба старого поколения зависит от времени завершения асинхронных событий (когда оборвутся соединения, когда вернутся колбэки) — в среде многих worker непредсказуемо. В истории XServer реально бытовали две стратегии: ранние версии выбирали «старое поколение течёт целиком» (не освобождать, абсолютная безопасность); действующая версия эволюционировала в «нуль ссылок + сигнал безопасности + отложенное освобождение» — сигналом безопасности служит обнуление счётчика соединений старого драйвера (все Close-колбэки завершены), после чего за широкой паузой идёт поэтапное освобождение. Причина эволюции проста: при стратегии утечек частые перезагрузки съедают всю память.

30.2 Reaper и порядок деструкции

generation reaper (выделенный поток, таймер движка сканирует каждые 500 мс) детект: lease старого поколения == 0 (сигнал безопасности: счётчик соединений старого драйвера в нуле) │ + широта-пауза (классы custom/udp: фиксированные 5 с; подстраховка тайм-аутом 30 с) ▼ деструкция по порядку (порядок фиксирован) 1. ServiceUnit /* ваша очистка: закрыть db, сбросить буферы */ 2. состояние TCC /* результаты компиляции и символы */ 3. рантайм драйвера /* состояние слоя протоколов */ 4. структура server /* дерево конфигурации и снимки */ лог: generation finalized (по разу на поколение) база утверждений: на всём пути ни одного force-free / ни одного timeout force

Смысл порядка: ваш ServiceUnit выполняется до сноса любых ресурсов хоста — при очистке инфраструктура хоста ещё доступна; и выполняется после того, как старое поколение уже не обслуживает — очистка не задевает ни один запрос. Эта позиция спроектирована.

30.3 Замеренный кейс

В истории репозитория записан приёмочный эксперимент этого механизма (трёхцикловая смена поколений 8080↔8081): одна и та же группа портов трижды меняет поколения, с утверждениями —

  • каждый раунд обслуживает безупречно (ноль отказов запросов)
  • все 6 старых поколений финализируются (счёт логов сходится с числом смен)
  • ноль утечек (отчёты памяти до и после совпадают), ноль падений, без force-free

Ваш сервис можно принимать той же триадой: N смен под нагрузкой → N finalized в логе → ровная кривая RSS (способ замера drill). Эти утверждения зафиксированы в lifecycle_reload_test и reload_matrix (таблица базлайнов гл. 06).

30.4 Таймеры умирают со своим поколением

В порядке GC спрятано важное для приложений следствие: таймеры старого поколения устаревают в момент смены, а не ждут GC. Механически таймер привязан к поколению, которое его создало (строго — к потоково-локальной метке «исполняющегося старого поколения»), — после смены таймеры старого поколения отменяются, а новое перевешивает свои из Init.

  • Результат: таймер никогда не выстрелит не в своё поколение — статики, к которым колбэк обращается, всегда принадлежат создавшему его поколению
  • Цена: периодические задачи обязаны пересоздаваться в Init нового поколения (яма файлового триггера из гл. 27 — ровно это)
  • Выстрел уже в полёте: начавший исполняться колбэк защищён lease своего поколения до завершения

30.5 Контракт

  • Порядок: ServiceUnit → TCC → драйвер → конфигурация, неизменен.
  • Наблюдаемость: счёт finalized = числу смен поколений; появление force-free — дефект.
  • Таймеры: привязаны к создавшему поколению; устаревают со сменой; периодические задачи пересоздаются в новом Init.

Частые ошибки

Долгая блокировка в ServiceUnit
Симптом
После нескольких смен копится очередь GC; лог finalize отстаёт от смен.
Причина
Поток reaper исполняет Unit-ы поколений последовательно — один Unit, застрявший на 30 секунд, ставит в очередь всех остальных. Очистка должна быть быстрой (закрыть дескрипторы, сбросить буферы); долгую работу — в фоновый поток под самоуправление.
Запаздывание finalize принято за утечку
Симптом
finalized появляется через несколько минут после смены; подозрение на утечку памяти.
Причина
Длинные соединения (WS/TAKEOVER) держат lease старого поколения — finalize ждёт их конца (таблица lease гл. 29). Сначала соединения, потом утечки.

Упражнения

БазовыйПодсчёт finalize

Подряд смените поколение 5 раз; посчитайте строки finalized и сверьте с числом смен.

Приёмка: соответствие 5:5, без force-free.
ПродвинутыйТрёхцикловая приёмка

Повторите эксперимент 30.3: три цикла смен под нагрузкой + кривая замеров RSS.

Приёмка: ноль отказов запросов; RSS устойчиво в ±5% от досменного уровня.
ВызовДоказательство устаревания таймеров

Старое поколение вешает периодическую задачу на 10 с (печать маркера поколения); на 3-й секунде смена — пронаблюдайте остановку старой задачи и продолжение после пересоздания новым.

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

Итоги

ТезисВывод
Эволюция стратегииранняя полная утечка → нуль ссылок + сигнал безопасности + отложенное освобождение
Порядок деструкцииServiceUnit → TCC → драйвер → конфигурация (фиксирован)
Наблюдаемостьсверка счёта finalized; force-free = сигнал дефекта
Таймерыпривязка к создавшему поколению, устаревание со сменой, пересоздание в Init
Что дальше по xrt