Алертер, коннекторы и правила: как мы научили систему меняться без релизов
2025-09-16 • Alexander Popov
Чтобы продукты не «застывали» между релизами, мы перенесли ключевые настройки в данные. Ниже — как это устроено и зачем так жить.
Проблема
Банальные вещи — ключи, токены, URL, прайс-сетки, когорты, бизнес-правила — меняются чаще, чем выпускаются версии.
Релиз ради правки процента скидки — дорого и рискованно.
Идея
- Динамические коннекторы (секреты/URLs/прайсы) лежат в Redis/PG.
- Правила (кохортная логика, фильтры, лимиты) — это данные (YAML/JSON) в Redis Hash.
- Глобальное read-only состояние держим в
atomic.Value: меняем весь снапшот атомарно, без гонок. - Environment-expansion — только для строк, без «магии» внутри структур.
- Singleton-лоадеры с
sync.RWMutex: один источник истины и безопасные обновления.
Упрощённый эскиз загрузчика (Go)
package config
import (
"context"
"sync"
"sync/atomic"
)
type DynamicConfig struct {
Data map[string]any
}
var (
dyn atomic.Value // хранит DynamicConfig
mu sync.RWMutex
)
type Source interface {
Fetch(ctx context.Context) ([]byte, error) // Redis HGETALL / PG query
}
func GetConfig() DynamicConfig {
if v := dyn.Load(); v != nil {
return v.(DynamicConfig)
}
return DynamicConfig{Data: map[string]any{}}
}
func LoadAndSet(ctx context.Context, src Source) error {
mu.Lock()
defer mu.Unlock()
raw, err := src.Fetch(ctx)
if err != nil {
return err
}
data := parse(raw) // JSON/YAML → map[string]any
expandEnvInPlace(data) // подстановка $VARS только в строках
dyn.Store(DynamicConfig{Data: data}) // атомарная замена снапшота
return nil
}
Идея: меняем данные — меняется поведение. Релиз не нужен.
Правила из Redis без релиза
- Храним Hash: ключ — текстовый
rule_id, значение — YAML/JSON с полямиis_active,timeframe,body,name. - Лоадер собирает срез правил и отдаёт иммутабельный снапшот воркерам.
- Воркеры ничего не мутируют, только читают актуальный снапшот.
Почему это стабильно
- Нет «чтения посередине обновления»:
atomic.Storeменяет указатель целиком. - Воркеры держат актуальную ссылку и не ждут глобальных блокировок.
- Обновления конфигов не требуют рестарта/релиза.
Наблюдаемость и безопасность
- Гистограммы lat/err для всех внешних вызовов, трассировки по задачам Asynq/Kafka.
- Алерты по SLO (ошибка/латентность), а не «по ощущениям».
- Secrets — только в секрет-хранилище, доступы по минимальным привилегиям.
- Аудит изменений: кто/что/когда — писаем событие и видим в дашбордах.
Инциденты, которые сделали нас лучше
- S3/Auth: «invalid semicolon…» — отказались от «умных» клиентов, сделали явный сигнинг/энкодинг запросов.
- Import cycles (Go): вынесли зависимости в интерфейсы и адаптеры, разорвали круг.
DISTINCTв window-функциях (Postgres): переписали на CTE + явныйGROUP BY, удержали производительность индексами.
Как это помогает продуктам
- Calendar / Tools — справочники и источники обновляются мгновенно.
- Nutcrackers — правила и лимиты включаются без релиза.
- Open Academy (Nuts Farm) — цены/когорты/эксперименты меняем данными, метрики и биллинг видны в Grafana.
Как работаем с клиентами и партнёрами
- Совместный бэклог и weekly-демо.
- Feature flags, превью-окружения, откаты одной кнопкой.
- Отчётность: бизнес-KPI + инженерные метрики в одном дашборде.
Вывод
Мы сделали систему, которая меняется быстрее релизов. Это снижает риск, ускоряет эксперименты и делает продукты устойчивее.