【从零构建分布式在线评测系统】二、Judge0服务器上的心跳程序

上一篇介绍了整体构架,这一篇介绍如何实现心跳程序。在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

相关推荐
Starry-sky(jing)2 小时前
Win11 装 ESP32 Arduino 库卡 3 小时?我把官方 3.3.10 全平台 522MB 搬到了蓝奏云,解压即用
运维·windows·esp32·arduino·离线安装
小此方3 小时前
Re:Linux系统篇(四十二)通信篇·七:SystemV标准三部曲其之三:信号量与并发编程初步认识
linux·运维·驱动开发
画中有画9 小时前
使用yolov8实现自动化脚本
运维·yolo·自动化
Piko61411 小时前
H3C IRF2 堆叠实战:打造高可靠核心交换网络
运维·网络·笔记
万联WANFLOW14 小时前
SD-WAN 控制平面高可用怎么做?SDWAN 控制器挂了,全网会发生什么
运维·网络·分布式·架构
酱学编程15 小时前
【从零到一实现一个 AI Agent 框架 · 第四篇】04. 任务规划:拆解复杂目标 -
服务器·网络·数据库·人工智能
SkyWalking中文站16 小时前
认识 Horizon UI · AI Assistant:用日常语言查询你的可观测性数据
运维·监控·自动化运维
艾莉丝努力练剑17 小时前
OpenCode AI 编程:Ubuntu 24.04 环境安装与使用指南
linux·服务器·网络·人工智能·tcp/ip·ubuntu
崇山峻岭之间17 小时前
Keil5输出hex转换为bin的设置
linux·运维·服务器