调度系统的可观测性建设
拆解 go-ai-scheduler 的可观测性体系,分析 Prometheus 指标设计、结构化日志规范和 Grafana 监控大盘的构建思路。
问题背景
分布式调度系统的调试难度远高于单体应用。一个问题可能涉及多个服务和节点,传统的 printf 日志在排查时效率极低。可观测性(Observability)通过**指标(Metrics)、日志(Logs)、追踪(Traces)**三个维度,让系统的内部状态对外可见。
go-ai-scheduler 的可观测性建设聚焦于指标和日志,追踪(Tracing)作为未来扩展点预留了接口。
Prometheus 指标设计
系统使用自研的轻量级指标库,直接输出 Prometheus 文本格式:
// internal/pkg/metrics/metrics.go
type Registry struct {
mu sync.RWMutex
counters map[metricKey]*int64
}
var DefaultRegistry = NewRegistry()
Counter 的使用场景
目前系统中使用了以下几类 Counter:
| 指标名 | 标签 | 说明 |
|---|---|---|
scheduler_dispatch_total | result: success/error | 任务分发成功/失败次数 |
leader_election_total | backend, result | Leader 选举结果统计 |
worker_executions_total | status: success/failed/timeout | Worker 执行状态分布 |
http_requests_total | service, method, path, status | HTTP 请求统计 |
指标埋点示例
// 任务分发成功
metrics.DefaultRegistry.IncCounter("scheduler_dispatch_total",
map[string]string{"result": "success"})
// 任务分发失败
metrics.DefaultRegistry.IncCounter("scheduler_dispatch_total",
map[string]string{"result": "error"})
// Leader 选举成功
metrics.DefaultRegistry.IncCounter("leader_election_total",
map[string]string{"backend": "etcd", "result": "acquired"})
指标渲染
Registry 实现了 Prometheus 文本格式的渲染:
func (r *Registry) Render() string {
var builder strings.Builder
for _, key := range keys {
value := atomic.LoadInt64(r.counters[key])
builder.WriteString(key.name)
if key.labels != "" {
builder.WriteByte('{')
builder.WriteString(key.labels)
builder.WriteByte('}')
}
builder.WriteByte(' ')
builder.WriteString(fmt.Sprintf("%d\n", value))
}
return builder.String()
}
输出示例:
scheduler_dispatch_total{result="success"} 15432
scheduler_dispatch_total{result="error"} 87
leader_election_total{backend="etcd",result="acquired"} 1
worker_executions_total{status="success"} 15230
worker_executions_total{status="failed"} 156
worker_executions_total{status="timeout"} 23
HTTP 指标自动采集
通过 Instrument 中间件,自动记录所有 HTTP 请求的指标:
func Instrument(service string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recorder := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(recorder, r)
DefaultRegistry.IncCounter("http_requests_total", map[string]string{
"service": service,
"method": r.Method,
"path": r.URL.Path,
"status": fmt.Sprintf("%d", recorder.status),
})
})
}
这个设计让业务代码不需要手动埋点 HTTP 指标,中间件自动完成。
结构化日志
系统使用 Go 1.21 的 log/slog 输出 JSON 格式的结构化日志:
l := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}).WithAttrs([]slog.Attr{
slog.String("service", cfg.ServiceName),
}))
日志输出示例
{"time":"2026-05-15T08:32:01Z","level":"INFO","msg":"task dispatched","task_id":42,"instance_id":128,"worker_id":"worker-1","bp":"green"}
{"time":"2026-05-15T08:32:05Z","level":"WARN","msg":"engine trigger: pick worker failed","task_id":43,"error":"no available worker"}
{"time":"2026-05-15T08:32:10Z","level":"DEBUG","msg":"leader election","backend":"etcd","role":"leader"}
日志级别策略
| 级别 | 使用场景 |
|---|---|
| DEBUG | 内部状态变化、循环迭代信息 |
| INFO | 关键生命周期事件(启动、注册、选举) |
| WARN | 可恢复的异常(网络超时、Worker 失联) |
| ERROR | 不可恢复的错误(数据库连接失败) |
<Callout type="tip" title="JSON 日志的优势"
JSON 格式可以直接被日志采集系统(Fluentd、Logstash)解析,不需要复杂的正则匹配。配合 slog 的 key-value 结构,每条日志都是一个可查询的文档。
Grafana 监控大盘
系统提供了预配置的 Grafana Dashboard,监控以下核心指标:
1. 调度器面板
- 分发成功率(
scheduler_dispatch_total{result="success"} / scheduler_dispatch_total) - Pending 任务数趋势
- 背压状态分布(Green/Yellow/Red)
- Leader 选举事件
2. Worker 面板
- Worker 在线状态
- 各 Worker 负载利用率(
CurrentLoad / MaxConcurrency) - 执行状态分布(success/failed/timeout)
- 心跳延迟热力图
3. AI 服务面板
- LLM API 请求成功率
- Token 使用量趋势
- Agent 工具调用分布
- 流式响应延迟 P99
告警规则建议
基于指标可以配置以下告警:
# 示例 Prometheus Alerting Rules
groups:
- name: scheduler
rules:
- alert: HighPendingTasks
expr: scheduler_pending_count > 800
for: 5m
annotations:
summary: "Pending tasks approaching limit"
- alert: DispatchFailureRate
expr: rate(scheduler_dispatch_total{result="error"}[5m]) / rate(scheduler_dispatch_total[5m]) > 0.1
for: 2m
annotations:
summary: "Dispatch failure rate > 10%"
- alert: WorkerOffline
expr: worker_online_count < worker_total_count * 0.8
for: 1m
annotations:
summary: "More than 20% workers are offline"
- alert: NoLeader
expr: leader_election_total{result="acquired"} == 0
for: 30s
annotations:
summary: "No scheduler leader elected"
可观测性的未来扩展
当前系统的可观测性体系已经覆盖了基本需求,但仍有扩展空间:
分布式追踪(Tracing)
一条任务从触发到完成可能经过 scheduler → worker → scheduler 多个 hop。引入 OpenTelemetry 追踪后,可以端到端地查看一条任务的全链路耗时:
[Trigger] 2ms → [Router] 1ms → [Dispatch] 15ms → [Worker Execute] 1200ms → [Report] 8ms
日志关联
将 ScheduleInstanceID 作为 trace ID 贯穿所有日志,可以在 ELK/Loki 中快速检索一条任务的完整日志:
{service="scheduler"} | json | ScheduleInstanceID="task-42-1715764321000"
总结
go-ai-scheduler 的可观测性设计遵循"够用就好、逐步演进"的原则:
- 指标覆盖核心链路(分发、执行、选举)
- 日志使用结构化 JSON,便于采集和查询
- 预配置 Grafana Dashboard,开箱即用
- 为分布式追踪预留了扩展接口
可观测性不是一次性工程,而是随着系统运行不断迭代的过程。初期关注"能不能看到问题",后期再逐步完善"能不能快速定位根因"。
下一篇是系列完结篇,分析 Redis 可选架构和 HTTP/gRPC 双协议的透明切换设计。