import logging
import threading
import time
from typing import List, Optional
import psutil
from olc.collector.collector import Measurable, Meter, T, AbsCollector
logger = logging.getLogger(__name__)
class LocalResourceMeasurable(Measurable):
"""本地资源测量结果"""
cpu: float
memory: float
io: float
network: float
loadaverage: float
def __init__(self, cpu: float, memory: float, io: float, network: float, loadaverage: float):
self.cpu = cpu
self.memory = memory
self.io = io
self.network = network
self.loadaverage = loadaverage
@classmethod
def get_properties(cls) -> List[str]:
return ["cpu", "memory", "io", "network", "loadaverage"]
def __str__(self) -> str:
return (f"LocalResourceMeasurable("
f"cpu={self.cpu}, memory={self.memory}, io={self.io},"
f"network={self.network}, loadaverage={self.loadaverage}")
def __repr__(self):
return self.__str__()
class LocalResourceMeter(Meter[LocalResourceMeasurable]):
_instance = None
_lock = threading.Lock()
def __init__(self):
self._prev_disk_io: Optional[tuple] = None
self._prev_disk_time: float = 0.0
self._prev_net_io: Optional[tuple] = None
self._prev_net_time: float = 0.0
@classmethod
def get_instance(cls) -> "LocalResourceMeter":
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def measure(self) -> Optional[LocalResourceMeasurable]:
default_value = -1.0
try:
cpu = self._get_cpu_usage()
except Exception as e:
logger.warning(f"get cpu usage faild : %s", e)
cpu = default_value
try:
memory = self._get_memory_usage()
except Exception as e:
logger.warning(f"get memory usage faild : %s", e)
memory = default_value
try:
io = self._get_io_usage()
except Exception as e:
logger.warning(f"get io usage faild : %s", e)
io = default_value
try:
network = self._get_network_usage()
except Exception as e:
logger.warning(f"get network usage faild : %s", e)
network = default_value
try:
load = self._get_load_usage()
except Exception as e:
logger.warning(f"get load usage faild : %s", e)
load = default_value
return LocalResourceMeasurable(cpu, memory, io, network, load)
def _get_cpu_usage(self) -> float:
"""
获取 CPU 使用率(0.0-1.0)。
实现说明:
- 使用 psutil.cpu_percent(interval=0) 进行非阻塞采样。
- interval=0 表示不等待立即返回,数值基于自上一次调用以来的
CPU 时间计算,因此连续两次调用之间需要间隔一段时间
才能得到有意义的数值;首次调用通常返回 0.0。
- 返回值除以 100 转换为 0.0-1.0 的浮点数。
"""
return psutil.cpu_percent(interval=0) / 100.0
def _get_memory_usage(self) -> float:
"""
获取物理内存使用率(0.0-1.0)。
实现说明:
- 通过 psutil.virtual_memory() 获取系统整体的虚拟内存信息。
- .percent 字段表示已用内存占总物理内存的百分比,
与主流监控工具(如 free、top、Prometheus node_exporter)口径一致。
- 返回值除以 100 转换为 0.0-1.0 的浮点数。
"""
return psutil.virtual_memory().percent / 100.0
def _get_io_usage(self) -> float:
"""
获取磁盘 IO 使用率(0.0-1.0)。
实现说明:
- 使用 psutil.disk_io_counters() 累计所有磁盘的读写字节数。
- 由于该 API 返回的是自系统启动以来的累计值,无法直接反映
瞬时 IO 负载,需要通过两次采样的差值计算单位时间吞吐。
- 状态保存在 self._prev_disk_io / self._prev_disk_time:
* 首次调用:没有历史数据,仅记录当前快照,返回 0.0。
* 后续调用:计算 (read_bytes + write_bytes) 的字节增量
与时间增量,得到平均吞吐(MB/s),再以 500MB/s 作为
参考带宽上限归一化到 0.0-1.0,避免出现超过 1.0 的不合理值。
- 500MB/s 是消费级 SSD / 普通磁盘阵列的常见参考值,
部署在高性能 NVMe 或更高速存储时可按需调高该基准。
"""
counters = psutil.disk_io_counters()
if counters is None:
return 0.0
current_time = time.time()
read_bytes = counters.read_bytes
write_bytes = counters.write_bytes
if self._prev_disk_io is not None and self._prev_disk_time > 0:
prev_read, prev_write = self._prev_disk_io
bytes_delta = (read_bytes - prev_read) + (write_bytes - prev_write)
time_delta = current_time - self._prev_disk_time
if time_delta > 0:
rate_mbps = bytes_delta / time_delta / 1024 / 1024
usage = min(rate_mbps / 500.0, 1.0)
self._prev_disk_io = (read_bytes, write_bytes)
self._prev_disk_time = current_time
return usage
self._prev_disk_io = (read_bytes, write_bytes)
self._prev_disk_time = current_time
return 0.0
def _get_network_usage(self) -> float:
"""
获取网络使用率(0.0-1.0)。
实现说明:
- 使用 psutil.net_io_counters() 获取所有网卡累计收发字节数。
- 与磁盘 IO 类似,累计值无法直接反映瞬时流量,
需要通过两次采样的差值计算平均吞吐。
- 状态保存在 self._prev_net_io / self._prev_net_time:
* 首次调用:没有历史数据,仅记录当前快照,返回 0.0。
* 后续调用:计算 (bytes_sent + bytes_recv) 的字节增量
与时间增量,得到平均吞吐(MB/s),按 1000MB/s
(≈1Gbps 满载)归一化到 0.0-1.0。
- 若服务器配备万兆(10Gbps)或更高带宽,应按比例调高基准。
"""
counters = psutil.net_io_counters()
current_time = time.time()
bytes_sent = counters.bytes_sent
bytes_recv = counters.bytes_recv
if self._prev_net_io is not None and self._prev_net_time > 0:
prev_sent, prev_recv = self._prev_net_io
bytes_delta = (bytes_sent - prev_sent) + (bytes_recv - prev_recv)
time_delta = current_time - self._prev_net_time
if time_delta > 0:
rate_mbps = bytes_delta / time_delta / 1024 / 1024
usage = min(rate_mbps / 1000.0, 1.0)
self._prev_net_io = (bytes_sent, bytes_recv)
self._prev_net_time = current_time
return usage
self._prev_net_io = (bytes_sent, bytes_recv)
self._prev_net_time = current_time
return 0.0
def _get_load_usage(self) -> float:
"""
获取系统负载使用率(0.0-1.0)。
实现说明:
- 优先使用 psutil.getloadavg() 获取 1/5/15 分钟的负载平均值,
与 Linux `uptime` / `top` 的 load average 概念一致:
表示单位时间内处于可运行 + 不可中断状态的平均任务数。
- 将 1 分钟负载除以 CPU 核心数,得到每核心的负载比例,
直接归一化到 0.0-1.0:
* load1 == cpu_count 时,约为 1.0(满载边界);
* load1 > cpu_count 时,说明存在等待队列,
值会被截断到 1.0。
- 兼容性处理:
* Windows 平台没有 getloadavg,会抛出 AttributeError;
* 部分受限环境可能抛出 OSError。
上述两种情况下回退使用 CPU 使用率作为负载的近似值,
以保证采集器在跨平台场景下不会失败。
"""
try:
load1, _, _ = psutil.getloadavg()
cpu_count = psutil.cpu_count()
if cpu_count and cpu_count > 0:
return min(load1 / cpu_count, 1.0)
return min(load1, 1.0)
except (AttributeError, OSError):
return psutil.cpu_percent(interval=0) / 100.0
class LocalResourceCollector(AbsCollector):
"""本地资源采集器"""
NAME = "localresource"
def __init__(self, meter: Optional[LocalResourceMeter] = None):
if meter is None:
meter = LocalResourceMeter.get_instance()
super().__init__(meter, LocalResourceMeasurable, True, True, "olc-localresource-collector-pool")
self._measurements: List[LocalResourceMeasurable] = []
def get_name(self) -> str:
return self.NAME
def get_storage(self) -> List[LocalResourceMeasurable]:
with self._lock:
return self._measurements
def get_properties(self) -> List[str]:
return LocalResourceMeasurable.get_properties()