Update internal/autoscaling/cluster_autoscaler.go

This commit is contained in:
gvsafronov committed 2026-09-16 21:51:20 +00:00
1 parent 17bc83d221
commit 891fcd04c1
1 file changed
+399 -314
+399 -314
View File
@@ -11,433 +11,518 @@
package autoscaling package autoscaling
import ( import (
"fmt" "fmt"
"sync" "math"
"sync/atomic" "sync"
"time" "sync/atomic"
"time"
"futriis/internal/config" "futriis/internal/config"
"futriis/internal/log" "futriis/internal/log"
) )
// =============================================================================
// СОВМЕСТИМЫЕ АТОМАРНЫЕ ОБЁРТКИ (Go 1.13+)
// =============================================================================
// В Go 1.19 появились atomic.Bool, atomic.Int32, atomic.Int64,
// atomic.Uint32, atomic.Uint64, atomic.Float64. Для совместимости
// с более старыми версиями Go (в частности, на OpenIndiana, где
// пакет Go может быть устаревшим) реализуем собственные обёртки.
// atomicBool — совместимая замена atomic.Bool.
type atomicBool struct {
v int32
}
func (b *atomicBool) Load() bool { return atomic.LoadInt32(&b.v) != 0 }
func (b *atomicBool) Store(val bool) {
atomic.StoreInt32(&b.v, boolToInt32(val))
}
func (b *atomicBool) CompareAndSwap(old, new bool) bool {
return atomic.CompareAndSwapInt32(&b.v, boolToInt32(old), boolToInt32(new))
}
func boolToInt32(b bool) int32 {
if b {
return 1
}
return 0
}
// atomicInt32 — совместимая замена atomic.Int32.
type atomicInt32 struct {
v int32
}
func (a *atomicInt32) Load() int32 { return atomic.LoadInt32(&a.v) }
func (a *atomicInt32) Store(val int32) { atomic.StoreInt32(&a.v, val) }
func (a *atomicInt32) Add(delta int32) int32 { return atomic.AddInt32(&a.v, delta) }
func (a *atomicInt32) CompareAndSwap(old, new int32) bool {
return atomic.CompareAndSwapInt32(&a.v, old, new)
}
// atomicInt64 — совместимая замена atomic.Int64.
type atomicInt64 struct {
v int64
}
func (a *atomicInt64) Load() int64 { return atomic.LoadInt64(&a.v) }
func (a *atomicInt64) Store(val int64) { atomic.StoreInt64(&a.v, val) }
func (a *atomicInt64) Add(delta int64) int64 { return atomic.AddInt64(&a.v, delta) }
// atomicUint64 — совместимая замена atomic.Uint64.
type atomicUint64 struct {
v uint64
}
func (a *atomicUint64) Load() uint64 { return atomic.LoadUint64(&a.v) }
func (a *atomicUint64) Store(val uint64) { atomic.StoreUint64(&a.v, val) }
func (a *atomicUint64) Add(delta uint64) uint64 { return atomic.AddUint64(&a.v, delta) }
// atomicFloat64 — совместимая замена atomic.Float64.
// Реализована через atomic.Uint64 и math.Float64bits.
type atomicFloat64 struct {
v uint64
}
func (f *atomicFloat64) Load() float64 {
return math.Float64frombits(atomic.LoadUint64(&f.v))
}
func (f *atomicFloat64) Store(val float64) {
atomic.StoreUint64(&f.v, math.Float64bits(val))
}
// =============================================================================
// ClusterAutoscaler
// =============================================================================
// ClusterAutoscaler управляет автоматическим масштабированием кластера // ClusterAutoscaler управляет автоматическим масштабированием кластера
type ClusterAutoscaler struct { type ClusterAutoscaler struct {
config *config.AutoscalingConfig config *config.AutoscalingConfig
logger *log.Logger logger *log.Logger
mu sync.RWMutex mu sync.RWMutex
stopChan chan struct{} stopChan chan struct{}
wg sync.WaitGroup stopOnce sync.Once
running atomic.Bool wg sync.WaitGroup
stats *AutoscalingStats running atomicBool
stats *AutoscalingStats
// Метрики нагрузки // Метрики нагрузки
cpuLoad atomic.Float64 cpuLoad atomicFloat64
memLoad atomic.Float64 memLoad atomicFloat64
qps atomic.Uint64 qps atomicUint64
connections atomic.Uint64 connections atomicUint64
// Состояние кластера // Состояние кластера
currentNodeCount int currentNodeCount int
targetNodeCount int targetNodeCount int
// Коллбэки для реального масштабирования // Коллбэки для реального масштабирования
scaleUpCallback func(count int) error scaleUpCallback func(count int) error
scaleDownCallback func(count int) error scaleDownCallback func(count int) error
getNodeCountCallback func() int getNodeCountCallback func() int
} }
// AutoscalingStats статистика автомасштабирования // AutoscalingStats статистика автомасштабирования
type AutoscalingStats struct { type AutoscalingStats struct {
TotalScaleUps atomic.Uint64 TotalScaleUps atomicUint64
TotalScaleDowns atomic.Uint64 TotalScaleDowns atomicUint64
LastScaleUpAt atomic.Int64 LastScaleUpAt atomicInt64
LastScaleDownAt atomic.Int64 LastScaleDownAt atomicInt64
LastEvaluationAt atomic.Int64 LastEvaluationAt atomicInt64
CurrentNodes int CurrentNodes int
TargetNodes int TargetNodes int
mu sync.RWMutex mu sync.RWMutex
} }
// NewClusterAutoscaler создаёт новый экземпляр автомасштабирования // NewClusterAutoscaler создаёт новый экземпляр автомасштабирования
func NewClusterAutoscaler(cfg *config.AutoscalingConfig, logger *log.Logger) *ClusterAutoscaler { func NewClusterAutoscaler(cfg *config.AutoscalingConfig, logger *log.Logger) *ClusterAutoscaler {
if cfg == nil { if cfg == nil {
return nil return nil
} }
a := &ClusterAutoscaler{ a := &ClusterAutoscaler{
config: cfg, config: cfg,
logger: logger, logger: logger,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
currentNodeCount: cfg.MinNodes, currentNodeCount: cfg.MinNodes,
targetNodeCount: cfg.MinNodes, targetNodeCount: cfg.MinNodes,
stats: &AutoscalingStats{}, stats: &AutoscalingStats{},
} }
return a return a
} }
// SetCallbacks устанавливает коллбэки для управления кластером // SetCallbacks устанавливает коллбэки для управления кластером
func (a *ClusterAutoscaler) SetCallbacks( func (a *ClusterAutoscaler) SetCallbacks(
scaleUp func(count int) error, scaleUp func(count int) error,
scaleDown func(count int) error, scaleDown func(count int) error,
getNodeCount func() int, getNodeCount func() int,
) { ) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
a.scaleUpCallback = scaleUp a.scaleUpCallback = scaleUp
a.scaleDownCallback = scaleDown a.scaleDownCallback = scaleDown
a.getNodeCountCallback = getNodeCount a.getNodeCountCallback = getNodeCount
} }
// Start запускает автомасштабирование // Start запускает автомасштабирование
func (a *ClusterAutoscaler) Start() { func (a *ClusterAutoscaler) Start() {
if !a.config.Enabled { if !a.config.Enabled {
if a.logger != nil { if a.logger != nil {
a.logger.Info("Autoscaling is disabled in configuration") a.logger.Info("Autoscaling is disabled in configuration")
} }
return return
} }
if !a.running.CompareAndSwap(false, true) { if !a.running.CompareAndSwap(false, true) {
return return
} }
a.wg.Add(1) a.wg.Add(1)
go a.scalingLoop() go a.scalingLoop()
if a.logger != nil { if a.logger != nil {
a.logger.Info(fmt.Sprintf("ClusterAutoscaler started: min_nodes=%d, max_nodes=%d, threshold_up=%.2f, threshold_down=%.2f", a.logger.Info(fmt.Sprintf("ClusterAutoscaler started: min_nodes=%d, max_nodes=%d, threshold_up=%.2f, threshold_down=%.2f",
a.config.MinNodes, a.config.MaxNodes, a.config.ScaleUpThreshold, a.config.ScaleDownThreshold)) a.config.MinNodes, a.config.MaxNodes, a.config.ScaleUpThreshold, a.config.ScaleDownThreshold))
} }
} }
// Stop останавливает автомасштабирование // Stop останавливает автомасштабирование.
// Безопасен для многократного вызова.
func (a *ClusterAutoscaler) Stop() { func (a *ClusterAutoscaler) Stop() {
if !a.running.Load() { if !a.running.Load() {
return return
} }
close(a.stopChan) a.stopOnce.Do(func() {
a.wg.Wait() close(a.stopChan)
a.running.Store(false) })
a.wg.Wait()
a.running.Store(false)
if a.logger != nil { if a.logger != nil {
a.logger.Info("ClusterAutoscaler stopped") a.logger.Info("ClusterAutoscaler stopped")
} }
} }
// ReloadConfig обновляет конфигурацию автомасштабирования // ReloadConfig обновляет конфигурацию автомасштабирования
func (a *ClusterAutoscaler) ReloadConfig(cfg *config.AutoscalingConfig) { func (a *ClusterAutoscaler) ReloadConfig(cfg *config.AutoscalingConfig) {
a.mu.Lock() if cfg == nil {
defer a.mu.Unlock() return
}
oldEnabled := a.config.Enabled a.mu.Lock()
a.config = cfg oldEnabled := a.config.Enabled
a.config = cfg
if a.logger != nil { if a.logger != nil {
a.logger.Info(fmt.Sprintf("ClusterAutoscaler configuration reloaded: enabled=%v, min_nodes=%d, max_nodes=%d", a.logger.Info(fmt.Sprintf("ClusterAutoscaler configuration reloaded: enabled=%v, min_nodes=%d, max_nodes=%d",
cfg.Enabled, cfg.MinNodes, cfg.MaxNodes)) cfg.Enabled, cfg.MinNodes, cfg.MaxNodes))
} }
if oldEnabled != cfg.Enabled { if a.currentNodeCount < cfg.MinNodes {
if cfg.Enabled { a.currentNodeCount = cfg.MinNodes
a.Start() a.targetNodeCount = cfg.MinNodes
} else { }
a.Stop() if a.currentNodeCount > cfg.MaxNodes {
} a.currentNodeCount = cfg.MaxNodes
} a.targetNodeCount = cfg.MaxNodes
}
a.mu.Unlock()
if a.currentNodeCount < cfg.MinNodes { if oldEnabled != cfg.Enabled {
a.currentNodeCount = cfg.MinNodes if cfg.Enabled {
a.targetNodeCount = cfg.MinNodes a.Start()
} } else {
if a.currentNodeCount > cfg.MaxNodes { a.Stop()
a.currentNodeCount = cfg.MaxNodes }
a.targetNodeCount = cfg.MaxNodes }
}
} }
// UpdateMetrics обновляет метрики нагрузки // UpdateMetrics обновляет метрики нагрузки
func (a *ClusterAutoscaler) UpdateMetrics(cpuLoad, memLoad float64, qps, conns uint64) { func (a *ClusterAutoscaler) UpdateMetrics(cpuLoad, memLoad float64, qps, conns uint64) {
a.cpuLoad.Store(cpuLoad) a.cpuLoad.Store(cpuLoad)
a.memLoad.Store(memLoad) a.memLoad.Store(memLoad)
a.qps.Store(qps) a.qps.Store(qps)
a.connections.Store(conns) a.connections.Store(conns)
} }
// SetCurrentNodes устанавливает текущее количество узлов // SetCurrentNodes устанавливает текущее количество узлов
func (a *ClusterAutoscaler) SetCurrentNodes(count int) { func (a *ClusterAutoscaler) SetCurrentNodes(count int) {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
if count < a.config.MinNodes { if count < a.config.MinNodes {
count = a.config.MinNodes count = a.config.MinNodes
} }
if count > a.config.MaxNodes { if count > a.config.MaxNodes {
count = a.config.MaxNodes count = a.config.MaxNodes
} }
a.currentNodeCount = count a.currentNodeCount = count
a.targetNodeCount = count a.targetNodeCount = count
a.stats.mu.Lock() a.stats.mu.Lock()
a.stats.CurrentNodes = count a.stats.CurrentNodes = count
a.stats.TargetNodes = count a.stats.TargetNodes = count
a.stats.mu.Unlock() a.stats.mu.Unlock()
} }
// GetCurrentNodes возвращает текущее количество узлов // GetCurrentNodes возвращает текущее количество узлов
func (a *ClusterAutoscaler) GetCurrentNodes() int { func (a *ClusterAutoscaler) GetCurrentNodes() int {
a.mu.RLock() a.mu.RLock()
defer a.mu.RUnlock() defer a.mu.RUnlock()
return a.currentNodeCount return a.currentNodeCount
} }
// GetTargetNodes возвращает целевое количество узлов // GetTargetNodes возвращает целевое количество узлов
func (a *ClusterAutoscaler) GetTargetNodes() int { func (a *ClusterAutoscaler) GetTargetNodes() int {
a.mu.RLock() a.mu.RLock()
defer a.mu.RUnlock() defer a.mu.RUnlock()
return a.targetNodeCount return a.targetNodeCount
} }
// GetStats возвращает статистику автомасштабирования // GetStats возвращает статистику автомасштабирования
func (a *ClusterAutoscaler) GetStats() map[string]interface{} { func (a *ClusterAutoscaler) GetStats() map[string]interface{} {
a.mu.RLock() a.mu.RLock()
defer a.mu.RUnlock() defer a.mu.RUnlock()
a.stats.mu.RLock() a.stats.mu.RLock()
defer a.stats.mu.RUnlock() defer a.stats.mu.RUnlock()
return map[string]interface{}{ return map[string]interface{}{
"enabled": a.config.Enabled, "enabled": a.config.Enabled,
"min_nodes": a.config.MinNodes, "min_nodes": a.config.MinNodes,
"max_nodes": a.config.MaxNodes, "max_nodes": a.config.MaxNodes,
"current_nodes": a.currentNodeCount, "current_nodes": a.currentNodeCount,
"target_nodes": a.targetNodeCount, "target_nodes": a.targetNodeCount,
"scale_up_threshold": a.config.ScaleUpThreshold, "scale_up_threshold": a.config.ScaleUpThreshold,
"scale_down_threshold": a.config.ScaleDownThreshold, "scale_down_threshold": a.config.ScaleDownThreshold,
"cpu_load": a.cpuLoad.Load(), "cpu_load": a.cpuLoad.Load(),
"memory_load": a.memLoad.Load(), "memory_load": a.memLoad.Load(),
"qps": a.qps.Load(), "qps": a.qps.Load(),
"connections": a.connections.Load(), "connections": a.connections.Load(),
"total_scale_ups": a.stats.TotalScaleUps.Load(), "total_scale_ups": a.stats.TotalScaleUps.Load(),
"total_scale_downs": a.stats.TotalScaleDowns.Load(), "total_scale_downs": a.stats.TotalScaleDowns.Load(),
"last_scale_up_at": a.stats.LastScaleUpAt.Load(), "last_scale_up_at": a.stats.LastScaleUpAt.Load(),
"last_scale_down_at": a.stats.LastScaleDownAt.Load(), "last_scale_down_at": a.stats.LastScaleDownAt.Load(),
"last_evaluation_at": a.stats.LastEvaluationAt.Load(), "last_evaluation_at": a.stats.LastEvaluationAt.Load(),
"predictive_enabled": a.config.PredictiveEnabled, "predictive_enabled": a.config.PredictiveEnabled,
"max_scale_up_nodes": a.config.MaxScaleUpNodes, "max_scale_up_nodes": a.config.MaxScaleUpNodes,
"max_scale_down_nodes": a.config.MaxScaleDownNodes, "max_scale_down_nodes": a.config.MaxScaleDownNodes,
"scale_up_cooldown_sec": a.config.ScaleUpCooldownSec, "scale_up_cooldown_sec": a.config.ScaleUpCooldownSec,
"scale_down_cooldown_sec": a.config.ScaleDownCooldownSec, "scale_down_cooldown_sec": a.config.ScaleDownCooldownSec,
"evaluation_interval_sec": a.config.EvaluationIntervalSec, "evaluation_interval_sec": a.config.EvaluationIntervalSec,
} }
} }
// scalingLoop основной цикл оценки и масштабирования // scalingLoop основной цикл оценки и масштабирования
func (a *ClusterAutoscaler) scalingLoop() { func (a *ClusterAutoscaler) scalingLoop() {
defer a.wg.Done() defer a.wg.Done()
ticker := time.NewTicker(a.config.GetEvaluationInterval()) ticker := time.NewTicker(a.config.GetEvaluationInterval())
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-a.stopChan: case <-a.stopChan:
return return
case <-ticker.C: case <-ticker.C:
a.evaluateAndScale() a.evaluateAndScale()
} }
} }
} }
// evaluateAndScale оценивает нагрузку и выполняет масштабирование // evaluateAndScale оценивает нагрузку и выполняет масштабирование
func (a *ClusterAutoscaler) evaluateAndScale() { func (a *ClusterAutoscaler) evaluateAndScale() {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
if !a.config.Enabled { if !a.config.Enabled {
return return
} }
a.stats.LastEvaluationAt.Store(time.Now().UnixMilli()) a.stats.LastEvaluationAt.Store(time.Now().UnixMilli())
// Получаем текущие метрики // Получаем текущие метрики
cpu := a.cpuLoad.Load() cpu := a.cpuLoad.Load()
mem := a.memLoad.Load() mem := a.memLoad.Load()
qps := a.qps.Load() qps := a.qps.Load()
conns := a.connections.Load() conns := a.connections.Load()
// Обновляем текущее количество узлов из внешнего источника // Обновляем текущее количество узлов из внешнего источника
if a.getNodeCountCallback != nil { if a.getNodeCountCallback != nil {
actualCount := a.getNodeCountCallback() actualCount := a.getNodeCountCallback()
if actualCount > 0 { if actualCount > 0 {
a.currentNodeCount = actualCount a.currentNodeCount = actualCount
} }
} }
needScaleUp := false needScaleUp := false
needScaleDown := false needScaleDown := false
reason := "" reason := ""
// Оценка масштабирования вверх // Оценка масштабирования вверх
if cpu > a.config.ScaleUpThreshold || mem > a.config.ScaleUpThreshold { if cpu > a.config.ScaleUpThreshold || mem > a.config.ScaleUpThreshold {
needScaleUp = true needScaleUp = true
reason = fmt.Sprintf("high resource usage: cpu=%.2f, mem=%.2f", cpu, mem) reason = fmt.Sprintf("high resource usage: cpu=%.2f, mem=%.2f", cpu, mem)
} else if qps > 0 && conns > 0 { } else if qps > 0 && conns > 0 {
expectedLoad := float64(qps) / float64(10000) expectedLoad := float64(qps) / float64(10000)
if expectedLoad > a.config.ScaleUpThreshold { if expectedLoad > a.config.ScaleUpThreshold {
needScaleUp = true needScaleUp = true
reason = fmt.Sprintf("high load: qps=%d, connections=%d", qps, conns) reason = fmt.Sprintf("high load: qps=%d, connections=%d", qps, conns)
} }
} }
// Оценка масштабирования вниз // Оценка масштабирования вниз
if cpu < a.config.ScaleDownThreshold && mem < a.config.ScaleDownThreshold && if cpu < a.config.ScaleDownThreshold && mem < a.config.ScaleDownThreshold &&
float64(qps) < 100 && float64(conns) < 100 { float64(qps) < 100 && float64(conns) < 100 {
needScaleDown = true needScaleDown = true
reason = "low resource usage" reason = "low resource usage"
} }
// Проверяем cooldown периоды // Проверяем cooldown периоды
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
lastUp := a.stats.LastScaleUpAt.Load() lastUp := a.stats.LastScaleUpAt.Load()
lastDown := a.stats.LastScaleDownAt.Load() lastDown := a.stats.LastScaleDownAt.Load()
if needScaleUp { if needScaleUp {
if now-lastUp < int64(a.config.GetScaleUpCooldown().Seconds())*1000 { if now-lastUp < int64(a.config.GetScaleUpCooldown().Seconds())*1000 {
needScaleUp = false needScaleUp = false
} }
} }
if needScaleDown { if needScaleDown {
if now-lastDown < int64(a.config.GetScaleDownCooldown().Seconds())*1000 { if now-lastDown < int64(a.config.GetScaleDownCooldown().Seconds())*1000 {
needScaleDown = false needScaleDown = false
} }
} }
// Выполняем масштабирование // Выполняем масштабирование
if needScaleUp && a.currentNodeCount < a.config.MaxNodes { if needScaleUp && a.currentNodeCount < a.config.MaxNodes {
a.scaleUp(reason) a.scaleUp(reason)
} else if needScaleDown && a.currentNodeCount > a.config.MinNodes { } else if needScaleDown && a.currentNodeCount > a.config.MinNodes {
a.scaleDown(reason) a.scaleDown(reason)
} }
} }
// scaleUp выполняет масштабирование вверх // scaleUp выполняет масштабирование вверх.
// Вызывается под удержанием a.mu.
func (a *ClusterAutoscaler) scaleUp(reason string) { func (a *ClusterAutoscaler) scaleUp(reason string) {
count := a.config.MaxScaleUpNodes count := a.config.MaxScaleUpNodes
if a.currentNodeCount+count > a.config.MaxNodes { if a.currentNodeCount+count > a.config.MaxNodes {
count = a.config.MaxNodes - a.currentNodeCount count = a.config.MaxNodes - a.currentNodeCount
} }
if count <= 0 { if count <= 0 {
return return
} }
a.targetNodeCount = a.currentNodeCount + count a.targetNodeCount = a.currentNodeCount + count
if a.logger != nil { if a.logger != nil {
a.logger.Info(fmt.Sprintf("Scaling up: %d -> %d nodes (adding %d) reason: %s", a.logger.Info(fmt.Sprintf("Scaling up: %d -> %d nodes (adding %d) reason: %s",
a.currentNodeCount, a.targetNodeCount, count, reason)) a.currentNodeCount, a.targetNodeCount, count, reason))
} }
if a.scaleUpCallback != nil { if a.scaleUpCallback != nil {
if err := a.scaleUpCallback(count); err != nil { if err := a.scaleUpCallback(count); err != nil {
if a.logger != nil { if a.logger != nil {
a.logger.Error(fmt.Sprintf("Scale up failed: %v", err)) a.logger.Error(fmt.Sprintf("Scale up failed: %v", err))
} }
return return
} }
} }
a.currentNodeCount = a.targetNodeCount a.currentNodeCount = a.targetNodeCount
a.stats.TotalScaleUps.Add(1) a.stats.TotalScaleUps.Add(1)
a.stats.LastScaleUpAt.Store(time.Now().UnixMilli()) a.stats.LastScaleUpAt.Store(time.Now().UnixMilli())
a.stats.mu.Lock() a.stats.mu.Lock()
a.stats.CurrentNodes = a.currentNodeCount a.stats.CurrentNodes = a.currentNodeCount
a.stats.TargetNodes = a.targetNodeCount a.stats.TargetNodes = a.targetNodeCount
a.stats.mu.Unlock() a.stats.mu.Unlock()
} }
// scaleDown выполняет масштабирование вниз // scaleDown выполняет масштабирование вниз.
// Вызывается под удержанием a.mu.
func (a *ClusterAutoscaler) scaleDown(reason string) { func (a *ClusterAutoscaler) scaleDown(reason string) {
count := a.config.MaxScaleDownNodes count := a.config.MaxScaleDownNodes
if a.currentNodeCount-count < a.config.MinNodes { if a.currentNodeCount-count < a.config.MinNodes {
count = a.currentNodeCount - a.config.MinNodes count = a.currentNodeCount - a.config.MinNodes
} }
if count <= 0 { if count <= 0 {
return return
} }
a.targetNodeCount = a.currentNodeCount - count a.targetNodeCount = a.currentNodeCount - count
if a.logger != nil { if a.logger != nil {
a.logger.Info(fmt.Sprintf("Scaling down: %d -> %d nodes (removing %d) reason: %s", a.logger.Info(fmt.Sprintf("Scaling down: %d -> %d nodes (removing %d) reason: %s",
a.currentNodeCount, a.targetNodeCount, count, reason)) a.currentNodeCount, a.targetNodeCount, count, reason))
} }
if a.scaleDownCallback != nil { if a.scaleDownCallback != nil {
if err := a.scaleDownCallback(count); err != nil { if err := a.scaleDownCallback(count); err != nil {
if a.logger != nil { if a.logger != nil {
a.logger.Error(fmt.Sprintf("Scale down failed: %v", err)) a.logger.Error(fmt.Sprintf("Scale down failed: %v", err))
} }
return return
} }
} }
a.currentNodeCount = a.targetNodeCount a.currentNodeCount = a.targetNodeCount
a.stats.TotalScaleDowns.Add(1) a.stats.TotalScaleDowns.Add(1)
a.stats.LastScaleDownAt.Store(time.Now().UnixMilli()) a.stats.LastScaleDownAt.Store(time.Now().UnixMilli())
a.stats.mu.Lock() a.stats.mu.Lock()
a.stats.CurrentNodes = a.currentNodeCount a.stats.CurrentNodes = a.currentNodeCount
a.stats.TargetNodes = a.targetNodeCount a.stats.TargetNodes = a.targetNodeCount
a.stats.mu.Unlock() a.stats.mu.Unlock()
} }
// GetStatus возвращает текущий статус автомасштабирования // GetStatus возвращает текущий статус автомасштабирования
func (a *ClusterAutoscaler) GetStatus() *AutoscalingStatus { func (a *ClusterAutoscaler) GetStatus() *AutoscalingStatus {
a.mu.RLock() a.mu.RLock()
defer a.mu.RUnlock() defer a.mu.RUnlock()
return &AutoscalingStatus{ return &AutoscalingStatus{
Enabled: a.config.Enabled, Enabled: a.config.Enabled,
MinNodes: a.config.MinNodes, MinNodes: a.config.MinNodes,
MaxNodes: a.config.MaxNodes, MaxNodes: a.config.MaxNodes,
CurrentNodes: a.currentNodeCount, CurrentNodes: a.currentNodeCount,
TargetNodes: a.targetNodeCount, TargetNodes: a.targetNodeCount,
CPU: a.cpuLoad.Load(), CPU: a.cpuLoad.Load(),
Memory: a.memLoad.Load(), Memory: a.memLoad.Load(),
QPS: a.qps.Load(), QPS: a.qps.Load(),
Connections: a.connections.Load(), Connections: a.connections.Load(),
LastEvaluationAt: a.stats.LastEvaluationAt.Load(), LastEvaluationAt: a.stats.LastEvaluationAt.Load(),
ScaleUpThreshold: a.config.ScaleUpThreshold, ScaleUpThreshold: a.config.ScaleUpThreshold,
ScaleDownThreshold: a.config.ScaleDownThreshold, ScaleDownThreshold: a.config.ScaleDownThreshold,
ScaleUpCooldown: a.config.GetScaleUpCooldown(), ScaleUpCooldown: a.config.GetScaleUpCooldown(),
ScaleDownCooldown: a.config.GetScaleDownCooldown(), ScaleDownCooldown: a.config.GetScaleDownCooldown(),
TotalScaleUps: a.stats.TotalScaleUps.Load(), TotalScaleUps: a.stats.TotalScaleUps.Load(),
TotalScaleDowns: a.stats.TotalScaleDowns.Load(), TotalScaleDowns: a.stats.TotalScaleDowns.Load(),
} }
} }
// AutoscalingStatus статус автомасштабирования // AutoscalingStatus статус автомасштабирования
type AutoscalingStatus struct { type AutoscalingStatus struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
MinNodes int `json:"min_nodes"` MinNodes int `json:"min_nodes"`
MaxNodes int `json:"max_nodes"` MaxNodes int `json:"max_nodes"`
CurrentNodes int `json:"current_nodes"` CurrentNodes int `json:"current_nodes"`
TargetNodes int `json:"target_nodes"` TargetNodes int `json:"target_nodes"`
CPU float64 `json:"cpu_load"` CPU float64 `json:"cpu_load"`
Memory float64 `json:"memory_load"` Memory float64 `json:"memory_load"`
QPS uint64 `json:"qps"` QPS uint64 `json:"qps"`
Connections uint64 `json:"connections"` Connections uint64 `json:"connections"`
LastEvaluationAt int64 `json:"last_evaluation_at"` LastEvaluationAt int64 `json:"last_evaluation_at"`
ScaleUpThreshold float64 `json:"scale_up_threshold"` ScaleUpThreshold float64 `json:"scale_up_threshold"`
ScaleDownThreshold float64 `json:"scale_down_threshold"` ScaleDownThreshold float64 `json:"scale_down_threshold"`
ScaleUpCooldown time.Duration `json:"scale_up_cooldown"` ScaleUpCooldown time.Duration `json:"scale_up_cooldown"`
ScaleDownCooldown time.Duration `json:"scale_down_cooldown"` ScaleDownCooldown time.Duration `json:"scale_down_cooldown"`
TotalScaleUps uint64 `json:"total_scale_ups"` TotalScaleUps uint64 `json:"total_scale_ups"`
TotalScaleDowns uint64 `json:"total_scale_downs"` TotalScaleDowns uint64 `json:"total_scale_downs"`
} }