Как удаление префиксов оставило NetBox без свободных воркеров
Коллега удалял через API дубликаты префиксов в NetBox. Операция выглядела безобидно: один запрос — один объект. Но примерно с 12:30 интерфейс перестал отвечать, а балансировщик почти 40 минут возвращал 502. При этом обе ноды приложения и PostgreSQL были живы.
Сначала я решил, что удаления идут последовательно, а тяжёлые запросы Django блокируют всю таблицу префиксов. Я выгрузил лог PostgreSQL с запросами дольше пяти секунд и попросил Claude Code помочь разобраться. Агент поправил меня сразу в двух важных местах: запросы шли параллельно, а конкурировали они за блокировки строк. Когда мы сопоставили лог с данными NetBox и логами баласировщика, картина сложилась.
Ниже — разбор инцидента и того, какую часть расследования удалось поручить агенту. Адреса, имена хостов и учётных записей заменены; время и количественные данные сохранены.
О подготовке статьи. При работе над текстом я использовал LLM. Итоговые выводы и ответственность за публикацию — мои. Если заметите неточность, прошу отнестись с пониманием и написать мне — исправлю.
Что происходило
NetBox работает на двух нодах, по 16 воркеров gunicorn на каждой. Примерно с 12:30 до 13:08 через HAProxy и nginx клиентам отдавались 502. В это время в PostgreSQL висели долгие запросы к ipam_prefix, а новые запросы к приложению некому было обслуживать.
Началось всё раньше. Первое удаление дубликатов попало в лог в 12:09:07, а уже в 12:09:30 PostgreSQL зафиксировал первую взаимоблокировку при обновлении _children. Между 12:10 и 12:19 таких случаев было 39, хотя приложение ещё отвечало. После очередной серии удалений около 12:30 число одновременных долгих запросов поднялось до 26–32 и держалось на этом уровне почти до 13:08.
Совпадение с числом воркеров выглядит убедительно, но само по себе не доказывает, что это был именно их потолок. Практический результат был виден и без них — запросы застряли, а пользователи получили 502.
Почему удаление одной записи оказалось дорогим
В таблице было много точных копий префиксов без VRF.
NetBox хранит для префикса денормализованные поля _depth и _children. После удаления обработчик post_delete пересчитывает иерархию в той же транзакции. Один запрос обновляет счётчики у содержащих удалённый префикс записей, другой — глубину у вложенных. В SQL используются операторы включения префиксов >>= и <<=, причём равные префиксы тоже попадают в выборку.
Вот где дубликаты становятся проблемой. При удалении префикса внутри вышестоящего префикса пересчёт затрагивает тысячи строк, включая копии этого же и общих предков. После выборки со счётчиками NetBox обновляет строки пачками по 100. Захваченные при обновлении блокировки удерживаются до завершения транзакции.
Одновременно началось несколько таких удалений. Транзакции пытались обновить пересекающиеся наборы строк, ждали друг друга и периодически попадали во взаимоблокировку. В логе за день набралось 84 записи о deadlock.
Это была не одна «тяжёлая команда», после которой всё сломалось. Множество запросов одновременно держали воркеры, ожидая возможности закончить транзакции.
Откуда взялись копии
Такое количество копий префиксов создала одна из наших интеграций, которая занимается инвентаризацией объектов различных систем, которые не имеют возможности поддерживать наш IPAM в актуальном состоянии. Ошибка в коде интеграции породила 54 тыс. дубликатов.
Как избавились от дубликатов
Для следующей очистки мы с агентом подготовили скрипт на Django ORM: он удаляет копии без запуска обработчика пересчёта на каждом удалении, а затем один раз перестраивает иерархию через manage.py rebuild_prefixes. Скрипт отключил как обработчик пересчета так и сигнал ORM для отправки вебхуков в сторону шины данных об удалении префиксов - получить 54 тыс.сообщений в Kafka при отстутвии (пока еще) потребителей этих сообщений мне не улыбалось. Такие работы позволили удалить 54к префикса без какой-либо значительной нагрузки на приложение или его БД.
Где в этой истории помог агент
У агента был доступ на чтение к локальному логу PostgreSQL и API NetBox. Права писать в прод я ему не давал. Я передал агенту окно деградации, устройство приложения, свою первоначальную гипотезу и 27,5 МБ лога с медленными запросами. От запроса до черновика разбора прошло около 20 минут.
Самой полезной оказалась не скорость написания текста, а то что агент связал три источника, которые мне пришлось бы сводить вручную: длительности и взаимоблокировки в PostgreSQL, повторные удаления и историю создания объектов в NetBox, обработчики post_delete в коде. Он построил поминутную картину запросов, посчитал дубликаты и показал, где именно моя версия не сходится с данными.
При этом он не стал уверенно отвечать там, где данных не хватало. По логу нельзя установить значение lock_timeout или подтвердить настройки gunicorn. Эти границы важны: при расследовании инцидента красивая причинно-следственная цепочка легко начинает выглядеть как доказанный факт.
За мной оставались сбор исходных данных, решения во время инцидента, проверка выводов агента и выбор способа очистки. Агент оказался полезен как второй расследователь, который быстро разбирает большой лог и спорит с первоначальной гипотезой. Его выводы всё равно приходится сверять с продовой системой.