Loading

Gorse Server 工作原理详解

深入理解 Server 的架构和工作流程

📊 Server 在架构中的位置

┌─────────────────────────────────────────────────────┐
│                     用户请求                         │
│         GET /api/recommend/user123                  │
└────────────────────┬────────────────────────────────┘
                     │
                     ▼
            ┌────────────────┐
            │  Gorse Server  │ ← 你在这里!
            │  (无状态服务)   │
            └────┬───────┬───┘
                 │       │
        ┌────────┘       └────────┐
        │                         │
        ▼                         ▼
┌───────────────┐         ┌──────────────┐
│ Redis (缓存)  │         │ Master (配置) │
│ 推荐结果      │         │ gRPC 同步     │
└───────────────┘         └──────────────┘

🎯 Server 的三大职责

1. API 网关 📡

对外提供 REST API,接收并响应推荐请求

2. 缓存读取 🚀

从 Redis 读取预先计算好的推荐结果

3. 配置同步 🔄

定期从 Master 同步配置和数据库连接信息


🔍 核心代码解析

第 1 步:创建 Server(main.go)

// cmd/gorse-server/main.go

func main() {
    // 从命令行参数获取配置
    masterHost := "127.0.0.1"  // Master 地址
    masterPort := 8086         // Master 端口
    serverHost := "0.0.0.0"    // Server 监听地址
    serverPort := 8087         // Server 端口
    
    // 创建 Server 实例
    s := server.NewServer(
        masterHost, masterPort, 
        serverHost, serverPort,
        cachePath, tlsConfig,
    )
    
    // 启动 Server
    s.Serve()
}

第 2 步:Server 初始化(server.go:64-88)

// NewServer 创建一个 Server 实例
func NewServer(
    masterHost string,    // Master 地址
    masterPort int,       // Master 端口
    serverHost string,    // Server 监听地址
    serverPort int,       // Server 端口
    cacheFile string,     // 缓存文件路径
    tlsConfig *util.TLSConfig,
) *Server {
    s := &Server{
        masterHost: masterHost,
        masterPort: masterPort,
        RestServer: RestServer{
            Config:      config.GetDefaultConfig(),
            CacheClient: new(cache.NoDatabase),  // 初始为空
            DataClient:  new(data.NoDatabase),   // 初始为空
            HttpHost:    serverHost,
            HttpPort:    serverPort,
            WebService:  new(restful.WebService),
        },
    }
    return s
}

关键点

  • ✅ Server 是无状态
  • ✅ 初始化时不连接数据库(等待从 Master 获取配置)
  • ✅ 只需要知道 Master 的地址

第 3 步:启动服务(server.go:91-126)

// Serve 启动 Server
func (s *Server) Serve() {
    // 1. 生成唯一的服务器名称
    s.serverName, _ = s.ServerName()
    
    log.Logger().Info("start server",
        zap.String("server_name", s.serverName),
        zap.String("master_host", s.masterHost),
        zap.Int("master_port", s.masterPort))
    
    // 2. 连接到 Master(gRPC)
    s.conn, err = grpc.Dial(
        net.JoinHostPort(s.masterHost, strconv.Itoa(s.masterPort)),
        opts...,
    )
    s.masterClient = protocol.NewMasterClient(s.conn)
    
    // 3. 启动后台同步任务
    go s.Sync()  // ← 关键!在后台运行
    
    // 4. 启动 HTTP 服务器
    container := restful.NewContainer()
    s.StartHttpServer(container)
}

启动流程

Server 启动
  ↓
1. 生成服务器名称 (基于主机名的 MD5)
  ↓
2. 连接 Master (gRPC)
  ↓
3. 启动后台同步 goroutine
  ↓
4. 启动 HTTP 服务器 (监听 8087)
  ↓
✅ 准备就绪,可以接收请求

第 4 步:后台同步任务(server.go:149-226)⭐ 核心

// Sync 定期从 Master 同步配置
func (s *Server) Sync() {
    defer util.CheckPanic()
    log.Logger().Info("start meta sync")
    
    for {  // 无限循环
        // 1️⃣ 向 Master 请求元数据
        meta, err := s.masterClient.GetMeta(context.Background(),
            &protocol.NodeInfo{
                NodeType:      protocol.NodeType_Server,
                Uuid:          s.serverName,
                BinaryVersion: version.Version,
                Hostname:      lo.Must(os.Hostname()),
            })
        if err != nil {
            log.Logger().Error("failed to get meta", zap.Error(err))
            goto sleep
        }
        
        // 2️⃣ 解析 Master 返回的配置
        err = json.Unmarshal([]byte(meta.Config), &s.Config)
        if err != nil {
            log.Logger().Error("failed to parse master config", zap.Error(err))
            goto sleep
        }
        
        // 3️⃣ 连接 Data Store(如果配置变了)
        if s.dataPath != s.Config.Database.DataStore {
            log.Logger().Info("connect data store",
                zap.String("database", s.Config.Database.DataStore))
            s.DataClient, err = data.Open(
                s.Config.Database.DataStore,  // 如 mysql://...
                s.Config.Database.DataTablePrefix,
            )
        }
        
        // 4️⃣ 连接 Cache Store(如果配置变了)
        if s.cachePath != s.Config.Database.CacheStore {
            log.Logger().Info("connect cache store",
                zap.String("database", s.Config.Database.CacheStore))
            s.CacheClient, err = cache.Open(
                s.Config.Database.CacheStore,  // 如 redis://...
                s.Config.Database.CacheTablePrefix,
            )
        }
        
    sleep:
        // 5️⃣ 休眠一段时间后再次同步
        time.Sleep(s.Config.Master.MetaTimeout)  // 默认 10 秒
    }
}

同步流程图

┌──────────────────────────────────────┐
│         Sync 循环 (每 10 秒)         │
└──────────────┬───────────────────────┘
               │
               ▼
   ┌─────────────────────────┐
   │ 1. 向 Master 请求配置   │
   │    GetMeta(ServerInfo)  │
   └────────┬────────────────┘
            │
            ▼
   ┌─────────────────────────┐
   │ 2. 解析配置 JSON        │
   │    Config, DataStore... │
   └────────┬────────────────┘
            │
            ▼
   ┌─────────────────────────┐
   │ 3. 连接 MySQL/PG       │
   │    (如果配置变化)       │
   └────────┬────────────────┘
            │
            ▼
   ┌─────────────────────────┐
   │ 4. 连接 Redis          │
   │    (如果配置变化)       │
   └────────┬────────────────┘
            │
            ▼
   ┌─────────────────────────┐
   │ 5. Sleep 10 秒         │
   └────────┬────────────────┘
            │
            └──────────► 回到步骤 1

第 5 步:REST API 处理(rest.go)

API 注册(rest.go:73-100)

// StartHttpServer 启动 HTTP 服务器
func (s *RestServer) StartHttpServer(container *restful.Container) {
    // 1. 创建 RESTful Web Service
    s.CreateWebService()
    container.Add(s.WebService)
    
    // 2. 注册 Swagger UI
    container.Add(restfulspec.NewOpenAPIService(specConfig))
    container.Handle(apiDocsPath, v5emb.New(...))
    
    // 3. 注册 Prometheus 监控
    container.Handle("/metrics", promhttp.Handler())
    
    // 4. 注册性能分析工具
    container.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
    
    // 5. 启动 HTTP 服务器
    s.HttpServer = &http.Server{
        Addr:    fmt.Sprintf("%s:%d", s.HttpHost, s.HttpPort),
        Handler: container,
    }
    s.HttpServer.ListenAndServe()
}

推荐 API 核心逻辑

// getRecommend 获取推荐结果
func (s *RestServer) getRecommend(request *restful.Request, response *restful.Response) {
    // 1️⃣ 解析请求参数
    userId := request.PathParameter("user-id")
    n, _ := strconv.Atoi(request.QueryParameter("n"))  // 推荐数量
    
    // 2️⃣ 从 Redis 缓存读取推荐结果
    scores, err := s.CacheClient.SearchScores(
        cache.RecommendItems,  // 集合名:recommend_items
        []string{userId},       // 用户 ID
        nil,                    // 过滤条件
        0,                      // offset
        n,                      // limit
    )
    
    // 3️⃣ 过滤已读/已买的物品
    excludeSet := mapset.NewSet[string]()
    feedbacks, _ := s.DataClient.GetUserFeedback(userId)
    for _, feedback := range feedbacks {
        excludeSet.Add(feedback.ItemId)
    }
    
    var results []cache.Score
    for _, score := range scores {
        if !excludeSet.Contains(score.Id) {
            results = append(results, score)
        }
    }
    
    // 4️⃣ 如果推荐不够,使用降级策略
    if len(results) < n {
        // 从流行物品中补充
        popular, _ := s.CacheClient.SearchScores(
            cache.PopularItems,
            nil, nil, 0, n-len(results),
        )
        results = append(results, popular...)
    }
    
    // 5️⃣ 返回结果
    response.WriteEntity(results)
}

🔄 完整的请求流程

用户请求推荐时发生了什么?

用户发起请求
GET /api/recommend/user123?n=10
       ↓
┌──────────────────────────────────────┐
│  Server HTTP Handler                 │
│  (rest.go:getRecommend)             │
└──────┬───────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│  1. 解析参数                         │
│     userId = "user123"               │
│     n = 10                           │
└──────┬───────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│  2. 从 Redis 读取推荐缓存            │
│     CacheClient.SearchScores()       │
│     集合: "recommend_items"          │
│     Key: "user123"                   │
└──────┬───────────────────────────────┘
       │
       ▼ (缓存命中)
┌──────────────────────────────────────┐
│  3. 过滤已消费物品                   │
│     查询用户历史反馈 (MySQL)         │
│     排除已点击/购买的物品            │
└──────┬───────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│  4. 如果数量不够,使用降级策略       │
│     - 补充流行物品                   │
│     - 或随机推荐                     │
└──────┬───────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│  5. 返回 JSON 响应                   │
│  [                                   │
│    {"Id":"item1", "Score":0.95},    │
│    {"Id":"item2", "Score":0.89},    │
│    ...                               │
│  ]                                   │
└──────────────────────────────────────┘

性能分析

  • Redis 读取:< 1ms(内存操作)
  • MySQL 查询:5-10ms(有索引)
  • 过滤排序:< 1ms(内存操作)
  • 总响应时间:< 20ms

🎯 Server 的关键特性

1. 无状态设计 ⭐⭐⭐

// Server 不存储任何用户数据
// 所有数据来自:
// - Redis (推荐结果缓存)
// - MySQL (用户行为数据)
// - Master (配置)

// 这意味着:
✅ 可以水平扩展(启动多个 Server 实例)
✅ 任何一个 Server 挂了,其他继续工作
✅ 负载均衡简单(随机或轮询)

2. 配置热更新 🔄

// Sync() 循环每 10 秒从 Master 获取最新配置
// 无需重启 Server 就能:
✅ 更新推荐算法参数
✅ 更改数据库连接
✅ 调整缓存策略

3. 降级策略 🛡️

// 如果个性化推荐不够:
1. 补充流行物品
2. 补充新物品
3. 随机推荐

// 确保用户永远能得到结果

📊 Server 的数据流

┌────────────────────────────────────────────────┐
│                   请求入口                      │
│         HTTP :8087 /api/recommend              │
└────────────────┬───────────────────────────────┘
                 │
        ┌────────┴────────┐
        │                 │
        ▼                 ▼
┌──────────────┐   ┌─────────────────┐
│ Redis (读)   │   │ MySQL (读)      │
│ 推荐结果     │   │ 用户历史行为    │
│ 缓存         │   │ 过滤已消费物品  │
└──────┬───────┘   └────────┬────────┘
       │                    │
       └────────┬───────────┘
                │
                ▼
        ┌───────────────┐
        │  计算 + 过滤  │
        │  排序 + 去重  │
        └───────┬───────┘
                │
                ▼
        ┌───────────────┐
        │  JSON 响应    │
        │  返回给用户   │
        └───────────────┘

🚀 Server 性能优化要点

1. 连接池

// Redis 连接池
redisClient := redis.NewClient(&redis.Options{
    PoolSize:     100,      // 连接池大小
    MinIdleConns: 20,       // 最小空闲连接
    PoolTimeout:  4 * time.Second,
})

// MySQL 连接池
db.SetMaxOpenConns(100)     // 最大连接数
db.SetMaxIdleConns(20)      // 最大空闲连接
db.SetConnMaxLifetime(time.Hour)

2. 缓存策略

// 推荐结果缓存
- 位置:Redis
- 过期时间:72 小时(可配置)
- 更新频率:Worker 每次计算后更新

// 热点数据缓存
- 流行物品:永久缓存
- 新物品:24 小时缓存

3. 批量操作

// ❌ 慢:逐个查询
for _, userId := range userIds {
    GetRecommend(userId)
}

// ✅ 快:批量查询
pipeline := redis.Pipeline()
for _, userId := range userIds {
    pipeline.Get("recommend:" + userId)
}
pipeline.Exec()

🔍 监控指标

Server 暴露的关键指标(/metrics):

# HTTP 请求
http_requests_total{method="GET",endpoint="/api/recommend"}
http_request_duration_seconds{quantile="0.95"}

# 缓存
cache_hits_total
cache_misses_total
cache_hit_rate

# 数据库
db_queries_total
db_query_duration_seconds

# 错误
errors_total{type="cache_error"}
errors_total{type="db_error"}

💡 关键点总结

Server 的本质

Server = REST API 网关 + Redis 读取器 + 配置同步器

核心工作:
1. 接收 HTTP 请求
2. 从 Redis 读取预计算的推荐
3. 过滤和排序
4. 返回 JSON

NOT 做的事:
❌ 不训练模型(Master 做)
❌ 不计算推荐(Worker 做)
❌ 不存储数据(MySQL/Redis 做)

为什么这样设计?

✅ 无状态 → 易扩展
✅ 只读取 → 高性能
✅ 配置同步 → 灵活
✅ 缓存优先 → 低延迟
posted @ 2026-01-09 17:57  技术漫游  阅读(1)  评论(0)    收藏  举报