调度系统的可观测性建设

拆解 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_totalresult: success/error任务分发成功/失败次数
leader_election_totalbackend, resultLeader 选举结果统计
worker_executions_totalstatus: success/failed/timeoutWorker 执行状态分布
http_requests_totalservice, method, path, statusHTTP 请求统计

指标埋点示例

// 任务分发成功
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 双协议的透明切换设计。