use std::collections::{BTreeMap, HashMap};
use tracing::{debug, info};
use std::time::{Duration, Instant};
use std::collections::VecDeque;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct ClusterMetrics {
pub cpu_utilization: f64, pub memory_utilization: f64, pub network_utilization: f64, pub active_tasks: usize, pub queue_depth: usize, pub response_time_ms: u64, pub error_rate: f64, pub timestamp: u64, }
#[derive(Debug, Clone, PartialEq)]
pub enum ScalingStrategy {
Reactive, Predictive, Hybrid, }
#[derive(Debug, Clone, PartialEq)]
pub enum ScalingAction {
ScaleUp(usize), ScaleDown(usize), NoOp, }
#[derive(Debug, Clone)]
pub struct AutoscalerConfig {
pub scale_up_threshold: f64, pub scale_down_threshold: f64, pub cooldown_period: Duration, pub min_nodes: usize, pub max_nodes: usize, }
#[derive(Debug, Clone)]
pub struct ScalingPolicy {
pub strategy: ScalingStrategy,
pub enable_predictive: bool,
pub prediction_window: Duration,
pub smoothing_factor: f64, }
#[derive(Debug)]
struct MetricsHistory {
metrics: VecDeque<ClusterMetrics>,
max_history: usize,
}
impl MetricsHistory {
fn new(max_history: usize) -> Self {
Self {
metrics: VecDeque::with_capacity(max_history),
max_history,
}
}
fn add(&mut self, metrics: ClusterMetrics) {
self.metrics.push_back(metrics);
if self.metrics.len() > self.max_history {
self.metrics.pop_front();
}
}
fn get_average(&self) -> Option<ClusterMetrics> {
if self.metrics.is_empty() {
return None;
}
let count: _ = self.metrics.len() as f64;
let sum: _ = self.metrics.iter().fold(
(0.0, 0.0, 0.0, 0, 0, 0.0, 0.0),
|acc, m| (
acc.0 + m.cpu_utilization,
acc.1 + m.memory_utilization,
acc.2 + m.network_utilization,
acc.3 + m.active_tasks,
acc.4 + m.queue_depth,
acc.5 + m.response_time_ms as f64,
acc.6 + m.error_rate,
)
);
Some(ClusterMetrics {
cpu_utilization: sum.0 / count,
memory_utilization: sum.1 / count,
network_utilization: sum.2 / count,
active_tasks: (sum.3 as f64 / count) as usize,
queue_depth: (sum.4 as f64 / count) as usize,
response_time_ms: (sum.5 / count) as u64,
error_rate: sum.6 / count,
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
})
}
fn len(&self) -> usize {
self.metrics.len()
}
}
#[derive(Debug)]
pub struct Autoscaler {
config: AutoscalerConfig,
policy: ScalingPolicy,
history: MetricsHistory,
last_scaling_time: Option<Instant>,
cooldown_remaining: Duration,
total_scale_up_events: u64,
total_scale_down_events: u64,
}
impl Autoscaler {
pub fn new(config: AutoscalerConfig) -> Self {
assert!(config.scale_up_threshold > config.scale_down_threshold,
"scale_up_threshold must be greater than scale_down_threshold");
assert!(config.min_nodes < config.max_nodes,
"min_nodes must be less than max_nodes");
Self {
policy: ScalingPolicy {
strategy: ScalingStrategy::Reactive,
enable_predictive: false,
prediction_window: Duration::from_secs(300),
smoothing_factor: 0.7,
},
history: MetricsHistory::new(100),
last_scaling_time: None,
cooldown_remaining: Duration::ZERO,
total_scale_up_events: 0,
total_scale_down_events: 0,
config,
}
}
pub fn new_with_policy(config: AutoscalerConfig, policy: ScalingPolicy) -> Self {
let mut autoscaler = Self::new(config);
autoscaler.policy = policy;
autoscaler
}
pub fn is_enabled(&self) -> bool {
true
}
pub fn record_metrics(&mut self, metrics: ClusterMetrics) {
self.history.add(metrics);
self.update_cooldown();
}
pub fn evaluate_scaling(&mut self, current_metrics: &ClusterMetrics) -> ScalingAction {
self.update_cooldown();
if !self.is_cooldown_complete() {
debug!("自动扩缩容在冷却期,跳过评估");
return ScalingAction::NoOp;
}
self.record_metrics(current_metrics.clone());
let avg_metrics: _ = self.history.get_average()
.unwrap_or_else(|| current_metrics.clone());
let load_score: _ = self.calculate_load_score(&avg_metrics);
let action: _ = self.make_scaling_decision(load_score, &avg_metrics);
if action != ScalingAction::NoOp {
self.last_scaling_time = Some(Instant::now());
self.cooldown_remaining = self.config.cooldown_period;
match action {
ScalingAction::ScaleUp(_) => self.total_scale_up_events += 1,
ScalingAction::ScaleDown(_) => self.total_scale_down_events += 1,
ScalingAction::NoOp => {}
}
info!("自动扩缩容决策: {:?}, 负载分数: {:.2}", action, load_score);
}
action
}
fn calculate_load_score(&self, metrics: &ClusterMetrics) -> f64 {
if metrics.cpu_utilization >= self.config.scale_up_threshold ||
metrics.memory_utilization >= self.config.scale_up_threshold {
return 1.0;
}
let cpu_weight: _ = 0.35; let memory_weight: _ = 0.35; let queue_weight: _ = 0.15;
let response_time_weight: _ = 0.10;
let error_rate_weight: _ = 0.03;
let task_weight: _ = 0.02;
let queue_score: _ = (metrics.queue_depth as f64 / 200.0).min(1.0);
let response_time_score: _ = (metrics.response_time_ms as f64 / 1000.0).min(1.0);
let task_score: _ = (metrics.active_tasks as f64 / 200.0).min(1.0);
let load_score: _ = metrics.cpu_utilization * cpu_weight +
metrics.memory_utilization * memory_weight +
queue_score * queue_weight +
response_time_score * response_time_weight +
metrics.error_rate * error_rate_weight +
task_score * task_weight;
debug!("负载分数计算: cpu={:.2}* {:.2} + mem={:.2}* {:.2} + queue={:.2}* {:.2} + rt={:.2}* {:.2} + err={:.2}* {:.2} + task={:.2}* {:.2} = {:.2}",
metrics.cpu_utilization, cpu_weight,
metrics.memory_utilization, memory_weight,
queue_score, queue_weight,
response_time_score, response_time_weight,
metrics.error_rate, error_rate_weight,
task_score, task_weight,
load_score);
load_score
}
fn make_scaling_decision(&self, load_score: f64, metrics: &ClusterMetrics) -> ScalingAction {
if load_score >= self.config.scale_up_threshold {
let scale_up_count: _ = self.calculate_scale_up_count(metrics);
return ScalingAction::ScaleUp(scale_up_count);
}
if load_score <= self.config.scale_down_threshold {
let scale_down_count: _ = self.calculate_scale_down_count(metrics);
return ScalingAction::ScaleDown(scale_down_count);
}
ScalingAction::NoOp
}
fn calculate_scale_up_count(&self, metrics: &ClusterMetrics) -> usize {
let base_count: _ = 1;
let load_factor: _ = if metrics.cpu_utilization > 0.9 {
2
} else if metrics.cpu_utilization > 0.85 {
1
} else {
0
};
(base_count + load_factor).max(1).min(3)
}
fn calculate_scale_down_count(&self, metrics: &ClusterMetrics) -> usize {
let base_count: _ = 1;
if metrics.queue_depth > 0 {
return 0;
}
let load_factor: _ = if metrics.cpu_utilization < 0.15 {
1 } else if metrics.cpu_utilization < 0.25 {
0 } else {
0 };
base_count + load_factor
}
fn update_cooldown(&mut self) {
if let Some(last_time) = self.last_scaling_time {
let elapsed: _ = last_time.elapsed();
if elapsed < self.config.cooldown_period {
self.cooldown_remaining = self.config.cooldown_period - elapsed;
} else {
self.cooldown_remaining = Duration::ZERO;
}
}
}
fn is_cooldown_complete(&self) -> bool {
self.cooldown_remaining == Duration::ZERO
}
pub fn get_cooldown_remaining(&self) -> Duration {
self.cooldown_remaining
}
pub fn get_statistics(&self) -> AutoscalerStats {
AutoscalerStats {
total_scale_up_events: self.total_scale_up_events,
total_scale_down_events: self.total_scale_down_events,
cooldown_remaining: self.cooldown_remaining,
history_size: self.history.len(),
is_in_cooldown: !self.is_cooldown_complete(),
}
}
pub fn predict_future_load(&self, horizon: Duration) -> Option<f64> {
if self.history.len() < 5 {
return None;
}
let metrics_vec: Vec<&ClusterMetrics> = self.history.metrics.iter().collect();
let recent_metrics: _ = &metrics_vec[metrics_vec.len().saturating_sub(5)..];
let mut trend = 0.0;
for i in 1..recent_metrics.len() {
let prev_load: _ = self.calculate_load_score(recent_metrics[i - 1]);
let curr_load: _ = self.calculate_load_score(recent_metrics[i]);
trend += curr_load - prev_load;
}
trend /= (recent_metrics.len() - 1) as f64;
let current_load: _ = self.calculate_load_score(recent_metrics[recent_metrics.len() - 1]);
let time_factor: _ = (horizon.as_secs_f64() / self.policy.prediction_window.as_secs_f64()).min(1.0);
Some(current_load + trend * time_factor)
}
}
#[derive(Debug, Clone)]
pub struct AutoscalerStats {
pub total_scale_up_events: u64,
pub total_scale_down_events: u64,
pub cooldown_remaining: Duration,
pub history_size: usize,
pub is_in_cooldown: bool,
}
impl Default for AutoscalerStats {
fn default() -> Self {
Self {
total_scale_up_events: 0,
total_scale_down_events: 0,
cooldown_remaining: Duration::ZERO,
history_size: 0,
is_in_cooldown: false,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_autoscaler_creation() {
let config: _ = AutoscalerConfig {
scale_up_threshold: 0.80,
scale_down_threshold: 0.30,
cooldown_period: Duration::from_secs(60),
min_nodes: 2,
max_nodes: 10,
};
let autoscaler: _ = Autoscaler::new(config);
assert!(autoscaler.is_enabled());
assert_eq!(autoscaler.get_cooldown_remaining(), Duration::ZERO);
}
#[test]
fn test_scale_up_decision() {
let mut autoscaler = Autoscaler::new(AutoscalerConfig {
scale_up_threshold: 0.80,
scale_down_threshold: 0.30,
cooldown_period: Duration::from_secs(60),
min_nodes: 2,
max_nodes: 10,
});
let high_load_metrics: _ = ClusterMetrics {
cpu_utilization: 0.85,
memory_utilization: 0.90,
network_utilization: 0.75,
active_tasks: 150,
queue_depth: 50,
response_time_ms: 500,
error_rate: 0.02,
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
};
let action: _ = autoscaler.evaluate_scaling(&high_load_metrics);
assert!(matches!(action, ScalingAction::ScaleUp(_)));
}
#[test]
fn test_scale_down_decision() {
let mut autoscaler = Autoscaler::new(AutoscalerConfig {
scale_up_threshold: 0.80,
scale_down_threshold: 0.30,
cooldown_period: Duration::from_secs(60),
min_nodes: 2,
max_nodes: 10,
});
let low_load_metrics: _ = ClusterMetrics {
cpu_utilization: 0.20,
memory_utilization: 0.25,
network_utilization: 0.15,
active_tasks: 10,
queue_depth: 0,
response_time_ms: 50,
error_rate: 0.0,
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
};
let action: _ = autoscaler.evaluate_scaling(&low_load_metrics);
assert!(matches!(action, ScalingAction::ScaleDown(_)));
}
#[test]
fn test_no_scaling_decision() {
let mut autoscaler = Autoscaler::new(AutoscalerConfig {
scale_up_threshold: 0.80,
scale_down_threshold: 0.30,
cooldown_period: Duration::from_secs(60),
min_nodes: 2,
max_nodes: 10,
});
let normal_load_metrics: _ = ClusterMetrics {
cpu_utilization: 0.50,
memory_utilization: 0.55,
network_utilization: 0.45,
active_tasks: 50,
queue_depth: 5,
response_time_ms: 100,
error_rate: 0.01,
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
};
let action: _ = autoscaler.evaluate_scaling(&normal_load_metrics);
assert!(matches!(action, ScalingAction::NoOp));
}
#[test]
fn test_cooldown_period() {
let mut autoscaler = Autoscaler::new(AutoscalerConfig {
scale_up_threshold: 0.80,
scale_down_threshold: 0.30,
cooldown_period: Duration::from_secs(60),
min_nodes: 2,
max_nodes: 10,
});
let high_load_metrics: _ = ClusterMetrics {
cpu_utilization: 0.85,
memory_utilization: 0.90,
network_utilization: 0.75,
active_tasks: 150,
queue_depth: 50,
response_time_ms: 500,
error_rate: 0.02,
timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
};
let action: _ = autoscaler.evaluate_scaling(&high_load_metrics);
assert!(matches!(action, ScalingAction::ScaleUp(_)));
let action: _ = autoscaler.evaluate_scaling(&high_load_metrics);
assert!(matches!(action, ScalingAction::NoOp));
assert!(autoscaler.get_cooldown_remaining() > Duration::ZERO);
}
}