很多MC服务器启动器高级功能中有一项是能够实时监控服务端进程的性能占用,这需要管理器进程能够获取到服务端进程的CPU和内存占用情况。
LSL是使用C#编写的,但是.NET没有任何现成的类能够集中监控这些数据。唯一一个能够获取到这些数据的类是.NET Framework 4.6.2中的PerformanceCounter,但是首先,这玩意儿在高版本的.NET中已经被移除了;其次,它只兼容Windows平台;第三,这东西虽然详细,但是没有任何卵用,因为它监控的是C#应用程序本身的性能信息,管不到Process对象上……
所以,我们只能自己动手丰衣足食了。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| using System; using System.Diagnostics; using System.Threading;
namespace LSL.Services.ServerServices;
public class ProcessMetricsMonitor : IDisposable { public event EventHandler<ProcessMetricsEventArgs>? MetricsUpdated; private readonly Timer _timer; private readonly Process _process; private readonly long _allocatedMemoryBytes; private TimeSpan _prevCpuTime; private DateTime _prevTime; private readonly object _lock = new(); private bool _disposed;
public ProcessMetricsMonitor(Process process, long allocatedMemoryBytes, int interval = 1000) { _process = process; _allocatedMemoryBytes = allocatedMemoryBytes; _prevCpuTime = process.TotalProcessorTime; _prevTime = DateTime.UtcNow; _timer = new Timer(OnTimerCallback, null, interval, interval); }
private void OnTimerCallback(object? state) { lock (_lock) { if (_disposed) return;
double cpuUsage = 0; long processMemory = 0; bool isExited;
try { isExited = _process.HasExited; if (!isExited) { _process.Refresh(); var currentTime = DateTime.UtcNow; var currentCpuTime = _process.TotalProcessorTime; var cpuElapsed = (currentCpuTime - _prevCpuTime).TotalSeconds; var timeElapsed = (currentTime - _prevTime).TotalSeconds; _prevTime = currentTime; _prevCpuTime = currentCpuTime;
if (timeElapsed > 0 && cpuElapsed > 0) { cpuUsage = (cpuElapsed / timeElapsed) * 100; cpuUsage /= Environment.ProcessorCount; }
processMemory = _process.PrivateMemorySize64; } } catch (InvalidOperationException) { isExited = true; } catch (Exception ex) { MetricsUpdated?.Invoke(this, new ProcessMetricsEventArgs(_id, 0, 0, 0, true, ex.Message)); return; }
MetricsUpdated?.Invoke(this, new ProcessMetricsEventArgs( cpuUsage, processMemory, _allocatedMemoryBytes, isExited )); } } public void Dispose() { lock (_lock) { if (_disposed) return; MetricsUpdated?.Invoke(this, new ProcessMetricsEventArgs( 0, 0, _allocatedMemoryBytes, true )); _timer.Dispose(); _disposed = true; GC.SuppressFinalize(this); } } }
|
详解
我个人对这个实现比较满意,它没有对Process类本身进行修改或者使用附加方法(.NET里面涉及到非托管资源的还是得慎重),而是让监控器持有一个Process对象,通过定时器定时获取进程的CPU使用率、内存占用、分配内存等信息,然后通过事件通知给外部。这样,外部只需要订阅事件,就可以获取到进程的性能信息了。
1. 获取CPU使用率
CPU占用率没有很好的指标,目前能想到的只有Process自带的TotalProcessorTime属性,它表示进程自启动以来所消耗的CPU时间,因此我们可以通过计算两次获取的TotalProcessorTime的差值,除以CPU核数(可以在一个全局的static class中缓存以减少对Environment类的调用开销),再除以两次获取的时间间隔,得到CPU使用率。这是一个笨办法,但能解决问题。唯一的缺点就是无法精确到单个线程,只能得到进程整体的CPU使用率。
关于为什么不直接除以一秒,是由于Timer本身就不是为高精度计时设计的,每次CallBack的时间间隔都有微小差异,当CPU忙的时候甚至有可能多个任务堆积导致极大的误差,因此我选择了采用TimeSpan计时的方式。
2. 获取内存占用
内存占用就相当简单了,可以直接通过Process自带的PrivateMemorySize64属性获取,它表示进程独占的内存提交量大小(不要使用WorkingSet64,它代表的是物理内存中的活跃部分,和程序占用的的总内存大小有本质差异)。我还在构造函数中直接引入了配置中给它分配的内存字节数,这样传出的就是两个表示占用的浮点数,格式较为统一。
这个方案有一个无法克服的缺点,就是给JVM传递的参数是JVM堆内存的限制,而JVM实际使用的内存不止这点,因此这个方案有概率使得内存占用超过100%,而且分配的堆内存越小出现这种情况的概率越大。想要破解这个问题只能在JVM上动手脚,但是这种侵入式的设计我不是很想干…就暂时这样了。
除此之外,还有一个容易遗漏的重点,就是用于刷新进程状态的_process.Refresh()方法的调用。该方法对于我们的实时监测非常重要,因为PrivateMemorySize64属性不会实时更新,如果不调用,性能监控图上就会是一条直线,因为获取到的内存占用值始终是同一个值。