Год Платите за год — и получите 2 месяца бесплатно на любом платном тарифе. Смотреть тарифы
Войти Начать бесплатно

Страница расходов показывала больше, а деньги не двигались

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

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

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

Ключ, искавший то, чего никто не записывал

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

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

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

Строки захвата не несли идентификатора арендатора вовсе. Все они были пустыми, и были такими с самого начала. Собственная документация сущности утверждала ровно теми же словами, что фреймворк проставляет его при вставке. Он не делал этого никогда. Измерено на продакшене в день находки: все 3 344 строки входящего ящика пусты, тогда как 1 174 из 1 451 строки на стороне-производителе несли реального арендатора — именно поэтому никто не заподозрил читающую сторону.

Итак, поиск спрашивал (арендатор, ключ), сохранённая строка была (ничего, ключ), и совпасть они не могли никогда.

Ничего не падало, поэтому это и шло неделями

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

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

Каждый потребитель заново обрабатывал собственный накопленный объём примерно каждые сорок пять секунд, бесконечно. Так делали двадцать шесть. Одна продакшен-строка завершилась через 333 часа после поступления — и завершалась снова и снова почти всё это время.

Что на самом деле сломалось

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

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

Чего это не затронуло, стоит перечислить точно, потому что «биллинговая проекция» звучит тревожно, а факты — нет:

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

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

Половина, на которую никто не смотрел

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

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

Исправление

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

Документацию, утверждавшую про проставление, исправили, а не удалили. Эта фраза и была ловушкой, и её удаление не оставило бы ничего, что говорит: кто-то наконец проверил.

Поведение закрепляют девять тестов. Шесть из них падают на старом коде — мы подтвердили это, вернув старый код на место.

Уборка после

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

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

Что мы из этого вынесли

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

Поделиться LinkedIn X Почта
Попробуйте на своих задачахКаждый ответ показывает, сколько он стоил. Начните с бесплатного тарифа. Начать бесплатно
Только новости о продукте — без шума Новые модели, возможности и главное из чейнджлога — короткое письмо, когда действительно что-то выходит.
Подписываясь, вы соглашаетесь получать письма ToRun об обновлениях продукта. Отписаться можно в любой момент. Политика конфиденциальности