Шесть ночей подряд ночная загрузка аналитики не привезла ни одной строки. Каждую ночь она падала на одном и том же месте: СУБД бросала исключение о переполнении числовой колонки. Колонка была объявлена как число из пяти знаков, два после запятой, то есть предел у неё 999,99. Внешняя система изредка присылала значение больше тысячи, например 1250,56.
Заметили на шестые сутки. Не по алерту, потому что алертов на ошибки этой загрузки не было вообще. Кто-то пошёл смотреть, почему цифры за неделю выглядят странно, открыл счётчик строк по дням и увидел шесть нулей подряд.
Дальше про то, что мы там разобрали: почему падала вся пачка целиком, при чём тут ширина колонки и почему она вообще не главный виновник, сколько строк оказалось бракованных и какая одна проверка поймала бы всё это в первую же ночь.
Чья это боль
Если у вас есть хоть один регулярный обмен, который забирает данные из чужой системы и складывает их пачкой, боль ваша. Неважно, приезжают данные из внешней торговой площадки, из биллинга, из складского софта подрядчика или из соседней базы: везде одинаковая конструкция. Приёмник объявляет структуру, источник эту структуру не читал и ничего вам не обещал.
Розничная сеть, о которой речь, живёт ровно так: витрина аналитики каждую ночь пополняется данными с внешней площадки. Никакого экзотического стека, обычная ночная заливка по расписанию.
Практический вывод из этого абзаца один: считайте своей эту статью, если у вас есть хоть одна таблица, которую наполняет не ваш код.
Что ломалось внутри
Вставка шла пачкой. Один запрос, много строк, одна транзакция. СУБД дочитывала до строки со значением 1250,56, видела, что в объявленный тип оно не помещается, бросала исключение и откатывала всю операцию.
Здесь и живёт неприятная часть. Плохих строк было мало. Хороших - тысячи. Но результат ночи двоичный: либо приехало всё, либо не приехало ничего. Промежуточного состояния конструкция не предусматривает, потому что изоляции по строкам в ней нет.
Отсюда странный симптом, из-за которого это трудно ловить глазами: в таблице не появляется мусор, не появляются кривые значения, не появляются частичные данные. Просто ничего не меняется. Отчёты продолжают открываться, дашборды рисуются, цифры вчерашние. Тишина выглядит как норма.
Практический вывод: если ваш обмен пишет пачкой в одной транзакции, у него есть режим отказа "ноль строк и никаких следов в данных". Проверьте, отличаете ли вы этот режим от "ночью просто нечего было грузить".
Гипотеза, которую отклонили первой
Первая версия при разборе была бытовая: внешняя система сломалась и присылает мусор. Она удобная, потому что снимает вопрос с нашей стороны и переводит его в письмо поставщику.
Отклонили по арифметике. Мусор выглядит иначе: это ноль, отрицательное значение, пустая строка, число на несколько порядков больше. Здесь пришло 1250,56 при пределе 999,99, то есть превышение на четверть. Так выглядит нормальная работа источника, о которой мы просто не знали. Карточка стояла глубоко в выдаче, площадка честно вернула её позицию, а трёхзначную границу придумали мы сами.
Признак, который я теперь использую как правило: если внешнее значение вылезло за границу в разы, идите к источнику; если вылезло чуть-чуть, идите к своей схеме. Второе означает, что граница была придумана вами и никогда не проверялась.
Практический вывод: прежде чем писать поставщику данных, посмотрите, насколько именно значение вышло за предел. Это две минуты и два разных плана действий.
Почему очевидное решение не закрывает вопрос
Очевидное решение звучит так: колонка узкая, расширьте её. Оно правильное, его надо сделать, и оно не про то.
Расширение типа с пяти знаков до семи поднимает предел с 999,99 до 99 999,99, то есть в сто раз. Для позиции карточки в выдаче это запас навсегда, там четырёхзначные значения уже экзотика, а пятизначных не бывает физически. Проблема в том, что этот запас защищает ровно от одного сценария: от того, который уже выстрелил. Завтра внешняя система пришлёт пустую строку в поле даты, или текст длиннее объявленного, или отрицательное число там, где вы ждали ноль. И вы снова получите ночь с нулём строк, потому что механизм отказа остался прежним.
Ширина колонки - это допущение о внешнем мире, записанное в схеме и нигде больше. "Позиция в выдаче трёхзначная" звучит очевидно ровно до того дня, когда придёт четырёхзначная. Обратите внимание, что одного лишнего разряда хватило бы с запасом: пришедшее значение вылезло за предел на четверть, и семизначный тип тут даже избыточен. Расширяют его ради всех следующих случаев, которых мы пока не видели.
Практический вывод: тип расширяйте, но не считайте это починкой. Это закрытие одной конкретной дырки, а класс аварии остаётся открытым.
Арифметика, ради которой стоило это разбирать
Когда механизм починили, простой закрыли дозаливкой за все дни. Приехало 12 237 строк. Из них обрезки потребовали 16.
Посчитаем. 16 из 12 237 это 0,13 %. Или, если удобнее в целых, одна строка из 765.
Тринадцать сотых процента данных остановили сто процентов загрузки на шесть суток. Вот это соотношение и есть главный результат разбора, и оно же переворачивает диагноз.
Пока смотришь на аварию как на "переполнение колонки", кажется, что виновата схема. Как только появляется дробь 16/12 237, видно другое: схема была спусковым крючком, а масштаб сделала вставка без изоляции ошибок. Была бы вставка построчной, или с пропуском сбойных строк в отдельный лог, шестидневного простоя не случилось бы ни при какой ширине колонки. Потерялись бы шестнадцать строк, и утром кто-то посмотрел бы, что это за строки.
Практический вывод: считайте долю, а не количество. "Шестнадцать плохих строк" звучит как проблема данных. "0,13 % против 100 %" звучит как проблема архитектуры загрузки, и это честнее.
Заплатка и почему она заплатка
Починили защитным обрезанием: перед записью значение прижимается к безопасному диапазону. Пришло 1250,56 - в таблицу уедет максимум, который влезает.
Называю это заплаткой без кокетства. Она портит данные сознательно. Но в этом конкретном месте она допустима, и вот аргумент. Позиция за тысячей означает "карточка невидима". Тысяча первое место и тысяча триста двадцатое одинаково не приносят ничего, для любого решения по рекламе или ассортименту они неразличимы. Сохранить строку целиком важнее, чем сохранить точность в диапазоне, который всё равно не влияет ни на одно решение.
А теперь то, что мне в этом решении не нравится, и это не мелочь. Обрезка убирает улику. После неё исходное значение не сохраняется нигде, и вопрос "а как часто внешняя система вообще выходит за наш диапазон" остаётся без ответа навсегда. Шестнадцать случаев на 12 237 строк - это за дни аварии. Сколько их в обычную неделю, никто уже не скажет.
Лечится дёшево: писать отвергнутое значение в отдельный лог, парой полей - когда, какой объект, что пришло. Через месяц у вас будет распределение вместо догадки. Мы этого не сделали, и я считаю это главным долгом по задаче.
Практический вывод: любая обрезка, округление и подстановка значения по умолчанию обязаны оставлять след. Заплатка, которая молчит, через полгода превращается в "у нас такого не бывает".
Проверка, которая поймала бы это в первую ночь
Самая обидная часть истории. Есть проверка, которая стоит полчаса работы и ловит весь класс аварий: сравнить число строк, приехавших за ночь, с прошлой ночью. Упало больше чем на 80 % - шлём алерт.
В нашем случае падение было стопроцентным. Строк приехало ноль. Алерт сработал бы в первую же ночь, и пять ночей простоя из шести - это прямая цена того, что проверки не было.
Теперь честно про сам порог. 80 % выбраны под этот случай и потому слишком мягкие. Они ловят обвал и не ловят деградацию: если завтра из тысячи строк приедет семьсот, никто ничего не узнает. Ради таких случаев статистический контроль обычно и строят. Так что считайте порог 80 % первой ступенью, которую ставят за полчаса, потому что она уже спасает от шестидневных дыр.
Ступень вторая, когда дойдут руки: сравнение со средним по дню недели за последний месяц вместо абсолютного порога. Понедельник и суббота не должны меряться одной линейкой.
Практический вывод: минимальный набор для любой ночной загрузки - две записи в журнал (сколько строк приехало, сколько отвергнуто) и один алерт на резкое падение первой цифры. Всё остальное можно потом.
Как поискать такие места у себя в 1С
В базе 1С этот же класс живёт в квалификаторах числовых реквизитов. Реквизит, объявленный как Число(5,2), даёт ровно то же самое ограничение: три знака до запятой, потолок 999,99. Если такой реквизит заполняется загрузкой из внешней системы, у вас та же мина.
Искать глазами по конфигурации бессмысленно, реквизитов тысячи. Обход метаданных занимает минуту и не читает ни одной строки данных:
Для Каждого ОписаниеДокумента Из Метаданные.Документы Цикл Для Каждого Реквизит Из ОписаниеДокумента.Реквизиты Цикл Для Каждого ТипРеквизита Из Реквизит.Тип.Типы() Цикл Если ТипРеквизита <> Тип("Число") Тогда Продолжить; КонецЕсли; Квалификаторы = Реквизит.Тип.КвалификаторыЧисла; ЦелаяЧасть = Квалификаторы.Разрядность - Квалификаторы.РазрядностьДробнойЧасти; Если ЦелаяЧасть <= 3 Тогда Сообщить(ОписаниеДокумента.Имя + "." + Реквизит.Имя + " - Число(" + Квалификаторы.Разрядность + "," + Квалификаторы.РазрядностьДробнойЧасти + "), до запятой знаков: " + ЦелаяЧасть); КонецЕсли; КонецЦикла; КонецЦикла; КонецЦикла;
Прогонял на 8.3.24, код платформенный, к СУБД не обращается вообще. По аналогии дописывается обход регистров сведений и накопления, там узкие типы встречаются даже чаще: коэффициенты, проценты, курсы.
Список получится длинный, и это нормально. Дальше его надо сузить руками до тех реквизитов, которые заполняются извне: обменом, загрузкой из файла, веб-сервисом. Внутренние реквизиты, которые считает сама конфигурация, к этой аварии отношения не имеют, потому что их значения ограничены вашей же логикой.
Практический вывод: заведите этот обход как разовую проверку после каждой крупной доработки обменов. Он бесплатный.
Границы применимости, честно
Три места, где я бы не стал натягивать наш опыт на чужую систему.
Первое. "Отказов после исправления - ноль" звучит убедительно, но проверено это на тех же данных, на которых нашли дефект. Обрезка снимает единственную известную причину падения, и она никак не защищает от другого типа переполнения, от неожиданного NULL или от смены формата на стороне источника. Ноль отказов здесь означает "эта причина закрыта", и ничего больше.
Второе. Миграцию схемы, то есть нормальное расширение типа, отложили с формулировкой "потребует простоя". Размер таблицы при этом никто не назвал и стоимость не считал. На небольшой таблице изменение типа занимает секунды. Решение приняли по общему соображению, а не по замеру, и я бы такое решение у себя оспорил.
Третье. Всё, что написано выше про заплатку, держится на одном свойстве данных: значения за пределом диапазона бессмысленны для решений. С позицией в выдаче это так. С суммой, количеством или курсом валюты это неверно категорически, там обрезка превратится в тихое искажение учёта, и делать её нельзя ни при каких порогах. Прежде чем копировать приём, спросите себя, теряется ли при обрезке информация, на которую кто-то смотрит.
Открытый вопрос
Вопрос к вам, и он мне правда интересен. У кого стояла самая длинная тихая дыра в регулярной загрузке, и что в итоге заставило пойти посмотреть? Меня в этой истории больше всего смущает, что систему уронила не сложная ошибка, а обычная позиция в выдаче, которую никто не считал опасной. Несите свои случаи с цифрами: сколько суток и чем поймали. Самое интересное в таких историях всегда в том, как именно заметили.
Другие наши инструменты диагностики 1С:
- Карта объёмов базы 1С - показывает, какие таблицы растут, а какие перестали расти; вторая ситуация как раз и означает, что загрузка встала.
- Чек-ап СУБД под 1С - проверка настроек сервера, с которой стоит начинать, если ночные операции падают веером, а не по одной.
- Трансформатор SQL в 1С - разбирает текст запроса из логов СУБД до объектов конфигурации, когда исключение прилетело с именами таблиц вида
_Document123.
Вступайте в нашу телеграмм-группу Инфостарт