mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +08:00
feat: 低于1GB内存的优化展示
This commit is contained in:
@@ -246,10 +246,32 @@ class ServerStatusMonitorManager extends ChangeNotifier {
|
||||
|
||||
if (client.isClosed) return;
|
||||
|
||||
// Group 2: Memory
|
||||
// Group 2: Memory (+ cgroup 内存/交换分区限制,容器化环境下 free -m 反映的是宿主机数据)
|
||||
try {
|
||||
final output = await _run(client, 'free -m');
|
||||
final memory = ServerStatusParser.parseMemory(output);
|
||||
const memSep = '---EASYNODE_MEMSEP---';
|
||||
final output = await _run(
|
||||
client,
|
||||
'free -m\necho $memSep'
|
||||
'\nif [ -f /sys/fs/cgroup/memory.max ]; then echo v2; '
|
||||
'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/memory.current; '
|
||||
'cat /sys/fs/cgroup/memory.swap.max 2>/dev/null || echo na; '
|
||||
'cat /sys/fs/cgroup/memory.swap.current 2>/dev/null || echo na; '
|
||||
'elif [ -f /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then echo v1; '
|
||||
'cat /sys/fs/cgroup/memory/memory.limit_in_bytes; '
|
||||
'cat /sys/fs/cgroup/memory/memory.usage_in_bytes; echo na; echo na; '
|
||||
'else echo none; fi',
|
||||
);
|
||||
final parts = output.split(memSep);
|
||||
final hostMemory = ServerStatusParser.parseMemory(
|
||||
parts.isNotEmpty ? parts[0] : '',
|
||||
);
|
||||
final cgroupMemory = parts.length > 1
|
||||
? ServerStatusParser.parseCgroupMemory(parts[1])
|
||||
: null;
|
||||
final memory = ServerStatusParser.applyCgroupOverride(
|
||||
hostMemory,
|
||||
cgroupMemory,
|
||||
);
|
||||
runtime.currentMemInfo = memory.memInfo;
|
||||
runtime.currentSwapInfo = memory.swapInfo;
|
||||
_emitSnapshot(runtime);
|
||||
|
||||
@@ -226,6 +226,113 @@ class ServerStatusParser {
|
||||
);
|
||||
}
|
||||
|
||||
// 解析 cgroup 内存/交换分区限制信息(一次命令拿全 5 行:
|
||||
// version, memTotal, memUsed, swapTotal, swapUsed)。
|
||||
// 容器化环境下 free -m 反映的是宿主机数据而非容器配额,需要 cgroup 数据来修正。
|
||||
// 返回 null 表示未容器化 / 权限不足 / 无法确定限制,调用方应继续用 free -m 的结果兜底。
|
||||
static CgroupMemoryInfo? parseCgroupMemory(String output) {
|
||||
final lines = output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.where((line) => line.isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (lines.isEmpty) return null;
|
||||
final version = lines[0];
|
||||
if (version != 'v1' && version != 'v2') return null;
|
||||
if (lines.length < 5) return null;
|
||||
|
||||
final memTotalRaw = lines[1];
|
||||
final memUsedRaw = lines[2];
|
||||
final swapTotalRaw = lines[3];
|
||||
final swapUsedRaw = lines[4];
|
||||
|
||||
// 处理内存限制:v2 用 "max" 表示无限制;
|
||||
// v1 无限制时是接近 2^63 的哨兵值,用 1PB 阈值过滤掉
|
||||
int? memTotalBytes;
|
||||
int? memUsedBytes;
|
||||
if (memTotalRaw != 'max') {
|
||||
final t = int.tryParse(memTotalRaw);
|
||||
final u = int.tryParse(memUsedRaw);
|
||||
final isSentinel = version == 'v1' && t != null && t >= 1000000000000000;
|
||||
if (t != null && u != null && t > 0 && !isSentinel) {
|
||||
memTotalBytes = t;
|
||||
memUsedBytes = u;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理交换分区限制:只支持 cgroup v2 的 memory.swap.max/current(swap 专属限制)
|
||||
// v1 只有 memsw(内存+交换合并计数),需要减法推导且依赖内核 swapaccount 参数,
|
||||
// 可靠性不足,这里不做处理,交由 free -m 的宿主机数据兜底
|
||||
int? swapTotalBytes;
|
||||
int? swapUsedBytes;
|
||||
if (swapTotalRaw != 'max' && swapTotalRaw != 'na') {
|
||||
final t = int.tryParse(swapTotalRaw);
|
||||
final u = int.tryParse(swapUsedRaw);
|
||||
if (t != null && u != null && t > 0) {
|
||||
swapTotalBytes = t;
|
||||
swapUsedBytes = u;
|
||||
}
|
||||
}
|
||||
|
||||
return CgroupMemoryInfo(
|
||||
memTotalBytes: memTotalBytes,
|
||||
memUsedBytes: memUsedBytes,
|
||||
swapTotalBytes: swapTotalBytes,
|
||||
swapUsedBytes: swapUsedBytes,
|
||||
);
|
||||
}
|
||||
|
||||
// 若 cgroup 限制存在且明显比宿主机数据更小(说明确实被限制了),则以 cgroup 数据覆盖
|
||||
static MemorySwapInfo applyCgroupOverride(
|
||||
MemorySwapInfo hostInfo,
|
||||
CgroupMemoryInfo? cgroupInfo,
|
||||
) {
|
||||
if (cgroupInfo == null) return hostInfo;
|
||||
|
||||
var memInfo = hostInfo.memInfo;
|
||||
final memTotalBytes = cgroupInfo.memTotalBytes;
|
||||
final memUsedBytes = cgroupInfo.memUsedBytes;
|
||||
if (memTotalBytes != null && memUsedBytes != null) {
|
||||
final cgroupTotalMb = (memTotalBytes / 1024 / 1024).round();
|
||||
if (cgroupTotalMb > 0 && cgroupTotalMb < memInfo.totalMemMb) {
|
||||
final cgroupUsedMb = max(
|
||||
0,
|
||||
min((memUsedBytes / 1024 / 1024).round(), cgroupTotalMb),
|
||||
);
|
||||
final cgroupFreeMb = cgroupTotalMb - cgroupUsedMb;
|
||||
memInfo = MemoryInfo(
|
||||
totalMemMb: cgroupTotalMb,
|
||||
usedMemMb: cgroupUsedMb,
|
||||
freeMemMb: cgroupFreeMb,
|
||||
usedMemPercentage: _round2(cgroupUsedMb / cgroupTotalMb * 100),
|
||||
freeMemPercentage: _round2(cgroupFreeMb / cgroupTotalMb * 100),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var swapInfo = hostInfo.swapInfo;
|
||||
final swapTotalBytes = cgroupInfo.swapTotalBytes;
|
||||
final swapUsedBytes = cgroupInfo.swapUsedBytes;
|
||||
if (swapTotalBytes != null && swapUsedBytes != null) {
|
||||
final cgroupSwapTotalMb = (swapTotalBytes / 1024 / 1024).round();
|
||||
if (cgroupSwapTotalMb > 0 &&
|
||||
(swapInfo.swapTotal == 0 || cgroupSwapTotalMb < swapInfo.swapTotal)) {
|
||||
final cgroupSwapUsedMb = max(
|
||||
0,
|
||||
min((swapUsedBytes / 1024 / 1024).round(), cgroupSwapTotalMb),
|
||||
);
|
||||
swapInfo = SwapInfo(
|
||||
swapTotal: cgroupSwapTotalMb,
|
||||
swapUsed: cgroupSwapUsedMb,
|
||||
swapFree: cgroupSwapTotalMb - cgroupSwapUsedMb,
|
||||
swapPercentage: _round2(cgroupSwapUsedMb / cgroupSwapTotalMb * 100),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return MemorySwapInfo(memInfo: memInfo, swapInfo: swapInfo);
|
||||
}
|
||||
|
||||
static List<DriveInfo> parseDrives(String output) {
|
||||
final drives = <DriveInfo>[];
|
||||
final lines = output.split('\n').skip(1);
|
||||
@@ -383,3 +490,17 @@ class MemorySwapInfo {
|
||||
final MemoryInfo memInfo;
|
||||
final SwapInfo swapInfo;
|
||||
}
|
||||
|
||||
class CgroupMemoryInfo {
|
||||
const CgroupMemoryInfo({
|
||||
this.memTotalBytes,
|
||||
this.memUsedBytes,
|
||||
this.swapTotalBytes,
|
||||
this.swapUsedBytes,
|
||||
});
|
||||
|
||||
final int? memTotalBytes;
|
||||
final int? memUsedBytes;
|
||||
final int? swapTotalBytes;
|
||||
final int? swapUsedBytes;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,17 @@ export default {
|
||||
if (netSpeedMB >= 1) return `${ netSpeedMB.toFixed(2) } MB/s`
|
||||
return `${ (netSpeedMB * 1024).toFixed(1) } KB/s`
|
||||
},
|
||||
// 内存/交换空间用量展示:低于1G时用MB整数展示(更直观,避免出现 0.2G 这种不精确的显示),否则用G保留1位小数
|
||||
formatMemPair(usedMb, totalMb) {
|
||||
usedMb = Number(usedMb) || 0
|
||||
totalMb = Number(totalMb) || 0
|
||||
if (totalMb > 0 && totalMb < 1024) {
|
||||
return `${ Math.round(usedMb) }/${ Math.round(totalMb) }MB`
|
||||
}
|
||||
const usedG = (usedMb / 1024).toFixed(1)
|
||||
const totalG = (totalMb / 1024).toFixed(1)
|
||||
return `${ usedG }/${ totalG }G`
|
||||
},
|
||||
// format: time OR date
|
||||
formatTimestamp: (timestamp, format = 'time', afterSeparator = ':') => {
|
||||
if (typeof(timestamp) !== 'number') return '--'
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
:color="handleUsedColor(usedMemPercentage)"
|
||||
/>
|
||||
<div class="position_right">
|
||||
{{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G
|
||||
{{ $tools.formatMemPair(memInfo.usedMemMb, memInfo.totalMemMb) }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
@@ -101,7 +101,7 @@
|
||||
:color="handleUsedColor(swapPercentage)"
|
||||
/>
|
||||
<div class="position_right">
|
||||
{{ $tools.toFixed(swapInfo.swapUsed / 1024) }}/{{ $tools.toFixed(swapInfo.swapTotal / 1024) }}G
|
||||
{{ $tools.formatMemPair(swapInfo.swapUsed, swapInfo.swapTotal) }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-for="(d,idx) in drivesInfo" :key="d.filesystem + idx">
|
||||
|
||||
Reference in New Issue
Block a user