
Всем привет, меня зовут Андрей Матвеев, я разработчик в команде платформы надёжности Яндекс Такси. Если проще — я занимаюсь тем, чтобы Такси работало стабильно. А ещё я техлид проекта virtual‑orders — это наша система нагрузочных учений. Изначально она была для Такси, но теперь мы активно внедряем её в Еду и Лавку.
В статье я расскажу, как мы проводим интеграционные нагрузочные тесты: что эмулируем, как настраиваем окружение, на какие метрики и дашборды смотрим. Поделюсь болями и находками — от первых тестов до регулярных прогонов с участием десятков команд.
Какие нагрузочные мы выбрали для себя
Нагрузочное тестирование — метод оценки производительности системы под высокой нагрузкой. Оно помогает выявлять узкие места в архитектуре, определять границы работоспособности и прогнозировать поведение сервиса: время отклика, пропускную способность и использование ресурсов.
Классический вариант — это нагрузка на чтение. Такие тесты легко запускать: можно написать простой скрипт, запустить — и вот у вас нагрузочные. Иногда их делают реалистичнее, собирая настоящие логи запросов. Кто‑то идёт дальше и симулирует фейковых пользователей, чтобы нагрузить аналитику или авторизацию. Полезно, но и ограничений у такого подхода хватает.
Главное — такие тесты малопишущие: не создают объекты, не вносят изменения в систему, а значит, вы не проходите полный сценарий использования сервиса. То есть проверяете не то, как живёт система под нагрузкой, а только как она отвечает на запросы. Когда мы продумывали подход к нагрузочному тестированию, пришли к мысли, что транзакционному бизнесу нужны интеграционные нагрузочные тесты.
Транзакционный бизнес — это когда в процессе участвуют несколько сторон: например, продавец и покупатель, или заказчик и исполнитель. В случае доставки — заказчик, сборщик, курьер. В общем, любая система, где есть сложное взаимодействие между участниками: маркетплейсы, банки. Даже если вы предоставляете услугу напрямую — у вас транзакционный бизнес.
Мы поняли, что Такси именно такой случай, и начали строить нагрузочные тесты с фокусом на интеграционные сценарии. Тесты мы решили запускать прямо в продакшене, потому что собрать полностью идентичное окружение почти невозможно, а для нас, с 1200 микросервисами, это было бы ещё и очень дорого.
На старте мы поставили себе три основные цели:
Проверять реальную нагрузку, а не имитацию.
Моделировать сценарии, максимально приближённые к пользовательским.
Учитывать геораспределение.
Кажется, пора делать? Но не сразу. Чтобы понять, откуда вырос наш подход, нужно немного предыстории.
Откуда взялся наш подход к нагрузочному тестированию
Как и у многих, у нас есть тестинг — оригинальная уменьшенная копия Такси, где разработчики выкатывают фичи, тестировщики тестируют, а потом всё уходит в прод. Проблема в том, что тестинг периодически ломается.
Когда тестинг лежит, разработчикам сложно писать и проверять фичи, тестировщики простаивают. Один день без тестинга — это день потерянной работы, а значит, страдает и бизнес: time‑to‑market растёт.
Первый ответ, который приходит в голову, как решить эту проблему, — интеграционные тесты, а точнее — не пускать новый код в тестинг, пока эти тесты не пройдены. По идее, хорошее решение, но на практике интеграционные тесты часто флапают. И ещё один вечный вопрос: с какими версиями сервисов их гонять — с теми, что уже в тестинге, или с продовыми? Ответов обычно нет, потому что это риторический вопрос из разряда «что было первым — курица или яйцо?».
Тогда мы попробовали обойтись вообще без них. Решение было простым: сделали два эмулятора — один создаёт заказы, другой играет роль водителя и выполняет их. Весь код поместился в один файл, писали за неделю, но импакт — огромный.
Когда в тестинге появляется н��грузка, система «оживает»: на графиках видны успешные и неуспешные запросы; продуктовые метрики начинают прокрашиваться; есть движение, можно что‑то наблюдать. И вот тут появляется настоящая ценность: если тестинг ломается, вы точно видите, в какое время. А значит, проще понять, что и когда его сломало. Не надо гадать, что произошло за последние сутки. Можно откатить конкретный релиз или конфиг.
Тестинг стал стабильнее, мы решили переиспользовать эти же эмуляторы для нагрузочных учений в продакшене — и тут начались вызовы.
Как мы двигались к идеальным нагрузочным
Изоляция в продакшене
В тестинге можно делать что угодно: генерировать фейковые аккаунты, создавать заказы, назначать водителей — никто не пострадает. В продакшене всё обстоит иначе.
Мы не хотим, чтобы реальные пользователи как‑то пересекались с нашими эмуляторами. Представьте своё лицо: вы заказали такси, а к вам выехал Роман Подделник.

Мы начали думать, что же с этим сделать. Первая очевидная мысль — добавить внутри кода условия:
def process_payment(order: Order) -> None: ... if order.tariff.name == 'virtual_orders': # do nothing return ... def dispatch(order: Order) -> Driver: ... if order.tariff.name == 'virtual_orders': return dispatch_virtual_drivers(order)
Чем дольше мы думали над этой идеей, тем больше и больше мест, куда добавить подобные условия, находили — и понимали, что количество этих мест близится к бесконечности.
Мы сделали шаг назад и подумали: как бы нам одних пользователей изолировать от других? Точно, у нас же есть тарифы! Они уже умеют фильтровать исполнителей по классу автомобиля. Если вы транзакционный бизнес, скорее всего, у вас тоже есть уже какие‑то сущности, которые могут отделять одних пользователей от других. Это может быть какое‑то гео, это могут быть, как в нашем случае, тарифы, возможно, какие‑то специальные продукты. И вам не нужно писать никакие IF — вы на базе своей же платформы уже изолируете фейковых пользователей от настоящих.
Но есть ещё один нюанс — эти заказы кто‑то должен оплачивать. Сначала мы подумали, что нам придётся делать фейковый биллинг, но идея показалась сложной. Нам хочется побыстрее сделать нагрузочные, а придумывать какие‑то костыли в платежах — не хочется. Нашли решение проще — делаем заказы бесплатными: не надо обрабатывать платёж, не нужно списывать с карт, биллинг не трогаем вообще.
Эмуляция исполнителей
Следующий вызов — эмуляции исполнителей. У исполнителей, в отличие от пользователей, нет линейного флоу. Они ждут на линии заказ, бесконечно в цикле пытаются получить новый, а при получении начинают его процессить. По сути, эмуляция исполнителя представляет собой граф перехода между статусами.

А ещё наш эмулятор в тестинге написан буквально в одном файле и работает на одном поде. У него есть простая настройка: конфиг, в котором список водителей и список правил, что за них нужно делать. Это простой линейный код, где в цикле запускает нескольких процессов водителей, и они ждут новые заказы.
{ "driver-1": { "location": "msk", ... }, "driver-N": { "location": "spb", ... } ... }
Дано: пул исполнителей, настройки к этим исполнителям и информация, что нужно делать за каждого из них. Нам нужно как‑то научиться распределять их между подами. Ведь если мы даже на очень жирном поде в облаке возьмём и запустим 50 000 водителей — он, скорее всего, не вывезет. Слишком много работы с сетью, упрёмся в сетевое ограничение.
Мы начали изучать варианты, и нам попался Temporal — оркестратор, который умеет хранить состояние, работать как State Machine, выполнять действия по графу. По функциональности — то что надо. Но было несколько нюансов: наша платформа большая, в ней куча обвязки — конфиги, логи, метрики, — а Temporal только внедрялось, и некоторые фичи ещё не поддерживались. Нагрузочные надо запускать уже сейчас, поэтому стали искать другой, более подходящий вариант.
Внутри Яндекса есть сервис с внешними блокировками, но он уже используется в боевом цикле заказа такси. Нагрузить его дополнительно — неэтично, он получит слишком много нагрузки, как от бэкендов Такси, так и от наших нагрузочных.
Мы пошли ещё проще — взяли Postgres, который умеет транзакционно блокировать записи, — то что нужно. В итоге получилось:
задачи эмуляторов записываются в таблицу Postgres;
каждый под пытается «забронировать» исполнителей;
нагрузка равномерно распределяется между нашими подами.
Осталось выбрать, на каком языке писать.
В Яндексе есть много языков, в Такси мы в основном используем C++, golang и python для автоматизаций. Выбрали golang, так как в нём есть горутины, которые очень хорошо подходят для задачи эмуляции множества водителей.

В общих чертах архитектура выглядела просто: крона заполняет задачи, а все поды параллельно пытаются их забронировать и исполнить.
Мы разобрались, как масштабировать нагрузку, научились запускать десятки тысяч водителей, поняли, как с этим работать. Но тут всплыла новая проблема: эмуляция постоянно ломалась.
Поломки эмуляции
Сценарий повторялся: мы проводим нагрузочные, убеждаемся, что бэкенд выдерживает нагрузку, идём дальше разрабатывать фичи. Проходит полгода, нужно снова проверить готовность системы — запускаем нагрузочные и внезапно понимаем, что что‑то сломано, новая фича не учитывает наш тариф или наши автомобили не прошли техосмотр. Чиним, проводим учения, снова забываем на несколько месяцев — и всё повторяется. Это быстро стало похоже на борьбу с симптомами, а не с причиной.

Из‑за этого мы много времени тратили на подготовку к учениям и много сил на попытки сделать так, чтобы всё работало. Начали проводить учения не раз в полгода, а раз в месяц — всё то же самое, ломается. Перешли даже на темп раз в неделю — мы проводим нагрузочные и убеждаемся, что всё хорошо, но потом сценарий повторяется.
Нам это что‑то напомнило — наш тестинг тоже постоянно ломался. Он вчера работал, а сегодня не работает. Так давайте в продакшене тоже включим нагрузку — очень маленькую, но постоянную.
Таким образом, наши графики прокрасятся. Мы будем видеть, что наши фейковые заказы создаются и назначаются, и на графиках будем сразу видеть, что оно сломалось.

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

Распределение нагрузки
Теперь поговорим про распределение нагрузки, и первое, что приходит на ум, — это геораспределение, которое критично для Такси. Разница между ситуацией, где 500 заказов одновременно ищут машину в Москве, и сценарием, где те же 500 заказов равномерно распределены по стране, — колоссальная. В первом случае вы получаете пиковую нагрузку на инфраструктуру в одном регионе, во втором — нагрузка распределяется и система ведёт себя иначе.
Но есть и второй аспект, который часто упускают, — тип роста, к которому вы готовитесь. И это влияет на характер нагрузочных учений не меньше, чем география.
Если вы готовитесь к пиковым нагрузкам — условному новогоднему сценарию для нас — то у вас растёт количество заказов, но не растёт количество исполнителей. В новогоднюю ночь у нас в Такси число заказов может увеличиться в три раза, но водителей при этом становится даже меньше: они тоже празднуют. Такой перекос даёт особую нагрузку на систему: рост RPS, нехватка исполнителей, увеличенное время ожидания.
Если же вы готовитесь к росту бизнеса — то есть к постепенному линейному увеличению и клиентов, и исполнителей, — характер нагрузки меняется. В этом случае важно проверять, как система справляется с увеличением количества исполнителей. Например, у вас может быть in‑memory хранилище данных по исполнителям, которое с ростом объёма просто перестаёт помещаться в память. А если в вашем облаке нет подходящего железа под это, то вы попали в ловушку и вынуждены экстренно менять архитектуру сервиса.
В Такси мы решили, что в основном хотим готовиться к высокому сезону. Мы хотим проверить новогодний сценарий, когда у нас единовременно в три раза больше заказов. В обычное время у нас примерно несколько сотен заказов в секунду, и нам нужно учесть их геораспределение, потому что очень важно понять, где искать исполнителей на эти заказы. А ещё у нас примерно миллион исполнителей онлайн. Но мы помним, что нам нельзя сделать в три раза больше водителей, ведь в Новый год их в три раза больше не будет. Мы начали думать, как же нам конфигурировать свои нагрузочные. Ведь если мы выведем несколько водителей на линию и начнём генерировать сотни заказов в секунду, нам буквально не хватит исполнителей, чтобы вывезти эти заказы.
Первая мысль: чтобы пройти учения, нужно очень быстро вывозить заказы. Представили: исполнители с ранцами‑турбинами, шлемами и GPS, который всё ещё не всегда работает в центре Москвы.
Мы выгрузили из DWH реальные заказы, немного их отфильтровали и выбрали те, что занимают меньше пяти минут — то есть короткие, быстроисполняемые. Стали прогонять учения: подаём 100 заказов в секунду — и что‑то идёт не так, заказы не назначаются. Оказалось, слишком быстро ездить — тоже плохо. Когда исполнители носятся со скоростью больше 100 км/ч, система начинает подозревать фрод.
Поэтому пришлось искусственно ограничить скорость — даже в нагрузочном тесте. Отлаживаем это, добавляем водителей — плюс 50 000 исполнителей (5% от общего кол‑ва исполнителей, что не даёт избыточную нагрузку на систему поиска) — и нагрузка уже успешно вывозится. Это и есть наш идеальный сценарий.
Как мы оцениваем результаты тестиро��ания
Чтобы понять, что всё работает, мы смотрим на дашборды. Их у нас много, но один из основных — со списком эндпоинтов, их таймингами, ok rps, ошибками.


Есть и «царь‑дашборд» — так мы его называем. Это главный дашборд, на который смотрят координаторы инцидентов. На него настроены алерты, и по нему понятно: сервис работает или нет. Мы фильтруем нагрузку так, чтобы было видно: резкий рост заказов — это не сбой, а проходят нагрузочные тесты.

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