Как мы сломали прод
Содержание
Я вчера сломал прод. Чинили с 21 до 2:30 утра.
Опять порадовался нашему процессу и отношениям.
В этом посте расскажу про то, что произошло и как мы это фиксили (в обратном порядке.)
Процесс
Обнаружение и диагностика
Я вижу, что приложение не работает (случайно) и пишу СТО, что что-то тут не так после последнего деплоя бэкенда. Он проверяет приложение у себя и видит то же самое. Он также проверяет телефон жены, но там всё в порядке. Значит, проблема затрагивает не всех пользователей.
Мы сразу созваниваемся. У нас открыты логи, БД в проде, BigQuery, код и эмуляторы, где мы можем быстро всё проверить.
Я выдвигаю гипотезу и начинаю собирать данные в BigQuery. В это же время СТО симулирует проблему локально и подтверждает её.
Остановка кровотечения
Мы быстро выкатываем фикс (см. технические детали ниже). Кровотечение остановлено, но небольшая часть данных уже корраптнута и приложение всё ещё не работает.
С расшаренным экраном я на ходу пишу запросы в BigQuery и мы вместе смотрим в данные. Импакт — около 50 пользователей. Фух, не так уж и плохо. Но импакт серьёзный. У них приложение не работает от слова совсем.
У СТО появляется идея, как это можно эффективно исправить. Мы в ручном режиме модифицируем данные в БД в проде и видим, что подход рабочий.
Я вытаскиваю из BQ данные, которые влияют на пользователей прямо сейчас. Мы кидаем их в скрипт, который изначально был создан для других задач, но решает и эту. Делаем очередной запрос в BQ и видим, что все пофиксили кроме одной записи в БД. Фиксим её руками.
Системный фикс
Повторяем то же самое для всех остальных данных, которые успели повредиться. Проблема решена.
В процессе находим ещё два бага. Один из них — критический и живёт в системе почти 2 года, фиксим его на ходу. Второй будем решать позже, он не критический.
Это ситуация из серии "не было бы счастья, да несчастье помогло". Мы бы неизвестно сколько ещё жили с этими багами в проде.
Процесс без вины
Как обычно весь процесс — с холодной головой, коллаборативно, без каких-либо обвинений и чувства вины.
Shit happens.
Прод не ломает только тот, кто ничего не шипает.
Что случилось
Чтобы понять, что произошло, нужно немного контекста про стэк Metacast.
Библиотека для парсинга RSS
Каждый раз когда пользователь хочет послушать подкаст, которого у нас ещё нет в базе, мы его импортируем. Подкасты распространяются в виде RSS файлов с метаданными и ссылками на аудио и изображения. Мы парсим XML в нужный нам формат и сохраняем подкаст и эпизоды в базу данных в Firestore.
Когда начинали проект, мы решили не писать свой парсер и использовали готовый из open source. Со временем мы стали упираться в его ограничения. Незадача в том, что парсер больше не поддерживается его создателем, и у нас встал выбор: потратить неделю-другую и написать свой или сделать форк существующего и добавить нужный функционал.
Мы выбрали второе, сделали нужные нам изменения и забыли о существовании этой библиотеки. Мы подошли к этому прагматично и приняли на себя технический долг. Мы не обновляли зависимости, не добавляли тесты (которые были сломаны) и даже не стали заморачиваться с версиями этой библиотеки. Просто добавили зависимость в наш бэк, скрестили пальцы и решили, что когда-нибудь мы обязательно эту либу перепишем.
Жалоба пользователя
На прошлой неделе мы запустили поддержку непубличных подкастов и люди стали добавлять подкасты по URL (обычно это ссылка на RSS файл с auth токеном) и начались сложности. У таких подкастов нет на входе цербера в виде Apple Podcasts, который не пропустит подкаст, если RSS не соответствует спецификации. Поэтому в таких файлах встречается дичь...
На прошлой неделе пользователь пожаловался, что не может послушать платный подкаст. Мы стали разбираться и увидели, что подкастер формируют свой RSS-файл немного нестандартно. Обойти в существующей библиотеке это было невозможно — импорт подкаста тупо заканчивался исключением.
Обратная несовместимость
На поверхности проблема выглядела несложной, но когда я начал ковыряться, то понял, что она значительно больше.
Сделать маленькое изменение не получилось. Пришлось менять логику в библиотеке парсера, к которому я надеялся никогда больше не прикасаться и заодно ещё и немного поменять логику в бэкенде.
Мы создали ситуацию обратной несовместимости. Обновленная библиотека не работала со старым бэкендом и наоборот.
Я пофиксил сломанные тесты, написал новые, обновил зависимости. Вроде бы как закрыл все пограничные кейсы. Протестировал вручную на эмуляторах.
Зуб даю, всё работало на моей машине!
Кэширование npm зависимостей в GitHub Actions
Когда я тестировал локально, я подтягивал библиотеку из рабочей ветки. Поэтому локально бэкенд использовал обновленную библиотеку.
Далее, мы сделали мердж в основную ветку (которая, к слову, называется master
, настолько она старая) и начали деплоить бэкенд в Firebase через GitHub Actions.
И вот что я узнал уже после инцидента — GitHub Actions кэширует зависимости npm. Поэтому он подхватил старую версию библиотеки в деплое нового кода бэкенда.
Мы это обнаружили сразу же после деплоя и закрепили библиотеку на конкретный коммит. После этого всё (вроде как) заработало на бэкенде, но у меня неожиданно сломалось приложение. Домашняя страница перестала отображать подкасты.
Что-то пошло не так
Мы импортируем и обновляем тысячи подкастов в день. За те несколько минут, которые бэкенд проработал с неправильной версией библиотеки, мы успели импортировать сотни подкастов с некорректным типом данных в одном из полей. Это сломало мобильное приложение (оно на Flutter, а Dart — строго типизированный язык).
Из-за этого нам пришлось фиксить корраптнутые данные, чтобы всё восстановить в прежнее состояние.
Как этого можно было избежать
-
Это изменение можно было сделать обратно-совместимым, протестировать, а потом выпилить старый код. Но я самонадеянно решил, что это будет лишним.
-
По-хорошему нужно было добавить версии в библиотеку или закрепить зависимость в
package.json
на конкретный коммит. Тогда мы бы смогли более уверенно деплоить. Теперь мы знаем... -
Этого бы не случилось, если бы у нас был стейджинг. Мы бы отловили проблему до прода, но делать стейджинг пока не хотим — нужно увязывать компоненты из разных облаков, приложение, сайт. Для нашего небольшого масштаба это слишком много инфраструктуры.
-
Надо бы уже переписать эту библиотеку с нуля под наши потребности. Возможно, так и сделаем, когда потребуется снова сделать существенные изменения.