上一篇介绍了整体构架,这一篇介绍如何实现心跳程序。在ubuntu上使用.net程序有不少方法,我们采用使用标准框架的方式。
一、工程配置
1、类型:控制台应用——莫担心,它会得到一个dll,部署这个dll即可
2、目标框架:.net 8.0
3、目标OS:无
二、心跳客户端
在分布式评测系统中,Judge0服务器节点需要向管理端(位于OJ服务器上,Manager)报告自己的状态。并且需要:
1、尽可能占用较少资源,不影响Judge0评测性能
2、Manager故障不影响心跳发送
3、支持多个Manager地址和参数调账
4、跨平台
2.1配置文件管理
Imports System.IO Imports System.Xml.Serialization <Serializable> Public Structure Judge0ServerConfig Public OJURL() As String ' Manager服务器地址列表 Public Judge0Port As Integer ' Judge0服务端口 Public Group() As Integer ' 服务器组别 (0=低性能, 1=高性能) Public QueueMaxSize As Integer ' 队列最大容量 Public RequestCountSize As Integer ' 最大并行请求数 Public AvailableMemoryThresholdOfMB As Double ' 内存保护阈值(MB) Sub New(OJURL() As String, Judge0Port As Integer, Group() As Integer, QueueMaxSize As Integer, RequestCountSize As Integer, AvailableMemoryThresholdOfMB As Double) Me.OJURL = OJURL Me.Judge0Port = Judge0Port Me.Group = Group Me.QueueMaxSize = QueueMaxSize Me.RequestCountSize = RequestCountSize Me.AvailableMemoryThresholdOfMB = AvailableMemoryThresholdOfMB End Sub End Structure Public Class Config Private Shared filePath As String = "AIOJ_Judge0.conf" ''' <summary> ''' 序列化配置文件(首次运行生成示例配置) ''' </summary> Friend Shared Sub SerializeJudge0Config() ' 示例:包含内网和外网两个Manager地址 Dim config As New Judge0ServerConfig( {"http://192.31.116.222:6666", "http://xxx.xxx.xxx.xxx:6666"}, 2358, {0, 1}, 100, 4, 200 ) Dim serializer As New XmlSerializer(GetType(Judge0ServerConfig), {GetType(String())}) Using writer As New StreamWriter(filePath) serializer.Serialize(writer, config) End Using Console.WriteLine($"[{DateTime.Now}] 配置文件已生成: {filePath}") End Sub ''' <summary> ''' 反序列化配置文件 ''' </summary> Friend Shared Function DeserializeJudge0Config() As Judge0ServerConfig Try Dim serializer As New XmlSerializer(GetType(Judge0ServerConfig), {GetType(String())}) Using reader As New StreamReader(filePath) Return CType(serializer.Deserialize(reader), Judge0ServerConfig) End Using Catch ex As Exception Console.WriteLine($"[{DateTime.Now}] 读取配置文件失败: {ex.Message}") Console.WriteLine("请确保配置文件存在,或运行 SerializeJudge0Config 生成示例配置") Return New Judge0ServerConfig(Nothing, -1, {-1, -1}, -1, -1, -1) End Try End Function End Class2.2心跳消息发送
Imports System.Net.Http Imports System.Text Imports System.Text.Json Imports System.Threading <Serializable> Public Class HeartbeatMessage Public Property Port As Integer ' Judge0服务器端口 Public Property QueueMaxSize As Integer ' 队列最大大小 Public Property RequestCountSize As Integer ' 同时最大投递数 Public Property Group As Integer ' 所属组别 Public Property AvailableMemoryOfMB As Double ' 可用内存大小(MB) Public Property AvailableMemoryThresholdOfMB As Double ' 内存保护阈值 End Class Public Class Heartbeat Private Shared ReadOnly _httpClient As New HttpClient() ''' <summary> ''' 发送心跳请求 ''' </summary> ''' <param name="Port">Judge0端口</param> ''' <param name="group">组别</param> ''' <param name="submissionURL">Manager接收地址</param> ''' <param name="queueMaxSize">队列最大容量</param> ''' <param name="requestCountSize">最大并行数</param> ''' <param name="availableMemoryMB">可用内存</param> ''' <param name="availableMemoryThresholdOfMB">内存阈值</param> ''' <param name="timeoutSeconds">超时时间(秒)</param> ''' <returns>0=成功, 负数=错误码</returns> Public Shared Async Function SendRequest( Port As Integer, group As Integer, submissionURL As String, queueMaxSize As Integer, requestCountSize As Integer, availableMemoryMB As Double, availableMemoryThresholdOfMB As Double, timeoutSeconds As Integer ) As Task(Of Integer) ' 构建心跳消息 Dim jsonContent As New HeartbeatMessage With { .Port = Port, .Group = group, .QueueMaxSize = queueMaxSize, .RequestCountSize = requestCountSize, .AvailableMemoryOfMB = availableMemoryMB, .AvailableMemoryThresholdOfMB = availableMemoryThresholdOfMB } ' 序列化为JSON Dim content As New StringContent( JsonSerializer.Serialize(jsonContent), Encoding.UTF8, "application/json" ) Using cts As New CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) Try ' 发送POST请求 Dim response As HttpResponseMessage = Await _httpClient.PostAsync( submissionURL, content, cts.Token ) If response.IsSuccessStatusCode Then Return 0 ' 成功 Else Console.WriteLine($"[{DateTime.Now}] {submissionURL} 请求失败,状态码: {response.StatusCode}") Return -2001 End If Catch ex As TaskCanceledException When cts.Token.IsCancellationRequested ' 超时处理 Console.WriteLine($"[{DateTime.Now}] {submissionURL} 请求超时({timeoutSeconds}秒)") Return -3001 Catch ex As Exception Console.WriteLine($"[{DateTime.Now}] {submissionURL} 发送错误: {ex.Message}") Return -1001 End Try End Using End Function End Class2.3系统监控模块
Imports System.Diagnostics Imports System.IO Public Class ServerMonitor Private Shared ReadOnly _memInfoPath As String = "/proc/meminfo" ''' <summary> ''' 获取Ubuntu系统可用内存(单位:MB) ''' 直接从 /proc/meminfo 读取,性能优于调用系统命令 ''' </summary> ''' <returns>可用内存(MB),失败返回-1</returns> Public Shared Function GetUbuntuAvailableMemoryMB() As Double Try ' 检查文件是否存在 If Not File.Exists(_memInfoPath) Then Console.WriteLine($"错误: {_memInfoPath} 文件不存在") Return -1 End If ' 读取内存信息 Dim memInfoLines As String() = File.ReadAllLines(_memInfoPath) Dim availableKb As Long = -1 ' 解析 MemAvailable 字段(Linux 3.14+ 内核支持) For Each line As String In memInfoLines If line.StartsWith("MemAvailable:") Then Dim parts As String() = line.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length >= 2 AndAlso Long.TryParse(parts(1), availableKb) Then Exit For End If End If Next ' 如果找不到 MemAvailable,尝试使用 MemFree + Buffers + Cached If availableKb = -1 Then Console.WriteLine("警告: 未找到 MemAvailable,尝试计算可用内存") availableKb = CalculateAvailableMemoryFromMeminfo(memInfoLines) End If If availableKb <> -1 Then Dim availableMB As Double = availableKb / 1024.0 Return Math.Round(availableMB, 2) Else Console.WriteLine("错误: 无法解析内存信息") Return -1 End If Catch ex As UnauthorizedAccessException Console.WriteLine($"错误: 没有权限读取 {_memInfoPath}") Return -1 Catch ex As Exception Console.WriteLine($"读取内存信息异常: {ex.Message}") Return -1 End Try End Function ''' <summary> ''' 备用方法:通过 MemFree + Buffers + Cached 计算可用内存 ''' </summary> Private Shared Function CalculateAvailableMemoryFromMeminfo(lines As String()) As Long Dim memFree As Long = 0 Dim buffers As Long = 0 Dim cached As Long = 0 For Each line As String In lines If line.StartsWith("MemFree:") Then Dim parts = line.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length >= 2 Then Long.TryParse(parts(1), memFree) ElseIf line.StartsWith("Buffers:") Then Dim parts = line.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length >= 2 Then Long.TryParse(parts(1), buffers) ElseIf line.StartsWith("Cached:") Then Dim parts = line.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length >= 2 Then Long.TryParse(parts(1), cached) End If Next If memFree > 0 OrElse buffers > 0 OrElse cached > 0 Then Return memFree + buffers + cached End If Return -1 End Function ''' <summary> ''' 获取CPU负载(扩展功能) ''' </summary> Public Shared Function GetCpuLoad() As Double Try ' 读取 /proc/loadavg 获取系统负载 Dim loadAvgContent = File.ReadAllText("/proc/loadavg") Dim parts = loadAvgContent.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length >= 3 Then Dim load1Min As Double If Double.TryParse(parts(0), load1Min) Then Return load1Min End If End If Catch ex As Exception ' 忽略错误,返回 -1 End Try Return -1 End Function ''' <summary> ''' 获取Judge0进程状态(扩展功能) ''' </summary> Public Shared Function IsJudge0Running() As Boolean Try Dim processes = Process.GetProcessesByName("judge0") Return processes.Length > 0 Catch Return False End Try End Function End Class2.4主程序入口
Imports System Imports System.Net Imports System.Net.Http Imports System.Threading Module Program Private OJ As Judge0ServerConfig Private Const HEARTBEAT_INTERVAL_MS As Integer = 10000 ' 10秒 Private Const JUDGE0_READY_TIMEOUT_MINUTES As Integer = 6 Private Const JUDGE0_PORT As Integer = 2358 Sub Main(args As String()) ' 处理命令行参数 If args.Length > 0 AndAlso args(0) = "--generate-config" Then Config.SerializeJudge0Config() Console.WriteLine("配置文件已生成,请根据需要修改后重新启动") Return End If Console.WriteLine($"[{DateTime.Now}] ====================================") Console.WriteLine($"[{DateTime.Now}] AIOJ_Judge0 心跳客户端启动") Console.WriteLine($"[{DateTime.Now}] 版本: 1.0.0") Console.WriteLine($"[{DateTime.Now}] ====================================") ' 等待 Judge0 服务就绪 If Not WaitForJudge0Ready() Then Console.WriteLine($"[{DateTime.Now}] 警告: Judge0 服务未就绪,继续启动心跳...") End If ' 读取配置 OJ = Config.DeserializeJudge0Config() If OJ.OJURL Is Nothing OrElse OJ.Judge0Port = -1 Then Console.WriteLine($"[{DateTime.Now}] 错误: 无法读取配置文件!") Console.WriteLine($"[{DateTime.Now}] 请运行: {AppDomain.CurrentDomain.FriendlyName} --generate-config") Console.ReadKey() Return End If ' 显示配置信息 DisplayConfiguration() ' 开始心跳循环 RunHeartbeatLoop() Console.WriteLine($"[{DateTime.Now}] 心跳客户端已停止") End Sub ''' <summary> ''' 等待 Judge0 服务就绪 ''' </summary> Private Function WaitForJudge0Ready() As Boolean Console.WriteLine($"[{DateTime.Now}] 等待 Judge0 服务就绪...") Dim maxRetries As Integer = (JUDGE0_READY_TIMEOUT_MINUTES * 60) / (HEARTBEAT_INTERVAL_MS / 1000) Dim retryCount As Integer = 0 While retryCount < maxRetries If CheckJudge0Service() Then Console.WriteLine($"[{DateTime.Now}] ✓ Judge0 服务已就绪") Return True End If retryCount += 1 If retryCount Mod 6 = 0 Then ' 每分钟输出一次 Console.WriteLine($"[{DateTime.Now}] 等待中... ({retryCount * 10}秒/{maxRetries * 10}秒)") End If Thread.Sleep(HEARTBEAT_INTERVAL_MS) End While Console.WriteLine($"[{DateTime.Now}] ✗ Judge0 服务超时({JUDGE0_READY_TIMEOUT_MINUTES}分钟)") Return False End Function ''' <summary> ''' 检查 Judge0 服务是否可用 ''' </summary> Private Function CheckJudge0Service() As Boolean Try Using hc As New HttpClient() hc.Timeout = TimeSpan.FromSeconds(5) Dim response As HttpResponseMessage = hc.GetAsync( $"http://localhost:{JUDGE0_PORT}/about" ).GetAwaiter().GetResult() Return response.IsSuccessStatusCode End Using Catch ex As Exception Return False End Try End Function ''' <summary> ''' 显示当前配置 ''' </summary> Private Sub DisplayConfiguration() Console.WriteLine($"[{DateTime.Now}] 配置信息:") Console.WriteLine($" - Manager地址: {String.Join(", ", OJ.OJURL)}") Console.WriteLine($" - Judge0端口: {OJ.Judge0Port}") Console.WriteLine($" - 组别: {String.Join(", ", OJ.Group)}") Console.WriteLine($" - 队列容量: {OJ.QueueMaxSize}") Console.WriteLine($" - 并行数: {OJ.RequestCountSize}") Console.WriteLine($" - 内存阈值: {OJ.AvailableMemoryThresholdOfMB} MB") Console.WriteLine($" - 心跳间隔: {HEARTBEAT_INTERVAL_MS / 1000}秒") End Sub ''' <summary> ''' 心跳主循环 ''' </summary> Private Sub RunHeartbeatLoop() Dim heartbeatCount As Integer = 0 Dim lastMemoryLog As DateTime = DateTime.MinValue While True Try heartbeatCount += 1 ' 获取系统可用内存 Dim availableMemoryMB As Double = ServerMonitor.GetUbuntuAvailableMemoryMB() ' 内存获取失败时的处理 If availableMemoryMB < 0 Then ' 使用上次成功获取的值或默认值 availableMemoryMB = 1024 ' 默认1GB End If ' 每60秒输出一次内存日志 If (DateTime.Now - lastMemoryLog).TotalSeconds >= 60 Then Console.WriteLine($"[{DateTime.Now}] 系统可用内存: {availableMemoryMB:F2} MB") lastMemoryLog = DateTime.Now ' 检查CPU负载(可选) Dim cpuLoad As Double = ServerMonitor.GetCpuLoad() If cpuLoad > 0 Then Console.WriteLine($"[{DateTime.Now}] 系统负载: {cpuLoad:F2}") End If End If ' 向所有Manager发送心跳 Dim successCount As Integer = 0 For i As Integer = 0 To OJ.OJURL.Length - 1 Dim url = OJ.OJURL(i) Dim group = OJ.Group(i) ' 异步发送心跳 Dim task = Heartbeat.SendRequest( OJ.Judge0Port, group, url, OJ.QueueMaxSize, OJ.RequestCountSize, availableMemoryMB, OJ.AvailableMemoryThresholdOfMB, 10 ) ' 等待结果(同步等待,确保顺序) Dim result = task.GetAwaiter().GetResult() If result = 0 Then successCount += 1 Else ' 仅在首次失败或错误码变化时输出日志 Console.WriteLine($"[{DateTime.Now}] 心跳失败 [{url}]: 错误码 {result}") End If Next ' 所有心跳都失败时的特殊处理 If successCount = 0 AndAlso heartbeatCount Mod 6 = 0 Then Console.WriteLine($"[{DateTime.Now}] 警告: 所有Manager均无法连接") End If ' 等待下一个心跳周期 Thread.Sleep(HEARTBEAT_INTERVAL_MS) Catch ex As Exception Console.WriteLine($"[{DateTime.Now}] 心跳循环异常: {ex.Message}") Thread.Sleep(HEARTBEAT_INTERVAL_MS) End Try End While End Sub End Module三、配置文件示例
<?xml version="1.0" encoding="utf-8"?> <Judge0ServerConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <OJURL> <string>http://192.31.116.222:6666</string> <string>http://xxx.xxx.xxx.xxx:6666</string> </OJURL> <Judge0Port>2358</Judge0Port> <Group> <int>0</int> <int>1</int> </Group> <QueueMaxSize>100</QueueMaxSize> <RequestCountSize>4</RequestCountSize> <AvailableMemoryThresholdOfMB>200</AvailableMemoryThresholdOfMB> </Judge0ServerConfig>四、部署和运行
4.1编译和发布
编译得到dll,将整个文件夹复制到服务器
4.2启动服务
# 生成配置文件(首次运行) ./AIOJ_Judge0 --generate-config # 编辑配置文件 vim AIOJ_Judge0.conf # 启动心跳客户端 ./AIOJ_Judge04.3配置为系统服务
[Unit] Description=Judge0 Heartbeat Client After=network.target judge0.service [Service] Type=simple User=judge0 WorkingDirectory=/opt/judge0-heartbeat ExecStart=/opt/judge0-heartbeat/AIOJ_Judge0 Restart=always RestartSec=10 StandardOutput=append:/var/log/judge0-heartbeat.log StandardError=append:/var/log/judge0-heartbeat-error.log [Install] WantedBy=multi-user.target4.4启动服务
sudo systemctl daemon-reload sudo systemctl enable judge0-heartbeat sudo systemctl start judge0-heartbeat sudo systemctl status judge0-heartbeatPS:如果安装.NET8运行时跑不起来,可以尝试安装.NET 8 SDK