OJ平台远端判题子系统开发(五):Worker并发控制与系统可观测性
前几周完成了HTTP API和沙箱核心的开发,系统已能串行处理判题请求。但在实际使用场景中——比如ACM比赛开场时的集中提交——串行处理会导致后续提交在队列中长时间等待。本周的核心工作是实现受控并发判题,并建立系统可观测性体系。
一、并发方案设计:Worker令牌池模式
1.1 方案评估
Go语言中控制并发有两条主流路径:
| 方案 | 优点 | 缺点 |
|---|---|---|
semaphore.Weighted |
可动态调整权重 | 抽象层级高,语义不够直观 |
| Buffered Channel令牌池 | Go惯用法,语义清晰 | 需手动管理令牌生命周期 |
选择Buffered Channel方案的原因是语义的直接性:make(chan struct{}, N) ——创建了恰好N个令牌,获取一个令牌往channel发送一个值,释放令牌从channel读取一个值。这个模型与"受控并发"的概念完全对应,代码阅读和维护的成本更低。
Go并发模式参考:https://go.dev/blog/pipelines
1.2 核心实现
type JudgeWorker struct {
queue queue.Queue
judger judger.Executor
repo repository.SubmissionRepository
stats *stats.Collector
sem chan struct{} // 令牌池
concurrency int
}
func NewJudgeWorker(q queue.Queue, j judger.Executor, r repository.SubmissionRepository,
s *stats.Collector, concurrency int) *JudgeWorker {
return &JudgeWorker{
queue: q,
judger: j,
repo: r,
stats: s,
sem: make(chan struct{}, concurrency),
concurrency: concurrency,
}
}
并发数通过 REMOTE_JUDGE_WORKER_CONCURRENCY 环境变量配置,默认值为4。
1.3 消费与令牌管理
func (w *JudgeWorker) Start(ctx context.Context) {
go func() {
w.queue.Consume(ctx, func(msg queue.Message) {
select {
case w.sem <- struct{}{}:
w.stats.AddActiveWorkers(1)
go func() {
defer func() {
<-w.sem
w.stats.AddActiveWorkers(-1)
}()
w.process(ctx, msg)
}()
default:
w.stats.IncBusyRejects()
w.repo.UpdateResult(ctx, domain.JudgeResult{
SubmissionID: msg.SubmissionID,
Status: domain.StatusSystemError,
ErrorMessage: "worker busy, please retry later",
})
}
})
}()
}
关键行为:
sem <- struct{}{}:尝试获取令牌。channel未满时立即成功,满时走default分支<-w.sem:通过defer确保无论判题成功或失败,令牌都会被释放- goroutine:每个判题任务跑在独立的goroutine中,Worker主循环不阻塞
- 繁忙拒绝:令牌池满时直接将提交标记为System Error并提示重试
二、系统可观测性
并发引入后,系统行为变得更复杂,必须有对应的监控手段。
2.1 统计采集器
type Collector struct {
mu sync.Mutex
totalSubmissions int64
statusCounts map[domain.SubmissionStatus]int64
activeWorkers int64
busyRejects int64
maxWorkers int
}
func (c *Collector) Stats() map[string]any {
return map[string]any{
"total_submissions": c.totalSubmissions,
"active_workers": atomic.LoadInt64(&c.activeWorkers),
"max_workers": c.maxWorkers,
"busy_rejects": atomic.LoadInt64(&c.busyRejects),
"status_counts": c.getStatusCounts(),
}
}
四个核心指标:
total_submissions:累计提交总数active_workers:当前正在执行判题的Worker数量busy_rejects:因令牌池满被拒绝的提交数——如果这个指标持续增长,说明并发数配置不足status_counts:各判题状态的分布统计
2.2 健康检查接口
GET /api/system/health 返回系统运行状况:
{
"status": "ok",
"queue": "memory",
"repository": "memory",
"sandbox": "mock",
"judger_mode": "embedded",
"worker_concurrency": 4,
"timestamp": "2026-06-02T10:00:00Z"
}
2.3 统计接口
GET /api/system/stats 返回运行时统计数据:
{
"total_submissions": 100,
"active_workers": 2,
"max_workers": 4,
"busy_rejects": 0,
"status_counts": {
"Accepted": 80,
"Wrong_Answer": 10,
"Compile_Error": 5,
"Runtime_Error": 5
}
}
2.4 结构化日志与TraceId
日志采用JSON结构化格式:
logger.Info("worker.process", traceID, "judge task completed", map[string]any{
"submissionId": submission.ID,
"status": submission.Status,
"runtimeMs": submission.RuntimeMs,
"memoryKB": submission.MemoryKB,
})
每个提交在创建时生成唯一的 traceId(格式:trace_{id}_{uuid前8位}),随日志贯穿HTTP API → Queue → Worker → Judger → Sandbox全链路。排查问题时,通过一个traceId即可串联所有相关日志。
三、测试验证
3.1 Mock模式服务启动验证
启动命令:
set REMOTE_JUDGE_SANDBOX=mock
set REMOTE_JUDGE_HTTP_ADDR=:8081
set REMOTE_JUDGE_GRPC_ADDR=127.0.0.1:9091
go run .\cmd\server
预期:健康检查返回 "sandbox":"mock",统计接口返回 "total_submissions":0。

健康检查接口验证:
curl http://127.0.0.1:8081/api/system/health

统计接口验证:
curl http://127.0.0.1:8081/api/system/stats

3.2 Worker单元测试
| 测试文件 | 测试用例 | 验证内容 |
|---|---|---|
| worker/judge_worker_test.go | TestJudgeWorkerHandle | Worker消费消息并正确更新提交状态 |
3.3 Benchmark
提交服务基准测试:
go test ./internal/service -bench . -benchmem -count=1 -timeout 60s
BenchmarkSubmissionServiceCreate-32 27211 206468 ns/op 549 B/op 6 allocs/op

单次提交创建约110μs,内存分配6次、600字节。对于判题系统而言,提交创建不是瓶颈——真正的耗时在Docker容器启动和代码编译运行阶段。Benchmark数据确认了这一点,因此并发控制的重点是判题执行阶段(Worker令牌池),而非提交创建阶段。
四、本周总结
完成内容
- Worker令牌池并发控制(Buffered Channel方案)
- 统计采集器(4项核心指标)
- 健康检查与统计接口
- 结构化日志 + TraceId全链路追踪
- Mock模式启动验证和Benchmark测试
调研查阅的资料
- Go Worker Pool:https://gobyexample.com/worker-pools
- Go Worker Pool Pattern:https://gobyexample.com/worker-pools
- Go Concurrency Patterns (Pipelines):https://go.dev/blog/pipelines
- Go Structured Logging (slog):https://pkg.go.dev/log/slog

浙公网安备 33010602011771号