Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Рассказ о крупном проекте с критическим сбоем в продакшене
Коллега, поделюсь реальной историей из практики, которая стала мощнейшим уроком на десятилетия вперед. Речь о полном отказе системы авторизации в крупном банковском приложении для iOS, произошедшем в пятницу вечером, в пик активности пользователей.
Контекст и архитектурные решения
Мы разрабатывали обновление ключевого модуля аутентификации и управления сессиями. Система использовала:
- Кастомную реализацию OAuth 2.0 с refresh-токенами
- Локальное хранилище в Keychain для чувствительных данных
- Собственный кэш сессий в памяти
- Механизм автоматического продления токенов при истечении
Основная архитектурная ошибка была допущена на этапе проектирования — мы решили отказаться от использования проверенных фреймворков (вроде AppAuth) в пользу "более оптимизированного" собственного решения. Код отвечал за критически важную функциональность:
class SessionManager {
private var currentToken: AuthToken?
private let keychainWrapper: KeychainWrapper
private let networkService: AuthNetworkService
func refreshToken(completion: @escaping (Result<Bool, Error>) -> Void) {
guard let refreshToken = keychainWrapper.getRefreshToken() else {
completion(.failure(SessionError.noRefreshToken))
return
}
networkService.refresh(refreshToken) { [weak self] result in
switch result {
case .success(let newToken):
// ОШИБКА: Не атомарная операция!
self?.keychainWrapper.save(newToken)
self?.currentToken = newToken
completion(.success(true))
case .failure(let error):
completion(.failure(error))
}
}
}
}
Цепочка событий, приведшая к катастрофе
-
Пятница, 18:30 — Выкатываем обновление в AppStore. Все тесты пройдены, включая нагрузочное тестирование.
-
20:45 — Первые пользователи начинают жаловаться на "выкидывание" из приложения. Логи показывают странное поведение: токены будто "теряются".
-
21:15 — Количество инцидентов растет в геометрической прогрессии. Мониторинг показывает 100% рост ошибок авторизации.
-
22:00 — Срабатывает алерт "критического уровня". Собирается экстренная команда.
Root Cause Analysis (RCA)
После 4 часов анализа мы обнаружили фатальную комбинацию проблем:
Основные технические причины:
-
Гонка условий (race condition) в обновлении токенов
- При одновременных запросах из разных частей приложения создавалось несколько refresh-запросов
- Более старый токен мог перезаписать новый
-
Некорректная обработка ошибок сети
// АНТИПАТТЕРН: Тихий сброс ошибки func handleNetworkError(_ error: Error) { if error.code == -1009 { // Нет интернета // Просто игнорируем - "пользователь сам разберется" return } // ... остальная обработка } -
Отсутствие механизма отката (rollback)
- При сбое сохранения в Keychain система не возвращалась в предыдущее стабильное состояние
- Пользователи оказывались в "полуаутентифицированном" состоянии
-
Недостаточное логирование
- Критические состояния не логировались
- Отсутствовали трассировочные ID для отслеживания цепочек запросов
Момент прозрения и исправление
В 3:30 ночи, просматривая логи параллельных потоков, я заметил паттерн: проблема возникала именно при быстром переключении между вкладками приложения, что активировало несколько фоновых процессов обновления токена.
Экстренный фикс потребовал:
// Решение: Введение атомарных операций с диспетчеризацией
actor TokenRefreshActor {
private var isRefreshing = false
private var pendingCompletions: [(Result<AuthToken, Error>) -> Void] = []
func safeRefresh(currentToken: AuthToken?) async throws -> AuthToken {
// Если обновление уже идет - ждем его результата
if isRefreshing {
return try await withCheckedThrowingContinuation { continuation in
pendingCompletions.append(continuation.resume(with:))
}
}
isRefreshing = true
defer {
isRefreshing = false
completePendingRequests(with: .failure(SessionError.refreshInProgress))
}
do {
let newToken = try await networkService.refreshToken()
completePendingRequests(with: .success(newToken))
return newToken
} catch {
completePendingRequests(with: .failure(error))
throw error
}
}
}
Бизнес-последствия и извлеченные уроки
Финансовые потери:
- 14 часов простоя критической функциональности
- ~40% активных пользователей не могли совершать операции
- Прямые убытки от невозможности проведения транзакций
- Резкий рост нагрузки на кол-центр
Технические выводы:
-
Не изобретай велосипеды для критической инфраструктуры
- Используй проверенные решения для авторизации, сетевого слоя, хранилищ
-
Конкурентность требует особого подхода
- Все операции с общим состоянием должны быть thread-safe
- Акторы (Swift Concurrency) — твой друг
-
Полноценное тестирование edge cases
- Обязательно тестируй сценарии:
* Потеря сети во время операции
* Параллельные запросы
* Быстрое повторное нажатие кнопок
* Перезапуск приложения в середине критической операции
-
Мониторинг и observability
- Внедри трейсинг запросов
- Настрой алерты на аномальное поведение
- Логируй достаточно, но с умом
-
План отката и feature flags
- Всегда имей возможность отключить функциональность без выпуска новой версии
- Используй phased rollout даже для критических исправлений
Личные выводы
Этот провал научил меня смиренности перед сложностью распределенных систем. Самые опасные баги часто возникают не в основном потоке выполнения, а на стыках, в угловых случаях, при неочевидных взаимодействиях компонентов.
Ключевое изменение в подходе: теперь перед любым значимым изменением я задаю себе вопрос: "Что произойдет, если этот код выполнится дважды одновременно? А если сеть пропадет на середине? А если пользователь закроет приложение прямо сейчас?"
Такой опыт — болезненный, но бесценный. Он формирует инженерную дисциплину, учит уважать сложность и ценить простоту решений. После этого инцидента мы внедрили mandatory code review для критических модулей, практику post-mortem встреч и культуру "безопасного падения" (fail-safe design).