Это вторая статья из цикла про то, как мы в Citymobil увеличивали стабильность сервиса (первую можете почитать здесь). В этой статье я углублюсь в конкретику разбора аварий. Но перед этим я освещу один момент, о котором я должен был подумать заранее и осветить в первой статье, но не подумал. И о котором узнал по фидбеку читателей. Вторая статья дает мне шанс устранить этот досадный недочет.
0. Пролог
Одним из читателей был задан очень справедливый вопрос: «Что сложного в бэкенде сервиса такси?» Вопрос хороший. Я его сам задавал себе летом прошлого года перед тем как начать работать в Citymobil. Я тогда думал «подумаешь, такси, приложение с тремя кнопками». Что в нем сложного может быть? Но оказалось, что это очень высокотехнологичный сервис и сложнейший продукт. Чтобы хотя бы примерно было понятно о чем речь и какая на самом деле это большая технологическая махина, я расскажу о нескольких направлениях продуктовой деятельности Citymobil:
- Ценообразование. Команда ценообразования занимается вопросами цен в каждой точке и в каждый момент времени. Цена определяется предсказанием баланса спроса и предложения на основе статистики и других данных. Все это делает большой, сложный и постоянно развивающийся сервис, работающий на основе машинного обучения.
- Прайсинг. Реализация различных методов оплаты, логика доплат после завершения поездки, удержание средств на банковских картах, биллинг, взаимодействие с партнерами и водителями.
- Распределение заказов. На какую машину распределить заказ клиента? Например, вариант распределения на самую ближайшую — не самый лучший с точки зрения увеличения количества поездок. Более правильный вариант — это сопоставить клиентов и машины таким образом, чтобы максимизировать количество поездок, учитывая вероятность отмены именно этим клиентом именно в этих условиях (потому что долго ждать) и отмены или саботажа заказа именно этим водителем (потому что слишком долго ехать или слишком низкий чек).
- Гео. Все что касается поиска и саджеста адресов, точек посадки, корректировки времени подачи (наши партнеры-поставщики карт и пробок не всегда дают точную информацию по ETA с учетом пробок), повышение точности прямого и обратного геокодинга, повышение точности подачи машины. Тут много работы с данными, много аналитики, много сервисов на базе машинного обучения.
- Антифрод. Различие в цене поездки для пассажира и для водителя (например, на коротких поездках) создает экономический стимул для фродеров, которые пытаются украсть наши деньги. Борьба с фродом чем-то похожа на борьбу со спамом в почтовом сервисе — важна полнота и точность. Надо заблокировать максимальное количество фродеров (полнота), но и хороших пользователей нельзя принимать за фродеров (точность).
- Мотивация водителей. Команда мотивации водителей занимается разработкой всего, что касается повышения используемости нашей платформы водителями и лояльности водителей за счет различных видов мотиваций. Например, сделай X поездок и получи за это дополнительно Y рублей. Или купи смену за Z рублей и катайся без комиссии.
- Backend водительского приложения. Список заказов, карта спроса (подсказка, куда надо ехать водителю, чтобы максимизировать свою выручку), прокидывание смен статусов, система коммуникации с водителями и еще много всего.
- Backend клиентского приложения (это, наверное, самая очевидная часть, и то, что обычно и понимают под backend’ом такси): размещение заказов, прокидывание статусов о смене состояния заказа, обеспечение движения машинок по карте на заказе и на подаче, backend чаевых и т.д.
Это все вершина айсберга. Функционала гораздо больше. За простым с точки зрения пользователя интерфейсом скрывается огромная подводная часть айсберга.
А теперь возвращаемся к авариям. За полгода ведения истории аварий мы составили следующую категоризацию:
- плохой релиз, 500-ые ошибки;
- плохой релиз, неоптимальный код, нагрузка на базу;
- неудачное ручное вмешательство в работу системы;
- пасхальное яйцо;
- внешние причины;
- плохой релиз, сломанная функциональность.
Ниже я распишу, какие выводы мы делали по самым распространенным видам аварий.
1. Плохой релиз, 500-ые ошибки
Почти весь наш backend написан на PHP — интерпретируемом языке со слабой типизацией. Бывает, выкатываешь код, а он падает из-за ошибки в имени класса или функции. И это только один из примеров, когда появляется 500-ая ошибка. Еще она может появляться в случае логической ошибки в коде; зарелизили не ту ветку; случайно удалили папки с кодом; оставили в коде временные артефакты, нужные для тестирования; не изменили структуру таблиц сообразно новому коду; не перезапустили или остановили необходимые cron-скрипты.
Мы с этой проблемой боролись последовательно в несколько этапов. Потерянные поездки из-за плохого релиза, очевидно, пропорциональны времени его нахождения в эксплуатации. То есть надо всеми силами делать так, чтобы плохой релиз находился в эксплуатации как можно меньше времени. Любое изменение в процессе разработки, которое сокращает среднее время нахождения плохого релиза в эксплуатации хотя бы на 1 секунду, позитивно для бизнеса и должно быть внедрено.
Плохой релиз или вообще любая авария в production проходит через два состояния, которые мы назвали «пассивная стадия» и «активная стадия». Пассивная стадия — это когда мы еще не в курсе про аварию. Активная стадия — это когда мы уже в курсе. Авария начинается в пассивной стадии, а с течением времени, когда мы узнаем о ней, авария переходит в активную стадию — мы начинаем с ней бороться: сначала диагностируем, а потом чиним.
Для уменьшения длительности любой аварии в production надо уменьшать среднюю длительность как пассивной, так и активной стадий. То же самое относится к плохому релизу, потому что он сам по себе является разновидностью аварии.
Мы стали анализировать наш текущий процесс починки аварий. Плохие релизы, с которыми мы сталкивались на момент начала анализа, приводили в простою (полному или частичному) в среднем на 20-25 минут. Пассивная стадия занимала обычно 15 минут, активная — 10 минут. Во время пассивной стадии начинались жалобы пользователей, которые обрабатывались контакт-центром, и после какого-то порога контакт-центр жаловался в общие чаты в Slack. Иногда жаловался кто-то из сотрудников, когда не мог заказать такси. Жалоба сотрудника для нас была сигналом о серьезной проблеме. После перехода плохого релиза в активную стадию мы начинали диагностировать проблему, анализировали последние релизы, различные графики и логи, чтобы установить причину аварии. После выяснения причины мы откатывали код, если плохой релиз был накачен последним, или делали новый накат с ревертом коммита плохого релиза.
Вот такой процесс борьбы с плохими релизами нам предстояло улучшить.
1.1. Сокращение пассивной стадии
Первым делом мы заметили, что, если плохой релиз сопровождается 500-ми ошибками, то мы можем и без жалоб понять, что случилась проблема. Благо, все 500-е ошибки записывались в New Relic (это одна из систем мониторинга, которую мы используем), и оставалось только прикрутить SMS- и IVR-уведомления о превышении определённой частоты «пятисоток» (с течением времени порог постоянно уменьшали).
Это привело к тому, что активная стадия аварии вида «Плохой релиз, 500-ые ошибки» начиналась фактически сразу после релиза. Процесс в случае аварии стал выглядеть следующим образом:
- Программист развёртывает код.
- Релиз приводит к аварии (массовые 500-ки).
- Приходит SMS.
- Программисты и админы начинают разбираться (иногда не сразу, а через 2-3 минуты: SMS может задержаться, звук на телефоне может быть выключен, и сама по себе культура немедленных действий после SMS не может появиться за один день).
- Начинается активная стадия аварии, которая длится те же 10 минут, что и раньше.
Таким образом, пассивная стадия была сокращена с 15 минут до 3.
1.2. Дальнейшее сокращение пассивной стадии
Несмотря на сокращение пассивной стадии до 3 минут, даже такая короткая пассивная стадия напрягала нас больше, чем активная, потому что во время активной стадии мы уже что-то делаем для решения проблемы, а во время пассивной сервис не работает целиком или частично, а «мужики не в курсе».
Для дальнейшего уменьшения пассивной стадии мы решили пожертвовать тремя минутами времени разработчика после каждого релиза. Идея была очень простой: выкатываешь код и в течение трёх минут смотришь в New Relic, Sentry, Kibana, есть ли 500-ые ошибки. Как только видишь там проблему, то априори предполагаешь, что она связана с твоим кодом и начинаешь разбираться.
Три минуты мы выбрали на основе статистики: иногда проблемы появлялись на графиках с задержкой в 1-2 минуты, но больше трёх минут не было ни разу.
Это правило было занесено в do’s & dont’s. В первое время оно исполнялось не всегда, но постепенно разработчики привыкли к правилу как к элементарной гигиене: чистка зубов по утрам — тоже трата времени, но делать это необходимо.
В итоге пассивная стадия сократилась до 1 минуты (графики всё равно иногда запаздывали). В качестве приятного сюрприза это попутно сократило и активную стадию. Ведь разработчик встречает проблему в тонусе и готов сразу откатить свой код. Хотя это не всегда помогает, т.к. проблема могла возникнуть из-за чужого, параллельно выкатываемого кода. Но, тем не менее, активная стадия в среднем сократилась до 5 минут.
1.3. Дальнейшее сокращение активной стадии
Более-менее удовлетворившись одной минутой пассивной стадии, мы стали думать о дальнейшем сокращении активной стадии. Первым делом обратили внимание на историю проблем (она является краеугольным камнем в здании нашей стабильности!) и обнаружили, что во многих случаях мы не откатываем сразу потому, что не понимаем, до какой версии откатить, потому что есть много параллельных релизов. Для решения этой проблемы ввели следующее правило (и записали его в do’s & dont’s): перед релизом пишешь в чат в Slack, что и для чего катишь, а в случае аварии пишешь в чат «авария, не катите!». Кроме того, мы начали автоматически сообщать по SMS о фактах релиза, чтобы уведомлять тех, кто не заходит в чат.
Это простое правило резко снизило количество релизов уже в ходе аварий и сократило активную стадию — с 5 минут до 3.
1.4. Еще большее сокращение активной стадии
Несмотря на то, что мы предупреждали в чате обо всех релизах и авариях, иногда всё же возникали состояния гонки (race conditions) — один написал про релиз, а другой в этот момент уже выкатывает; или началась авария, написали о ней в чат, а кто-то только что выкатил новый код. Эти обстоятельства удлиняли диагностику. Для решения этой проблемы мы реализовали автоматический запрет параллельных релизов. Идея очень простая: после каждого релиза CI/CD-система запрещает выкатывать в течение следующих 5 минут всем, кроме автора последнего релиза (чтобы он мог в случае необходимости откатить или накатить hotfix) и нескольких особо опытных разработчиков (на экстренный случай). Кроме того, CI/CD-система запрещает выкатывать в ходе аварии (то есть с момента получения уведомления о начале аварии и до момента получения уведомления о её завершении).
Таким образом, процесс стал таким: разработчик выкатывает, три минуты отслеживает графики, и после этого ещё две минуты никто не может ничего выкатывать. Если есть проблема, то разработчик откатывает релиз. Это правило кардинально упростило диагностику, а общая длительность активной и пассивной стадий сократилась с 3 + 1 = 4 минут до 1 + 1 = 2 минут.
Но и две минуты аварии — это много. Поэтому мы продолжили оптимизировать процесс.
1.5. Автоматическое определение аварии и откатывание
Мы долго думали, как уменьшить длительность аварии из-за плохих релизов. Даже пытались заставить себя смотреть в
tail -f error_log | grep 500
. Но в итоге все же остановились на кардинальном автоматическом решении.Если кратко, то это автооткат. Мы завели отдельный веб-сервер, на который с балансировщика пустили нагрузку в 10 раз меньше, чем на остальные веб-серверы. Каждый релиз автоматически развёртывался CI/CD-системой на этот отдельный сервер (мы его назвали preprod, хотя, несмотря на название, туда шла самая настоящая нагрузка от реальных пользователей). И далее автоматика выполняла
tail -f error_log | grep 500
. Если в течение одной минуты не появлялось ни одной 500-ой ошибки, то CI/CD развёртывала новый код в production. Если ошибки появлялись, то система сразу все откатывала. При этом на уровне балансировщика все запросы, завершенные 500-ми ошибками на preprod’е дублировались на один из production-серверов.Эта мера свела влияние «пятисоточных» релизов к нулю. При этом на случай багов в автоматике мы не отменили правило три минуты следить за графиками. На этом про плохие релизы и 500-ые ошибки все. Переходим к следующему типу аварий.
2. Плохой релиз, неоптимальный код, нагрузка на базу
Начну сразу с конкретного примера аварии этого типа. Выкатили оптимизацию: добавили
USE INDEX
в SQL-запрос, при тестировании это ускорило короткие запросы, как и в production, но длинные запросы замедлились. Замедление длинных запросов заметили только в production. В итоге поток длинных запросов положил всю мастер-базу на один час. Мы досконально разобрались, как работает USE INDEX
, описали это в файле do’s & dont’s и предостерегли разработчиков от неправильного использования. Также мы проанализировали запрос и поняли, что он возвращает, в основном, исторические данные, а значит его можно запускать на отдельной реплике для исторических запросов. Если даже эта реплика ляжет под нагрузкой, бизнес не остановится.После этого случая мы еще нарывались на подобные проблемы, и в какой-то момент решили подойти к вопросу системно. Прошерстили весь код частой гребенкой и вынесли на реплики все запросы, которые можно туда вынести без ущерба для качества сервиса. При этом сами реплики мы разделили по уровням критичности, чтобы падение никакой из них не останавливало бы работу сервиса. Как итог, мы пришли к архитектуре, в которой есть следующие базы:
- мастер-база (для операций записи и для запросов, которые суперкритичны к свежести данных);
- production-реплика (для коротких запросов, которые чуть менее критичны к свежести данных);
- реплика для расчета коэффициентов на цены, так называемый surge pricing. Эта реплика может отставать на 30-60 секунд — это не критично, коэффициенты меняются не так часто, и если эта реплика упадет, то сервис не остановится, просто цены будут не совсем соответствовать балансу спроса и предложения;
- реплика для админки бизнес-пользователей и контакт-центра (если упадёт, то основной бизнес не встанет, но не будет работать поддержка и не сможем временно просматривать и менять настройки);
- много реплик для аналитики;
- MPP-база для тяжелой аналитики с полными срезами по историческим данным.
Эта архитектура нам дала большее пространство для роста и уменьшила на порядок количество аварий из-за неоптимальных SQL-запросов. Но она всё еще далека от идеальной. В планах сделать шардинг, чтобы можно было масштабировать updates и deletes, а также короткие суперкритичные к свежести данных запросы. Запас прочности MySQL не бесконечен. Скоро нам понадобится тяжелая артиллерия в виде Tarantool. Про это будет обязательно в следующих статьях!
В процессе разбирательства с неоптимальным кодом и запросами мы поняли следующее: лучше любую неоптимальность устранять до релиза, а не после. Это снижает риск аварии и уменьшает временные затраты разработчиков на оптимизацию. Потому что если код уже выкачен и поверх него есть новые релизы, то оптимизировать гораздо сложнее. В результате мы ввели обязательную проверку кода на оптимальность. Её проводят самые опытные разработчики, фактически наш спецназ.
Дополнительно мы стали собирать в do’s & dont’s лучшие способы оптимизации кода, работающие в наших реалиях, они перечислены ниже. Пожалуйста, не воспринимайте эти практики как абсолютную истину и не пытайтесь их слепо повторить у себя. Каждый способ имеет смысл только для конкретной ситуации и конкретного бизнеса. Они тут приведены просто для примера, чтобы была ясна конкретика:
- Если SQL-запрос не зависит от текущего пользователя (например, карта спроса для водителей с указанием тарифов минимальных поездок и коэффициентов по полигонам), то этот запрос надо делать по cron с определённой частотой (в нашем случае одного раза в минуту достаточно). Результат записывать в кэш (Memcached или Redis), который уже использовать в production-коде.
- Если SQL-запрос оперирует с данными, отставание которых не критично для бизнеса, то его результат надо класть в кэш с некоторым TTL (например, 30 секунд). И далее в последующих запросах читать из кэша.
- Если в контексте обработки запроса на вебе (в нашем случае — в контексте работы реализации конкретного серверного метода на PHP) хочется сделать SQL-запрос, то надо точно убедиться, что эти данные не «приехали» уже ни с каким-нибудь другим SQL-запросом (и не приедут ли они далее по коду). То же самое касается обращений к кэшу: его тоже можно завалить запросами при желании, поэтому, если данные уже «приехали» из кэша, то не надо ходить в кэш как к себе домой и забирать из него, что уже и так забрано.
- Если в контексте обработки запроса на вебе хочется вызвать любую функцию, то надо убедиться, что в ее потрохах не будет сделано ни одного лишнего SQL-запроса или обращения к кэшу. Если вызов такой функции неизбежен, то надо убедиться, что ее нельзя модифицировать или разбить ее логику так, чтобы не делать лишние запросы в базы/кэши.
- Если сходить в SQL всё же необходимо, то надо точно убедиться, что в уже существующие в коде запросы нельзя добавить необходимые поля выше или ниже по коду.
3. Неудачное ручное вмешательство в работу системы
Примеры таких аварий: неудачный ALTER (который слишком нагрузил базу или спровоцировал отставание реплики) или неудачный DROP (нарвались на баг в MySQL, заблокировали базу при дропе свежей таблицы); тяжелый запрос на мастер, сделанный по ошибке руками; проводили работы на сервере под нагрузкой, хотя думали, что он выведен из работы.
Чтобы минимизировать падения по этим причинам, приходится, к сожалению, каждый раз разбираться в природе аварии. Общего правила мы пока не нащупали. Опять же, попробуем на примерах. Скажем, в какой-то момент перестали работать surge-коэффициенты (на них умножается цена поездки в месте и времени повышенного спроса). Причина была в том, что на реплике базы, откуда брались данные для расчета коэффициентов, работал питоновский скрипт, который съел всю память, и реплика ушла в даун. Скрипт был запущен давно, он работал на реплике просто для удобства. Проблему решили перезапуском скрипта. Выводы сделали следующие: не запускать на машине с базой сторонних скриптов (записали в do’s & dont’s, иначе это холостой выстрел!), мониторить окончание памяти на машине с репликой и алертить по SMS, если память скоро закончится.
Очень важно всегда делать выводы и не скатываться в комфортную ситуацию «увидели проблему, починили и забыли». Качественный сервис можно построить только в том случае, если делаются выводы. Кроме того, очень важны SMS-алерты — они задают качество сервиса на более высоком уровне, чем было, не дают ему упасть и позволяют далее повышать надёжность. Как скалолаз из каждого стабильного состояния подтягивает себя наверх и фиксируется в еще одном стабильном состоянии, но на большей высоте.
Мониторинги и алертинги невидимыми, но жесткими железными крюками врезаются в скалу неизвестности и никогда не дают нам упасть ниже заданного нами уровня стабильности, который мы постоянно поднимаем только вверх.
4. Пасхальное яйцо
То, что мы называем «пасхальное яйцо» — это мина замедленного действия, которая существует уже давно, но на которую мы не нарывались. За пределами этой статьи под этим термином понимается недокументированная фича, сделанная специально. В нашем случае это совсем не фича, а скорее баг, но который работает как мина замедленного действия и который есть побочный эффект деятельности с добрыми намерениями.
Например: переполнение 32 битного
auto_increment
; неоптимальность в коде/конфигурации, «стрельнувшая» из-за нагрузки; отставшая реплика (обычно или из-за неоптимального запроса на реплику, который был спровоцирован новым паттерном использования, или более высокой нагрузкой, или из-за неоптимального UPDATE на мастере, который был вызван новым паттерном нагрузки и нагрузил реплику).Еще один популярный вид пасхалки — неоптимальный код, а конкретней, неоптимальный SQL-запрос. Раньше таблица была меньше и нагрузка была меньше — запрос работал хорошо. А с увеличением таблицы, линейным по времени и ростом нагрузки, линейными по времени, потребление ресурсов СУБД росло квадратично. Обычно это приводит к резкому негативному эффекту: типа было всё «ок», и тут — бац.
Более редкие сценарии — сочетание бага и пасхалки. Релиз с багом привел к увеличению размера таблицы или увеличению количества записей в таблице определенного вида, а уже имеющая ранее пасхалка привела к избыточной нагрузке на базу из-за более медленных запросов к этой разросшейся таблице.
Хотя, были у нас и пасхальные яйца, не связанные с нагрузкой. Например, 32-битный
auto increment
: после двух с небольшим миллиардов записей в таблицу перестают выполняться вставки. Так что поле auto increment
в современном мире надо делать 64-битным. Этот урок мы усвоили хорошо.Как бороться с «пасхальными яйцами»? Ответ звучит просто: а) искать старые «яйца», и б) не допускать появления новых. Мы стараемся выполнять оба пункта. Поиск старых «яиц» у нас сопряжен с постоянной оптимизацией кода. Мы выделили двоих самых опытных разработчиков на почти-fulltime оптимизацию. Они находят в slow.log запросы, которые потребляют больше всего ресурсов баз, оптимизируют эти запросы и код вокруг них. Вероятность появления новых яиц мы снижаем через проверку на оптимальность кода каждого коммита вышеупомянутыми резработчиками-сэнсэями. Их задача — указать на ошибки, влияющие на производительность; подсказать, как сделать лучше, и передать знание другим разработчикам.
В какой-то момент после очередного найденной пасхалки мы поняли, что поиск медленных запросов — это хорошо, но стоило бы дополнительно искать запросы, которые внешне выглядят как медленные, но работают быстро. Это как раз следующие кандидаты на то, чтобы все «положить» в случае взрывного роста очередной таблицы.
5. Внешние причины
Это причины, которые, как нам кажется, плохо нами контролируются. Например:
- Тротлинг со стороны Google Maps. Можно обойти с помощью контроля за использованием этого сервиса, соблюдением определённого уровня нагрузки на него, планированием роста нагрузки заранее и закупкой расширения сервиса.
- Падение сети в дата-центре. Можно обойти размещением копии сервиса в резервном ЦОДе.
- Авария платежного сервиса. Можно обойти резервированием платежных сервисов.
- Ошибочная блокировка трафика со стороны сервиса защиты от DDoS. Можно обойти отключением сервиса защиты от DDoS по умолчанию и включением его только в случае DDoS-атаки.
Поскольку устранение внешней причины — это долгое и дорогостоящее мероприятие (по определению), мы начали просто собирать статистику по авариям из-за внешних причин и ждать накопления критической массы. Рецепта, как определять критическую массу, нет. Тут работает просто интуиция. Например, если мы 5 раз были в полном даунтайме из-за проблем, скажем, сервиса борьбы с DDoS, то при каждом следующем падении будет всё острей и острей вставать вопрос об альтернативе.
С другой стороны, если можно каким-то образом сделать так, чтобы всё работало при недоступном внешнем сервисе, то мы это обязательно делаем. И в этом нам помогает post-mortem-анализ каждого падения. Всегда должен быть вывод. А значит, всегда хочешь-не-хочешь, но придумаешь workaround.
6. Плохой релиз, сломанная функциональность
Это самый неприятный вид аварий. Единственный вид аварий, который не виден ни по каким симптомам, кроме жалоб пользователей/бизнеса. Поэтому такая авария, особенно, если она не крупная, может долго существовать в production незамеченной.
Все остальные виды аварий в той или иной степени похожи на «плохой релиз, 500-ые ошибки». Просто триггером будет не релиз, а нагрузка, ручная операция или проблема на стороне внешнего сервиса.
Чтобы описать метод борьбы с этим видом аварий, достаточно вспомнить бородатый анекдот:
Математику и физику была предложена одна и та же задача: вскипятить чайник. Даны подсобные инструменты: плита, чайник, водопроводный кран с водой, спички. Оба поочередно наливают воду в чайник, включают газ, зажигают его и ставят чайник на огонь. Затем задачу упростили: предложен чайник, наполненный водой и плита с горящим газом. Цель та же — вскипятить воду. Физик ставит чайник на огонь. Математик выливает из чайника воду, выключает газ и говорит: «Задача свелась к предыдущей». anekdotov.net
Этот вид аварии надо всеми силами сводить к «плохому релизу, 500-ым ошибкам». Идеально, если бы баги в коде сохранялись в лог в виде ошибки. Ну или хотя бы оставляли следы в базе данных. По этим следам можно понимать, что случился баг, и тут же алертить. Как этому поспособствовать? Мы начали разбирать каждый крупный баг и предлагать решения, какой мониторинг/SMS-алертинг можно сделать, чтобы этот баг сразу проявлял себя так же, как 500-я ошибка.
6.1. Пример
Появились массовые жалобы: не закрываются заказы, оплаченные через Apple Pay. Начали разбираться, проблему повторили. Нашли причину: вносили доработку в формат
expire date
для банковских карт при взаимодействии с эквайрингом, в результате чего стали передавать его конкретно для оплат через Apple Pay не в том формате, в котором он ожидался со стороны сервиса обработки платежей (по сути, одно лечим, другое калечим), поэтому все платежи через Apple Pay стали отклоняться. Быстро исправили, выкатили, проблема пропала. Но «жили» с проблемой 45 минут.По следам этой проблемы мы сделали мониторинг количества неудачных оплат через Apple Pay, а также сделали SMS/IVR-алерт с некоторым ненулевым порогом (потому что неудачные оплаты бывают нормой с точки зрения сервиса, например, у клиента на карте нет денег или карта заблокирована). С этого момента, при превышении порога мы моментально узнаем о проблеме. Если новый релиз внесет ЛЮБУЮ проблему в обработку Apple Pay, которая приведет к неработоспособности сервиса, даже частичной, мы моментально узнаем об этом из мониторинга и откатим релиз в течение трёх минут (выше рассказано, как устроен процесс ручного откатывания). Было 45 минут частичного простоя, стало 3 минуты. Профит.
6.2. Другие примеры
Выкатили оптимизацию списка предлагаемых водителям заказов. В код вкрался баг. Как результат — водители в некоторых случаях не видели список заказов (он был пустым). О баге узнали случайно — один из сотрудников посмотрел в приложение водителя. Быстро откатили. В качестве вывода из аварии сделали график среднего количества заказов в списке у водителей по данным из базы, посмотрели на график задним числом на месяц, увидели там провал и сделали SMS-алерт по SQL-запросу, который формирует этот график при снижении среднего количества заказов в списке ниже порога, выбранного на основе исторического минимума за месяц.
Меняли логику раздачи пользователям cashback-а за поездки. В том числе раздали не той группе пользователей. Проблему починили, построили график розданных cashback’ов, увидели там резкий рост, увидели также, что никогда такого роста не было, сделали SMS-алерт.
С релизом сломали функциональность закрытия заказов (заказ закрывался вечно, оплата по карточкам не работала, водители требовали с клиентов оплату налом). Проблема была 1,5 часа (суммарно пассивная и активная стадии). О проблеме узнали из контакт-центра по жалобам. Внесли исправление, сделали мониторинг и алерт по времени закрытия заказов с порогами, найденными по исследованию исторических графиков.
Как можно заметить, подход к этому виду аварий всегда одинаковый:
- Выкатываем релиз.
- Узнаем о проблеме.
- Чиним ее.
- Определяем, по каким следам (в базе, логах, Кибане) можно узнать признаки проблемы.
- Строим график этих признаков.
- Отматываем его в прошлое и смотрим на всплески/падения.
- Подбираем правильный порог для алерта.
- Когда проблема возникает снова, мы тут же о ней узнаём через алерт.
Что приятного в подобном способе: одним графиком и алертом закрывается сразу огромный класс проблем (примеры классов проблем: незакрытие заказов, лишние бонусы, неоплата через Apple Pay и т. д.).
Со временем мы сделали построение алертов и мониторингов на каждый крупный баг частью культуры разработки. Чтобы эта культура не потерялась, мы ее чуть-чуть формализовали. На каждую аварию стали требовать сами от себя создание отчета. Отчет — это заполненная форма с ответами на следующие вопросы: корневая причина, способ устранения, влияние на бизнес, выводы. Все пункты обязательны. Поэтому хочешь не хочешь, но выводы напишешь. Это изменение процесса, разумеется, записали это do’s & dont’s.
7. Котан
Степень автоматизации процесса росла, и в итоге мы решили, что пора делать веб-интерфейс, в котором было бы видно текущее состояние процесса. Этот веб-интерфейс (а по сути, уже продукт) мы назвали «Котан». От слова «катить». :-)
В «Котане» есть следующая функциональность:
Список инцидентов. Он содержит список всех уже сработавших алертов в прошлом — того, на что требовалась немедленная реакция человека. Для каждого инцидента фиксируется время его начала, время закрытия (если уже закрыт), ссылка на отчет (если инцидент окончился и отчет написан) и ссылка на справочник алертов, чтобы понимать к какому типу алерта относится инцидент. История по таким же инцидентам (чтобы знать, как мы фактически устраняли такие инциденты).
Справочник алертов. По сути, это список всех алертов. Чтобы было понятней, отличие между алертом и инцидентом: алерт — это как класс, а инцидент — это как объект. Например, «количество 500-ок больше 1 %» — это алерт. А «количество 500-ок больше 1 %, случившееся в такую-то дату, такое-то время, с такой-то продолжительностью» — это инцидент. Каждый алерт добавляется в систему после решения конкретной проблемы, которая ранее не обнаруживалась системой алертов. Такой итеративный подход гарантирует высокую вероятность отсутствия ложных алертов (по которым ничего делать не надо). В справочнике есть полная история отчетов по каждому типу, это помогает быстрее диагностировать проблему: пришел алерт, зашел в «Котан», кликнул на справочник, увидел всю историю, и уже примерно понимаешь, куда копать. Залог успешной починки аварии — наличие всей информации под рукой. Ссылка на исходный код алерта (чтобы точно понимать, какую конкретно ситуацию этот алерт алертит). Текстовое описание текущих лучших методик устранения.
Отчеты. Это все отчеты за всю историю. В каждом отчете есть ссылки на все инциденты, к которым он привязан (иногда инциденты приходят группой, причина проблемы при этом одинаковая, и отчет получается один на всю группу), дата написания отчета, флаг подтверждения решения проблемы и самое главное: корневая причина, способ устранения, влияние на бизнес, выводы.
Список выводов. По каждому выводу помечено, реализован ли он, планируется реализация или она не нужна (с объяснением, почему не нужна).
8. А что поменялось в самом процессе?
Очень важный компонент в повышении стабильности — это процесс. Процесс постоянно подвергается изменением. Цель изменений: улучшить процесс так, чтобы уменьшить вероятность аварий. Решения по изменению процесса должны приниматься в идеале не умозрительно, а на основе опыта, фактов и цифр. Строиться процесс должен не сверху директивно, а снизу, с участием всех заинтересованных членов команды, т.к. одна голова руководителя — это хорошо, но много голов всей команды — лучше! Процесс должен строго соблюдаться и контролироваться, иначе в нем нет смысла. Члены команды должны поправлять друг друга в случае отступления от процесса, ибо если не они, то кто? Должна быть максимальная автоматизация, которая брала бы на себя контрольные функции, т.к. человек, особенно на творческой работе, постоянно ошибается.
Для автоматического контроля за формированием выводов из аварии мы сделали следующее. По каждому алерту автоматом блокируются релизы. Когда приходит закрывающий алерт (СМСка с информацией о том, что инцидент завершен), релизы не разблокируются сразу, вместо этого появляется возможность ввести в систему отчет со следующей информацией: причина аварии, как починили, как авария повлияла на бизнес, какие сделали выводы. Отчет пишут участники разбора аварии, то есть те люди, у которых есть максимально полная информация о происшествии. До появления и одобрения отчета в системе релизы запрещены автоматикой. Это мотивирует команду после устранения аварии быстро собраться и сформировать отчет. Он должен быть обязательно одобрен ещё кем-то, кто не участвовал в его написании, чтобы было второе мнение. Таким образом мы добились, с одной стороны, самодисциплины при сохранении истории об аварии, а с другой стороны — обеспечили автоматический контроль: теперь физически невозможно не сделать выводы или не написать отчет.
9. Вместо эпилога
Вместо эпилога суммаризирую в таблице кратко, что мы поменяли в процессе с целью уменьшить количество потерянных поездок.
Что поменяли? | Почему поменяли? |
---|---|
Стали вести дневник аварий. |
Чтобы делать выводы и не получать аварии вновь. |
По большим авариям (с большим количеством потерь поездок) начали делать post-mortem. |
Чтобы на будущее научиться быстрее устранять аварию. |
Стали вести файл do’s & dont’s. |
Чтобы формировать знание о том, что можно и что нельзя в разработке, и почему нельзя. |
Запретили релизы чаще, чем раз в 5 минут. |
Чтобы снизить задержку в диагностике аварии. |
Выкатываем сначала на один сервер с низким приоритетом, а потом на все. |
Чтобы снизить эффект от плохого релиза. |
Автоматически откатываем плохой релиз. |
Чтобы снизить эффект от плохого релиза. |
Запрещаем выкатки в момент аварии |
Чтобы ускорить диагностику. |
Пишем о релизах и авариях в чат. |
Чтобы ускорить диагностику. |
После релиза отслеживаем графики в течение трёх минут. |
Чтобы ускорить обнаружение проблемы. |
SMS/IVR-алерты о проблемах. |
Чтобы ускорить обнаружение проблемы. |
Каждый баг (особенно крупный) закрываем мониторингом и алертом. |
Чтобы ускорить обнаружение проблемы. |
Анализ на оптимальность кода. |
Чтобы снизить вероятность аварий из-за нового неоптимального кода. |
Периодическая оптимизация кода (в качестве инпута — slow.log). |
Чтобы снизить количество аварий из-за «пасхальных яиц». |
По каждой аварии делаем вывод. |
Уменьшает вероятность такой же аварии на будущее. |
По каждой аварии делаем алерт. |
Уменьшает длительность устранения такой же аварии в будущем. |
Автоматический запрет релизов после аварии до написания и одобрения отчета. |
Увеличивает вероятность того, что после аварии будут сделаны выводы, а значит уменьшает вероятность такой же аварии в будущем. |
«Котан» — автоматический инструмент повышения качества сервиса. |
Уменьшает длительность аварии, снижает её вероятность. |
Справочник инцидентов. |
Уменьшает длительность диагностики аварии. |
Спасибо большое, что дочитали до конца! Успехов вашему бизнесу и поменьше потерянных заказов, транзакций, покупок, поездок и всего, что для вас критично!
Комментарии (14)
pilot911
01.04.2019 15:20Столько описанных вопросов решает Java со Spring в связке, все эти альтеры вне бранчей, отсутствие типизации
После перехода на микросервисы + Marathon всегда есть возможность безболезненно в течение секунд переключиться на предыдущую рабочую версию согласованных сервисов или базdanikin Автор
01.04.2019 15:28«Столько описанных вопросов решает Java со Spring в связке, все эти альтеры вне бранчей, отсутствие типизации» а что такое альтеры вне бранчей?
«После перехода на микросервисы + Marathon всегда есть возможность безболезненно в течение секунд переключиться на предыдущую рабочую версию согласованных сервисов или баз»
А как понять, какая версия рабочая?
Matvey-Kuk
01.04.2019 16:36+1Уже традиционно оставлю комментарий, что эффективность реакции на инцидент сильно зависит от конкретного инцидент менеджера — тулы, которая рассылает алерты по дежурным, шлет СМСки, звонит, эскалирует если кто-то проспал. Для тех, у кого нет «Котана», есть целый набор готовых штук:
PagerDuty
OpsGeany
VictorOps
И мой любимый, работающий прямо в Slack https://amixr.io (Предвзятое мнение)danikin Автор
01.04.2019 16:41Интересно было бы послушать истории внедрения этих менеджеров, как внедряли, какие конкретно бизнес показатели улучшились, как уменьшалась средняя продолжительность аварий и за счет чего
Matvey-Kuk
01.04.2019 17:10+1Поддерживал PagerDuty, потом мигрировал на VictorOps. Отдельно интегрировал в тестовом режиме OpsGeany. В среднем, функционал одинаковый. У всех трех проблема — интерфейс.
Когда начали вводить разработчиков в On-Call ротацию, это стало реальной болью. Я каждые несколько дней заново объяснял дежурным простейшие операции в VictorOps.
Сейчас веду интеграцию amixr.io в несколько компаний уже со стороны вендора. Он простой как пробка, так что пока таких проблем не наблюдаем :)
По цифрам хорошая идея. Быстрым набегом на базу, видно улучшение времени реакции после введения эскалаций и расписаний. Нужно обстоятельно сесть и выгрузить в отчетик…
gnuman
04.04.2019 14:55Денис, спасибо за статью! Было интересно.
Не приходилось сталкиваться с ошибками в старом коде, когда сломали что-то незначительное, но связанное с деньгами/поездками, но очень давно?
Пример из другой предметной области, но тем не менее — в каком-то определенном хитром случае подписки перестали продляться или комиссия перестала списываться. То есть где-то течет, но чуть-чуть и на графиках этого после релиза не заметно и мониторинг не замечает.
Находишь такое через год, умножаешь на 365 вроде бы небольшую сумму потерь за день, и волосы начинают шевелиться на самых нескромных местах от масштабов потерь. Самые неприятные, на мой взгляд, аварии.danikin Автор
04.04.2019 14:56«Не приходилось сталкиваться с ошибками в старом коде, когда сломали что-то незначительное, но связанное с деньгами/поездками, но очень давно? „
Постоянно. Это то, что я в статье как раз называю “пасхалки».
Anton23
Эх, а я то думал вам зачем то реальные аварии потребовалось разбирать…
danikin Автор
Чтобы получить опыт и улучшить сервис
Anton23
Вы не поняли, изначально я подумал что вы разбирали авто-аварии.
danikin Автор
Ааа, понял. Нет, тут речь только про IT-инфраструктуру