Upload files to "internal/autoscaling"

This commit is contained in:
2026-07-18 20:43:22 +00:00
parent bda5c25ec6
commit 021e3aa835

View File

@@ -0,0 +1,443 @@
/*
* 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
*/
package autoscaling
import (
"fmt"
"sync"
"sync/atomic"
"time"
"futriis/internal/config"
"futriis/internal/log"
)
// ClusterAutoscaler управляет автоматическим масштабированием кластера
type ClusterAutoscaler struct {
config *config.AutoscalingConfig
logger *log.Logger
mu sync.RWMutex
stopChan chan struct{}
wg sync.WaitGroup
running atomic.Bool
stats *AutoscalingStats
// Метрики нагрузки
cpuLoad atomic.Float64
memLoad atomic.Float64
qps atomic.Uint64
connections atomic.Uint64
// Состояние кластера
currentNodeCount int
targetNodeCount int
// Коллбэки для реального масштабирования
scaleUpCallback func(count int) error
scaleDownCallback func(count int) error
getNodeCountCallback func() int
}
// AutoscalingStats статистика автомасштабирования
type AutoscalingStats struct {
TotalScaleUps atomic.Uint64
TotalScaleDowns atomic.Uint64
LastScaleUpAt atomic.Int64
LastScaleDownAt atomic.Int64
LastEvaluationAt atomic.Int64
CurrentNodes int
TargetNodes int
mu sync.RWMutex
}
// NewClusterAutoscaler создаёт новый экземпляр автомасштабирования
func NewClusterAutoscaler(cfg *config.AutoscalingConfig, logger *log.Logger) *ClusterAutoscaler {
if cfg == nil {
return nil
}
a := &ClusterAutoscaler{
config: cfg,
logger: logger,
stopChan: make(chan struct{}),
currentNodeCount: cfg.MinNodes,
targetNodeCount: cfg.MinNodes,
stats: &AutoscalingStats{},
}
return a
}
// SetCallbacks устанавливает коллбэки для управления кластером
func (a *ClusterAutoscaler) SetCallbacks(
scaleUp func(count int) error,
scaleDown func(count int) error,
getNodeCount func() int,
) {
a.mu.Lock()
defer a.mu.Unlock()
a.scaleUpCallback = scaleUp
a.scaleDownCallback = scaleDown
a.getNodeCountCallback = getNodeCount
}
// Start запускает автомасштабирование
func (a *ClusterAutoscaler) Start() {
if !a.config.Enabled {
if a.logger != nil {
a.logger.Info("Autoscaling is disabled in configuration")
}
return
}
if !a.running.CompareAndSwap(false, true) {
return
}
a.wg.Add(1)
go a.scalingLoop()
if a.logger != nil {
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))
}
}
// Stop останавливает автомасштабирование
func (a *ClusterAutoscaler) Stop() {
if !a.running.Load() {
return
}
close(a.stopChan)
a.wg.Wait()
a.running.Store(false)
if a.logger != nil {
a.logger.Info("ClusterAutoscaler stopped")
}
}
// ReloadConfig обновляет конфигурацию автомасштабирования
func (a *ClusterAutoscaler) ReloadConfig(cfg *config.AutoscalingConfig) {
a.mu.Lock()
defer a.mu.Unlock()
oldEnabled := a.config.Enabled
a.config = cfg
if a.logger != nil {
a.logger.Info(fmt.Sprintf("ClusterAutoscaler configuration reloaded: enabled=%v, min_nodes=%d, max_nodes=%d",
cfg.Enabled, cfg.MinNodes, cfg.MaxNodes))
}
if oldEnabled != cfg.Enabled {
if cfg.Enabled {
a.Start()
} else {
a.Stop()
}
}
if a.currentNodeCount < cfg.MinNodes {
a.currentNodeCount = cfg.MinNodes
a.targetNodeCount = cfg.MinNodes
}
if a.currentNodeCount > cfg.MaxNodes {
a.currentNodeCount = cfg.MaxNodes
a.targetNodeCount = cfg.MaxNodes
}
}
// UpdateMetrics обновляет метрики нагрузки
func (a *ClusterAutoscaler) UpdateMetrics(cpuLoad, memLoad float64, qps, conns uint64) {
a.cpuLoad.Store(cpuLoad)
a.memLoad.Store(memLoad)
a.qps.Store(qps)
a.connections.Store(conns)
}
// SetCurrentNodes устанавливает текущее количество узлов
func (a *ClusterAutoscaler) SetCurrentNodes(count int) {
a.mu.Lock()
defer a.mu.Unlock()
if count < a.config.MinNodes {
count = a.config.MinNodes
}
if count > a.config.MaxNodes {
count = a.config.MaxNodes
}
a.currentNodeCount = count
a.targetNodeCount = count
a.stats.mu.Lock()
a.stats.CurrentNodes = count
a.stats.TargetNodes = count
a.stats.mu.Unlock()
}
// GetCurrentNodes возвращает текущее количество узлов
func (a *ClusterAutoscaler) GetCurrentNodes() int {
a.mu.RLock()
defer a.mu.RUnlock()
return a.currentNodeCount
}
// GetTargetNodes возвращает целевое количество узлов
func (a *ClusterAutoscaler) GetTargetNodes() int {
a.mu.RLock()
defer a.mu.RUnlock()
return a.targetNodeCount
}
// GetStats возвращает статистику автомасштабирования
func (a *ClusterAutoscaler) GetStats() map[string]interface{} {
a.mu.RLock()
defer a.mu.RUnlock()
a.stats.mu.RLock()
defer a.stats.mu.RUnlock()
return map[string]interface{}{
"enabled": a.config.Enabled,
"min_nodes": a.config.MinNodes,
"max_nodes": a.config.MaxNodes,
"current_nodes": a.currentNodeCount,
"target_nodes": a.targetNodeCount,
"scale_up_threshold": a.config.ScaleUpThreshold,
"scale_down_threshold": a.config.ScaleDownThreshold,
"cpu_load": a.cpuLoad.Load(),
"memory_load": a.memLoad.Load(),
"qps": a.qps.Load(),
"connections": a.connections.Load(),
"total_scale_ups": a.stats.TotalScaleUps.Load(),
"total_scale_downs": a.stats.TotalScaleDowns.Load(),
"last_scale_up_at": a.stats.LastScaleUpAt.Load(),
"last_scale_down_at": a.stats.LastScaleDownAt.Load(),
"last_evaluation_at": a.stats.LastEvaluationAt.Load(),
"predictive_enabled": a.config.PredictiveEnabled,
"max_scale_up_nodes": a.config.MaxScaleUpNodes,
"max_scale_down_nodes": a.config.MaxScaleDownNodes,
"scale_up_cooldown_sec": a.config.ScaleUpCooldownSec,
"scale_down_cooldown_sec": a.config.ScaleDownCooldownSec,
"evaluation_interval_sec": a.config.EvaluationIntervalSec,
}
}
// scalingLoop основной цикл оценки и масштабирования
func (a *ClusterAutoscaler) scalingLoop() {
defer a.wg.Done()
ticker := time.NewTicker(a.config.GetEvaluationInterval())
defer ticker.Stop()
for {
select {
case <-a.stopChan:
return
case <-ticker.C:
a.evaluateAndScale()
}
}
}
// evaluateAndScale оценивает нагрузку и выполняет масштабирование
func (a *ClusterAutoscaler) evaluateAndScale() {
a.mu.Lock()
defer a.mu.Unlock()
if !a.config.Enabled {
return
}
a.stats.LastEvaluationAt.Store(time.Now().UnixMilli())
// Получаем текущие метрики
cpu := a.cpuLoad.Load()
mem := a.memLoad.Load()
qps := a.qps.Load()
conns := a.connections.Load()
// Обновляем текущее количество узлов из внешнего источника
if a.getNodeCountCallback != nil {
actualCount := a.getNodeCountCallback()
if actualCount > 0 {
a.currentNodeCount = actualCount
}
}
needScaleUp := false
needScaleDown := false
reason := ""
// Оценка масштабирования вверх
if cpu > a.config.ScaleUpThreshold || mem > a.config.ScaleUpThreshold {
needScaleUp = true
reason = fmt.Sprintf("high resource usage: cpu=%.2f, mem=%.2f", cpu, mem)
} else if qps > 0 && conns > 0 {
expectedLoad := float64(qps) / float64(10000)
if expectedLoad > a.config.ScaleUpThreshold {
needScaleUp = true
reason = fmt.Sprintf("high load: qps=%d, connections=%d", qps, conns)
}
}
// Оценка масштабирования вниз
if cpu < a.config.ScaleDownThreshold && mem < a.config.ScaleDownThreshold &&
float64(qps) < 100 && float64(conns) < 100 {
needScaleDown = true
reason = "low resource usage"
}
// Проверяем cooldown периоды
now := time.Now().UnixMilli()
lastUp := a.stats.LastScaleUpAt.Load()
lastDown := a.stats.LastScaleDownAt.Load()
if needScaleUp {
if now-lastUp < int64(a.config.GetScaleUpCooldown().Seconds())*1000 {
needScaleUp = false
}
}
if needScaleDown {
if now-lastDown < int64(a.config.GetScaleDownCooldown().Seconds())*1000 {
needScaleDown = false
}
}
// Выполняем масштабирование
if needScaleUp && a.currentNodeCount < a.config.MaxNodes {
a.scaleUp(reason)
} else if needScaleDown && a.currentNodeCount > a.config.MinNodes {
a.scaleDown(reason)
}
}
// scaleUp выполняет масштабирование вверх
func (a *ClusterAutoscaler) scaleUp(reason string) {
count := a.config.MaxScaleUpNodes
if a.currentNodeCount+count > a.config.MaxNodes {
count = a.config.MaxNodes - a.currentNodeCount
}
if count <= 0 {
return
}
a.targetNodeCount = a.currentNodeCount + count
if a.logger != nil {
a.logger.Info(fmt.Sprintf("Scaling up: %d -> %d nodes (adding %d) reason: %s",
a.currentNodeCount, a.targetNodeCount, count, reason))
}
if a.scaleUpCallback != nil {
if err := a.scaleUpCallback(count); err != nil {
if a.logger != nil {
a.logger.Error(fmt.Sprintf("Scale up failed: %v", err))
}
return
}
}
a.currentNodeCount = a.targetNodeCount
a.stats.TotalScaleUps.Add(1)
a.stats.LastScaleUpAt.Store(time.Now().UnixMilli())
a.stats.mu.Lock()
a.stats.CurrentNodes = a.currentNodeCount
a.stats.TargetNodes = a.targetNodeCount
a.stats.mu.Unlock()
}
// scaleDown выполняет масштабирование вниз
func (a *ClusterAutoscaler) scaleDown(reason string) {
count := a.config.MaxScaleDownNodes
if a.currentNodeCount-count < a.config.MinNodes {
count = a.currentNodeCount - a.config.MinNodes
}
if count <= 0 {
return
}
a.targetNodeCount = a.currentNodeCount - count
if a.logger != nil {
a.logger.Info(fmt.Sprintf("Scaling down: %d -> %d nodes (removing %d) reason: %s",
a.currentNodeCount, a.targetNodeCount, count, reason))
}
if a.scaleDownCallback != nil {
if err := a.scaleDownCallback(count); err != nil {
if a.logger != nil {
a.logger.Error(fmt.Sprintf("Scale down failed: %v", err))
}
return
}
}
a.currentNodeCount = a.targetNodeCount
a.stats.TotalScaleDowns.Add(1)
a.stats.LastScaleDownAt.Store(time.Now().UnixMilli())
a.stats.mu.Lock()
a.stats.CurrentNodes = a.currentNodeCount
a.stats.TargetNodes = a.targetNodeCount
a.stats.mu.Unlock()
}
// GetStatus возвращает текущий статус автомасштабирования
func (a *ClusterAutoscaler) GetStatus() *AutoscalingStatus {
a.mu.RLock()
defer a.mu.RUnlock()
return &AutoscalingStatus{
Enabled: a.config.Enabled,
MinNodes: a.config.MinNodes,
MaxNodes: a.config.MaxNodes,
CurrentNodes: a.currentNodeCount,
TargetNodes: a.targetNodeCount,
CPU: a.cpuLoad.Load(),
Memory: a.memLoad.Load(),
QPS: a.qps.Load(),
Connections: a.connections.Load(),
LastEvaluationAt: a.stats.LastEvaluationAt.Load(),
ScaleUpThreshold: a.config.ScaleUpThreshold,
ScaleDownThreshold: a.config.ScaleDownThreshold,
ScaleUpCooldown: a.config.GetScaleUpCooldown(),
ScaleDownCooldown: a.config.GetScaleDownCooldown(),
TotalScaleUps: a.stats.TotalScaleUps.Load(),
TotalScaleDowns: a.stats.TotalScaleDowns.Load(),
}
}
// AutoscalingStatus статус автомасштабирования
type AutoscalingStatus struct {
Enabled bool `json:"enabled"`
MinNodes int `json:"min_nodes"`
MaxNodes int `json:"max_nodes"`
CurrentNodes int `json:"current_nodes"`
TargetNodes int `json:"target_nodes"`
CPU float64 `json:"cpu_load"`
Memory float64 `json:"memory_load"`
QPS uint64 `json:"qps"`
Connections uint64 `json:"connections"`
LastEvaluationAt int64 `json:"last_evaluation_at"`
ScaleUpThreshold float64 `json:"scale_up_threshold"`
ScaleDownThreshold float64 `json:"scale_down_threshold"`
ScaleUpCooldown time.Duration `json:"scale_up_cooldown"`
ScaleDownCooldown time.Duration `json:"scale_down_cooldown"`
TotalScaleUps uint64 `json:"total_scale_ups"`
TotalScaleDowns uint64 `json:"total_scale_downs"`
}