Files
futriix/internal/cluster/backpressure.go

302 lines
10 KiB
Go
Raw Normal View History

2026-07-12 01:39:26 +03:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/cluster/backpressure.go
// Назначение: Backpressure при перегрузке системы
package cluster
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// BackpressureLevel представляет уровень перегрузки
type BackpressureLevel int
const (
LevelNone BackpressureLevel = iota // Нет перегрузки
LevelLow // Низкая перегрузка - задержки
LevelMedium // Средняя перегрузка - отклонение части запросов
LevelHigh // Высокая перегрузка - отклонение большинства
LevelCritical // Критическая - только чтение
)
// BackpressureManager управляет backpressure
type BackpressureManager struct {
mu sync.RWMutex
currentLevel BackpressureLevel
cpuThreshold float64
memoryThreshold float64
queueSizeThreshold int
connectionThreshold int
currentCPU atomic.Uint64
currentMemory atomic.Uint64
currentQueueSize atomic.Int64
currentConnections atomic.Int64
rejectedCount atomic.Uint64
delayedCount atomic.Uint64
lastCheck time.Time
checkInterval time.Duration
logger LoggerInterface // Используем LoggerInterface из node.go
enabled bool
writeAllowed bool
readAllowed bool
rejectProbability atomic.Uint32
delayDuration atomic.Int64
}
// BackpressureConfig содержит настройки backpressure
type BackpressureConfig struct {
Enabled bool `json:"enabled"`
CPUThreshold float64 `json:"cpu_threshold"`
MemoryThreshold float64 `json:"memory_threshold"`
QueueSizeThreshold int `json:"queue_size_threshold"`
ConnectionThreshold int `json:"connection_threshold"`
CheckIntervalMs int `json:"check_interval_ms"`
LowDelayMs int64 `json:"low_delay_ms"`
MediumRejectProb uint32 `json:"medium_reject_prob"`
HighRejectProb uint32 `json:"high_reject_prob"`
}
// DefaultBackpressureConfig возвращает конфигурацию по умолчанию
func DefaultBackpressureConfig() *BackpressureConfig {
return &BackpressureConfig{
Enabled: true,
CPUThreshold: 0.8,
MemoryThreshold: 0.85,
QueueSizeThreshold: 10000,
ConnectionThreshold: 5000,
CheckIntervalMs: 1000,
LowDelayMs: 100,
MediumRejectProb: 30,
HighRejectProb: 70,
}
}
// NewBackpressureManager создаёт новый менеджер backpressure
func NewBackpressureManager(cfg *BackpressureConfig, logger LoggerInterface) *BackpressureManager {
if cfg == nil {
cfg = DefaultBackpressureConfig()
}
bpm := &BackpressureManager{
currentLevel: LevelNone,
cpuThreshold: cfg.CPUThreshold,
memoryThreshold: cfg.MemoryThreshold,
queueSizeThreshold: cfg.QueueSizeThreshold,
connectionThreshold: cfg.ConnectionThreshold,
checkInterval: time.Duration(cfg.CheckIntervalMs) * time.Millisecond,
logger: logger,
enabled: cfg.Enabled,
writeAllowed: true,
readAllowed: true,
rejectProbability: atomic.Uint32{},
delayDuration: atomic.Int64{},
}
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(0)
if cfg.Enabled {
go bpm.monitorLoop()
}
if logger != nil {
logger.Debug("Backpressure manager initialized")
}
return bpm
}
// monitorLoop периодически проверяет метрики
func (bpm *BackpressureManager) monitorLoop() {
ticker := time.NewTicker(bpm.checkInterval)
defer ticker.Stop()
for range ticker.C {
bpm.updateLevel()
}
}
// updateLevel обновляет уровень перегрузки
func (bpm *BackpressureManager) updateLevel() {
cpu := float64(bpm.currentCPU.Load()) / 100.0
memory := float64(bpm.currentMemory.Load()) / 100.0
queueSize := bpm.currentQueueSize.Load()
connections := bpm.currentConnections.Load()
newLevel := LevelNone
if cpu >= bpm.cpuThreshold || memory >= bpm.memoryThreshold {
newLevel = LevelHigh
} else if queueSize > int64(bpm.queueSizeThreshold) {
if queueSize > int64(bpm.queueSizeThreshold*2) {
newLevel = LevelCritical
} else {
newLevel = LevelMedium
}
} else if connections > int64(bpm.connectionThreshold) {
newLevel = LevelLow
}
bpm.mu.Lock()
oldLevel := bpm.currentLevel
bpm.currentLevel = newLevel
bpm.mu.Unlock()
// Применяем политики в зависимости от уровня
bpm.applyPolicies(newLevel)
if oldLevel != newLevel && bpm.logger != nil {
bpm.logger.Info(fmt.Sprintf("Backpressure level changed from %v to %v (cpu=%.2f%%, mem=%.2f%%, queue=%d, conns=%d)",
bpm.levelToString(oldLevel), bpm.levelToString(newLevel), cpu*100, memory*100, queueSize, connections))
}
}
// applyPolicies применяет политики в зависимости от уровня
func (bpm *BackpressureManager) applyPolicies(level BackpressureLevel) {
bpm.mu.Lock()
defer bpm.mu.Unlock()
switch level {
case LevelNone:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(0)
case LevelLow:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(0)
bpm.delayDuration.Store(100) // 100ms задержка
case LevelMedium:
bpm.writeAllowed = true
bpm.readAllowed = true
bpm.rejectProbability.Store(30) // 30% отклонение
bpm.delayDuration.Store(200)
case LevelHigh:
bpm.writeAllowed = false // Запись запрещена
bpm.readAllowed = true
bpm.rejectProbability.Store(70) // 70% отклонение
bpm.delayDuration.Store(500)
case LevelCritical:
bpm.writeAllowed = false
bpm.readAllowed = true // Только чтение
bpm.rejectProbability.Store(90)
bpm.delayDuration.Store(1000)
}
}
// BeforeRequest вызывается перед обработкой запроса
func (bpm *BackpressureManager) BeforeRequest(isWrite bool) error {
if !bpm.enabled {
return nil
}
bpm.mu.RLock()
level := bpm.currentLevel
writeAllowed := bpm.writeAllowed
readAllowed := bpm.readAllowed
rejectProb := bpm.rejectProbability.Load()
delayDur := bpm.delayDuration.Load()
bpm.mu.RUnlock()
// Проверяем разрешение на операцию
if isWrite && !writeAllowed {
bpm.rejectedCount.Add(1)
return fmt.Errorf("write operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
}
if !isWrite && !readAllowed {
bpm.rejectedCount.Add(1)
return fmt.Errorf("read operations rejected due to backpressure (level: %v)", bpm.levelToString(level))
}
// Вероятностное отклонение
if rejectProb > 0 {
// Простая вероятностная проверка
if uint32(time.Now().UnixNano()%100) < rejectProb {
bpm.rejectedCount.Add(1)
return fmt.Errorf("request rejected due to backpressure (probability: %d%%)", rejectProb)
}
}
// Добавляем задержку если нужно
if delayDur > 0 {
bpm.delayedCount.Add(1)
time.Sleep(time.Duration(delayDur) * time.Millisecond)
}
return nil
}
// AfterRequest вызывается после обработки запроса
func (bpm *BackpressureManager) AfterRequest(duration time.Duration, success bool) {
// Можно использовать для дополнительной статистики
}
// UpdateMetrics обновляет метрики для backpressure
func (bpm *BackpressureManager) UpdateMetrics(cpuPercent, memoryPercent uint64, queueSize, connections int64) {
bpm.currentCPU.Store(cpuPercent)
bpm.currentMemory.Store(memoryPercent)
bpm.currentQueueSize.Store(queueSize)
bpm.currentConnections.Store(connections)
}
// GetCurrentLevel возвращает текущий уровень перегрузки
func (bpm *BackpressureManager) GetCurrentLevel() BackpressureLevel {
bpm.mu.RLock()
defer bpm.mu.RUnlock()
return bpm.currentLevel
}
// GetStats возвращает статистику backpressure
func (bpm *BackpressureManager) GetStats() map[string]interface{} {
bpm.mu.RLock()
defer bpm.mu.RUnlock()
return map[string]interface{}{
"current_level": bpm.levelToString(bpm.currentLevel),
"write_allowed": bpm.writeAllowed,
"read_allowed": bpm.readAllowed,
"reject_probability": bpm.rejectProbability.Load(),
"delay_ms": bpm.delayDuration.Load(),
"rejected_count": bpm.rejectedCount.Load(),
"delayed_count": bpm.delayedCount.Load(),
"cpu_threshold": bpm.cpuThreshold,
"memory_threshold": bpm.memoryThreshold,
"queue_threshold": bpm.queueSizeThreshold,
"conn_threshold": bpm.connectionThreshold,
}
}
func (bpm *BackpressureManager) levelToString(level BackpressureLevel) string {
switch level {
case LevelNone:
return "none"
case LevelLow:
return "low"
case LevelMedium:
return "medium"
case LevelHigh:
return "high"
case LevelCritical:
return "critical"
default:
return "unknown"
}
}