Паттерн Kill Switch: мгновенное отключение функций
2 часа ночи. Телефон звонит. Платежи падают. Пользователи злятся. Баг где-то в новом checkout-процессе, который вы задеплоили вчера.
У вас два варианта: откатить весь деплой (рискованно, долго) или исправить баг прямо сейчас (рискованно, долго). Оба не очень.
Но если бы у вас был kill switch, вы бы его переключили. Новый checkout отключён. С тарый checkout работает. Проблема локализована. Исправите баг завтра, когда выспитесь.
В этой статье мы научимся реализовывать kill switches, чтобы быть готовыми, когда что-то пойдёт не так.
Что такое kill switch?
Kill switch — это feature flag, который обычно включён, но может быть мгновенно выключен для отключения функции:
if (config.get('payments-enabled', { default: true })) {
processPayment()
} else {
showMaintenanceMessage()
}В отличие от обычных feature flags (которые часто выключены по умолчанию и включаются для раскатки), kill switches:
- Включены по умолчанию — функция работает нормально
- Аварийное управление — переключаются только при поломке
- Мгновенные — изменения применяются за секунды, а не минуты
Зачем нужны kill switches
Быстрое реагирование на инциденты
Без kill switch:
- Получить алерт в 2 ночи
- Отладить проблему (30+ минут)
- Написать исправление (15+ минут)
- Получить код-ревью (заблокировано до утра?)
- Задеплоить (10+ минут)
- Молиться, что сработает
С kill switch:
- Получить алерт в 2 ночи
- Переключить флаг (30 секунд)
- Вернуться спать
- Исправить нормально завтра
Уменьшение радиуса поражения
Если новая функция ломается, kill switch позволяет отключить только её вместо отката всего:
// Они независимы
if (config.get('new-checkout-enabled', { default: true })) {
// Баг здесь, отключаем это
}
if (config.get('new-search-enabled', { default: true })) {
// Это работает, продолжаем
}Спокойствие
Зная, что можно мгновенно отключить любую функцию, вы меньше нервничаете при деплое. Выпускайте быстрее, чините быстрее.
В чём ключевое отличие между обычным feature flag и kill switch?
Реализация kill switches
Базовый паттерн
Каждый kill switch следует одному паттерну:
if (isFeatureEnabled('feature-name')) {
// Нормальный путь (функция работает)
doFeature()
} else {
// Запасной путь (функция отключена)
showFallback()
}Запасной путь должен быть:
- Дружелюбным к пользователю (не страница ошибки)
- Безопасным (без сломанного состояния)
- Информативным (сообщать пользователям, что происходит)
Реализация в Node.js с удалённой конфигурацией
Вот production-ready реализация:
import { Replane } from '@replanejs/sdk'
interface Configs {
'payments-enabled': boolean
'checkout-enabled': boolean
'notifications-enabled': boolean
}
// Инициализируем клиент конфигурации
const replane = new Replane<Configs>({
defaults: {
// Kill switches по умолчанию true (включены)
'payments-enabled': true,
'checkout-enabled': true,
'notifications-enabled': true,
},
})
await replane.connect({
sdkKey: process.env.REPLANE_SDK_KEY!,
baseUrl: 'https://cloud.replane.dev',
})
function isEnabled(feature: keyof Configs): boolean {
const enabled = replane.get(feature, { default: true })
if (!enabled) {
console.warn(`Kill switch active: ${feature} is disabled`)
}
return enabled
}Использование в коде:
interface OrderResult {
success: boolean
error?: string
message?: string
}
function processOrder(order: Order): OrderResult {
if (!isEnabled('checkout-enabled')) {
return {
success: false,
error: 'Checkout временно недоступен. Пожалуйста, попробуйте позже.',
}
}
// Нормальный checkout-процесс
if (isEnabled('payments-enabled')) {
chargeCustomer(order)
} else {
// Платежи отключены, ставим в очередь
queuePayment(order)
return {
success: true,
message: 'Заказ получен. Оплата будет обработана позже.',
}
}
if (isEnabled('notifications-enabled')) {
sendConfirmationEmail(order)
}
return { success: true }
}Мониторинг активации kill switch
Когда kill switch переключается, вы хотите об этом знать:
replane.subscribe('payments-enabled', (config) => {
if (config.value === false) {
// Kill switch активирован!
alertTeam(`Kill switch активирован: payments-enabled`)
logAudit(`Функция отключена: payments-enabled`)
}
})
replane.subscribe('checkout-enabled', (config) => {
if (config.value === false) {
alertTeam(`Kill switch активирован: checkout-enabled`)
logAudit(`Функция отключена: checkout-enabled`)
}
})Паттерны проектирования для kill switches
Graceful degradation
Когда функция отключена, предоставляйте разумную альтернативу:
function getRecommendations(user) {
if (isEnabled('ml-recommendations')) {
// ML-рекомендации
return mlEngine.recommend(user)
} else {
// Fallback: популярные товары
return getPopularItems()
}
}Пользователь всё ещё получает рекомендации, просто менее персонализированные.
Ставим в очередь
Для критических операций ставьте их в очередь вместо отказа:
function sendNotification(user, message) {
if (isEnabled('notifications-enabled')) {
notificationService.send(user, message)
} else {
// Очередь до восстановления сервиса
notificationQueue.add(user, message)
console.log(`Уведомление в очереди для ${user.id}`)
}
}Понятные сообщения пользователям
Сообщайте пользователям, что происходит:
function processPayment(order) {
if (!isEnabled('payments-enabled')) {
return res.render('payment_unavailable', {
message: 'Наша платёжная система временно недоступна. ' +
'Ваша корзина сохранена, и вы можете завершить оформление, ' +
'когда проблема будет решена.',
retryUrl: `/checkout/${order.id}`,
})
}
// Нормальный процесс оплаты
// ...
}Что должно произойти, когда kill switch отключает функцию оплаты?
Каким функциям нужны kill switches?
Не каждой функции нужен kill switch. Сосредоточьтесь на:
Внешних интеграциях
С торонние API могут падать или иметь outage:
if (isEnabled('stripe-payments')) {
chargeViaStripe(order)
} else if (isEnabled('paypal-payments')) {
chargeViaPaypal(order)
} else {
queuePayment(order)
}Новых или рискованных функциях
Недавние деплои и сложные функции:
if (isEnabled('new-checkout-flow')) {
newCheckout(order)
} else {
legacyCheckout(order)
}Путях с высоким трафиком
Функции, затрагивающие много пользователей:
if (isEnabled('homepage-recommendations')) {
showRecommendations()
} else {
showStaticContent()
}Ресурсоёмких операциях
Функции, которые могут перегрузить систему:
if (isEnabled('real-time-analytics')) {
computeAnalytics()
} else {
showCachedAnalytics()
}Тестирование kill switches
Kill switches тоже нуждаются в тестировании:
Тестируем fallback-путь
describe('processOrder', () => {
it('should queue payment when payments disabled', () => {
// Arrange
mockConfig.set('payments-enabled', false)
// Act
const result = processOrder(testOrder)
// Assert
expect(result.success).toBe(true)
expect(result.message).toContain('Оплата будет обработана позже')
expect(paymentQueue.contains(testOrder)).toBe(true)
})
})Тестируем сам переключатель
describe('kill switch propagation', () => {
it('should update when switch is flipped', async () => {
// Arrange
expect(isEnabled('feature-x')).toBe(true)
// Act
await config.set('feature-x', false)
// Assert (должно обновиться за секунды)
await sleep(2000)
expect(isEnabled('feature-x')).toBe(false)
})
})Симулируем outage в staging
Регулярно переключайте kill switches в staging для проверки fallback’ов:
# Chaos engineering: отключаем случайные функции
./scripts/chaos-test.sh --disable-random-feature