Опирается на правила:
R-RES-CFG-1…R-RES-CFG-3иR-RES-CFG-X1из Resilience Style Guide → раздел 8. Конфигурация.
Важно знать
- Параметры CB, retry, bulkhead и timeout — через
envconfig-теги (default:,required:), не числами прямо в коде.- Это позволяет менять их через env-переменные / Kubernetes ConfigMap без пересборки.
- Defaults задаются тегом
default:в struct-полях; per-system — через префиксSBER_,RECEIPT_,INSURANCE_.- Имя системы в
gobreaker.Settings.Name= ENV-префикс = метрика:sber,receipt,insurance,odnakassa.- Структура одинакова для каждой системы:
*ClientConfigс таймаутами,MaxConcurrent,BaseURL.- Хардкод чисел в
gobreaker.NewCircuitBreaker(...)— скрытая конфигурация; она не управляется через ConfigMap/env.
Параметры resilience — это operational knobs: их подкручивают по реальному поведению в проде. Если они зашиты в Go-коде, любое изменение требует пересборки и выкатки. Если вынесены в env — SRE меняет ConfigMap и перезапускает pod за 30 секунд. Раскрытие раздела 8 гайда для Go-стека.
Конфигурация через envconfig
R-RES-CFG-1: параметры — вне кода, через внешний конфиг.
В Go-стеке стандарт — пакет github.com/kelseyhightower/envconfig. Каждая система получает свою struct с тегами:
// adapters/out/sber/config.go
type SberClientConfig struct {
ConnectTimeout time.Duration `envconfig:"SBER_CONNECT_TIMEOUT" default:"2s"`
ReadTimeout time.Duration `envconfig:"SBER_READ_TIMEOUT" default:"10s"`
CallTimeout time.Duration `envconfig:"SBER_CALL_TIMEOUT" default:"15s"`
MaxConcurrent int `envconfig:"SBER_MAX_CONCURRENT" default:"20"`
BaseURL string `envconfig:"SBER_BASE_URL" required:"true"`
CBWindowSize int `envconfig:"SBER_CB_WINDOW_SIZE" default:"50"`
CBMinRequests int `envconfig:"SBER_CB_MIN_REQUESTS" default:"10"`
CBFailureRate float64 `envconfig:"SBER_CB_FAILURE_RATE" default:"0.30"` // платёжная — порог ниже
CBOpenTimeout time.Duration `envconfig:"SBER_CB_OPEN_TIMEOUT" default:"30s"`
CBMaxRequests int `envconfig:"SBER_CB_MAX_REQUESTS" default:"3"`
RetryAttempts int `envconfig:"SBER_RETRY_ATTEMPTS" default:"3"`
RetryDelay time.Duration `envconfig:"SBER_RETRY_DELAY" default:"200ms"`
}
// adapters/out/receipt/config.go
type ReceiptClientConfig struct {
ConnectTimeout time.Duration `envconfig:"RECEIPT_CONNECT_TIMEOUT" default:"2s"`
ReadTimeout time.Duration `envconfig:"RECEIPT_READ_TIMEOUT" default:"8s"`
CallTimeout time.Duration `envconfig:"RECEIPT_CALL_TIMEOUT" default:"12s"`
MaxConcurrent int `envconfig:"RECEIPT_MAX_CONCURRENT" default:"15"`
BaseURL string `envconfig:"RECEIPT_BASE_URL" required:"true"`
CBWindowSize int `envconfig:"RECEIPT_CB_WINDOW_SIZE" default:"50"`
CBMinRequests int `envconfig:"RECEIPT_CB_MIN_REQUESTS" default:"10"`
CBFailureRate float64 `envconfig:"RECEIPT_CB_FAILURE_RATE" default:"0.50"`
CBOpenTimeout time.Duration `envconfig:"RECEIPT_CB_OPEN_TIMEOUT" default:"30s"`
}
Чтение при старте:
// adapters/out/config.go
type OutboundConfig struct {
Sber SberClientConfig
Receipt ReceiptClientConfig
Insurance InsuranceClientConfig
}
func LoadOutboundConfig() (OutboundConfig, error) {
var cfg OutboundConfig
if err := envconfig.Process("", &cfg.Sber); err != nil {
return OutboundConfig{}, fmt.Errorf("sber config: %w", err)
}
if err := envconfig.Process("", &cfg.Receipt); err != nil {
return OutboundConfig{}, fmt.Errorf("receipt config: %w", err)
}
if err := envconfig.Process("", &cfg.Insurance); err != nil {
return OutboundConfig{}, fmt.Errorf("insurance config: %w", err)
}
return cfg, nil
}
Kubernetes ConfigMap с этими переменными меняется без пересборки. SRE в инцидент снижает SBER_CB_FAILURE_RATE=0.20 и перекатывает под.
Defaults vs per-system
R-RES-CFG-2: общие типовые значения идут через теги default:, per-system отличия — в соответствующей struct.
Если у большинства систем порог failure rate 0.50, а у платёжной 0.30 — это видно сразу при чтении struct:
// receipt — стандартный порог
CBFailureRate float64 `envconfig:"RECEIPT_CB_FAILURE_RATE" default:"0.50"`
// sber — критичная система, порог ниже
CBFailureRate float64 `envconfig:"SBER_CB_FAILURE_RATE" default:"0.30"`
Это отклонение от стандарта немедленно заметно при code review. Если кто-то добавляет новую систему Insurance и забывает обосновать 0.20 — вопрос возникнет сам.
В Kubernetes deployment.yaml env-переменные группируются по префиксу системы, не вперемежку:
env:
# sber
- name: SBER_BASE_URL
valueFrom:
secretKeyRef:
name: sber-credentials
key: base-url
- name: SBER_CALL_TIMEOUT
value: "15s"
- name: SBER_CB_FAILURE_RATE
value: "0.30"
# receipt
- name: RECEIPT_BASE_URL
valueFrom:
secretKeyRef:
name: receipt-credentials
key: base-url
- name: RECEIPT_CALL_TIMEOUT
value: "12s"
При появлении новой системы OdnaKassa — добавляется struct OdnaKassaClientConfig с тегами ODNAKASSA_* и defaults, и новая группа env-переменных в ConfigMap.
Имена систем — единая ось
R-RES-CFG-3: имя системы в gobreaker.Settings.Name = ENV-префикс = метрика. Это одна и та же строка, везде.
// adapters/out/sber/sber_adapter.go
func newSberBreaker(cfg SberClientConfig) *gobreaker.CircuitBreaker {
return gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "sber", // ← имя для логов, метрик
MaxRequests: uint32(cfg.CBMaxRequests),
Timeout: cfg.CBOpenTimeout,
ReadyToTrip: func(counts gobreaker.Counts) bool {
if counts.Requests < uint32(cfg.CBMinRequests) {
return false
}
ratio := float64(counts.TotalFailures) / float64(counts.Requests)
return ratio >= cfg.CBFailureRate
},
OnStateChange: func(name string, from, to gobreaker.State) {
slog.Warn("circuit breaker state changed",
"system", name,
"prev_state", from.String(),
"new_state", to.String(),
)
cbStateGauge.WithLabelValues(name).Set(float64(to))
},
})
}
Prometheus-метрика circuit_breaker_state{system="sber"} содержит то же "sber", что и ENV-префикс SBER_ и поле slog. SRE, глядя в Grafana на system="sber", сразу знает, какой ConfigMap-ключ менять.
Имена — короткие, строчные, без окружения и хостов: sber, odnakassa, insurance, receipt. Не sber-payment-prod-eu-west-1.
Конфигурация и сборка адаптера
Конфиг передаётся в конструктор адаптера, внутри которого собирается весь resilience-стек:
// adapters/out/sber/sber_adapter.go
type SberAdapter struct {
client *http.Client
breaker *gobreaker.CircuitBreaker
sem *semaphore.Weighted
cfg SberClientConfig
}
func NewSberAdapter(cfg SberClientConfig) *SberAdapter {
return &SberAdapter{
client: newSberHTTPClient(cfg),
breaker: newSberBreaker(cfg),
sem: semaphore.NewWeighted(int64(cfg.MaxConcurrent)),
cfg: cfg,
}
}
Параметры не дублируются: конфиг читается один раз при старте, передаётся в конструктор, используется при инициализации http.Transport, gobreaker.Settings и semaphore.Weighted. Если SBER_MAX_CONCURRENT=25 — http.Transport.MaxIdleConnsPerHost и semaphore.NewWeighted(25) получат одно значение.
Для CustomerAdapter (внутренний s2s) набор меньше — timeout + CB, без bulkhead и retry (по R-RES-WHERE-2):
// adapters/out/customer/config.go
type CustomerClientConfig struct {
ConnectTimeout time.Duration `envconfig:"CUSTOMER_CONNECT_TIMEOUT" default:"1s"`
ReadTimeout time.Duration `envconfig:"CUSTOMER_READ_TIMEOUT" default:"5s"`
CallTimeout time.Duration `envconfig:"CUSTOMER_CALL_TIMEOUT" default:"8s"`
BaseURL string `envconfig:"CUSTOMER_BASE_URL" required:"true"`
CBFailureRate float64 `envconfig:"CUSTOMER_CB_FAILURE_RATE" default:"0.50"`
CBOpenTimeout time.Duration `envconfig:"CUSTOMER_CB_OPEN_TIMEOUT" default:"30s"`
}
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
gobreaker.NewCircuitBreaker(gobreaker.Settings{Timeout: 30 * time.Second}) числами в коде | R-RES-CFG-X1 | Поле конфига CBOpenTimeout с тегом default:"30s" |
Один SberClientConfig с полями на несколько систем (SberCallTimeout, ReceiptCallTimeout) | R-RES-CFG-2 | Отдельная struct per-system |
gobreaker.Settings{Name: "payment"} при ENV-префиксе SBER_ | R-RES-CFG-3 | Name: "sber" — единое имя везде |
Разные имена CB и bulkhead одной системы ("sber-cb" и "sber-bh") | R-RES-CFG-3 | "sber" везде |
envconfig.Process("", &cfg) без проверки ошибки | R-RES-CFG-1 | Проверять и возвращать ошибку при старте |
MaxConcurrent: 20 в конструкторе без конфига | R-RES-CFG-X1 | semaphore.NewWeighted(int64(cfg.MaxConcurrent)) |
Куда дальше
- Изоляция по системам — почему имя в
gobreaker.Settings.Nameсовпадает с ENV-префиксом и метрикой. - Circuit Breaker — параметры
ReadyToTrip,Timeout,MaxRequests, slow-call через контекстный timeout. - Bulkhead — sizing
semaphore.NewWeightedотносительноMaxIdleConnsPerHost. - Retry —
retry.Attempts,retry.BackOffDelay,RetryIf— без 4xx иgobreaker.ErrOpenState. - Timeouts — иерархия
connectTimeout < readTimeout < callTimeoutвhttp.Transport. - Observability — метрики
promautoи OTel-атрибуты при tuning по метрикам. - Health Checks — TTL-кеш probe и readiness по всем системам.
- Async и polling — task-queue вместо
time.Sleep-цикла при долгом retry. - Fallback — деградация без money-операций и без тихого «успеха».
- Где какая защита — полный набор по типу вызова (external / internal / scheduler).