24 июн. 2025 г.·6 мин чтения

Сначала путь чтения при изменениях базы данных — прежде чем переключать запись

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

Сначала путь чтения при изменениях базы данных — прежде чем переключать запись

Почему переключения записи идут не так\n\nБольшинство неудачных переключений записи происходят по одной скучной причине: тестовые данные слишком чистые. Ваш набор тестов может покрывать счастливые сценарии, известные баги и пару уродливых записей. В продакшене же данные содержат старые значения по умолчанию, частичные импорты, дубли строк, null в странных местах и ручные правки трёхлетней давности, которых никто не записал.\n\nЭта разница важнее, чем команды ожидают. Новый запрос может возвращать правильную структуру и при этом неверные числа. Ответ выглядит нормально. Ничего не падает. Но один join может отбросить строку, один фильтр пропустить архивные элементы, которые ещё считаются, или одно правило округления изменит итог на пару центов.\n\nТакие расхождения легко пропустить при код‑ревью. Они кажутся безобидными, пока кто‑то не сравнит реальные результаты в масштабе. Баланс клиента оказывается неверным на 1%. Количество заказов ежедневно пропускает несколько пограничных случаев. Система продолжает работать, поэтому баг скрывается на виду.\n\nПроблема усугубляется после переключения записи. Как только новый путь начинает писать данные, неправильные предположения распространяются быстро: новые записи наследуют неверные итоги, downstream‑задачи копируют их, и служба поддержки получает жалобы, которые трудно связать с одним релизом. Теперь это не просто баг в коде — ещё и работа по очистке данных.\n\nИменно поэтому полезно сначала менять путь чтения. Позвольте новому коду читать реальные продакшен‑данные, пока старый путь записи остаётся источником истины. Сравните старые и новые результаты запросов на живом трафике, найдите дрейф и исправьте его до того, как новый путь начнёт создавать свежие ошибки.\n\nПроверки чтения не убирают весь риск, но блокируют один распространённый сценарий ошибки: доверять новому пути записи, не убедившись, что он соответствует реальности. Такая пауза может сэкономить дни на исправлениях и много неловких объяснений.\n\n## Что значит «сначала путь чтения»\n\nНе направляйте запись на новую схему в первом релизе. Оставьте текущий путь записи как есть, чтобы система продолжала сохранять данные в проверённом месте. Это быстро снижает риск — если в новом дизайне есть баг, вы ещё не начали создавать плохие данные.\n\nНачните с чтений. Для одного и того же запроса запускайте старый и новый запросы к новой таблице, индексу или форме данных. Сравнивайте результаты за кулисами, но продолжайте отдавать пользователю ответ от старого пути. Интерфейс, API‑ответ или отчёт не меняются.\n\nЭто хорошо работает для безопасной миграции БД, потому что тестовые данные аккуратны, а продакшен‑данные — нет. Реальный трафик приносит странные таймстемпы, частично заполненные записи, редкие фильтры, дубли и старые данные, о которых никто не думал во время планирования. Когда новый запрос стабильно совпадает со старым на живом трафике, вы узнаёте гораздо больше, чем на стейджинге.\n\nБольшинство команд выкатывает это по шагам. Начните с небольшой доли трафика — внутренние пользователи или 1% запросов. Запускайте оба чтения для этой доли и сравнивайте выводы. Исправляйте расхождения до расширения доли. Переходите к широкому трафику только после того, как процент расхождений остаётся низким.\n\nПользователи не должны ничего заметить. Они продолжают видеть проверенный ответ, пока команда убеждается, что новый путь ведёт себя так же. Если новый запрос падает или возвращает странный результат, логируйте разницу и продолжайте отдавать старый результат.\n\nСначала кажется, что это медленнее. Обычно это экономит гораздо больше времени, чем откат, скрипт очистки или бессмысленная ночная разбирательство, почему после релиза сменились итоги, счётчики или статусы.\n\n## Какие результаты сравнивать\n\nНе сравнивайте все столбцы всех запросов — это создаёт шум, а шум прячет нужный баг. Сравнивайте то, что влияет на бизнес‑решение, действие пользователя или число, которое кто‑то заметит.\n\nНачните с рискованных запросов. Сюда входят суммы денег, налоги, скидки, возвраты и балансы. Также проверьте проверки прав, видимые записи, счётчики, показываемые пользователю, и поля статуса, которые запускают письма, продления или правила блокировки.\n\nХорошее сравнение небольшое, но значимое. Если поле не меняет поведение приложения, пропустите его. Внутренние ID, маркеры кэша и форматирование для отображения часто отличаются по безобидным причинам.\n\nЗапрос заказа может возвращать 25 колонок, но только шесть реально влияют: subtotal, tax, total, currency, payment status и customer ID. Сравнивайте их в первую очередь. Если старый и новый пути расходятся по полю заметки или метке для показа, это обычно менее критично, чем разница в 1 цент или неверный статус.\n\nТакже нужны правила для значений, которые выглядят разными, но означают одно и то же. Решите это до запуска теневых чтений в продакшене — иначе логи будут наводнены ложными тревогами.\n\nЗапишите несколько простых правил:\n\n- должен ли совпадать порядок строк или важен только итоговый набор\n- считать ли null и пустую строку разными\n- как округлять десятичные и денежные значения\n- должны ли таймстемпы совпадать точно или в небольшом окне\n- считать ли изменение регистра, например «PAID» и «paid», проблемой\n\nБудьте строги там, где пользователи или финансовая команда почувствуют разницу. Будьте мягче там, где приложение может игнорировать косметические отличия.\n\nНаконец, определите, что значит реальное расхождение простыми словами до старта: «разный total», «пропал видимый проект» или «число пользователей изменилось более чем на один» — такие правила понятны. «Всё что‑то отличается» — не годится. Если команда не может объяснить, почему расхождение важно, оно, вероятно, не должно блокировать миграцию.\n\n## Как запускать чтения бок‑о‑бок\n\nПодход лучше всего работает, когда вы держите объём маленьким. Выберите один endpoint и одно семейство запросов, а не всё приложение сразу. Если пользователи бьют по /orders, начните с чтения, которое загружает сводку заказа. Оставьте все остальные чтения в покое, пока это не станет чистым.\n\nДержите пользователя на старом пути. Пусть текущий запрос собирает реальный ответ и отсылает его как обычно. В том же запросе запустите новый запрос в теневом режиме с теми же входными данными, но не позволяйте его результату менять то, что видит пользователь.\n\nЭто важно, потому что вы получаете реальный продакшен‑трафик, реальные фильтры и реальные пограничные случаи без риска релиза. Если новый путь медленный или неверный, пользователь всё равно получит проверенный ответ от старого пути.\n\nПеред сравнением нормализуйте оба результата. Многие ложные тревоги возникают из-за безвредных различий в форме, порядке или форматировании.\n\nКороткая нормализация обычно включает:\n\n- сортировку массивов без естественного порядка\n- округление денег и процентов до одной точности\n- единое правило для null, пустых строк и отсутствующих полей\n- удаление сгенерированных таймстемпов или трасс‑данных\n- сопоставление старых и новых имён полей к единым именам\n\nЗатем сравните нормализованные результаты и логируйте только отличающиеся части. Используйте request ID, чтобы проследить одно расхождение от API‑лога до запроса в базе и обратно к действию пользователя, которое его вызвало. Сохраняйте небольшой пример полезной нагрузки с обеих сторон, когда это безопасно, или сначала маскируйте чувствительные поля.\n\nНе гонитесь за каждым расхождением сразу. Группируйте их по классам. Один класс может быть из‑за округления, другой — из‑за отсутствующего join, третий — из‑за различий в порядке строк. Исправьте один класс, задеплойте и наблюдайте, как падает процент расхождений, прежде чем переходить к следующему.\n\nТакая скорость кажется медленной. Но она экономит время. Десять смешанных багов в одном лог‑стриме трудно осмыслить. Один известный класс багов с ясным примером запроса обычно легко воспроизвести и исправить.\n\nКогда процент расхождений для endpoint близок к нулю, переходите к следующему запросу в том же семействе. Маленькие победы создают доверие и делают переключение записи менее драматичным.\n\n## Что логировать и за чем следить\n\nПростые счётчики ошибок недостаточны. Нужны детали, чтобы понять, новый путь чтения неверен, медлен или ошибается только для небольшой доли трафика.\n\nНачните с простых счётчиков для каждого бок‑о‑бок чтения. Считайте совпадения, расхождения, таймауты и пустые результаты для обоих путей. Если старый запрос возвращает данные, а новый ничего не возвращает, отслеживайте это отдельно от расхождения значений — такие случаи обычно указывают на разные ошибки.\n\nКороткий чеклист достаточен:\n\n- всего бок‑о‑бок чтений\n- точные совпадения\n- расхождения\n- таймауты старого и нового пути\n- пустые результаты на каждом пути\n\nЗадержка важна не меньше корректности. Измеряйте оба запроса в одном и том же запросе, чтобы сравнивать честно. Среднее может скрыть проблемы, поэтому отслеживайте p50, p95 и худшую задержку для каждого пути.\n\nКогда вы сравниваете старые и новые результаты в продакшене, сохраняйте небольшой набор неудачных пар. Не логируйте всю полезную нагрузку, если данные большие или чувствительные. Логуйте достаточно, чтобы понять форму проблемы: request ID, tenant ID, состояние feature flag, входные параметры запроса, старый результат, новый результат и короткий diff.\n\n### Группируйте расхождения, прежде чем паниковать\n\nОдин глобальный процент расхождений может ввести в заблуждение. 0.5% ошибок может показаться мало, но если всё это исходит от одного тенанта или одного типа запроса, вы уже знаете, куда смотреть.\n\nГруппируйте результаты по тенанту, feature flag, endpoint, типу запроса и любому параметру, меняющему поведение. Если новый путь падает только при применении скидки или только для аккаунтов со старыми записями, этот паттерн быстро проявится.\n\n### Наблюдайте дрейф во времени\n\nБольшие всплески привлекают внимание, но медленный дрейф часто хуже. Процент расхождений, который растёт с 0.1% до 0.3% и затем до 0.8% за три дня, может означать, что бэфилы отстают, свежесть данных нарушена или одна задача не успевает.\n\nХорошие дашборды помогают, но держите и простой текстовый след. Если вы видите ошибки, задержки и примеры запросов в одном месте, решение о переключении записи можно принимать на основании доказательств, а не надежды.\n\n## Пример: перенос итогов заказа в новую таблицу\n\nИтоги заказа — хороший пример: они кажутся простыми и всё же ломаются на грязных данных. Старый код часто считает итог при каждом запросе, суммируя позиции, затем добавляя налоги, доставку, скидки или сборы. Этот путь может быть медленнее, но команды ему доверяют, потому что он использует те же сырые данные каждый раз.\n\nНовый дизайн хранит предварительно рассчитанное число в отдельной таблице, например order_totals. Страница оформления или админ‑панель может прочитать одну строку вместо пересчёта каждый раз. Звучит безопасно — но не всегда так.\n\nЧисла расходятся, когда в игру вступает реальная жизнь. Возврат может прийти через час. Налог может измениться после правки адреса. Задержка события может обновить новую таблицу позже, чем клиент загрузил страницу. Если один путь видит эти изменения, а другой — нет, один и тот же заказ покажет два разных итога.\n\nЗдесь «сначала чтение» окупает себя. Оставьте текущий путь записи как есть. Пусть приложение продолжит использовать старый расчёт для того числа, которое видят пользователи. Параллельно в продакшене запускайте новое чтение и сравнивайте результаты для одного и того же заказа.\n\nДля каждого расхождения логируйте достаточно деталей, чтобы объяснить его. Небольшой набор полей обычно расскажет историю:\n\n- ID заказа и ID клиента\n- старый total, новый total и точная разница\n- таймстемпы возврата, налога и скидок\n- версия или номер последовательности событий для новой таблицы\n\nЧерез день‑два паттерн часто очевиден. Может быть, все плохие строки — с возвратом, который пришёл после того, как итог записали. Может быть, перерасчёт налога обновляет запись заказа, но не обновляет order_totals. Может быть, импортированные заказы пропускали один фоновой джоб.\n\nВот реальная польза от чтений бок‑о‑бок. Вы узнаёте не просто о существовании дрейфа, а какие заказы дрейфуют, когда это начинается и какое событие его вызывает.\n\nНе переключайте запись только потому, что большинство строк совпадает. Держите запись на старом пути, пока дрейф не прекратится или пока вы не сможете объяснить каждое оставшееся расхождение и исправить его. Если двенадцать заказов всё ещё расходятся после поздних возвратов — новая таблица не готова. Быстрый неверный результат всё равно неверный.\n\n## Обычные ошибки, которые прячут проблемы\n\nБок‑о‑бок чтение может выглядеть чистым и всё же пропустить баг, который травит вас после переключения записи. Большая часть ложной уверенности приходит от плохих правил сравнения, тонкой выборки трафика или чтения из разных мест в разное время.\n\nОдна распространённая ошибка — сравнивать сырой JSON как текст. Если старый путь отдаёт поля в одном порядке, а новый — в другом, строковый diff покажет «разное», хотя оба результата значат одно и то же. Сравнивайте нормализованные значения: одни и те же поля, одни и те же типы, один и тот же смысл.\n\nПравила по временным зонам и деньгам приводят к более коварным промахам. Отчёт по дням может сдвинуть строки вокруг полуночи, если одна система использует UTC, а другая — локальное время. Итого может уйти на цент, если один путь округляет каждую позицию, а другой округляет только в конце. Это не косметика — это изменение счетов, возвратов и отчётов.\n\n### Где сравнения дают сбой\n\nНесколько паттернов прячут баги дольше, чем команды ожидают:\n\n- чтение старого результата с реплики и нового — с primary\n- выборка только маленькой, простой части трафика, часто дневных кейсов\n- игнорирование null, пустых списков или отсутствующих опциональных полей\n- принятие одного чистого дня за достаточно обоснование для переключения записи\n\nЗадержка реплики может заставить старый и новый ответы расходиться по причинам, не связанным с кодом. Или ещё хуже — оба чтения совпадут, потому что они одинаково устарели. Держите источники чтения выровненными при тестировании.\n\nМалые выборки пропускают те редкие случаи, которые вам действительно нужны: возвращённые заказы, гостевые покупки, бэфилы, переход на летнее/зимнее время и очень большие суммы. Если в вашем продукте есть недельные или месячные паттерны, один день мало что скажет.\n\nЗапускайте сравнение достаточно долго, чтобы пересечь обычные бизнес‑циклы. Логируйте расхождения с контекстом, позволяющим воспроизвести запрос и проверить исходные данные. Именно это снижает риск, а не иллюзия безопасности.\n\nКоманды, которые безопасно переключают запись, ведут себя немного скучно: они сравнивают смысл, а не сырой текст; тестируют достаточно реального трафика, чтобы поймать редкие кейсы; и ждут стабильных результатов во времени, а не надеются на удачный тихий день.\n\n## Быстрые проверки перед переключением записи\n\nПереключение записи должно ощущаться немного скучным. Если работа «сначала чтение» прошла хорошо, вы уже знаете, как новые данные ведут себя на живом трафике. Последний шаг — убедиться, что оставшийся риск мал, именуем и легко откатывается.\n\nНачните с процента совпадений. Не смотрите только на тихий час или один чистый сэмпл. Сравните старые и новые результаты на нормальном трафике: в пиковые часы, в медленные периоды, при ретраях, фоновых задачах и редких запросах. Если процент совпадений держится устойчиво, вы можете больше доверять ему, чем короткому всплеску идеальных результатов.\n\nЗадержка важна не меньше корректности. Новый путь запроса, который совпадает 100% времени, но добавляет 80 мс к горячему endpoint, может всё ещё ухудшать продукт. Проверьте p50, p95 и процент таймаутов на тех чтениях, которые будут зависеть от новых данных. Если новый путь близок к целям на тихом дне, но проваливается под нагрузкой — подождите.\n\nОткрытые расхождения должны укладываться в известные группы. Может быть, разница в округлении на 1 цент, архивные записи используют старую мапу статусов или бэфил пропустил строки с повреждёнными данными. Всё это не идеально, но управляемо, если команда может объяснить каждую группу и оценить частоту. Неопознанные мистические расхождения — плохой знак.\n\nКороткий чеклист полезен:\n\n- процент совпадений держится на нормальном трафике\n- новый путь влезает в бюджет задержки под реальной нагрузкой\n- каждое оставшееся расхождение имеет имя и владельца\n- команда может откатиться одной флаг‑операцией, конфигом или деплоем\n- поддержка и продакт знают, какие экраны и метрики требуют внимания\n\nПоследний пункт часто пропускают. Поддержка должна понимать, как выглядит плохой итог заказа, пропавшая запись или задержка обновления. Продакт должен знать, какие метрики могут смещаться несколько часов после переключения. Когда люди знают, за чем смотреть, они быстрее ловят реальные проблемы и игнорируют шум.\n\n## Следующие шаги для более безопасного релиза\n\nНачните с одного изменения, которое реально может навредить пользователям, если пойдёт не так. Не начинайте с полного переписывания. Выберите перенос таблицы, индексный запрос или новую таблицу итогов и добавьте сравнение чтений до того, как тронете путь записи.\n\nДержите текущую систему источником истины, запускайте новое чтение в продакшене и сравнивайте ответы. Если числа часто совпадают и расхождения понятны, вы заработали право планировать cutover.\n\nКороткий план выката лучше расплывчатого. Перед окном выката запишите точное правило переключения записи. Это может быть просто: переключаемся только если ошибка не выросла, процент расхождений держится ниже порога и команда может откатиться в один шаг.\n\nДержите план видимым и простым:\n\n- назовите человека, который делает переключение\n- определите порог расхождений, блокирующий cutover\n- зафиксируйте триггер отката простыми словами\n- укажите, как долго будете наблюдать после переключения\n\nВо время переключения не полагайтесь на память. Держите дашборды открытыми, приготовьте несколько известных записей и наблюдайте расхождения вживую. Набор выбранных примеров полезен, потому что агрегированные графики могут выглядеть нормально, пока у одного клиента проявляется реальный баг.\n\nЕсли вы видите расхождение — остановитесь и разберите его, прежде чем продолжить. Пять минут паузы дешевле, чем часы очистки после попадания плохих записей в продакшн.\n\nНекоторые изменения заслуживают второго взгляда. Если выкатываемое изменение касается выручки, биллинга, инвентаря или любого рабочего процесса, который клиенты используют каждый день, возьмите дополнительный ревью до окна. Если нужна внешняя помощь, Oleg Sotnikov at oleg.is помогает в роли Fractional CTO и советника по стартапам по архитектуре, инфраструктуре, рискам выката и практическим окружениям для разработки с поддержкой ИИ.\n\nОдин аккуратный выкaт даёт шаблон для следующего. Сохраните правило cutover, набор дашбордов и несколько проверок, затем используйте их при следующем рискованном изменении.