Skip to main content

Когда репозиторий перестаёт описывать прод

Команда KTTC01.12.20268 min read
дрейф-конфигурациимиграции-бдэксплуатацияданные-это-кодразбор-случая

Мы поставляем шестнадцать шаблонов документов. Каждый — это схема: какие поля извлекать, каких они типов, и промпт, который их извлекает. И каждый живёт строкой в таблице базы данных.

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

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

Два пути в одни и те же строки

Схемы шаблонов попадают в базу двумя способами.

Сидер определяет все шестнадцать и записывает их целиком. Он и есть источник правды в репозиторном смысле: прочитал — знаешь, каким шаблон должен быть.

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

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

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

Дрейф в такой конструкции не риск. Он состояние по умолчанию.

Сразу в обе стороны

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

У прода были правки, которых репозиторий не видел. Шаблон свидетельства о рождении нёс четыре определения полей, отсутствующих в любом коммите: back_side, father_citizenship, mother_citizenship, seal. Кто-то что-то починил напрямую, верно, а репозиторий об этом не знал. Хуже: наши собственные тесты записали эти четыре как принятые «сироты» — несопоставленные поля, которые мы решили терпеть. Никакие это были не сироты. Это была та самая починка.

У репозитория были правки, которых не видел прод, и вот это направление теряло данные. Схема корейской экспортной декларации объявляла тринадцать полей — фрахт, пломба контейнера, номер лицензии, дата окончания отгрузки и ещё девять, — которых боевой промпт не просил. Экстрактор привязывает ключи JSON к объявленным именам, но поле, о котором промпт не упоминает, модели незачем заполнять. Эти тринадцать возвращались в проде пустыми, на каждой корейской декларации, всё то время, пока стороны были врозь.

Никто не жаловался. Пустое поле выглядит как поле, которого в документе не было.

То, чего у нас не было

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

Сравнить означало прочитать сидер, выгрузить строки прода и глазами сличить шестнадцать вложенных структур JSON. Никто не станет делать это по наитию, а алерта не существовало, потому что никто не смотрел.

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

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

Вот в этом и урок, и он не про шаблоны. Конфигурации, живущей в базе, нужен diff, иначе она уедет и никто не узнает. У кода он встроен: git status отвечает на этот вопрос непрерывно и бесплатно. У данных нет ничего, и отсутствие невидимо: вы не замечаете недостающую проверку — вы замечаете отказы, которые она поймала бы, то есть не замечаете.

Как мы совершили ту же ошибку этажом ниже

Вот часть, ради которой стоит читать, потому что случилась она уже после того, как мы сочли проблему решённой.

Diff сравнивал две колонки: определения полей и промпт извлечения. Это и есть схема. Ощущалось как полнота.

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

И она уехала, с последствиями. Наш паспортный шаблон забирал выписки из реестра индивидуальных предпринимателей: уверенные 0.367 при собственном пороге 0.3, на шестистраничном документе, где паспорта нет. Не совпал ни один опознающий ключ шаблона. Дотащили его две общие фразы — слова «гражданина» и «Российской Федерации», встречающиеся практически в любом российском официальном документе, — потому что выписка цитирует документ, удостоверяющий личность предпринимателя.

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

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

Что бы мы сказали тому, у кого конфигурация в базе

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

2. Сравнивайте всю строку, а не только ту часть, которую вы считаете «схемой». Колонка, которую вы не включили, и есть место следующего сюрприза. Спросите себя, что ещё в этой строке меняет поведение.

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

4. Считайте необъяснённое состояние прода починкой, а не шумом. Наши тесты классифицировали четыре законных боевых поля как терпимые «сироты». Список исключений — это место, куда улики дрейфа уходят, чтобы быть забытыми. Проверьте свой.

5. Убедитесь, что проверка ловит то, что обещает. Сломайте что-нибудь намеренно и подтвердите, что отчёт это назвал. Diff, который никто не видел падающим, — ещё не diff.

Общая форма

Фраза «источник правды — репозиторий» описывает стремление, а не механизм. Она верна лишь там, где что-то её обеспечивает. Для кода обеспечивает деплой: работает то, что закоммичено.

Для конфигурации в базе по умолчанию не обеспечивает ничто. Строка — это то, что записал последний писатель, а писатели друг с другом не согласованы. «Данные — тоже код» это лозунг; операционное его содержание в том, что данным нужно то же, что коду достаётся даром: непрерывное, дешёвое, скучное сравнение задуманного с тем, что лежит.

Наше занимает четыре секунды и печатает одну строку, когда всё совпало. Эта строка — 16 templates match the seeder — теперь часть чек-листа выкатки и единственная причина, по которой мы заметим, если это повторится.

Главное

  • Два пути записи в одни строки гарантируют дрейф, особенно когда один из них нельзя безопасно запускать в проде. У нас разошлись восемь шаблонов из шестнадцати.
  • Дрейф идёт в обе стороны. Прод нёс четыре поля, которых нет ни в одном коммите; репозиторий нёс тринадцать, которых боевой промпт не просил, — и они молча возвращались пустыми на каждом документе.
  • Тянулось это месяцами потому, что вопрос было некому задать. Diff на сотню строк, только на чтение, нашёл всё на первом прогоне.
  • Дальше мы совершили ту же ошибку этажом ниже, сузив diff до схемных колонок и оставив колонки маршрутизации невидимыми, — так паспортный шаблон и начал забирать выписки из реестра.
  • Список исключений — это место, где прячется дрейф. Четыре поля, принятые нашими тестами как «сироты», на деле были незадокументированной боевой починкой.

FAQ

Что такое дрейф конфигурации в базе данных?

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

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

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

Как обнаружить дрейф?

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

Какие колонки должен покрывать diff?

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

Как убедиться, что diff работает?

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

Заключение

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

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

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

We use cookies to improve your experience. Learn more in our Cookie Policy.