From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 901E58E120 for ; Thu, 10 Nov 2022 15:38:08 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 79E14287DC for ; Thu, 10 Nov 2022 15:38:08 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Thu, 10 Nov 2022 15:38:05 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id ADD6F44A65 for ; Thu, 10 Nov 2022 15:38:04 +0100 (CET) From: Fiona Ebner To: pve-devel@lists.proxmox.com Date: Thu, 10 Nov 2022 15:37:41 +0100 Message-Id: <20221110143800.98047-3-f.ebner@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221110143800.98047-1-f.ebner@proxmox.com> References: <20221110143800.98047-1-f.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH proxmox-resource-scheduling 2/3] add pve_static module X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 10 Nov 2022 14:38:08 -0000 Models usage of guests and nodes, and allows scoring nodes on which to start a new service via TOPSIS. For this scoring, each node in turn is considered as if the service was already running on it. CPU and memory usage are used as criteria, with memory being weighted much more, because it's a truly limited resource. For both, CPU and memory, highest usage among nodes (weighted more, as ideally no node should be overcommited) and average usage of all nodes (to still be able to distinguish in case there already is a more highly commited node) are considered. Signed-off-by: Fiona Ebner --- Cargo.toml | 2 + src/lib.rs | 1 + src/pve_static.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 src/pve_static.rs diff --git a/Cargo.toml b/Cargo.toml index ec8e12f..85d0ec1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ path = "src/lib.rs" [dependencies] anyhow = "1.0" +lazy_static = "1.4" +serde = { version = "1.0", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index dda0563..c82bd40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ +pub mod pve_static; pub mod topsis; diff --git a/src/pve_static.rs b/src/pve_static.rs new file mode 100644 index 0000000..cb8a823 --- /dev/null +++ b/src/pve_static.rs @@ -0,0 +1,143 @@ +use anyhow::Error; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +use crate::topsis::{TopsisCriteria, TopsisCriterion, TopsisMatrix}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Static usage information of a node. +pub struct StaticNodeUsage { + /// Hostname of the node. + pub name: String, + /// CPU utilization. Can be more than `maxcpu` if overcommited. + pub cpu: f64, + /// Total number of CPUs. + pub maxcpu: usize, + /// Used memory in bytes. Can be more than `maxmem` if overcommited. + pub mem: usize, + /// Total memory in bytes. + pub maxmem: usize, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Static usage information of an HA resource. +pub struct StaticServiceUsage { + /// Number of assigned CPUs or CPU limit. + pub maxcpu: f64, + /// Maximum assigned memory in bytes. + pub maxmem: usize, +} + +/// Calculate new CPU usage in percent. +/// `add` being `0.0` means "unlimited" and results in `max` being added. +fn add_cpu_usage(old: f64, max: f64, add: f64) -> f64 { + if add == 0.0 { + old + max + } else { + old + add + } +} + +impl StaticNodeUsage { + /// Add usage of `service` to the node's usage. + pub fn add_service_usage(&mut self, service: &StaticServiceUsage) { + self.cpu = add_cpu_usage(self.cpu, self.maxcpu as f64, service.maxcpu); + self.mem += service.maxmem; + } +} + +/// A given alternative. +struct PveTopsisAlternative { + average_cpu: f64, + highest_cpu: f64, + average_memory: f64, + highest_memory: f64, +} + +const N_CRITERIA: usize = 4; + +// NOTE It is essenital that the order of the criteria definition and the order in the +// From implementation match up. + +lazy_static! { + static ref PVE_HA_TOPSIS_CRITERIA: TopsisCriteria = TopsisCriteria::new([ + TopsisCriterion::new("average CPU".to_string(), -1.0), + TopsisCriterion::new("highest CPU".to_string(), -2.0), + TopsisCriterion::new("average memory".to_string(), -5.0), + TopsisCriterion::new("highest memory".to_string(), -10.0), + ]) + .unwrap(); +} + +impl From for [f64; N_CRITERIA] { + fn from(alternative: PveTopsisAlternative) -> Self { + [ + alternative.average_cpu, + alternative.highest_cpu, + alternative.average_memory, + alternative.highest_memory, + ] + } +} + +/// Scores candidate `nodes` to start a `service` on. Scoring is done according to the static memory +/// and CPU usages of the nodes as if the service would already be running on each. +/// +/// Returns a vector of (nodename, score) pairs. Scores are between 0.0 and 1.0 and a higher score +/// is better. +pub fn score_nodes_to_start_service( + nodes: &[&StaticNodeUsage], + service: &StaticServiceUsage, +) -> Result, Error> { + let len = nodes.len(); + + let matrix = nodes + .iter() + .enumerate() + .map(|(target_index, _)| { + // all of these are as percentages to be comparable across nodes + let mut highest_cpu = 0.0; + let mut sum_cpu = 0.0; + let mut highest_mem = 0.0; + let mut sum_mem = 0.0; + + for (index, node) in nodes.iter().enumerate() { + let new_cpu = if index == target_index { + add_cpu_usage(node.cpu, node.maxcpu as f64, service.maxcpu) + } else { + node.cpu + } / (node.maxcpu as f64); + highest_cpu = f64::max(highest_cpu, new_cpu); + sum_cpu += new_cpu; + + let new_mem = if index == target_index { + node.mem + service.maxmem + } else { + node.mem + } as f64 + / node.maxmem as f64; + highest_mem = f64::max(highest_mem, new_mem); + sum_mem += new_mem; + } + + PveTopsisAlternative { + average_cpu: sum_cpu / len as f64, + highest_cpu, + average_memory: sum_mem / len as f64, + highest_memory: highest_mem, + } + .into() + }) + .collect::>(); + + let scores = + crate::topsis::score_alternatives(&TopsisMatrix::new(matrix)?, &PVE_HA_TOPSIS_CRITERIA)?; + + Ok(scores + .into_iter() + .enumerate() + .map(|(n, score)| (nodes[n].name.clone(), score)) + .collect()) +} -- 2.30.2