Мы удалили 97,5 % боевой базы 1С - и ни одна цифра в отчётах не изменилась
Иван Недомолков · Chop etilgan: · Yangilangan:
Одна база 1С на 4,2 терабайта. Бэкап по ночам весит 556 ГБ и идёт два часа. Диски забиты почти под завязку. Кассир ждёт список чеков 25 секунд, а к вечеру, когда закрываются смены, всё “залипает”. Узнаёте? Разбираться с этим выпало нам. Мы удалили 97,5 % объёма базы, не потеряли ни одного документа и ни на день не остановили торговлю.
Дальше будет расследование: три красивые гипотезы, которые развалились, ночь Х, ошибка 3140 в самый неудачный момент и неожиданный виновник, который съел 3,85 терабайта из 4,2. Забегая вперёд: история продаж и версии документов тут ни при чём.
Дисклеймер. В проекте применялись операции уровня СУБД. Это осознанный инженерный выбор для объёмов, на которых штатные механизмы физически не укладываются в окно простоя, - и он требует дисциплины: доказательство “мёртвости” данных до любого удаления, полные репетиции на копии, побитовые сверки “до/после”, точки отката и приёмка на стороне заказчика. Не повторяйте на продуктиве без этого контура и без согласия владельца системы: типовые задачи решаются типовыми средствами.
Завязка: пациент и симптомы
Клиент - розничная сеть fashion-сегмента, около 200 касс в трёх странах. Боевых баз 1С у сети 11, это Розница и ERP. Основная весила 4,2 ТБ и толстела быстрее, чем рос сам бизнес. Когда к нам обратились, симптомы были такие:
- Диски. Файл данных разросся до 4 197 ГБ, свободное место почти кончилось. Дальше только покупать СХД.
- Бэкапы. Полный бэкап ночью шёл около 2 часов и даже сжатым весил 556 ГБ. Восстановить тестовый контур было целым событием, его планировали заранее.
- Кассы. Под кассиром список чеков грузился ~25 секунд, а закрытие смен в вечерний пик подвисало.
- Соседи. Один инстанс SQL-сервера держит десятки баз, и процессорные ядра, память, диск и tempdb у них общие.
И всё это на работающей рознице, в боевом контуре. Остановить торговлю или потерять данные нам бы никто не позволил. С правами доступа ещё строже: именно механизм разграничения доступа показывает кассиру его магазин и прячет от него чужой.
Диагностика: две недели на карту “что сколько занимает”
Первым делом мы ничего не трогали. Опыт подсказывает, что при жалобе “база огромная и всё тормозит” дороже всего стоит верный диагноз, до решения потом дойти проще. Поэтому две недели ушли на карту: привязать каждую из 3 585 SQL-таблиц к объекту метаданных 1С и посчитать, что сколько места занимает.
Подозреваемые у всех были привычные: документы за десять лет, разросшиеся версии объектов, логи. Реальная картина оказалась любопытнее:
| Что | Объём |
|---|---|
| История версий шаблонов ограничения доступа | 3,34 ТБ |
| Очередь “пересчитать ключи доступа пользователей” (1,18 млрд строк) | 287 ГБ |
| Очередь “пересчитать ключи доступа к данным” (1,19 млрд строк) | 221 ГБ |
| Итого: служебный кэш механизма разграничения доступа | ≈ 3,85 ТБ из 4,2 ТБ |
Вчитайтесь в эту таблицу. Свыше 90 % базы в четыре терабайта к данным бизнеса отношения не имеет. Это служебный кэш, который копит универсальный механизм ограничения доступа к записям (в терминах БСП это RLS). Рос он годами и ни дня не работал по назначению: регламентные задания, пересчитывающие ключи доступа, отключили ещё в 2020 году. С тех пор механизм добросовестно ставил в очередь задания “пересчитать”, забирать их было некому, и так же исправно копил версии шаблонов ограничений, которых никто не читал.
Выходит, раздуло базу вовсе не документами. Годами в ней копился ТОДО-лист, который никто и не думал выполнять, и строк в нём набралось 2,37 миллиарда.
Небольшое отступление: откуда вообще берётся такой кэш
Короткая справка для тех, кто не из мира 1С (кто знает БСП, может этот абзац пропустить). Типовые конфигурации на “Библиотеке стандартных подсистем” умеют ограничивать доступ к записям двумя способами. В классическом условие дописывается в запрос каждого списка: хранить почти нечего, зато за вычисление условия платит каждое чтение. В универсальном система заранее считает “ключи доступа”, то есть материализует для каждой пары пользователя и объекта ответ “видит или не видит”, и при чтении остаётся соединиться с таблицей, где ключи уже лежат готовыми.
Если присмотреться, универсальный механизм и есть огромный материализованный кэш прав. Любой кэш приходится инвалидировать, и этот не исключение. Поменялись роли, профили или состав пользователей, и в очередь ложатся задания на пересчёт ключей; поменялся шаблон ограничения, и сохраняется его новая версия. Пока работают регламентные задания пересчёта, очереди разбираются и всё в равновесии. Представьте, что задания однажды выключили, например из-за того, что пересчёт нагружал сервер. Механизм от этого не встаёт: он по-прежнему ставит задачи в очередь, откуда их уже никто не забирает, и складывает версии шаблонов, которые уже никто не читает. Ни ошибок, ни предупреждений, идеальное преступление. Шесть лет спустя в базе две очереди, в каждой по миллиарду строк, и история версий на 3,34 ТБ.
Глава, которую мы любим больше всего: отброшенные гипотезы
Отчёт об удачном проекте обычно показывает только то, что сработало, и у читателя складывается впечатление, будто к результату шли по прямой. У нас по прямой не вышло. Три правдоподобные гипотезы мы проверили и отбросили, а на любую из них можно было впустую потратить неделю. Рассказываем о них потому, что вам они, скорее всего, тоже придут в голову.
Гипотеза 1. “RLS отключён - значит, всё это мусор, сносим целиком”
Задания пересчёта стоят выключенными с 2020 года, и очень хочется объявить мёртвым весь механизм: кэш снести, ограничения отключить, разойтись по домам. Проверили роли, и вышло обратное: ограничения доступа по условию реально используют 208 ролей. Механизмом пользуются каждый день, он живой, свалкой стала только его кэширующая часть. Снеси мы всё подчистую, и чужие магазины стали бы видны кассирам.
Гипотеза 2. “Мусор накопили уволенные сотрудники - почистим по ним”
Версия выглядела убедительно. Карточек в справочнике пользователей набралось 5 600, а работают из них около 150, так что массу кэша, казалось бы, должны давать ключи доступа уволенных. Мы посчитали. Живых ключей доступа нашлось 660 строк на фоне миллиардов. Чистка по уволенным дала бы десятки килобайт. Килобайт, не гигабайт.
Гипотеза 3. “Виноват вот этот конкретный медленный регистр”
Один регистр в метриках SQL-сервера упорно выдавал чудовищные чтения, лучшего подозреваемого не придумать. Разобрались подробнее, и оказалось, что регистр… живёт в соседней базе на том же инстансе. Правило, выученное на своей шкуре: если сервер общий, при разборе DMV всегда фильтруйте по database_id. Без этого фильтра можно неделю оптимизировать чужую базу.
Решение: восемь этапов вместо одного “большого выключателя”
Главная развилка проекта была такая. Пока список чеков строится через универсальный механизм, удалять кэш нельзя: RLS в универсальном варианте станет считать права “на лету”, и тогда кассы лягут окончательно. Поэтому первым шагом база уходит с универсального разграничения доступа на классическое, где отбор по магазину стоит прямо в тексте запросов динамических списков. Только тогда трёхтерабайтный кэш по-настоящему умирает и его можно безопасно удалять. Заодно ускоряются списки: каскад служебных соединений за ними больше не тянется.
Дальше - по этапам, каждый со своим замером “до/после”:
- Анализ и тестовая копия прода. Карта объёмов, план на четыре фазы, полный прогон сценария на копии со снятием таймингов каждого шага. Ночное окно на проде должно быть исполнением известного плана, а не экспериментом.
- Чистка справочника пользователей: 5 600 карточек → ~150 активных. С предохранителем: не трогать никого, у кого были транзакции за последние два месяца, - формальный статус “уволен” в справочнике и реальная активность, как выяснилось, живут разными жизнями.
- Вынос лога действий кассира в архивную базу. 14,5 млн записей, −52 ГБ из горячей базы. Архив - отдельная база с веб-сервисами и ретеншном 180 дней. Два принципа, на которых всё держится: из рабочей базы запись удаляется только после подтверждённой записи в архив (никаких “отправили и надеемся”), а штатные формы продолжают показывать историю прозрачно - пользователь вообще не знает, что читает из другой базы. Бонусом лог перестал утяжелять транзакции кассиров: писать в горячем пути десятки гигабайт, которые читают раз в месяц, - сомнительная сделка.
- Индексы под кассовые операции. Открытие списка чеков: было 25 секунд, стало мгновенно. В логических чтениях: 505 410 до и 4 после. Именно четыре чтения, не путать с “в четыре раза меньше”. Разница такого порядка почти всегда значит, что раньше запрос сканировал всю таблицу, а теперь спускается по индексу к нужной странице. Никакой магии, просто годами не хватало индекса.
- Оптимизация шести тяжёлых отчётов: −22…−42 % времени. Находки везде примерно одни и те же: дважды указанный фильтр, из-за которого платформа делает двойное обращение к справочнику на каждую строку; построчные обращения к реквизитам вместо соединения; четыре полных прохода по регистру на 15,7 млн строк там, где хватает одного; “мёртвая” временная таблица, которая строится и не используется. Обязательное условие деплоя - результат “до” и “после” совпадает побайтово: выгрузка нормализуется (сортировка, форматы) и сравнивается по SHA-256, а не “на глаз похоже”. И честная деталь: часть отчётов после разбора осталась без изменений - узкое место было не в запросе, и переписывание не дало бы ничего, кроме строки в отчёте о проделанной работе.
- Аудит вечернего пика. Тут нас ждал второй поворот. Вечерние залипания все дружно списывали на блокировки, а аудит насчитал их в пик ноль. Кассы душил параллелизм, разогнанный заводскими настройками SQL Server (
cost threshold for parallelism = 5,MAXDOP = 0): на сервере с 48 ядрами даже лёгкий запрос растекался по всем ядрам, и в пик две трети запросов попросту ждали в очереди свободных потоков. Лечение:cost threshold5 → 50 иMAXDOP0 → 8, динамически, без перезапуска, откат наготове. - Переход на классический RLS. В 23 документах появился отбор по магазину. По списку чеков с учётом RLS чтения упали так: 162 млн → 94. Рискованнее момента во всём проекте не было: только в правах доступа “почти как раньше” означает аварию. Увидел кассир чужой магазин, и это уже инцидент безопасности. Потерял менеджер свой, и у него встала работа. Так что до переключения мы прогнали матрицу эквивалентности прав, 329 проверок того, кто что видел до и что видит после, на комбинациях ролей и сценариев, и записали результат по каждой строке. Расхождений оказалось 0. Скучнее таблицы в проекте не было, но о часах на её заполнение мы не пожалели ни разу: пусть лучше скучная таблица, чем интересный инцидент.
- Пересчёт итогов регистров. Таблицы итогов −75 %: 73,5 → 18,3 млн строк.
И только после всего этого - финал: ночное окно, в котором кэш умирает.
Ночь Х
К ночи Х всё было отрепетировано на копии, тайминги каждого шага известны заранее, время “точки невозврата” назначено по часам. Отдельным пунктом чек-листа - свежий полный бэкап перед стартом: да, тот самый, двухчасовой на 556 ГБ. В последний раз.
Хронология:
- Удаление мёртвого кэша. Таблицы кэша к этому моменту ни один живой механизм не читает и не пишет: универсальный RLS отключён, списки работают на классических отборах, и это проверено неделями наблюдения, а не предположением. Значит, вместо
DELETEс его построчным журналированием (на 2,37 млрд строк это отдельная вечность плюс журнал транзакций размером с саму базу) можно позволить себеTRUNCATE- операцию, которая деаллоцирует экстенты целиком. Занятое место: 4 108 ГБ → 108 ГБ. За секунды. После двух месяцев подготовки сама “обрезка” выглядит почти оскорбительно буднично. Так и должно быть: если кульминация ночного окна выглядит захватывающе - что-то пошло не так ещё на этапе планирования. - Сжатие файла данных: 2 999,6 ГБ → 300 ГБ за 89 минут одним проходом. Здесь мы сознательно пошли против популярной рекомендации “шринкайте порциями по 1-4 ГБ, чтобы не держать долгую операцию”. На репетициях замерили оба варианта: порционное сжатие на этих объёмах оказалось заметно медленнее - каждый вызов заново оплачивает накладные расходы, а выигрыш в “прерываемости” в согласованном окне не стоит ничего. Не утверждаем, что так будет на любом железе и любой фрагментации: утверждаем, что это надо замерять на своей базе, а не принимать на веру из чек-листов в интернете (включая этот).
- Перестроение 200 фрагментированных индексов: 44 минуты, 0 ошибок. Шринк печально известен тем, что перемешивает страницы и разносит индексы в клочья, - поэтому перестроение после него не “желательно”, а входит в ту же ночь тем же пунктом плана, с заранее замеренным таймингом.
- Обновление статистики, возврат в полный режим восстановления, свежий полный бэкап: на 18,3 ГБ против 556, снят за минуты. Чистая точка отката на утро и новая базовая линия для цепочки журналов.
Утром кассы открылись штатно. Регресса нет, жалоб нет. Пользователи не заметили ни одного переключения за весь проект - и мы считаем это не побочным фактом, а главным критерием качества.
Итоговые цифры:
| Метрика | Было | Стало |
|---|---|---|
| Размер файла базы | 4 197 ГБ | 105 ГБ |
| Занято данными | 4 108 ГБ | ~91 ГБ |
| Полный бэкап (сжатый) | 556 ГБ / ~2 ч | 18,3 ГБ |
| Список чеков под кассиром | ~25 с | мгновенно |
| Логических чтений на список чеков | 505 410 | 4 |
| Чтений на список чеков с RLS | 162 млн | 94 |
| Тяжёлые отчёты (6 шт.) | базовые тайминги | −22…−42 % |
| Веб-выгрузка остатков для сайта | 192 с | 59 с (до 8× на других складах) |
Работу принимал заказчик, своими силами: его сотрудники зашли под ограниченными правами (кассир, менеджер) и прошли чеки, отчёты, остатки и разграничение доступа. Замечаний не было. Приёмка силами исполнителя, по-нашему, оксюморон: кто делал работу, тот и смотрит туда, где ждёт успеха.
Грабли, о которых стоит знать заранее
Без этого раздела статья была бы рекламой. С ним - отчётом.
Ошибка 3140 и залипший флаг шринка. Сжатие терабайтного файла конфликтует с бэкапом: параллельно стартовавший плановый бэкап валил операцию сжатия с ошибкой 3140. Хуже того: после прерванной попытки у базы остаётся “залипший” внутренний флаг незавершённого сжатия, из-за которого следующие операции ведут себя странно. Вылечилось только переводом базы offline/online. Мораль: перед ночным окном отключайте плановые задания бэкапа руками и проверяйте, что они действительно отключены.
Соседи по инстансу. В разгар работ весь сервер начал задыхаться. Виновник - чужой отчёт из соседней базы: 24 параллельных потока, 4,3 часа исполнения, и это на общем инстансе. Пришлось выходить за пределы “своей” зоны ответственности, согласовывать и снимать. Если ваш SQL-сервер общий - закладывайте в план ночных работ и дипломатию тоже.
Запас места после сжатия. После шринка у базы осталось ~14 ГБ свободного пространства при естественном приросте ~0,6 ГБ в сутки. Это осознанное решение, проговорённое с заказчиком (место на дисках было нужно немедленно), а не недосмотр. Но проговорить его - обязательно: молча оставить базе три недели запаса и уйти - верный способ получить аварийный звонок.
Формальные признаки врут. Дважды за проект формальный признак расходился с реальностью: “уволенные” пользователи с живыми транзакциями и “выключенный” RLS с 208 активными ролями. Любое решение об удалении мы принимали по фактическому использованию (транзакции, чтения, ссылки), а не по статусам в справочниках.
Чек-лист для тех, у кого база тоже “весит как чугунный мост”
Конфигурация на БСП, ограничение доступа универсальное, а база подозрительно большая? Тогда начинайте отсюда, пункты идут по возрастанию смелости:
- Постройте карту объёмов. Сопоставьте SQL-таблицы объектам метаданных (обычно через
ПолучитьСтруктуруХраненияБазыДанных()) и отсортируйте по размеру. Сначала диагноз, потом действия. Кэш на 3,85 ТБ мы нашли как раз на этом шаге. Если писать такое самим не хочется, есть наша обработка “Карта объёмов базы 1С”, она проходит сразу по всем объектам. - Загляните в служебные таблицы механизма доступа БСП: там копятся очереди на обновление ключей доступа и версии шаблонов ограничений. Таблицы растут при выключенных регламентных заданиях пересчёта? Похоже, пожиратель дисков у вас найден.
- Кэш не удаляйте, пока не ясно, кто им пользуется. Выключенные задания ≠ мёртвый механизм. Посчитайте, у скольких ролей есть ограничения по условию.
- Инстанс общий? Метрики отбирайте по своей базе. Иначе недолго начать оптимизировать соседей.
- Прежде чем охотиться на блокировки, проверьте параллелизм.
cost threshold for parallelism = 5иMAXDOP = 0- заводская мина: на многоядерном сервере она выглядит как “блокировки в пик”. - Права меняйте только через матрицу эквивалентности. Каждое изменение механизма доступа сверяйте до и после по таблице “роль × сценарий × видимость”. У нас в ней вышло 329 строк, и ни разу не было жаль потраченного времени.
- Отчёты сверяйте контрольной суммой, а не глазами. SHA-256 по нормализованной выгрузке результата стоит копейки, и спорить об “идентичности данных” больше не о чем.
- Репетируйте ночь на копии с таймингами каждого шага. “Точка невозврата” должна быть назначена по часам заранее, а не определяться на месте в три часа ночи.
- На время шринка гасите плановые бэкапы - см. ошибку 3140 выше.
- Замерьте свой вариант сжатия. “Всегда шринкайте порциями” - из тех советов, которые обязаны проверяться на вашем железе.
Вместо эпилога
Цифра −97,5 % здесь, на наш взгляд, не главное. Поучительнее другое: четыре терабайта проблем не появились из-за чьей-то одной ошибки. Механизм доступа был нужен, его и включили. Пересчёт грузил сервер, задания выключили. Рукой на растущую базу махнули, раз “диски пока есть”. Каждый шаг в своё время был разумным, система деградировала постепенно, и “вдруг” оказалось, что бэкап тянется два часа, бизнес пользуется лишь 2,5 % базы, а остальные 97,5 таскает на горбу.
Отсюда скучный, как всё главное, вывод: измеряйте. Две недели на карту “что сколько занимает” окупаются, потому что она отвечает на самый дорогой вопрос из всех: какая часть базы вообще ваши данные.
Когда и ваша база тянет на чугунный мост, начинайте с карты объёмов, первого пункта чек-листа выше. Если по карте окажется, что топ занимают не документы, а служебные таблицы, дальше это наша работа: свертка и обрезка больших баз 1С, от карты объёмов до протокола сверки остатков. Техническая версия этой же ночи - со скриптами карты объёмов, доказательства “мёртвости”, TRUNCATE, шринка и перестроения индексов - вынесена в отдельную статью: “SQL-версия обрезки базы 1С”. Краткая версия этого проекта с таблицей результатов - в кейсе “Обрезка базы 4,2 ТБ → 105 ГБ”, диагностическая часть отдельно разобрана в нашей статье на Инфостарте, а методика свертки соседнего проекта на 2,3 ТБ - в ещё одной.