31 мая 2025 г.·7 мин чтения

Очистка базы данных без влияния на живой трафик

Научитесь запускать задачи очистки базы данных малыми пакетами, с коротким временем блокировок и безопасными окнами, чтобы пользователи не замечали замедлений.

Очистка базы данных без влияния на живой трафик

Почему задания по очистке замедляют живой трафик

Запрос на очистку может выглядеть безобидно и всё равно создать хаос в продакшене. Проблема редко в самой идее удаления старых строк. Проблема в том, что делают слишком много за раз, пока реальные пользователи читают и записывают в ту же базу.

Большие операторы DELETE или UPDATE держат блокировки дольше, чем многие команды ожидают. Пока эти блокировки висят, обычные запросы ждут. Пара ожидающих запросов быстро превращаются в очередь, и задержки растут.

Запрос может также конкурировать с живым трафиком ещё до того, как блокировки станут очевидны. Если база не может найти целевые строки по узкому индексу, она просканирует большую часть таблицы. Этот скан жрёт CPU, память, диск и кэш, которые вашему приложению тоже нужны для обычных чтений и записей.

Пиковые часы всё ухудшают. Задача, которая кажется нормальной в 2:00 ночи, может поднять время ответа за секунды в 14:00. Загруженные системы имеют мало запасов, поэтому даже умеренная очистка может подтолкнуть их к замедлениям.

Один слишком большой пакет часто достаточно, чтобы вызвать видимые проблемы. Удаление 50 000 или 100 000 строк за раз может создать длинные транзакции, большие всплески записи и дополнительное давление на репликацию. Реплики отстают, чтение видит устаревшие данные, и некоторые запросы начинают таймаутиться.

Простой пример: приложение удаляет старые сессии одним огромным запросом, пока пользователи логинятся. Задача трогает ту же таблицу, что и активные запросы. Пользователям всё равно, что это "обслуживание" — они видят медленный экран логина.

Именно поэтому у задач очистки должны быть жёсткие лимиты. Без ограничений по времени блокировок, размеру сканов и окнам выполнения — обслуживание перестаёт быть фоновым и начинает конкурировать с клиентами.

Как выглядит незаметное обслуживание

Лучшие задания по очистке — те, которые никто не замечает. Клиент открывает страницу, обновляет заказ или сохраняет форму, и приложение чувствуется нормально. Время ответа остаётся стабильным. Записи проходят. Саппорт не получает странных жалоб на зависшие страницы или случайные таймауты.

Обычно это происходит, когда задача работает малыми, быстрыми всплесками. Она выбирает ограниченное количество строк, удаляет или обновляет их, подтверждает транзакцию и уходит. Поскольку каждый пакет завершается быстро, блокировки не сидят долго на горячих таблицах. Другие сессии могут продолжать читать и писать без выстраивания за обслуживанием.

Хорошая очистка также умеет отступать. Если задержки запросов начинают расти, ожидания блокировок увеличиваются или база становится загруженнее обычного, задача замедляется или приостанавливается. Это важнее, чем сырая скорость. Закончить на час позже обычно нормально. Сделать продукт медленным на десять минут — нет.

Важно аккуратно останавливаться. Вы должны уметь поставить задачу на паузу в любой момент и оставить базу в чистом состоянии. Это значит: никаких гигантских транзакций, держащих работу открытой навсегда, и никаких наполовину выполненных проходов, требующих ручного ремонта. Следующий запуск должен стартовать с понятной контрольной точки.

Подумайте о занятом приложении в рабочее время. Люди добавляют записи каждые несколько секунд, пока задача в фоне удаляет старые логи. Если пользователи не замечают разницы, задача справляется. Если сохранение страниц начинает тормозить, обслуживание слишком громкое.

Незаметное обслуживание — не магия. Это сдержанность: короткие пакеты, короткие блокировки, быстрые коммиты и задача, которая знает, что живой трафик важнее.

Выбирайте строки прежде, чем трогать их

Задача очистки должна знать свои цели до первого удаления. «Старые данные» — слишком расплывчато. Используйте правило, которое можно протестировать, например «сессии с истёкшим сроком более 30 дней» или «логи почты, уже архивированные и старше 90 дней». Если вы архивируете перед удалением, используйте одно и то же правило в обоих шагах, чтобы не переместить один набор строк и не удалить другой.

Используйте фильтры по индексированным столбцам, когда это возможно. Даты, поля статуса, tenant ID и первичные ключи — стандартные варианты. Если базе придётся сканировать всю таблицу, чтобы найти кандидатов, очистка уже давит на живой трафик. Проверьте индекс сначала, затем выполните count с тем же фильтром.

Обрабатывайте строки в стабильном порядке. id и created_at обычно самые надёжные, потому что они предсказуемы между пакетами. Это упрощает паузу и возобновление, а каждый пакет остаётся близко к предыдущему. Случайный порядок — плохая привычка: он может втянуть активные строки в очистку и заставить пользовательские запросы ждать те же страницы или блокировки.

Перед первым реальным запуском посчитайте, сколько строк соответствует правилу. Это число меняет план. 10 000 строк можно спокойно убрать за один вечер. 12 миллионов требуют более медленного графика и жёсткого контроля. Первый подсчёт также покажет, не слишком ли широк ваш фильтр.

Ещё одна полезная проверка: сделайте сухой прогон, который выбирает, но не удаляет первый небольшой пакет. Посмотрите первые и последние id, время выполнения и вернёт ли второй прогон следующие ожидаемые строки. Если превью выглядит плохо, исправьте селектор до вмешательства в продовые данные.

Установите размер пакета, который остаётся незаметным

Хороший размер пакета — тот, который никто не замечает. Если пользователи замечают паузу, видят медленные страницы или дольше ждут записи — пакет слишком большой.

Начните меньше, чем думаете. Во многих системах это значит тестировать 25, 50 или 100 строк, а не 5 000. Запускайте задачу при обычном трафике и измеряйте каждый пакет. Один быстрый тест на тихой базе почти ничего не говорит.

Наблюдайте за самим пакетом, но и за приложением. Проверяйте задержки запросов, ожидания блокировок, CPU, диск и время выполнения запросов во время работы. Пакет может завершиться за 40 мс и всё равно поднять время загрузки страниц. Если пользователи чувствуют изменение, предел уже слишком высок.

Увеличивайте размер малыми шагами. Удвоение работает сначала, если показатели остаются плоскими, затем лучше делать меньшие прыжки. Путь 50 → 100 → 200 → 300 обычно лучше, чем скачок сразу до 1 000. Как только задержки начинают расти заметно для пользователей, вернитесь к последнему тихому числу и оставьте его.

Разные задачи требуют разных лимитов. Удаления обычно требуют самых маленьких пакетов, потому что они создают больше churn и могут держать блокировки дольше. Обновления требуют осторожности, особенно если трогают индексированные столбцы. Бэкафилы иногда можно выполнять большими пакетами, если они записывают новые данные и не воюют с горячими строками.

Занятое приложение может спокойно отрабатывать 200-строчные бэкафилы весь день, и одновременно страдать от 50-строчных удалений в той же таблице. Это нормально. Относитесь к каждой задаче как к отдельному тесту, а не как к правилу, которое можно копировать везде.

Простой здравый смысл: после каждого изменения спрашивайте себя, заметит ли это пользователь. Если да — пакет слишком амбициозен.

Останавливайте, когда время блокировки растёт

Переосмыслить ретеншн и очистку
Пересмотрите правила хранения данных, прежде чем старые данные станут проблемой в продакшене.

Задача очистки должна быстро сдаваться, когда сталкивается с живой работой. Если один пакет ждет строку, которую уже использует пользовательская операция, безопасный ход — остановить этот пакет. Ожидание звучит безобидно, но пара зависших запросов может накапливаться и превратить тихую задачу в проблему для пользователей.

Установите короткий лимит ожидания блокировки для каждого пакета. Держите его настолько низким, чтобы задача растворялась в обычном трафике — часто дробь секунды или пара секунд максимум. Точное число зависит от приложения, но правило простое: если пакет не берет блокировку быстро, должен уступить дорогу.

Это меняет поведение задания к лучшему. Оно отменяет маленький пакет, вместо того чтобы блокировать активные запросы. Оставляет занятые строки в покое и возвращается позже. Также это даёт полезные логи, когда система действительно загружена.

Записывайте каждое превышение времени блокировки или раннюю остановку. Считайте, сколько пакетов завершились раньше времени, сколько они работали и какая таблица или запрос вызвали проблемы. Через день-два обычно проявляется закономерность. Возможно, задача спокойна ночью и шумит в 9:00. Возможно, одна таблица горячая весь день.

Не пытайтесь сразу ретраить тот же неудавшийся пакет. Это часто создаёт цикл, где задача снова натыкается на тот же пользовательский трафик. Подождите минуту или до следующего запланированного запуска, затем попробуйте снова с тем же маленьким размером пакета.

Очистка остаётся в стороне, когда она относится к давлению блокировок как к стоп-сигналу. Если время блокировки растёт — отступайте и пробуйте позже.

Используйте окно времени, которое соответствует реальному трафику

Трафик редко тих настолько, что просто потому, что часы показывают полночь. У некоторых приложений есть второй всплеск из другого часового пояса. Другие спокойны ночами, но вспыхивают в конце месяца или после маркетинговой рассылки. Выбирайте самые тихие часы по реальным данным, а не по привычке.

Дайте задаче фиксированное время старта и остановки. Это важнее, чем ожидают многие. Очистка с 1:00 до 3:30 каждый день предсказуема, её легко наблюдать и остановить. Если она не завершилась, пусть продолжит на следующем запуске. Очистка лучше работает как фоновый шум.

Установите простое правило для преждевременной паузы. Если растёт задержка запросов, увеличивается число активных соединений или выстраивается очередь, остановитесь до того, как пользователи заметят. Подождать 15–20 минут обычно недорого. Допускать заход задачи в пик — дорого.

Команды также попадают в проблемы, назначая очистку в то же окно, что и другие тяжёлые задачи. База не заботится о том, что каждая задача выглядела безопасной отдельно. Бэкапы могут заполнить I/O и хранилище. Отчёты сканируют огромные таблицы. Импорты добавляют запись. Синхронизаторы держат блокировки дольше, чем ожидали. Ребилд индексов и vacuum могут конкурировать за те же ресурсы.

Хорошее окно оставляет пространство вокруг таких задач, а не перекрывается с ними. Даже 30 минут разрыва помогают.

Если у вас глобальный продукт, используйте более одного окна. Короткий запуск в самое тихое американское время и ещё один в самое тихое европейское время часто мягче, чем один длинный. Короткие окна дисциплинируют. Они держат задачу скучной, а скутая очистка — самая безопасная.

Постройте задачу шаг за шагом

Начните с самого безопасного: селектора. Напишите запрос, который находит строки, подлежащие очистке, затем выполните его как обычное чтение. Проверьте count, просмотрите несколько примерных строк и убедитесь, что он трогает только те старые данные, которые вы действительно хотите удалить.

Далее выполните один очень маленький delete или архивный пакет. Думайте о 50–100 строках, а не о 50 000. Во время выполнения следите за задержками приложения, количеством ошибок и временем запросов. Если пользователи ничего не почувствовали, можно двигаться дальше.

Простой порядок разработки работает хорошо. Проверьте селектор read-only. Запустите один крошечный пакет в проде и наблюдайте за приложением. Добавьте короткий sleep между пакетами, даже секунду-две. Сохраняйте контрольную точку после каждого успешного пакета. Дайте команде простой переключатель паузы/возобновления.

Эта короткая пауза важнее, чем многие думают. Очистка чаще становится заметной, когда пакеты идут подряд без перерыва. Небольшая пауза даёт нормальному трафику шанс пройти и успокоить систему.

Всегда сохраняйте прогресс после каждого пакета. Самая простая контрольная точка — последний обработанный ID или отметка времени. Если задача остановится наполовину, вы не захотите снова сканировать те же строки или гадать, где она остановилась.

Сделайте паузу и возобновление простыми. Храните флаг остановки в таблице, конфиге или панели управления задачами, который команда может менять без деплоя. Если саппорт видит рост задержек, они должны уметь поставить задачу на паузу за секунды.

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

Простой пример из занятого приложения

Снизить риск отставания реплик
Ужесточите темпирование пакетов и схему коммитов до того, как лаг реплик затронет пользователей.

SaaS-продукт хранит старые строки сессий слишком долго. Со временем таблица сессий становится тяжёлой, но настоящая проблема — трафик. Пользователи логинятся, обновляют сессии и выходят весь день, так что таблица никогда по-настоящему не отдыхает.

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

Обычный прогон выглядит так:

  • Выбрать 500 старейших истёкших строк сессий.
  • Удалить только эти строки.
  • Немедленно закоммитить.
  • Заснуть на несколько секунд.
  • Запустить следующий пакет только если база остаётся спокойной.

Это и есть хорошая очистка на практике. Она остаётся скучной. Если один пакет занимает 60–100 мс, пользователи обычно не замечают. Если больший пакет поднимает ожидания блокировок до 800 мс и выше, люди начинают чувствовать это при логине.

Команда также следит за кривой трафика, а не только за базой. Утренние логины, обеденные паузы и вечерние всплески меняют безопасный размер пакета. Пакет, который в порядке в 2:00 ночи, может быть слишком большим в 9:05 утра.

Поэтому у задачи есть два правила остановки. Она приостанавливается, когда время ожидания блокировок превышает малый лимит, и приостанавливается, когда трафик логинов превышает нормальную базу. Затем она пробует снова позже в своём окне обслуживания.

Это медленнее, и да — медленнее. Но медленно часто правильный выбор. Удалить 2 миллиона старых строк за несколько ночей лучше, чем заморозить занятую таблицу на 20 секунд однажды. В живом приложении незаметная работа побеждает.

Ошибки, которые делают очистку заметной

Большинство проблем с очисткой начинается с одной плохой догадки: приложение выдержит большой запрос обслуживания при активных пользователях. Обычно нет.

Самая громкая ошибка — удалить слишком много в одной транзакции. Один DELETE на миллионы строк может держать блокировки слишком долго, быстро разрастить лог транзакций и оставить гору "мертвых" строк до коммита. Пользователи могут не видеть сам delete, но почувствуют медленные записи, зависшие запросы или случайные таймауты.

Ещё одна частая ошибка происходит ещё до удаления. Если задача выбирает строки сортировкой или фильтром по неиндексированным столбцам, базе придётся просканировать гораздо больше данных, чем ожидалось. Эта дополнительная работа жрёт CPU и I/O, и живой трафик платит цену.

Команды также делают очистку заметной, запуская несколько воркеров одновременно. Звучит быстрее, но параллельная очистка часто превращает одну тихую задачу в конфликт за таблицы и индексы. Воркеры конкурируют за одни и те же страницы, растут ожидания блокировок и иногда вынуждают повторные попытки. Один стабильный воркер часто менее разрушителен, чем четыре шумных.

Последняя ловушка — смотреть только на primary и игнорировать побочные эффекты. Очистка может выглядеть нормально на главном узле, в то время как реплики отстают, логи быстро растут или vacuum не успевает. Это всё равно влияние на пользователей, просто запоздалое.

Несколько предупредительных сигналов проявляются рано:

  • Отставание реплик растёт и не восстанавливается быстро.
  • Объём транзакций или binlog резко увеличивается.
  • Vacuum/autovacuum остаётся позади.
  • Ожидания блокировок растут во время окна очистки.

Пример с удалением старых сессий прост. Если команда запускает три воркера, каждый из которых удаляет по 100 000 строк за проход, приложение может продержаться пару минут и затем начать шуметь. Меньшие пакеты, меньше воркеров и запросы, использующие правильный индекс, держат очистку тихой — а это и есть цель.

Быстрые проверки перед запуском

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

Короткий тест ловит большинство неприятных сюрпризов. Цель проста: приложение продолжает обслуживать пользователей, и почти никто не замечает работу.

Запустите селектор отдельно и посмотрите небольшой сэмпл строк. Проверьте даты, флаги статуса, фильтры по tenant и порядок сортировки. Если несколько строк выглядят неверно — остановитесь.

Замерьте один реальный пакет при условиях, как в проде. Он должен завершаться значительно быстрее вашего лимита ожидания блокировки, а не едва укладываться. Если лимит 200 мс, пакет на 180 мс уже слишком близко.

Наблюдайте дашборды во время теста. Задержки API, медленные запросы, ожидания блокировок и ошибки должны оставаться ровными. Маленький всплеск в одном пакете может превратиться в видимую боль при длительном запуске.

Решите, кто может мгновенно приостановить задачу. Назначьте одного владельца и одного резервного. Общий чат без ясной ответственности теряет время, когда трафик прыгает.

Опишите план остановки простым языком. Запишите, как выключить планировщик, как найти последний обработанный ID или отметку времени и что проверить после остановки.

Письменный план важнее, чем многие думают. Как только задача начинает удалять малыми пакетами, люди расслабляются и считают её безопасной. Именно тогда плохой фильтр, отсутствующий индекс или обеденный всплеск трафика проскальзывают.

Практичное правило: не начинайте, пока не проверили селектор, выборку строк, время пакета, живые задержки, владельцев и шаги остановки. Пять аккуратных минут сейчас могут сэкономить часы охоты за блокировками позже.

Что делать, если задача всё ещё конфликтует с трафиком

Если одна и та же таблица всё время блокирует пользовательские записи, задача трогает данные, которые остаются слишком горячими. Меньшие пакеты помогают ненадолго, но не решат проблему таблицы, где старые и активные строки живут рядом и делят горячие индексы.

Более чистое решение — разделить холодные данные и живые. Сначала переместите старые строки в таблицу-архив или старые партиции, затем удаляйте уже из архива по своему расписанию. Это снижает число столкновений, потому что самые активные записи остаются на меньшем, новом срезе данных.

Прямые удаления часто не подходят, когда трафик высок весь день. В таком случае помечайте строки для архива, копируйте их, проверяйте совпадения по количеству и удаляйте позже в более спокойное время. Это добавляет шаг, но обычно менее болезненно, чем постоянная борьба за блокировки в проде.

Если очистка всё ещё шумит, пересмотрите основы. Проверьте правила хранения: команды часто удаляют данные раньше, чем нужно, или хранят слишком много, потому что политика не обновлялась. Посмотрите индексы для фильтра удаления и порядка сортировки. Отсутствующий или раздутый индекс может превратить маленький delete в долгий скан. Сравните время работы задачи с реальным трафиком, потому что пиковые периоды со временем смещаются. При необходимости разбейте одну большую задачу на несколько по tenant, диапазону дат или статусу, чтобы один проблемный срез не тормозил всё.

На этом этапе проблема часто перестаёт быть скриптовой и становится архитектурной. Свежий взгляд может сэкономить много проб и ошибок.

Если вашей команде всё время не удаётся пройти этот рубеж, Oleg Sotnikov at oleg.is может помочь пересмотреть план ретеншна, лимиты пакетов и блокировок, порядок выката и компромиссы инфраструктуры. Внешний обзор часто быстрее, чем ещё неделя правок одной и той же неработающей задачи.

Часто задаваемые вопросы

С какого размера пакета начать?

Начинайте очень аккуратно, обычно с 25–100 строк. Запускайте задачу при обычном пользовательском трафике, а не на пустой базе, и следите за задержками запросов, ожиданиями блокировок и временем выполнения запросов. Если что-то меняется — уменьшите размер пакета, затем повышайте его малыми шагами, пока задача остаётся незаметной.

Как понять, заметят ли пользователи задачу очистки?

Смотрите на приложение, а не только на базу данных. Медианные логины, замедленные сохранения, рост ожиданий блокировок, увеличение числа ошибок и отставание реплик — всё это признаки того, что задача слишком шумная. В этом случае приостановите задачу, уменьшите размер пакета или добавьте паузу между пакетами.

Есть ли когда-то смысл в одном большом ночном удалении?

Как правило — нет. Один большой ночной delete часто держит блокировки слишком долго, создаёт всплески записи и отставание реплик. Малые пакеты с быстрыми коммитами занимают больше времени в целом, но позволяют приложению оставаться работоспособным во время очистки.

Какой тайм-аут ожидания блокировки использовать?

Используйте короткий лимит, чтобы задача быстро уходила с дороги — часто значительно меньше секунды или до пары секунд. Если пакет не может получить блокировки быстро, отменяйте его, фиксируйте факт и пытаетесь позже. Длительное ожидание обычно превращает обслуживание в проблему для пользователей.

Как выбрать строки, не сканируя всю таблицу?

Выберите правило, которое можно проверить, например: сессии с истёкшим сроком старше 30 дней. Фильтруйте по индексированным столбцам и обрабатывайте строки в стабильном порядке, например по id или created_at. Перед удалением выполните предварительный выбор (read-only) и подтвердите, что выборка выглядит правильно.

Стоит ли запускать несколько воркеров?

Начинайте с одного рабочего процесса. Параллельная очистка часто приводит к борьбе за одни и те же страницы таблицы и индексы, что увеличивает ожидания блокировок и число повторных попыток. Добавляйте воркеры только после того, как один воркер работает тихо под реальной нагрузкой.

Какую контрольную точку должен сохранять процесс?

Сохраняйте контрольную точку после каждого успешного пакета — обычно это последний обработанный id или отметка времени. Держите простой флаг паузы, который команда может изменить без деплоя. Это позволит быстро остановить и возобновить работу без догадок о месте остановки.

Почему очистка работает в 2:00, но ломает систему в 14:00?

Потому что безопасные лимиты меняются в зависимости от реального трафика. Пакет, который спокойно проходит ночью, будет конкурировать с логинами, записями и кэшем в пиковые часы. Используйте короткие окна обслуживания на основе реального использования и разрешайте задаче приостанавливаться при росте трафика.

Что делать, если даже крошечные пакеты цепляются за горячие строки?

Возможно, старые и активные строки слишком тесно перемешаны. В таком случае переместите холодные данные в таблицу архива или в старые партиции, а затем удаляйте из архива по отдельному расписанию. Это снижает число столкновений гораздо эффективнее, чем бесконечная доводка параметров на живой таблице.

Что протестировать перед первым реальным запуском?

Проверьте селектор, посмотрите несколько примеров строк, замерьте один реальный пакет и убедитесь, что во время его выполнения задержки не растут. Также назначьте одного владельца и одного дублёра, которые умеют мгновенно приостановить планировщик и найти последнюю контрольную точку. Короткий тест обычно ловит большинство неожиданных проблем.