上一篇介绍了整体构架,这一篇介绍如何实现心跳程序。在ubuntu上使用.net程序有不少方法,我们采用使用标准框架的方式。
一、工程配置
1、类型:控制台应用------莫担心,它会得到一个dll,部署这个dll即可
2、目标框架:.net 8.0
3、目标OS:无
二、心跳客户端
在分布式评测系统中,Judge0服务器节点需要向管理端(位于OJ服务器上,Manager)报告自己的状态。并且需要:
1、尽可能占用较少资源,不影响Judge0评测性能
2、Manager故障不影响心跳发送
3、支持多个Manager地址和参数调账
4、跨平台
2.1配置文件管理
vbnet
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 Class
2.2心跳消息发送
vbnet
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 Class
2.3系统监控模块
vbnet
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 Class
2.4主程序入口
vbnet
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
<?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启动服务
XML
# 生成配置文件(首次运行)
./AIOJ_Judge0 --generate-config
# 编辑配置文件
vim AIOJ_Judge0.conf
# 启动心跳客户端
./AIOJ_Judge0
4.3配置为系统服务
XML
[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.target
4.4启动服务
XML
sudo systemctl daemon-reload
sudo systemctl enable judge0-heartbeat
sudo systemctl start judge0-heartbeat
sudo systemctl status judge0-heartbeat
PS:如果安装.NET8运行时跑不起来,可以尝试安装.NET 8 SDK