Всем привет! Меня зовут Андрей, с Банки.ру я связан уже больше 14 лет, а сейчас руковожу командой Платформы и забочусь о гильдии девопсов.

Скоро Новый год. Накануне принято подводить итоги: рассказывать, сколько миллионов заработал, проехал километров на такси и какие твои "песни года" (я в этом году сильно больше всего слушал Goo Goo Muck и Be Italien). Я же решил поделиться нашими "легендарными" факапами (забавно представлять, что я как Дед-Ops, который рассказывает истории из далеких бородатых годов, когда еще про DevOps еще мало кто слышал, и все были "старшими системными администраторами").

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

#1. Фатальная миллисекунда.

Все, наверное, знают, что бывает фатальное стечение обстоятельств: когда все должно было идти нормально, но по независящим друг от друга причинам случилась катастрофа? Первая история как раз про это.

Что произошло

Первый в Банки.ру редизайн случился в 2013 году. На тот момент мы еще не начали распиливать наши монолиты на микро-фронтенды с bff-слоями: у нас было просто два монолита: старый BX (чтоб никто не догадался) и новый NG (new generation, по задумке, должен был быть легким и современным бэкэндом перед слоем микросервисов). Новые разделы делались в NG, а старые оставались в BX. 

Мы решили, что не хотим править одно и то же в двух местах (DRY, все дела), поэтому вынесли этот редизайн в отдельный пакет и поставляли его сразу в оба монолита.

Также мы подумали, что будет правильно сделать один из монолитов основным источником истины (NG), а второй настроить так, чтобы он оттуда подхватывал данные (BX).

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

Для BX пришлось немного адаптировать механизм: мы преобразовывали json в php-файл  с хэшами, который принимал шаблонизатор приложения. Мы написали CronJob , который раз в минуту дергал с NG json, преобразовывал его в php, а тот уже правил шаблоны и пользователь получал красивую страничку.

Еще на этапе тестирования мы задумались о консистентности: как только мы деплоили свежий NG, у нас еще в течение минуты пользователи BX-страниц сталкивались с проблемами. Мы нашли такое решение: на этапе релиза сразу генерируется php-файл. Как только NG готов к переключению симлинка — а это занимает всего несколько миллисекунд — свежий файл загружается в BX, шаблонизатор его подхватывает, все довольны. При этом мы решили не убирать вариант с кронджобами: теперь у нас было два дублирующих механизма, и если один вдруг выйдет из строя, второй страхует. Мы были уверены, что сделали надежную, отказоустойчивую систему.

Прошло несколько лет, система работала стабильно и без проблем, пока однажды, после очередного релиза NG, мы не заметили на мониторинге резкий рост 500-х ошибок. Вскоре оказалось, что BX вообще перестал работать. Откат релиза не помог, и мы полезли в логи — пусто, хотя явно что-то было с приложением. Всё бы ничего – не так много разделов оставалось на BX – но мы еще не успели вынести баннерокрутилку со спонсорами, и бизнесовые разделы сайта резко и заметно опустели.

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

Если присмотреться, то ошибку можно увидеть невооруженным взглядом - разные скобки. Открывает массив скобка из старого синтаксиса php (5.5 и ниже), а закрывает - скобка из нового синтаксиса. Думаю, каждый разработчик хоть раз в жизни сталкивался с такой тупой и простой ошибкой – просто не на продакшене же!..

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

Что выявило расследование?

У нас был файлик hashes.json, который двумя разными способами преобразовывался в hashes.php. Когда мы занимались архитектурными задачами и приводили код стайл к единому виду, везде привели синтаксис к современному виду – то есть перешли на запись с квадратными скобками.

При этом скрипт деплоя NG был написан на Ruby и, разумеется, ничего не знал о php-синтаксисе. Его задача была простой: склеить файл по шаблону - в начале добавить return array, в конце - закрывающую скобку.

И вот в одну крайне неудачную миллисекунду произошла “гонка”: CronJob начал перезаписывать файл, в это время более шустрый постдеплой перезаписал его с другим синтаксисом, но CronJob поставил последнюю квадратную скобку и точка. С запятой. Поскольку никто заранее не подумал о возможности такой коллизии, то никакой эксклюзивной блокировки не было предусмотрено. Результат немного предсказуем: всё упало, и, как назло, во всех конфигах продакшена вывод синтаксических ошибок был задавлен - ведь пайплайн включал тесты, которые любую ошибку покажут до выкладки.

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

Прошла, наверное, тысяча успешных выкладок NG, и только на тысяча первой крепко упало даже не то приложение, которое мы выкладывали.

Мораль

Какой вывод мы сделали из этой истории? Во-первых, когда что-то рефакторишь, важно учитывать контекст изменений. Во-вторых, периодические механизмы, вроде кронджобов или триггеров, тоже стоит время от времени пересматривать и проверять их актуальность. Подобные ситуации могут возникнуть в любых сценариях, где генерируется код. И, конечно, хороший тон – валидировать такой код сразу после генерации. В общем, свою пользу из этого интересного и довольно болезненного факапа мы извлекли.

Наконец, самое интересное – мы не одни в таком классе факапов – вместе с нами, например, Amazon – их проблема с DNS была вызвана примерно таким же наслоением разных операций, которые меняют одну и ту же сущность.

#2. Троттлинг

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

Что произошло?

Думаю, немногие вдавались в подробности того, как именно Kubernetes занимается распределением ресурсов. А, между тем, у него интересная система распределения mcpu (микроядер, которые мы выдаем нашим сервисам). У каждого сервиса есть квота по количеству микроядер, которые он передает друг за другом. Соответственно, если у вас много ядер и много сервисов, то может получиться так, что у вас каждый сервис каждую секунду успевает поработать примерно 50 миллисекунд.

При этом, если у вас случаются какие-то моменты, когда в свою квоту микросервис не успевает что-то досчитать, то он продолжает работу через какое-то кратное число. В нашей системе вышло так, что это кратное число было равно 2000 миллисекунд. И в какой-то момент мы увидели у себя на мониторинге “ступеньку” и резко появившиеся 499 ошибки в одном из сервисов. Стали смотреть, почему это происходит (в очень сжатые сроки).

Выяснили, что микросервис за 2 секунды не может завершить запрос в третий сервис и бросает соединение по таймауту. Мы начали смотреть третий сервис – все вроде было неплохо, кроме метрики CPU throttling. 

При этом включилось это одномоментно - то есть у сервиса есть граница, после которой он начал троттлить. Сервис обрабатывал запросы за 80 мс, в момент, когда у него время ответа перешагнуло 100, он начал работать уже не 102 миллисекунды, как было бы логично, а уперся в CPU лимит и стал работать 2109 миллисекунд. 

Конечно, ответы перестали укладываться в таймауты. При этом, судя по мониторингу, у нас была какая-то жуткая магия – небольшой рост потребления cpu приводил к падению производительности в десять раз.

Окей, есть простое эмпирическое правило – видишь что-то странное в кубере, дай больше ресурсов. Вуаля – сервис начал снова укладываться в свои лимиты. 

Что выявило расследование?

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

Например, здесь скриншот от компании Zalando. Они опубликовали несколько публичных отчетов о троттлинге  (в видео это инцидент № 8). 

Оказывается, эта проблема возникает из-за бага в Kubernetes при работе на Ubuntu 16 (которую мы как раз использовали). Баг был исправлен в следующей версии ядра, а мы начали более пристально следить за потреблением ресурсов сервисами.

Мораль

Что мы извлекли как инженеры из этой ситуации? Во-первых, мы поняли, что нужно проводить больше нагрузочных тестов. К тому моменту мы их не делали системно, а редкие эксперименты  на монолитах (на виртуальных машинах) никак не позволили бы нам выявить такого плана сюрпризы. Во-вторых, мы стали чаще обновлять свое ПО. Актуальная версия кубера на достаточно новых версиях линуксовых ядер, но так, чтобы было пара месяцев на грабли для крупных ребят типа того же Заландо.

Этот инцидент мы успешно закрыли, уроки выучили и начали разрабатывать нагрузочную платформу Банки.ру.

#3. Нагрузочный тест Redis

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

Что произошло?

Упал один из наших ключевых сервисов – Мастер Подбора Кредитов (МПК). С собой он утащил часть бэкэндов и Redis, причем выглядело на мониторинге это всё как довольно массированная DDOS-атака, прошедшая через защиту. 

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

Как работает МПК?

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

Мы сразу же пришли к тому, что страница результатов подбора должна быть асинхронной, чтобы пользователь быстрее увидел какой-то оптимистичный результат. Обычно первым мы показываем кредитный рейтинг, который посчитали по анкете, иногда немного корректируем с учетом данных из БКИ (но, как правило, мы их успеваем обработать по ходу прохождения анкеты). Дальше уже система с бизнес-процессами собирает ответы от банков – кто готов человеку предоставить кредит и на каких условиях.

Это легкая страница со скриптом, который каждые 10 секунд опрашивает бэкенд о готовности данных. Когда мы только начинали интеграции, было много ошибок, и мы решили добавить отказоустойчивость во фронтенд: если бэкенд возвращает 500-ю ошибку, скрипт повторяет запрос. Логика простая: бэкенд мог “споткнуться” один раз, а при повторном обращении уже вернёт результат, и клиент получит больше предложений. А больше предложений – выше конверсия и больше прибыль.

На бэкенде мы тоже оптимизировали процесс: если на одну страницу приходит несколько повторных запросов, мы не обращаемся к банкам заново, а достаём уже полученные ответы из Redis – удобного key-value хранилища. Там по хэшу расчёта хранится целая пачка данных, и мы просто возвращаем её фронтенду.

Тонкий экономичный клиент? Да, но… 

Что же пошло не так? Правая часть (где мы опрашиваем бэки) преподнесла нам неприятный сюрприз – повторный запрос по ошибке делался сразу, а не через 10 секунд.

В один прекрасный момент возник расчёт, который стабильно отдавал 500-ю ошибку. Цикл опроса приводил к тому, что, если один клиент получал эксепшн и начинал повторять запросы со всей скоростью своего соединения. А сам эксепшн был фактически стабильно закэширован в редисе.

В итоге бэкенд принимает сотни запросов в секунду, генерирует тысячи запросов в редис и выкачивает гигабиты бесполезных данных. Система примерно за 10 минут достигла  физического предела сети – тогда это был 1 Гбит/с. И создала еще кучу пользователей с эксепшенами, которые окончательно уронили сервис. 

Когда стало понятно, что хотфиксом дело не решить, мы отключили раздел, нашли и откатили проблемный релиз – до момента, когда внедрили этот прекрасный (анти) паттерн.

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

Мораль

В целом получилось и весело (ну, потом), и поучительно. Мы провели отдельный небольшой семинар для разработчиков, участвовавших в этой истории, чтобы наглядно показать, как делать не стоит. Итоговый вывод был прост: отказоустойчивость для таких сценариев лучше реализовывать уровнем ниже – например, на балансировщике – через max_fails и повтор неидемпотентных запросов. Именно так мы впоследствии и сделали.

#4. Бюллетень качества

Это своеобразная экскурсия в старые времена. Все знают мем – “Наташа, мы все уронили!”. Наверное, все разработчики рано или поздно чувствуют себя на месте таких котиков. В Банки.ру, как вы поняли, время от времени случаются разные факапы, как и в Amazon, Cloudflare, Yandex или Microsoft. Часто это просто какие-то баги в коде, и мы решили, что надо мотивировать разработчиков делать меньше ошибок. Поэтому завели традицию рассылать регулярный “бюллетень качества”. 

Тут я покажу скриншот из писем 2013 года, за авторством вашего покорного. 

В те времена я трудился лидом команды оперативной разработки – 2 линия поддержки, то есть мы получали от 1 линии баги и пытались их починить.

Здесь «котики» – это коллеги из разных команд, которые генерили какие-то баги, в итоге выехавшие на продакшн.

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

Наверное, можно сказать, что эти письма похожи на постмортемы. Но здесь и кроется очередной факап. Ведь мы с вами знаем, что важная часть постмортемов – они должны быть безобвинительными.

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

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


Заключение

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

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

Напоследок – тост для новогоднего стола: пусть коллеги звонят вам в новогоднюю ночь только лишь для того, чтобы поздравить, последний релиз будет не позднее 25-го, а все тревоги унесут в лес единороги!

Комментарии (0)