Git bisect под нагрузкой: что ломается первым
Регрессия проявляется только при сотне параллельных запросов, а обычный тест её не видит. Разбираем, как провести Git bisect на нагрузочном сценарии и не принять нестабильный тест за плохой коммит.

Если после одного из релизов сервис начинает отдавать ошибки или резко увеличивает задержку только под нагрузкой, обычный поиск по логам быстро упирается в десятки изменений. git bisect сокращает область поиска до одного коммита, но на нагрузочном баге первым часто ломается не код, а сам эксперимент: тест становится недетерминированным, генератор нагрузки упирается в собственный лимит, а состояние базы переносится между запусками.
Что именно ищет Git bisect
Команда выполняет двоичный поиск между известным хорошим и плохим состояниями репозитория. Git последовательно переключает рабочее дерево на промежуточный коммит, а вы или автоматический тест сообщаете результат: проблема есть, проблемы нет или этот запуск нельзя оценить.
Для обычного теста граница выглядит просто: старые коммиты проходят, новые падают. Для нагрузки критерий должен быть формальным. Например: «тест считается плохим, если доля ответов 5xx выше порога или p95 задержки превышает согласованный предел». Сам порог берите из требований проекта и зафиксированного базового прогона, а не выбирайте после просмотра результатов.
Bisect не доказывает, что найденный коммит породил проблему. Он показывает первый коммит, на котором ваш тест начал стабильно классифицировать состояние как плохое.
Почему нагрузочный bisect даёт ложные результаты
Случайный тест вместо бинарного оракула
Оракул теста[1] — правило, которое превращает результат запуска в good или bad. Если один и тот же коммит иногда проходит, а иногда падает, двоичный поиск теряет смысл: одна ошибочная классификация сдвигает границу в неправильную сторону.
Причина может быть в фоновой сборке мусора, разогреве кеша, конкуренции за CPU, внешнем сервисе или недостаточной длительности прогона. Повторите базовый коммит несколько раз до старта bisect. Если результаты расходятся, сначала стабилизируйте тест или используйте явное решение skip для непригодного запуска.
Генератор нагрузки стал узким местом
При росте параллельности генератор может исчерпать CPU, файловые дескрипторы или сетевые соединения раньше приложения. Тогда вы измеряете не сервер, а машину, которая отправляет запросы. Разнесите генератор и тестируемый сервис по узлам либо убедитесь по метрикам, что у генератора есть запас.
Состояние переживает смену коммита
Git переключает файлы, но не откатывает базу данных, Redis, очередь, локальные кеши и созданные тестом файлы. Из-за этого следующий коммит получает уже прогретое или повреждённое состояние. Для каждого прогона нужен одинаковый способ очистки или восстановления окружения.
Как подготовить сценарий до запуска
- Зафиксируйте границы. Найдите коммит, на котором проблема точно воспроизводится, и последний коммит, на котором её точно нет. Запишите версии Git, рантайма, базы, схему нагрузки, длительность и лимит результата. Проверка: эти два коммита можно отдельно развернуть и получить ожидаемую классификацию.
- Отделите сборку от теста. Для каждого коммита собирайте приложение заново либо используйте кеш только там, где он не меняет результат. Проверка: артефакт содержит идентификатор текущего коммита, а не остался от предыдущего запуска.
- Подготовьте чистое состояние. Восстановите тестовую базу из одного снимка, очистите очередь и кеши, дождитесь готовности зависимостей. Проверка: стартовые версии схемы, количество тестовых записей и состояние очередей совпадают между прогонами.
- Проверьте повторяемость. Запустите хороший и плохой коммиты несколько раз в одинаковых условиях. Проверка: хороший стабильно проходит порог, плохой стабильно его пересекает; если нет, bisect пока не запускайте.
- Снимите служебные метрики. Сохраняйте время прогона, число запросов, ошибки, p50 и p95 или другую выбранную метрику, загрузку CPU и памяти на генераторе и сервисе. Проверка: по результату можно понять, упёрся ли тест в приложение или в инфраструктуру теста.
Запуск bisect для нагрузки
Ниже приведены команды Git 2.44 и новее; базовая схема git bisect давно стабильна, но поведение конкретного нагрузочного раннера зависит от его версии. Перед началом сохраните незакоммиченные изменения или уберите их во временную ветку: bisect переключает рабочее дерево.
git status
git bisect start
git bisect bad <bad-commit>
git bisect good <good-commit>
git bisect run ./ci/bisect-load-test.sh
Файл ./ci/bisect-load-test.sh должен запускать именно ваш нагрузочный сценарий и завершаться кодом, который Git понимает однозначно. Это не универсальная команда конкретного генератора: имя и параметры теста берите из документации используемого инструмента и текущего репозитория.
#!/usr/bin/env bash
set -u
set -o pipefail
# Скрипт проекта должен сам собрать текущий коммит,
# подготовить чистое окружение и записать отчёт.
./ci/run-load-regression.sh
result=$?
if [ "$result" -eq 0 ]; then
exit 0
fi
# Код 125 означает: этот коммит нельзя надёжно оценить.
if [ "$result" -eq 125 ]; then
exit 125
fi
exit 1
Нулевой код означает good, ненулевой — bad, а код 125 просит Git пропустить ревизию. Не превращайте в bad ошибки подготовки окружения: если база не восстановилась, сервис не собрался из-за отсутствующей зависимости или генератор не стартовал, это повод завершить запуск кодом 125 и разобраться отдельно.
После завершения Git сообщит первый коммит, на котором тест зафиксировал bad. Проверьте его вручную и запустите сценарий на родителе и самом коммите несколько раз. Затем завершите режим bisect:
git bisect reset
Что ломается первым: четыре типичных сценария
| Симптом | Что, вероятно, сломалось | Как проверить |
|---|---|---|
| Один и тот же коммит чередует good и bad | Оракул или окружение недетерминированы | Повторить прогон на чистом состоянии и сравнить распределение результатов |
| Ошибки появляются только при увеличении параллельности | Лимит соединений, пул потоков, очередь или блокировка | Проверить метрики сервиса и генератора, затем прогнать несколько фиксированных уровней нагрузки |
| Первые коммиты проходят, затем тест начинает падать по тайм-ауту | Накопилось состояние: кеш, очередь, временные файлы или данные | Сравнить результаты после полного восстановления окружения |
| Все ревизии падают одинаково | Проблема вне исследуемого диапазона или сломана сборка теста | Проверить границы на чистом стенде и отдельно выполнить health-check |
Где подход ломается
Плохой коммит не единственный виновник
Регрессия может зависеть от конфигурации, версии ядра, размера данных или внешнего сервиса. Bisect по приложению найдёт изменение, после которого проявление стало возможным в текущей среде, но не обязательно корень всей цепочки. Поэтому сохраняйте конфигурацию и версии зависимостей рядом с результатом каждого прогона.
Изменились сами тесты или контракт
Если между коммитами менялся формат ответа, схема базы или маршрут, один и тот же нагрузочный сценарий перестаёт быть сопоставимым. В таком случае диапазон нужно разделить, а проверку адаптировать к контракту каждой эпохи. Иначе bisect будет искать коммит, где тест стал несовместимым, а не коммит с регрессией производительности.
Проблема появляется только после прогрева
Короткий прогон может не дойти до утечки памяти, заполнения пула или накопления очереди. Длинный прогон увеличивает точность, но резко повышает цену одного шага bisect. Сначала определите минимальную длительность, при которой хороший и плохой коммиты разделяются стабильно, затем используйте её для поиска.
Как читать найденный коммит
Не ограничивайтесь строкой с хешем. Сравните его с родителем и задайте три вопроса: изменился ли путь обработки запроса, появились ли новые операции ввода-вывода, и поменялись ли лимиты параллелизма или размеры пулов. Отдельно проверьте миграции и конфигурацию: код мог быть лишь триггером для уже существующей проблемы данных.
Полезный результат bisect — не только коммит, но и минимальный воспроизводимый сценарий: нагрузка, длительность, порог, состояние данных и наблюдаемая метрика. Без этого исправление трудно проверить, а повторный инцидент снова превратится в поиск по истории.
Что делать дальше
Сохраните скрипт bisect в репозитории, но не запускайте его на каждом коммите в обычном CI без ограничений: нагрузочный поиск дорог по времени и ресурсам. Для продакшена добавьте отдельный регрессионный тест с фиксированной нагрузкой, следите за p95, долей ошибок, насыщением пулов и памятью. После исправления повторите прогон на найденном коммите, его родителе и актуальной вершине ветки.
[1] В инженерной документации такой оракул часто называют test oracle: это проверяемое правило, по которому тест решает, считается ли результат правильным.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Официальная документация Git: git-bisect, Официальная документация Git: git-bisect-lk
Частые вопросы
Сколько раз повторять нагрузочный тест на одном коммите?
Что делать, если git bisect нашёл коммит, но баг не повторяется вручную?
Что произойдёт, если один из коммитов не собирается?
Можно ли запускать bisect прямо на продакшене?
Что делать, если баг зависит от базы или очереди?
Можно ли использовать другую версию Git?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.