За последние полгода самый сложный баг, с которым я столкнулся, это была race condition в нашей распределённой системе обработки платежей, из-за которой примерно 0,1% транзакций обрабатывались дважды. Это было особенно сложно, потому что происходило только при определённых условиях высокой нагрузки и было почти невозможно воспроизвести в нашей staging-среде. Баг имел серьёзные бизнес-последствия — подрывал доверие клиентов и создавал расхождения в бухгалтерии на тысячи долларов.
Я начал с анализа наших дашбордов мониторинга и заметил, что дублирующиеся транзакции кластеризовались вокруг всплесков трафика. Потом я:
Проблема была в том, что стандартные техники дебага не работали — баг исчезал, когда я добавлял логирование, из-за изменения тайминга выполнения.
После трёх дней расследования я обнаружил, что проблема была в нашей логике проверки idempotency key. Когда два платежа с одинаковым idempotency key приходили почти одновременно:
Thread 1: Проверить наличие ключа → Не найден
Thread 2: Проверить наличие ключа → Не найден
Thread 1: Обработать платёж → Сохранить ключ
Thread 2: Обработать платёж → Сохранить ключ (перезаписать)
Наша Redis-based проверка idempotency имела зазор между операциями чтения и записи. При нормальной нагрузке это окно было незначительным, но во время всплесков трафика оно становилось достаточно большим, чтобы спровоцировать race condition.
Я реализовал distributed lock с помощью Redis-команды SET NX с автоматическим истечением:
Я также поработал с QA-командой, чтобы написать специализированный нагрузочный тест, который мог надёжно воспроизвести race condition и подтвердить, что исправление работает корректно.
После деплоя исправления мы две недели мониторили систему и подтвердили ноль дублирующихся транзакций даже при пиковых нагрузках. Этот опыт научил меня нескольким важным вещам:
Я также задокументировал этот паттерн и рассказал о нём команде, что привело нас к аудиту других критических путей и помогло заранее предотвратить похожие проблемы.
Race condition в системе платежей была в основном вызвана неатомарным паттерном check-then-set в логике валидации idempotency key, где временное окно между операциями чтения и записи позволяло дублировать транзакции при высокой нагрузке.
Новый — ещё не проверен сообществом
Вы