Redis 可选架构与多协议通信
完结篇:分析 go-ai-scheduler 的 Redis 可选缓存设计和 HTTP/gRPC 双协议透明切换的实现思路。
问题背景
在设计分布式系统时,一个常见的争论是:要不要强制依赖 Redis?
支持强制依赖的论点:
- Redis 可以大幅降低数据库负载
- 缓存预热、分布式锁等功能开箱即用
反对强制依赖的论点:
- 增加了运维复杂度(部署、监控、故障恢复)
- 小公司/初创团队可能只有 MySQL
- Redis 故障时系统不应该完全不可用
go-ai-scheduler 选择了一条中间道路:Redis 是可选的。当 Redis 可用时,系统享受缓存加速;当 Redis 不可用时,系统自动降级到直接查询 MySQL。
Redis 的使用场景
在 go-ai-scheduler 中,Redis 被用于三个场景:
1. 到期任务缓存
调度器需要频繁查询"未来一段时间内将要触发的任务"。Redis 使用 Sorted Set 存储任务 ID 和触发时间戳:
// internal/scheduler/cache/manager.go
func (m *Manager) warmDueTasks(ctx context.Context) {
tasks, err := m.taskRepo.ListDueTasks(ctx, 500)
ids := make([]int64, 0, len(tasks))
scores := make(map[int64]float64, len(tasks))
for _, t := range tasks {
ids = append(ids, t.ID)
scores[t.ID] = float64(t.NextTriggerTime.Unix())
}
m.redis.WarmDueTasks(ctx, ids, scores)
}
Trigger Loop 扫描时优先从缓存读取:
if l.cache != nil && l.cache.Enabled() {
cachedIDs, cacheErr := l.cache.GetCachedDueTaskIDs(ctx, time.Now())
if cacheErr == nil && len(cachedIDs) > 0 {
// 使用缓存数据
}
}
if len(tasks) == 0 {
tasks, _ = l.taskRepo.ListDueTasks(ctx, 100) // 降级到数据库
}
2. Worker 状态缓存
Worker 的心跳数据也可以缓存在 Redis 中,减少数据库写入压力:
func (m *Manager) warmWorkerCache(ctx context.Context) {
workers, _ := m.workerRepo.ListAvailableWorkers(ctx)
for _, w := range workers {
data[w.ID] = map[string]any{
"hostname": w.Hostname,
"status": w.Status,
"current_load": w.CurrentLoad,
// ...
}
}
m.redis.WarmWorkerCache(ctx, ids, data)
}
3. AI 查询结果缓存
AI 服务对相同问题的回答可以缓存一段时间,降低 LLM API 调用成本。
优雅降级
Cache Manager 的核心设计是**"无 Redis 也能工作"**:
// internal/scheduler/cache/manager.go
func (m *Manager) Enabled() bool {
return m.redis != nil
}
func (m *Manager) StartWarmLoop(ctx context.Context, interval time.Duration) {
if !m.Enabled() {
return // 无 Redis 时直接退出
}
// ...
}
当 Redis 不可用时:
- Trigger Loop 直接查 MySQL
- Router 直接从数据库读取 Worker 状态
- 系统性能可能下降,但功能不受影响
<Callout type="info" title="降级后的性能影响"
在无 Redis 模式下,Trigger Loop 每 5 秒查一次 MySQL。如果任务量很大(数万级别),这种查询会成为瓶颈。建议生产环境部署 Redis,开发测试环境可以只用 MySQL。
HTTP/gRPC 双协议透明切换
go-ai-scheduler 在多个通信场景支持双协议:
| 通信方向 | 协议选择依据 | 实现位置 |
|---|---|---|
| Worker → Scheduler(注册/心跳) | INTERNAL_PROTOCOL 环境变量 | HeartbeatClient |
| Scheduler → Worker(任务分发) | Worker 注册的 Protocol 字段 | Dispatch Client |
| AI Service → LLM API | 固定 HTTP/SSE | LLMAdapter |
Scheduler → Worker 的协议切换
Dispatch Client 是双协议设计的典型代表:
// internal/scheduler/dispatch/client.go
func (c *Client) Dispatch(ctx context.Context, worker *model.WorkerNode, req model.ExecuteTaskRequest) error {
if c.rateLimiter != nil {
if !c.rateLimiter.Allow() {
return fmt.Errorf("dispatch rate limit exceeded")
}
}
if strings.EqualFold(worker.Protocol, "grpc") {
return c.dispatchGRPC(ctx, worker.GRPCAddr, req)
}
return c.dispatchHTTP(ctx, worker.CallbackURL, req)
}
Worker 在注册时声明自己支持的协议:
registerReq := apiservice.WorkerRegistrationRequest{
Protocol: cfg.InternalProtocol, // "http" or "grpc"
CallbackURL: "http://127.0.0.1" + cfg.HTTPAddr,
GRPCAddr: "127.0.0.1" + cfg.GRPCAddr,
}
Worker → Scheduler 的协议切换
Heartbeat Client 同样支持双协议:
// internal/worker/heartbeat_client.go
func (c *HeartbeatClient) Heartbeat(ctx context.Context, req service.WorkerHeartbeatRequest) error {
if c.protocol == "grpc" {
return c.heartbeatGRPC(ctx, req)
}
return c.post(ctx, "/api/v1/workers/heartbeat", req)
}
协议选择的权衡
| 维度 | HTTP | gRPC |
|---|---|---|
| 连接管理 | 短连接,每次新建 | 长连接,复用 |
| 序列化 | JSON | Protobuf |
| 带宽占用 | 高(文本格式) | 低(二进制) |
| 调试难度 | 低(curl 即可) | 高(需要 grpcurl) |
| 流式支持 | SSE / WebSocket | 原生双向流 |
| 跨语言 | 极好 | 好(需要 protobuf 定义) |
<Callout type="tip" title="为什么默认是 HTTP?"
HTTP 的调试成本更低。当 Worker 分发失败时,可以直接用 curl 模拟请求排查问题。gRPC 虽然性能更好,但在问题排查时需要额外的工具支持。系统默认 HTTP,gRPC 作为可选优化。
统一请求模型
无论使用哪种协议,任务执行请求使用统一的 Go 结构体:
// internal/model/task.go
type ExecuteTaskRequest struct {
ScheduleInstanceID string
TaskID int64
TaskType string
Payload string
Image string
TimeoutSeconds int
RetryCount int
ShardNo int
ShardTotal int
IdempotencyKey string
SchedulerURL string
}
gRPC 的 proto 定义与这个结构体一一对应,通过生成的代码自动转换:
message ExecuteTaskRequest {
string schedule_instance_id = 1;
int64 task_id = 2;
string task_type = 3;
string payload = 4;
// ...
}
这种统一模型让协议切换对业务逻辑完全透明。
系列总结
十篇文章拆解了 go-ai-scheduler 的完整设计:
| 篇目 | 主题 | 核心设计 |
|---|---|---|
| 1 | 架构概览 | 四服务架构、控制平面强一致、执行平面高自治 |
| 2 | 混合调度引擎 | 时间轮 + 最小堆、预热机制 |
| 3 | 可靠性设计 | 触发/重试链路、去重幂等、本地缓冲 |
| 4 | Leader 选举 | etcd/MySQL/Local 三级降级 |
| 5 | 路由与背压 | 乐观预留、三态背压、令牌桶 |
| 6 | Worker 高可用 | 心跳、去重、沙箱、断网续传 |
| 7 | LLM Agent | 10 轮推理、12 工具、SSE 流式 |
| 8 | AI 服务架构 | 建议不决策、自然语言转任务 |
| 9 | 可观测性 | Prometheus + JSON 日志 + Grafana |
| 10 | Redis 与多协议 | Optional 缓存、HTTP/gRPC 透明切换 |
这个项目的核心设计哲学是**"够用就好、逐步演进"**:
- 不追求最复杂的方案,而是选择工程上可维护的实现
- 每个依赖都是可选的,系统始终能降级运行
- AI 是增强而不是替代,核心链路保持确定性
希望这个系列对你的分布式系统设计有所帮助。