Skip to content

Commit 2ce3d73

Browse files
authored
Feature/resource reservation (#380)
Add resource reservation of memory for nodes
1 parent b809345 commit 2ce3d73

6 files changed

Lines changed: 106 additions & 1 deletion

File tree

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ build/
55
dist/
66
*.egg-info/
77
proxlb_dev.yaml
8+
log.log
9+
proxlb/log.log
10+
proxlb.yaml
11+
proxlb_reservation.yaml

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ Connect with us in our dedicated chat room for immediate support and live intera
530530
| Support Channel | Link |
531531
|------|:------:|
532532
| Matrix | [#proxlb:gyptazy.com](https://matrix.to/#/#proxlb:gyptazy.com) |
533-
| Discord | [Discord](https://discord.gg/JemGu7WbfQ) |
533+
| Discord | [Discord](https://discord.gg/JemGu7WbfQ) |
534534
| GitHub Community | [GitHub Community](https://github.com/gyptazy/ProxLB/discussions/)
535535
| GitHub | [ProxLB GitHub](https://github.com/gyptazy/ProxLB/issues) |
536536

‎config/proxlb_example.yaml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ balancing:
6969
pin: # Define a pinning og guests to specific node(s)
7070
- virt66
7171
- virt77
72+
# reserve some ressource for proxmox and / or other application on the nodes
73+
node_resource_reserve:
74+
# defaults - cpu in %, memory in GB, disk in %
75+
defaults:
76+
cpu: 10
77+
memory: 4
78+
disk: 20
79+
node1:
80+
memory: 4
7281

7382
service:
7483
daemon: True

‎proxlb/main.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def main():
9090
# Update the initial node resource assignments
9191
# by the previously created groups.
9292
Calculations.set_node_assignments(proxlb_data)
93+
Helper.log_node_metrics(proxlb_data, init=False)
9394
Calculations.set_node_hot(proxlb_data)
9495
Calculations.set_guest_hot(proxlb_data)
9596
Calculations.get_most_free_node(proxlb_data, cli_args.best_node)

‎proxlb/models/nodes.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import time
2525
from typing import Dict, Any
2626
from utils.logger import SystemdLogger
27+
from utils.helper import Helper
2728

2829
logger = SystemdLogger()
2930

@@ -102,6 +103,7 @@ def get_nodes(proxmox_api: any, proxlb_config: Dict[str, Any]) -> Dict[str, Any]
102103
nodes["nodes"][node["node"]]["disk_pressure_full_spikes_percent"] = Nodes.get_node_rrd_data(proxmox_api, node["node"], "disk", "full", spikes=True)
103104
nodes["nodes"][node["node"]]["disk_pressure_hot"] = False
104105

106+
Nodes.apply_resource_reservation(nodes["nodes"][node["node"]]["name"], proxlb_config, nodes["nodes"][node["node"]])
105107
# Evaluate if node should be set to maintenance mode
106108
if Nodes.set_node_maintenance(proxmox_api, proxlb_config, node["node"]):
107109
nodes["nodes"][node["node"]]["maintenance"] = True
@@ -253,3 +255,62 @@ def get_node_pve_version(proxmox_api, node_name: str) -> float:
253255
logger.debug(f"Got version {version['version']} for node {node_name}.")
254256
logger.debug("Finished: get_node_pve_version.")
255257
return version["version"]
258+
259+
260+
@staticmethod
261+
def apply_resource_reservation(node_name, proxlb_config: Dict[str, Any], node_data: Dict[str, Any]) -> None:
262+
"""
263+
Check if there is a a configured resource reservation for the current nodes and apply it as needed.
264+
Checks for a node specific config first, then if there is any configured default and if neither then nothing is reserved.
265+
Reservations are applied by directly modifying the data gathered from the nodes.
266+
267+
Args:
268+
node_name: (str) the name of the node
269+
proxlb_config (Dict[str, Any]): A dictionary containing the ProxLB configuration.
270+
node_data: (Dict[str, Any]): Dict containing the current nodes data
271+
Returns: none
272+
"""
273+
logger.debug(f"Starting: Resource reservation")
274+
logger.debug(f"Processing resource reservation for node {node_name}")
275+
# load the balancing section from the config dict, will create an empty dict if there isnt anything
276+
balancing_cfg = proxlb_config.get("balancing", {})
277+
# get the reservation dict from the previously loaded dict , will create an empty dict if there isnt anything
278+
reserve_cfg = balancing_cfg.get("node_resource_reserve", {})
279+
# try to get the reserved memory for the node
280+
reserved_memory_gb_node = reserve_cfg.get(node_name, {}).get("memory")
281+
# try to load the default reserved memory - will set to 0 if there isn't anything
282+
reserved_memory_gb_default = reserve_cfg.get("defaults", {}).get("memory", 0)
283+
284+
# make sure the reservation is a numeric value - check for both, default and node specific
285+
if not isinstance(reserved_memory_gb_default, (int, float)):
286+
if reserved_memory_gb_default is not None:
287+
logger.info("Invalid default memory reservation: Found a string while expecting a numeric value - skipping default reservation")
288+
reserved_memory_gb_default = 0
289+
290+
if not isinstance(reserved_memory_gb_node, (int, float)):
291+
if reserved_memory_gb_node is not None:
292+
logger.info(f"Invalid memory reservation: Found a string while expecting a numeric value - applying current default of {reserved_memory_gb_default} Bytes")
293+
reserved_memory_gb_node = reserved_memory_gb_default
294+
295+
# make sure the reservation is positive
296+
if reserved_memory_gb_node < 0:
297+
logger.info(f"{nodes['nodes'][node['node']]['name']}: Invalid assigned memory reservation, applying defaults")
298+
reserved_memory_gb_node = reserved_memory_gb_default
299+
300+
# convert the reservation from GB to Bytes, get the current nodes physical memory
301+
reserved_memory_node = int(round(reserved_memory_gb_node * 1024 ** 3))
302+
total_mem = node_data.get("memory_total")
303+
304+
# check if the reservation doesnt exceed the nodes total memory
305+
if reserved_memory_node > total_mem:
306+
logger.debug(f"Reservation of {reserved_memory_node} Bytes exceeds available memory of {total_mem} Bytes- skipping reservation")
307+
reserved_memory_node = 0
308+
reserved_memory_gb_node = 0
309+
310+
logger.debug(f"Reserved Memory: {reserved_memory_gb_node} GB ({reserved_memory_node} Bytes)")
311+
node_data["memory_total"] -= reserved_memory_node
312+
Helper.update_node_resource_percentages(node_data)
313+
314+
logger.debug(f"End: Resource reservation")
315+
return
316+

‎proxlb/utils/helper.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def log_node_metrics(proxlb_data: Dict[str, Any], init: bool = True) -> None:
8181
"""
8282
logger.debug("Starting: log_node_metrics.")
8383
nodes_usage_memory = " | ".join([f"{key}: {value['memory_used_percent']:.2f}%" for key, value in proxlb_data["nodes"].items()])
84+
nodes_assigned_memory = " | ".join([f"{key}: {value['memory_assigned_percent']:.2f}%" for key, value in proxlb_data["nodes"].items()])
8485
nodes_usage_cpu = " | ".join([f"{key}: {value['cpu_used_percent']:.2f}%" for key, value in proxlb_data["nodes"].items()])
8586
nodes_usage_disk = " | ".join([f"{key}: {value['disk_used_percent']:.2f}%" for key, value in proxlb_data["nodes"].items()])
8687

@@ -90,6 +91,7 @@ def log_node_metrics(proxlb_data: Dict[str, Any], init: bool = True) -> None:
9091
proxlb_data["meta"]["statistics"]["after"] = {"memory": nodes_usage_memory, "cpu": nodes_usage_cpu, "disk": nodes_usage_disk}
9192

9293
logger.debug(f"Nodes usage memory: {nodes_usage_memory}")
94+
logger.debug(f"Nodes usage memory assigned: {nodes_assigned_memory}")
9395
logger.debug(f"Nodes usage cpu: {nodes_usage_cpu}")
9496
logger.debug(f"Nodes usage disk: {nodes_usage_disk}")
9597
logger.debug("Finished: log_node_metrics.")
@@ -333,3 +335,31 @@ def tcp_connect_test(addr_family: int, host: str, port: int, timeout: int) -> tu
333335
return (rc == 0, rc if rc != 0 else None)
334336
finally:
335337
test_socket.close()
338+
339+
@staticmethod
340+
def update_node_resource_percentages(node_data: Dict[str, any]) -> None:
341+
"""
342+
This function updates the percentages of the node's resources in its respective dict
343+
Args:
344+
node_data: (Dict[str, any]) dict containing the nodes data to be updated
345+
Returns: none
346+
"""
347+
logger.debug(f"Starting: Update resource percentages")
348+
logger.debug(f"node data: {node_data}")
349+
# memory
350+
node_data["memory_assigned_percent"] = node_data["memory_assigned"] / node_data["memory_total"] * 100
351+
node_data["memory_free_percent"] = node_data["memory_free"] / node_data["memory_total"] * 100
352+
node_data["memory_used_percent"] = node_data["memory_used"] / node_data["memory_total"] * 100
353+
# cpu
354+
node_data["cpu_assigned_percent"] = node_data["cpu_assigned"] / node_data["cpu_total"] * 100
355+
node_data["cpu_free_percent"] = node_data["cpu_free"] / node_data["cpu_total"] * 100
356+
node_data["cpu_used_percent"] = node_data["cpu_used"] / node_data["cpu_total"] * 100
357+
# disk
358+
node_data["disk_assigned_percent"] = node_data["disk_assigned"] / node_data["disk_total"] * 100
359+
node_data["disk_free_percent"] = node_data["disk_free"] / node_data["disk_total"] * 100
360+
node_data["disk_used_percent"] = node_data["disk_used"] / node_data["disk_total"] * 100
361+
362+
logger.debug(f"node data: {node_data}")
363+
logger.debug(f"End: Update resource percentages")
364+
return
365+

0 commit comments

Comments
 (0)