Этой статьей я открываю короткий цикл из двух статей, в которых подробно расскажу, как нам удалось за несколько месяцев в разы увеличить стабильность сервисов Citymobil. Статья начинается с рассказа про наш бизнес, про задачу, про причину появления самой задачи повышения стабильности и про ограничения. Citymobil — это быстрорастущий агрегатор такси. За 2018 год он вырос более чем в 15 раз по количеству успешно совершенных поездок. В некоторые месяцы рост превышал 50 % по сравнению с предыдущим месяцем.
Бизнес рос как на дрожжах во все стороны (и растет до сих пор): повысилась и нагрузка на серверы, и размер команды, и частота выкаток. Вместе с этим появились и новые угрозы стабильности сервиса. Перед компанией встала важнейшая задача — не останавливая рост бизнеса повысить стабильность. В этой статье я расскажу, как нам удалось реализовать эту задачу в короткие сроки.
1. Формулировка задачи: что конкретно хотим улучшать
Прежде чем что-то улучшать, надо научиться это измерять, чтобы наглядно понимать, есть ли улучшение. Чем ближе измеряемая величина к понятным бизнесу терминам, тем лучше. Потому что тем больше вероятность, что мы улучшим то, что реально необходимо бизнесу. С точки зрения его успешности важнейший параметр для нас — это количество успешно совершенных поездок (далее кратко — количество поездок). Именно по этому параметру оценивают нас инвесторы, когда принимают решение об инвестициях. Чем больше поездок, тем дороже стоит компания.
Какие-то поездки приносят прибыль, какие-то — убыток. Но нам одинаково важны все поездки, даже убыточные, потому что они позволяют увеличивать долю рынка (по сути, убыток на поездках — это плата за рост доли рынка). Поэтому каждая дополнительно полученная поездка — это хорошо, а каждая потерянная — плохо. Все поездки равноправны с точки зрения успешности бизнеса.
Отсюда у нас появился понятный критерий измерения стабильности: количество потерянных поездок — это поездки, которые мы явно потеряли из-за технических проблем. Под технической проблемой подразумевается, например, баг в коде, 500-я ошибка, авария на инфраструктуре, сломанная интеграция с сервисом партнеров (например картами Google).
2. Как считать потерянные поездки?
Потерянные поездки иногда подсчитать просто, иногда сложно. Например, в случае полного отказа в обслуживании, когда вообще ничего не работает (тьфу-тьфу-тьфу), потерянные поездки посчитать очень просто. Мы знаем тренд графика количества поездок до падения, видим тренд этого графика после падения, достраиваем линию между точкой, когда начался простой, и точкой, когда он закончился. Площадь графика количества поездок под этой достроенной линией — это и есть потерянные поездки.
На нижеследующем графике черной линией показаны поездки в некоторый день и зеленой линией — поездки неделю назад. По оси X — время. По оси Y — количество поездок в некоторый временной интервал вокруг точки X. Виден явный пик вниз в виде остроугольного треугольника. Площадь этого треугольника — это и есть количество потерянных поездок. Разумеется, это приблизительное количество, т.к. график флуктуирующий, но мы понимаем, что точности даже 10-20 % нам достаточно, чтобы оценить масштаб аварии для бизнеса.
Если простой не полный, а частичный (тоже, тьфу-тьфу-тьфу), то подсчет чуть сложнее. Например, если есть баг, из-за которого 10 % заказов никогда не распределяются по автомобилям, то мы видим на графике поездок провал, а потом отскок (после исправления бага). В подобной ситуации потерянные поездки — это площадь, ограниченная сверху линией тренда; снизу — фактическим графиком количества поездок, слева — вертикальной линией начала простоя, справа — вертикальной линией окончания простоя.
На графике ниже видно, что пик вниз не такой явный, но наличие поездок за предыдущую неделю без пика вниз помогает понять, что пик вниз это — потери. Кроме того, сравнение поездок за текущий день и тот же день недели в предыдущую неделю дает понять, что самый правый пик вниз — это не потерянные поездки, а обычный провал в это время дня, т.к. он коррелирует с предыдущей неделей.
Обычно линию тренда строить сложно, т.к. график пилообразный. В этом случае нам как раз помогает сравнение неделя-к-неделе. Если на одном и том же графике нарисовать две линии — прошлую неделю и текущую, — то оказывается, что обе кривые плюс-минус схожи по форме, а отличаются только тем, что одна находится над другой (обычная текущая неделя выше предыдущей, хотя бывают и исключения). Важно именно сравнение неделя-к-неделе, потому что каждый день недели в силу разных обстоятельств имеет разную форму графика. Глядя на график неделя-к-неделе можно понять, где могла бы быть линия тренда сегодняшних поездок.
Очевидно, что сама по себе потерянная поездка означает гораздо большую проблему, чем просто одна потерянная поездка. Клиент, желающий уехать, так или иначе уедет, например, воспользуется конкурентным сервисом и впоследствии может к нам не вернуться, или вернуться только тогда, когда конкурент его разочарует, что маловероятно, т.к. конкуренты очень сильны. Более того, даже если конкурент разочарует клиента, то и тогда клиент не факт, что вернется, потому что в его голове будет все выглядеть так, что у всех сервис плохой, и нет смысла скакать по конкурентам.
То есть одна потерянная поездка из-за технических проблем означает, на самом деле, несколько потерянных поездок.
Чтобы не путаться в терминах, назовем поездки, потерянные непосредственно из-за технической проблемы, первичными потерянными поездками, а поездки, потерянные из-за ухода к конкуренту, вторичными потерянными поездками.
В идеале, чтобы считать полный урон для бизнеса от одной первичной потерянной поездки, надо понять, сколько она сгенерировала вторичных потерянных поездок. Т.е. надо умножать количество первичных потерянных поездок на некоторый коэффициент K, который можно вычислить на основе средней частоты пользования сервисом и среднего времени возвращения пользователя после ухода к конкуренту.
В предположении, что коэффициент K не особо меняется со временем нам для понимания тренда потери поездок достаточно считать первичные потерянные поездки и стремиться уменьшать их число, поскольку отношение первичных потерянных поездок период-к-периоду будет такое же как отношение вторичных потерянных поездок период-к-периоду. Пример: если мы в прошлом месяце потеряли 1000 первичных поездок, то вторичных поездок мы потеряли 1000*K, а в сумме потеряли 1000*(1+K). Если, далее, мы в текущем месяце потеряли 500 первичных поездок, то вторичных поездок мы потеряли 500*K, а в сумме потеряли 500*(1+K). При этом вне зависимости от значения коэффициента K мы стали в 1000*(1+K) / (500 * (1+K)) = 2 раза меньше терять поездок.
Даже если коэффициент K со временем изменяется, т.е. является функцией от времени K(t), то все равно мы заинтересованы в снижении количества первичных потерянных поездок. Потому что, если K(t) растет со временем, то мы тем более обязаны приложить усилия, чтобы меньше терять первичных поездок, т.к. урон от потери каждой из них все выше и выше. С другой стороны, если K(t) падает со временем, то это означает, что по какой-то причине пользователи нам все более и более лояльны, несмотря на плохой сервис, а значит мы просто обязаны оправдать их ожидания!
Итого: мы боремся за постоянное снижение первичных потерянных поездок.
3. Ок, потерянные поездки считаем. Что дальше?
Вооружившись понятным инструментом измерения потерянных поездок, переходим к самому интересному — как сделать так, чтобы уменьшить потери? И при этом не замедлить текущий рост! Поскольку субъективно нам казалось, что львиная доля технических проблем, из-за которых теряются поездки, связана с бэкендом, то мы решили в первую очередь обратить внимание на процесс разработки бэкенда. Забегая вперед скажу, что так и оказалось — бэкенд стал основным полем боя за потерянные поездки.
4. Как устроен процесс
Проблемы обычно происходят из-за выкатки кода и других ручных действий. Сервисы, которые никогда не меняются и никогда не трогаются руками, тоже иногда отказывают, но это исключение лишь подтверждающее правило.
Самым прикольным таким исключением на моем опыте был следующий сервис. Когда я работал в РБК в далеком 2006 году (боже, как я стар!), то на одном из почтовых сервисов компании РБК был гейтвей, через который проходил весь трафик, и который проверял IP-адреса на вхождение в черные списки. Сервис работал на FreeBSD, и работал исправно. Но в один прекрасный день работать перестал. Угадайте почему? На этой машине рассыпался диск (bad blocks копились-копились и накопились). Рассыпался за 3 года до отказа сервиса в обслуживании (!). И все жило с рассыпавшимся диском. А далее «фряха» по никому неизвестным ее фряховским мотивам решила, вдруг, обратиться к рассыпавшемуся диску и в итоге зависла. Уверен, что Linux бы не стал так делать. Но это уже отдельный холивар.
Если обобщить вышесказанное, то проблемы происходят из-за ручного вмешательства. Когда-то в детстве, лет в 10-12 в одном из походов в лес я услышал от отца фразу, которую запомнил на всю жизнь: «чтобы костер не погас, его просто не надо трогать». Думаю, многие из нас помнят моменты, когда в уже горящий костер подкидывали дров и он по непонятной причине гас.
В сухом остатке: проблемы создаются ручными действиями человека, например, закидыванием дров в уже хорошо горящий костер, которые перекрывают кислород и гасят огонь, или выкатками кода с багами в production. Поэтому, чтобы понимать причину проблем в сервисах, надо понимать, как именно происходит выкатка и как устроен процесс разработки.
Процесс был полностью ориентирован на быстрое развитие и был устроен следующим образом:
- 20-30 релизов в сутки;
- разработчики выкатывают сами;
- быстрое тестирование в тестовой среде силами разработчика;
- минимум автоматизированных/модульных тестов, минимум ревью.
Разработчики в сложнейших условиях, по сути, без прикрытых тылов в лице QA, с огромным потоком важных для бизнеса продуктовых задач и экспериментов, работали максимально сосредоточенно и слаженно, решали сложные проблемы простыми путями, не давали коду «зарастать», хорошо понимали бизнес-проблематику, очень ответственно относились к изменениям, быстро откатывали неработающее. Citymobil тут не уникален. 8 лет назад в Почте Mail.ru, когда я пришел туда работать, была похожая ситуация. И Облако Mail.ru мы тоже стартовали быстро и просто, без реверансов. И уже впоследствии меняли процессы, чтобы добиться большей стабильности.
Наверняка, вы замечали это по себе: когда вашу спину никто не прикрывает, когда только вы наедине с production, когда давит огромный груз ответственности — вы творите чудеса. У меня у самого был такой опыт (даже еще более хардкорный). Когда-то давно, еще в прошлом веке (прикиньте, в прошлом веке был уже Интернет, я сам удивляюсь, когда вспоминаю), я работал почти единственным разработчиком почтового сервиса newmail.ru, развёртывал в эксплуатацию сам, и тестировал в production тоже сам на себе через
if (!strcmp(username, “danikin”)) { … some new code… }
:-) Поэтому мне эта ситуация близка.Я не удивился, если бы узнал, что с такого простого подхода «на коленке» начинали многие стартапы, как впоследствии успешные, так и неуспешные, но движимые одной страстью — ориентацией на быстрый рост бизнеса и захват рынка.
Почему конкретно в Citymobil процесс был именно такой? Разработчиков было изначально мало. Они работали в компании давно и хорошо понимали код и бизнес. Выкатки были несколько раз в сутки. Ошибки были крайне редки. Процесс работал идеально для тех условий.
5. Почему с хорошим процессом появилась угроза стабильности?
С ростом инвестиций в проект мы сделали наши продуктовые планы более агрессивными и начали нанимать много разработчиков. Количество выкаток на production выросло, но при этом ожидаемо упало их качество, потому что новые ребята вникали в систему и в суть бизнеса прямо в боевых условиях. С увеличением штата разработчиков стабильность начала падать даже не линейно, а квадратично (количество выкаток росло линейно, и качество средней выкатки падало тоже линейно; «линейно» * «линейно» == «квадратично»).
Очевидно было, что оставлять процесс в таком виде нельзя. Он просто не был заточен под новые условия. Но менять его нужно было без ущерба для time-to-market, то есть с сохранением 20-30 релизов в сутки (и с ростом их количества пропорционально размеру команды). Ведь именно в большом количестве релизов и был весь смысл. Мы быстро росли, ставили много экспериментов, быстро оценивали их результаты и ставили новые эксперименты. Быстро проверяли продуктовые и бизнес-гипотезы, учились на них и выдвигали новые гипотезы, которые опять быстро проверяли, и т. д. Мы не хотели ни в коем случае снижать этот темп. Более того, хотели его наращивать, и наращивать скорость найма разработчиков. Т.е. наши действия, направленные на рост бизнеса, создавали угрозу стабильности, но и корректировать эти действия мы ни в коем случае не хотели.
6. Ok, задача ясна, процесс понятен. Что дальше?
Имея опыт работы в Почте Mail.ru и Облаке Mail.ru, где стабильность с какого-то момента была поставлена во главу угла, где выкатки раз в неделю, описанная в деталях функциональность и тест-кейсы, всё покрыто авто- и модульными тестами, код ревьюится минимум один раз, а иногда и по три раза, я столкнулся с абсолютно новой для себя ситуацией.
Казалось бы, всё просто: повторить в Citymobil процесс как в Почте или Облаке и повысить стабильность сервиса. Но, как в том самом похабном анекдоте, есть нюансы: а) выкатки в Почте/Облаке проводятся один раз в неделю, а не 30 раз в день, и в Citymobil мы не хотели жертвовать частотой релизов, б) в Почте/Облаке весь код покрыт авто/модульными тестами, а в Citymobil у нас не было на это времени и ресурсов, все силы бэкенд-разработки были брошены на проверку гипотез и продуктовых улучшений. При этом бэкенд-разработчиков физически не хватало, даже при высокой скорости найма (отдельное спасибо HR Citymobil — это лучшие HR в мире! Думаю, что про наш HR-процесс будет отдельная статья), то есть не было никакой возможности плотно заниматься тестами и ревью без замедления темпа.
7. Когда не знаешь, что делать, то учись на ошибках
Итак, что же мы сделали волшебного в Citymobil? Мы решили учиться на ошибках. Метод улучшения сервиса через обучение на ошибках стар как мир. Если система работает хорошо, то это хорошо. Если система работает с ошибками, то это тоже хорошо, потому что на этих ошибках можно учиться. Звучит просто. Сделать… тоже просто. Главное — задаться целью.
Как мы учились? Начали с того, что стали скрупулезно записывать информацию о каждой большой и маленькой аварии. Признаюсь честно, мне изначально очень не хотелось этого делать, потому что я надеялся на чудо и думал, что аварии прекратятся сами собой. Разумеется, ничего не прекращалось. Новые реалии безжалостно требовали изменений.
Мы начали журналировать все аварии в общей гуглодоковской таблице. По каждой аварии была следующая краткая информация:
- дата, время, продолжительность;
- корневая причина;
- что сделали, чтобы решить проблему;
- влияние на бизнес (количество потерянных поездок, другие эффекты);
- выводы.
По крупным авариям мы создавали отдельные большие файлы с детальным поминутным описанием, начиная с момента начала аварии и до момента завершения: что мы делали, какие решения принимали (обычно такие описания называют post-mortem analysis). А в общую таблицу мы добавляли ссылки на такие пост-мортемы.
Цель этого файлика была одна: сделать выводы, реализация которых снизит потери поездок. При этом очень важно было точно сформулировать, что такое «корневая причина» и что такое «выводы». Эти слова сами по себе понятны. Но каждому понятно по-своему.
8. Пример ошибки, на которой научились
Корневая причина — это то, устранение чего предотвратит подобные аварии в будущем. А выводы — это как устранить корневую причину (или снизить вероятность её возникновения).
Корневая причина всегда глубже чем кажется. Выводы всегда сложнее чем кажутся. Чтобы не успокоиться и не остановиться на том, что кажется, надо всегда быть недовольным якобы найденной корневой причиной и всегда быть недовольным якобы выводами. Это недовольство создает задор для дальнейшего анализа.
Приведу пример: выкатили код — всё упало, откатили — всё заработало. Что есть корневая причина? Выкатка, скажете вы. Если бы её не было, то не было бы аварии. Значит какой вывод: не делаем выкатки? Плохой вывод (вредный для бизнеса, если быть точнее). То есть это, скорее всего, не корневая причина, нужно копать глубже. Выкатка с багом. Корневая причина? Допустим. Как её устранить? Тестами, скажете вы. Какими тестами? Например, полным регрессом всей функциональности. Это хороший вывод, запомним его. Но стабильность надо повысить здесь и сейчас, пока нет полного регресса. Нужно копать еще глубже. Выкатка с багом, который случился из-за того, что мы сделали отладочную печать в таблицу в базе, нагрузили её сверх меры и база сломалась под нагрузкой. Вот это уже интересней. Сразу становится понятно, что даже полный регрессионный тест не спасет от этой проблемы. Ведь на тестовой базе не будет такой же нагрузки, как на production.
В чём же корневая причина этой проблемы, если копнуть еще глубже? Чтобы ее узнать, мы пообщались с разработчиком. Оказалось, он привык к тому, что база справляется с нагрузками. Но в условиях стремительного роста проекта вчера база справлялась, а сегодня — уже нет. Мало кто из нас работал в проектах, которые растут на 50 % от месяца к месяцу. Например, для меня это первый такой проект. Погрузившись в такой проект, ты начинаешь осознавать новые реалии. Пока первый раз не столкнешься с чем-то, то никогда не узнаешь, что бывает и такое.
Разработчик сразу предложил правильное решение причины падения: отладочную печать делать в файл, файл в офлайне по cron записывать в базу в один поток со слипами. Если отладочной печати будет слишком много, то база не ляжет, просто отладочная информация будет появляться в ней несвоевременно. Очевидно, что этот разработчик уже научился на своей ошибке и не повторит её в будущем. Но другие-то разработчики тоже должны про это узнать. Как? Нужно рассказать им. Как сделать так, чтобы услышали? Рассказать им всю историю от начала и до конца, объяснить, к чему это привело и сразу предложить, как надо делать, а также выслушать их вопросы и ответить на них.
9. Чему еще можно научиться на этой ошибке, или do’s & dont’s
Итак, продолжаем разбирать эту аварию. Компания быстро растет, приходят новые сотрудники. Как они научатся на этой ошибке? Рассказать каждому новому сотруднику? Очевидно, что ошибки будут еще и еще — как сделать так, чтобы все на них учились? Ответ почти очевиден: завести файл do’s & don’ts (читается как «дус энд донтс»)! В вольном переводе на русский идиома do’s & don’ts означает «что такое хорошо и что такое плохо». В этот файл мы записываем все выводы на тему разработки. Файл показываем всем новым сотрудникам, а также показываем его в общем чате разработчиков каждый раз, когда файл дополняется, и убедительно просим всех прочесть его еще раз (чтобы и старое знание освежить, и новое увидеть).
Вы скажете, что не все будут читать внимательно. Вы скажете, что многие сразу забудут после прочтения. И будете оба раза правы. Но вы не станете отрицать, что у кого-то что-то останется в голове. И это уже хорошо. По опыту Citymobil, разработчики очень серьезно относятся к этому файлу, и случаи, когда какие-то уроки забывались, происходили крайне редко. Кстати, сам факт того, что урок забылся, можно считать проблемой и сделать из нее вывод, то есть разобраться в деталях и понять, как на будущее изменить процесс. Очень часто такое копание приводит к более точным и четким формулировкам в do’s & don’ts.
Вывод из вышеописанной аварии: завести файл do’s & don’ts, записать в него то, чему научились, показать файл всей команде и попросить изучать его всех новичков.
Из общих советов, что мы поняли в разборе аварий: не надо использовать словосочетание «человеческих фактор». Как только вы это произнесли, сразу все это понимают так, что ничего делать не надо, выводы не нужны, люди же ошибались, ошибаются и будут ошибаться. Поэтому вместо произнесения этого словосочетания надо сделать конкретный вывод. Вывод — это хотя бы маленький, но шажок в изменении процесса, улучшении мониторинга, улучшении автоматических тулзов. Из таких маленьких шажков шьется ткань стабильного сервиса!
10. Вместо эпилога
Во второй части я расскажу про виды аварий по опыту Citymobil и погружусь в детали каждого из видов аварий, а также расскажу, какие выводы мы делали из аварий, как меняли процесс, какую вводили автоматику. Самое интересное во второй части! Stay tuned!
Комментарии (8)
pifagor_mc
25.03.2019 22:05Отличная статья, Денис. В лучших традициях того, чему я у тебя в своё время учился!Сразу вспомнилась твоя «Библия разработчика высоконагруженного сервиса». Буду ждать вторую статью и цикла.
semibiotic
26.03.2019 16:18Все рассказанное было бы куда интересней, если бы бизнес, так называемых «агрегаторов» не был бы процессом неправомерного выдавливания сверхприбылей из чужих затрат, и не нес вреда всем участникам процесса перевозок.
aPiks
26.03.2019 16:51Почему? Вас кто-то заставляет работать с агрегатором? Может быть вас кто-то заставляет пользоваться им? Нет! Агрегатор сбивает цену на перевозки, агрегатор делает удобное приложение и прозрачные цены на перевозку. С точки зрения водителей — тоже. Вам не надо получать лицензию, проходить медосмотры и тп. Вам не надо работать в определенное время. Поэтому агрегатор — это положительный момент, поэтому они и растут как грибы…
nikolau
26.03.2019 21:03Если вы привлекаете инвестиции, проект еще не является прибыльным?
danikin Автор
26.03.2019 21:55Привлечение инвестиций не означает не прибыльности бизнеса. Инвестиции могут привлекаться под рост, который может требовать больших вложений, чем можно получить из прибыли. Что касается конкретно экономики Ситимобил, то мы ее не раскрываем.
beatleboy
Все бы хорошо. Но партнерское API конечно у вас странноватое, впервые вижу чтобы auth_token передавался не в заголовке, т.е. если это GET запрос будь добр передать его query параметрами, если POST то в body.
В связи с чем так сделали? Не проще ли заголовки юзать для этого?
danikin Автор
Так исторически сложилось