Loading

Gorse Worker 架构详解

目录

  1. Worker 概述
  2. 核心数据结构
  3. 启动流程
  4. 三大核心协程
  5. 用户推荐流程
  6. 负载均衡机制
  7. 模型同步机制
  8. 监控和健康检查
  9. 与 Master/Server 的关系
  10. 实战示例

Worker 概述

Worker 是什么?

Worker 是 Gorse 的推荐计算引擎,负责为用户生成个性化推荐。它的核心职责是:

  1. 从 Master 同步配置和模型:获取最新的推荐策略和训练好的模型
  2. 生成离线推荐:为分配给自己的用户计算推荐结果
  3. 写入缓存:将推荐结果存储到 Cache Store,供 Server 读取

为什么需要 Worker?

推荐系统的计算密集特点:
- 协同过滤计算:为每个用户计算向量相似度
- 排序算法:CTR 预测、LLM 排序等
- 规则过滤:去重、过期、分类等

如果由 Server 实时计算 → 响应太慢
解决方案:Worker 预先计算并缓存 → Server 直接读取

Worker vs Master vs Server

组件 主要职责 计算类型 数据操作
Master 模型训练 + 调度协调 离线批量训练 读 Data Store,写 Blob Store
Worker 生成用户推荐 离线批量推荐 读 Data Store,写 Cache Store
Server 对外提供 API 在线实时服务 读 Cache Store + Data Store

核心数据结构

1. Worker 结构体

type Worker struct {
    Pipeline                              // 嵌入 Pipeline,包含推荐计算逻辑
    testMode bool                         // 测试模式标志
    
    // 模型版本追踪
    collaborativeFilteringModelId int64   // 当前协同过滤模型版本
    clickThroughRateModelId       int64   // 当前 CTR 模型版本
    latestCollaborativeFilteringModelId int64  // 最新协同过滤模型版本
    latestClickThroughRateModelId       int64  // 最新 CTR 模型版本
    
    // Worker 配置
    workerName string    // Worker 唯一标识(基于 hostname + host + port 的 MD5)
    httpHost   string    // HTTP 服务地址(用于 Prometheus metrics)
    httpPort   int
    masterHost string    // Master 地址
    masterPort int
    tlsConfig  *util.TLSConfig
    cacheFile  string    // 缓存文件路径
    
    // 数据库连接
    cachePath   string   // Cache Store 连接路径
    cachePrefix string   // Cache Store 表前缀
    dataPath    string   // Data Store 连接路径
    dataPrefix  string   // Data Store 表前缀
    blobConfig  string   // Blob Store 配置(S3/GCS)
    blobStore   blob.Store
    
    // Master 连接
    conn         *grpc.ClientConn
    masterClient protocol.MasterClient
    
    // 集群信息(一致性哈希用)
    peers []string  // 所有 Worker 节点列表
    me    string    // 当前 Worker 在集群中的标识
    
    // 事件通道(协程间通信)
    tickDuration time.Duration
    ticker       *time.Ticker
    syncedChan   *parallel.ConditionChannel  // 元数据同步事件
    pulledChan   *parallel.ConditionChannel  // 模型拉取事件
    triggerChan  *parallel.ConditionChannel  // 手动触发事件
}

关键点解读

  • Pipeline:嵌入式结构,包含推荐计算的核心逻辑
  • 双版本机制xxx_ModelIdlatestXxx_ModelId 用于检测模型更新
  • 三个通道:用于协程间通信(Sync → Pull → Recommend)

2. Pipeline 结构体

type Pipeline struct {
    Config       *config.Config            // 配置
    CacheClient  cache.Database            // Cache Store 客户端
    DataClient   data.Database             // Data Store 客户端
    Tracer       *monitor.Monitor          // 进度监控
    Jobs         int                       // 并行任务数
    
    // 推荐模型
    MatrixFactorizationItems *logics.MatrixFactorizationItems  // 物品向量索引
    MatrixFactorizationUsers *logics.MatrixFactorizationUsers  // 用户向量
    ClickThroughRateModel    ctr.FactorizationMachines        // CTR 预测模型
    
    dontskipColdStartUsers bool  // 是否跳过冷启动用户
}

Pipeline 的作用

  • 封装推荐计算的核心逻辑
  • 包含模型和数据客户端
  • 与 Worker 的区别:Pipeline 是纯计算逻辑,Worker 负责调度和协调

启动流程

主函数入口(cmd/gorse-worker/main.go)

func main() {
    // 1. 解析命令行参数
    masterHost, masterPort  // Master 地址
    httpHost, httpPort      // HTTP 服务地址
    workingJobs            // 并行任务数
    cachePath              // 缓存文件路径
    
    // 2. 创建 Worker
    w := worker.NewWorker(masterHost, masterPort, httpHost, httpPort, 
                          workingJobs, cachePath, tlsConfig, interval)
    
    // 3. 启动 Worker
    w.Serve()
}

Worker.Serve() 启动流程

func (w *Worker) Serve() {
    // 1. 生成 Worker 名称(基于 hostname + IP + Port 的 MD5)
    w.workerName, err = w.WorkerName()
    
    // 2. 创建进度追踪器
    w.Tracer = monitor.NewTracer(w.workerName)
    
    // 3. 连接 Master(gRPC)
    w.conn, err = grpc.Dial(masterHost:masterPort, opts...)
    w.masterClient = protocol.NewMasterClient(w.conn)
    
    // 4. 启动三大核心协程
    go w.Sync()       // 同步配置和元数据
    go w.Pull()       // 拉取模型
    go w.ServeHTTP()  // HTTP 服务(metrics + health check)
    
    // 5. 主循环:生成推荐
    loop := func() {
        // 5.1 拉取分配给自己的用户
        workingUsers, err := w.pullUsers(w.peers, w.me)
        
        // 5.2 为这些用户生成推荐
        w.Recommend(workingUsers, func(completed, throughput int) {
            // 上报进度到 Master
            w.masterClient.PushProgress(...)
        })
    }
    
    // 6. 事件驱动循环
    for {
        select {
        case <-w.ticker.C:         // 定时触发(默认 1 分钟)
            loop()
        case <-w.pulledChan.C:     // 模型更新触发
            loop()
        }
    }
}

启动流程图

启动 Worker
    ↓
生成 Worker 名称(MD5)
    ↓
连接 Master (gRPC)
    ↓
┌─────────────────────────────────────────┐
│   启动三大协程                             │
│   ┌──────────┐  ┌──────────┐  ┌───────┐│
│   │  Sync    │  │  Pull    │  │ HTTP  ││
│   │ 同步配置  │  │ 拉取模型  │  │ 服务  ││
│   └──────────┘  └──────────┘  └───────┘│
└─────────────────────────────────────────┘
    ↓
主循环:监听事件
    ↓
┌─────────────┐     ┌──────────────┐
│ Timer 触发   │ or  │ 模型更新触发  │
└─────────────┘     └──────────────┘
    ↓
拉取用户(一致性哈希分配)
    ↓
生成推荐(并行计算)
    ↓
写入 Cache Store
    ↓
上报进度到 Master
    ↓
继续监听事件...

三大核心协程

协程 1: Sync() - 元数据同步

作用:定期从 Master 同步配置、数据库连接、模型版本等元数据

func (w *Worker) Sync() {
    for {
        // 1. 从 Master 获取元数据
        meta, err := w.masterClient.GetMeta(context.Background(), &protocol.NodeInfo{
            NodeType:      protocol.NodeType_Worker,
            Uuid:          w.workerName,
            BinaryVersion: version.Version,
            Hostname:      os.Hostname(),
        })
        
        // 2. 解析并更新配置
        json.Unmarshal(meta.Config, &w.Config)
        
        // 3. 连接 Data Store(根据配置)
        if w.dataPath != w.Config.Database.DataStore {
            if strings.HasPrefix(w.Config.Database.DataStore, "sqlite://") {
                w.DataClient = data.NewProxyClient(w.conn)  // 通过 Master 代理
            } else {
                w.DataClient, err = data.Open(w.Config.Database.DataStore, ...)
            }
        }
        
        // 4. 连接 Cache Store
        if w.cachePath != w.Config.Database.CacheStore {
            if strings.HasPrefix(w.Config.Database.CacheStore, "sqlite://") {
                w.CacheClient = cache.NewProxyClient(w.conn)
            } else {
                w.CacheClient, err = cache.Open(w.Config.Database.CacheStore, ...)
            }
        }
        
        // 5. 连接 Blob Store(S3/GCS)
        if w.Config.S3.Endpoint != "" {
            w.blobStore, err = blob.NewS3(w.Config.S3)
        } else if w.Config.GCS.Bucket != "" {
            w.blobStore, err = blob.NewGCS(w.Config.GCS)
        } else {
            w.blobStore = blob.NewMasterStoreClient(w.conn)  // 通过 Master
        }
        
        // 6. 检测模型版本更新
        w.latestCollaborativeFilteringModelId = meta.CollaborativeFilteringModelId
        if w.latestCollaborativeFilteringModelId > w.collaborativeFilteringModelId {
            log.Info("new ranking model found")
            w.syncedChan.Signal()  // 触发 Pull 协程
        }
        
        w.latestClickThroughRateModelId = meta.ClickThroughRateModelId
        if w.latestClickThroughRateModelId > w.clickThroughRateModelId {
            log.Info("new click model found")
            w.syncedChan.Signal()
        }
        
        // 7. 更新集群信息(用于一致性哈希)
        w.peers = meta.Workers  // 所有 Worker 节点
        w.me = meta.Me         // 当前节点标识
        
    sleep:
        time.Sleep(w.Config.Master.MetaTimeout)  // 默认 10 秒
    }
}

关键点

  • 配置热更新:无需重启即可更新配置
  • 数据库代理:SQLite 通过 Master 代理访问(因为 SQLite 是单机文件)
  • 模型版本比较:通过 latestXxxModelId > xxxModelId 检测更新
  • 集群感知:更新 peersme,用于一致性哈希分配用户

协程 2: Pull() - 模型拉取

作用:当检测到新模型时,从 Blob Store 下载并加载模型

func (w *Worker) Pull() {
    for range w.syncedChan.C {  // 等待 Sync 协程的信号
        pulled := false
        
        // 1. 拉取协同过滤模型
        if w.latestCollaborativeFilteringModelId > w.collaborativeFilteringModelId {
            log.Info("start pull collaborative filtering model")
            
            // 1.1 从 Blob Store 打开模型文件
            r, err := w.blobStore.Open(strconv.Itoa(w.latestCollaborativeFilteringModelId))
            
            // 1.2 反序列化物品向量和用户向量
            items := logics.NewMatrixFactorizationItems(time.Time{})
            users := logics.NewMatrixFactorizationUsers()
            err = items.Unmarshal(r)   // 加载物品向量 + HNSW 索引
            err = users.Unmarshal(r)   // 加载用户向量
            
            // 1.3 更新 Pipeline 中的模型
            w.MatrixFactorizationItems = items
            w.MatrixFactorizationUsers = users
            w.collaborativeFilteringModelId = w.latestCollaborativeFilteringModelId
            
            pulled = true
        }
        
        // 2. 拉取 CTR 模型
        if w.latestClickThroughRateModelId > w.clickThroughRateModelId {
            log.Info("start pull click model")
            
            r, err := w.blobStore.Open(strconv.Itoa(w.latestClickThroughRateModelId))
            model, err := ctr.UnmarshalModel(r)
            
            w.ClickThroughRateModel = model
            w.clickThroughRateModelId = w.latestClickThroughRateModelId
            
            pulled = true
        }
        
        // 3. 如果有模型更新,触发推荐重新计算
        if pulled {
            w.pulledChan.Signal()  // 通知主循环
        }
    }
}

模型下载流程

Sync 检测到新模型
    ↓
发送信号到 syncedChan
    ↓
Pull 协程被唤醒
    ↓
┌────────────────────────────────────┐
│ 协同过滤模型                         │
│  ├─ blobStore.Open(model_id)      │
│  ├─ items.Unmarshal()  (物品向量)  │
│  └─ users.Unmarshal()  (用户向量)  │
└────────────────────────────────────┘
    ↓
┌────────────────────────────────────┐
│ CTR 模型                            │
│  ├─ blobStore.Open(model_id)      │
│  └─ ctr.UnmarshalModel()          │
└────────────────────────────────────┘
    ↓
发送信号到 pulledChan
    ↓
主循环触发推荐重新计算

协程 3: ServeHTTP() - HTTP 服务

作用:提供 Prometheus metrics 和健康检查 API

func (w *Worker) ServeHTTP() {
    // 1. Prometheus metrics
    http.Handle("/metrics", promhttp.Handler())
    
    // 2. 存活检查
    http.HandleFunc("/api/health/live", w.checkLive)
    
    // 3. 就绪检查
    http.HandleFunc("/api/health/ready", w.checkReady)
    
    http.ListenAndServe(fmt.Sprintf("%s:%d", w.httpHost, w.httpPort), nil)
}

健康检查逻辑

func (w *Worker) checkHealth() HealthStatus {
    healthStatus := HealthStatus{}
    
    // Ping Data Store
    healthStatus.DataStoreError = w.DataClient.Ping()
    healthStatus.DataStoreConnected = (healthStatus.DataStoreError == nil)
    
    // Ping Cache Store
    healthStatus.CacheStoreError = w.CacheClient.Ping()
    healthStatus.CacheStoreConnected = (healthStatus.CacheStoreError == nil)
    
    // 只有两个数据库都连接正常才认为 Ready
    healthStatus.Ready = healthStatus.DataStoreConnected && 
                         healthStatus.CacheStoreConnected
    
    return healthStatus
}

Kubernetes 中的应用

livenessProbe:
  httpGet:
    path: /api/health/live
    port: 8089
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /api/health/ready
    port: 8089
  initialDelaySeconds: 10
  periodSeconds: 5

用户推荐流程

主循环中的 Recommend 调用

loop := func() {
    // 1. 拉取分配给当前 Worker 的用户
    workingUsers, err := w.pullUsers(w.peers, w.me)
    
    // 2. 为这些用户生成推荐
    w.Recommend(workingUsers, func(completed, throughput int) {
        log.Info("ranking recommendation", 
                 zap.Int("n_complete_users", completed),
                 zap.Int("throughput", throughput))
        
        // 上报进度到 Master
        w.masterClient.PushProgress(context.Background(), 
                                     monitor.EncodeProgress(w.Tracer.List()))
    })
}

Pipeline.Recommend() 详解

这是 Worker 最核心的方法,负责为用户生成推荐。

func (p *Pipeline) Recommend(users []data.User, progress func(completed, throughput int)) {
    ctx := context.Background()
    startRecommendTime := time.Now()
    
    // 1. 创建物品缓存(避免重复查询数据库)
    itemCache := NewItemCache(p.DataClient)
    
    log.Info("ranking recommendation",
             zap.Int("n_working_users", len(users)),
             zap.Int("n_jobs", p.Jobs),
             zap.Int("cache_size", p.Config.Recommend.CacheSize))
    
    // 2. 进度追踪
    completed := make(chan struct{}, 1000)
    _, span := p.Tracer.Start(context.Background(), "Generate recommendation", len(users))
    defer span.End()
    
    // 3. 进度上报协程
    go func() {
        completedCount, previousCount := 0, 0
        ticker := time.NewTicker(10 * time.Second)
        for {
            select {
            case _, ok := <-completed:
                if !ok { return }
                completedCount++
            case <-ticker.C:
                throughput := completedCount - previousCount
                span.Add(throughput)
                if progress != nil {
                    progress(completedCount, throughput)
                }
                previousCount = completedCount
            }
        }
    }()
    
    // 4. 并行为每个用户生成推荐
    parallel.Detachable(len(users), p.Jobs, p.Config.OpenAI.ChatCompletionRPM,
        func(pCtx *parallel.Context, jobId int) {
            defer func() { completed <- struct{}{} }()
            
            user := users[jobId]
            userId := user.UserId
            
            // 4.1 检查是否需要更新推荐
            if !p.checkUserActiveTime(ctx, userId) ||
               !p.checkRecommendCacheOutOfDate(ctx, userId) {
                return  // 跳过不活跃或缓存未过期的用户
            }
            
            // 4.2 创建推荐器(负责生成候选集)
            recommender, err := logics.NewRecommender(
                p.Config.Recommend,
                p.CacheClient,
                p.DataClient,
                false,
                userId,
                nil,
            )
            
            if recommender.IsColdStart() {
                return  // 跳过冷启动用户
            }
            
            // 4.3 协同过滤推荐
            if p.MatrixFactorizationUsers != nil && p.MatrixFactorizationItems != nil {
                if userEmbedding, ok := p.MatrixFactorizationUsers.Get(userId); ok {
                    err = p.updateCollaborativeRecommend(
                        p.MatrixFactorizationItems,
                        userId,
                        userEmbedding,
                        recommender.ExcludeSet(),
                        itemCache,
                    )
                }
            }
            
            // 4.4 生成推荐候选集
            var scores []cache.Score
            var digest string
            recommenderNames := p.Config.Recommend.Ranker.Recommenders
            scores, digest, err = recommender.RecommendSequential(
                context.Background(),
                scores,
                0,
                recommenderNames...,
            )
            
            // 4.5 过滤不存在的物品
            candidates := make([]cache.Score, 0, len(scores))
            candidateSet := mapset.NewSet[string]()
            items, err := itemCache.GetMap(...)
            for _, score := range scores {
                if _, exist := items[score.Id]; exist {
                    candidates = append(candidates, score)
                    candidateSet.Add(score.Id)
                }
            }
            
            // 4.6 添加替换候选(Replacement)
            if p.Config.Recommend.Replacement.EnableReplacement {
                candidates, replacementPositive, replacementNegative, err =
                    p.addReplacementCandidates(candidates, candidateSet, ...)
            }
            
            // 4.7 排序(Ranking)
            var results []cache.Score
            if p.Config.Recommend.Ranker.Type == "fm" {
                // CTR 模型排序
                results, err = p.rankByClickTroughRate(
                    p.ClickThroughRateModel,
                    &user,
                    candidates,
                    itemCache,
                    recommendTime,
                )
            } else if p.Config.Recommend.Ranker.Type == "llm" {
                // LLM 排序
                ranker, err := logics.NewChatRanker(...)
                results, err = p.rankByLLM(
                    pCtx,
                    ranker,
                    &user,
                    recommender.UserFeedback(),
                    candidates,
                    itemCache,
                    recommendTime,
                )
            } else {
                // 不排序,直接使用候选集的分数
                results = candidates
            }
            
            // 4.8 应用替换衰减
            if p.Config.Recommend.Replacement.EnableReplacement {
                results = p.applyReplacementDecay(
                    results,
                    replacementPositive,
                    replacementNegative,
                )
            }
            
            // 4.9 写入缓存
            err = p.CacheClient.AddScores(ctx, cache.Recommend, userId, results)
            err = p.CacheClient.Set(ctx,
                cache.Time(cache.Key(cache.RecommendUpdateTime, userId), recommendTime),
                cache.String(cache.Key(cache.RecommendDigest, userId), digest),
            )
        })
    
    close(completed)
    log.Info("complete ranking recommendation", 
             zap.String("used_time", time.Since(startTime).String()))
}

推荐流程图

开始推荐
    ↓
创建物品缓存
    ↓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    并行处理每个用户(p.Jobs 个并发)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ↓
检查用户是否需要更新推荐
    ├─ 不活跃? → 跳过
    ├─ 缓存未过期? → 跳过
    └─ 冷启动用户? → 跳过
    ↓
┌────────────────────────────────────┐
│ 第一步:协同过滤推荐                 │
│  - 获取用户向量                     │
│  - 在物品向量索引中搜索 TopK         │
│  - 写入 cache.CollaborativeFiltering│
└────────────────────────────────────┘
    ↓
┌────────────────────────────────────┐
│ 第二步:生成候选集                   │
│  - 调用多个 Recommender             │
│    ├─ ItemBased                    │
│    ├─ UserBased                    │
│    ├─ Latest                       │
│    ├─ Popular                      │
│    └─ CustomRecommender            │
│  - 合并去重                         │
└────────────────────────────────────┘
    ↓
┌────────────────────────────────────┐
│ 第三步:过滤候选集                   │
│  - 过滤已删除的物品                  │
│  - 过滤隐藏物品                      │
│  - 添加替换候选(用户历史)          │
└────────────────────────────────────┘
    ↓
┌────────────────────────────────────┐
│ 第四步:排序(Ranking)              │
│  - FM 模型排序(CTR 预测)           │
│  - LLM 排序(ChatGPT)              │
│  - 或不排序(使用召回分数)           │
└────────────────────────────────────┘
    ↓
┌────────────────────────────────────┐
│ 第五步:应用替换衰减                 │
│  - 降低历史物品的分数                │
│  - 重新排序                         │
└────────────────────────────────────┘
    ↓
┌────────────────────────────────────┐
│ 第六步:写入缓存                     │
│  - cache.Recommend (推荐结果)       │
│  - cache.RecommendUpdateTime       │
│  - cache.RecommendDigest           │
└────────────────────────────────────┘
    ↓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    汇总所有用户
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    ↓
上报 Metrics
    ↓
结束

推荐缓存过期检查

Worker 不会每次都为用户重新生成推荐,而是检查缓存是否过期:

func (p *Pipeline) checkRecommendCacheOutOfDate(ctx context.Context, userId string) bool {
    // 1. 如果缓存为空 → 过期
    items, err := p.CacheClient.SearchScores(ctx, cache.Recommend, userId, nil, 0, -1)
    if len(items) == 0 {
        return true
    }
    
    // 2. 如果 digest 不匹配 → 过期(配置变化)
    digest, err := p.CacheClient.Get(ctx, cache.Key(cache.RecommendDigest, userId)).String()
    if digest != p.Config.Recommend.Hash() {
        return true
    }
    
    // 3. 如果更新时间 + 过期时间 < 当前时间 → 过期
    recommendTime, err := p.CacheClient.Get(ctx, cache.Key(cache.RecommendUpdateTime, userId)).Time()
    if recommendTime.Before(time.Now().Add(-p.Config.Recommend.CacheExpire)) {
        return true
    }
    
    // 4. 如果用户活跃时间 > 推荐时间 → 需要重新推荐(用户有新行为)
    activeTime, err := p.CacheClient.Get(ctx, cache.Key(cache.LastModifyUserTime, userId)).Time()
    if activeTime.After(recommendTime) {
        timeoutTime := recommendTime.Add(p.Config.Recommend.Ranker.CacheExpire)
        return timeoutTime.Before(time.Now())
    }
    
    return false
}

缓存过期条件总结

  1. 缓存为空
  2. 配置变化(digest 不匹配)
  3. 缓存超时(CacheExpire)
  4. 用户有新行为(activeTime > recommendTime)

负载均衡机制

一致性哈希分配用户

Worker 集群使用一致性哈希来分配用户,保证:

  • 每个用户只被一个 Worker 处理
  • Worker 扩缩容时,只影响少部分用户重新分配
func (w *Worker) pullUsers(peers []string, me string) ([]data.User, error) {
    ctx := context.Background()
    
    // 1. 检查当前节点是否在集群中
    if !lo.Contains(peers, me) {
        return nil, errors.New("current node isn't in worker nodes")
    }
    
    // 2. 创建一致性哈希环
    c := consistent.New()
    for _, peer := range peers {
        c.Add(peer)
    }
    
    // 3. 从数据库拉取所有用户
    var users []data.User
    userChan, errChan := w.DataClient.GetUserStream(ctx, batchSize)
    
    // 4. 通过一致性哈希筛选属于当前 Worker 的用户
    for batchUsers := range userChan {
        for _, user := range batchUsers {
            p, err := c.Get(user.UserId)  // 计算用户应该分配给哪个 Worker
            if p == me {                  // 如果是当前 Worker
                users = append(users, user)
            }
        }
    }
    
    return users, nil
}

一致性哈希示意图

假设有 3 个 Worker:worker-1, worker-2, worker-3

一致性哈希环:
         worker-1 (hash=100)
            ↓
user-A → 分配给 worker-1
user-B → 分配给 worker-2
user-C → 分配给 worker-1
user-D → 分配给 worker-3
            ↓
         worker-2 (hash=500)
            ↓
         worker-3 (hash=900)

规则:用户哈希值顺时针找到的第一个 Worker

扩缩容影响

添加 Worker

添加前:
  user-A → worker-1
  user-B → worker-2
  user-C → worker-3

添加 worker-4 后:
  user-A → worker-1  (不变)
  user-B → worker-4  (重新分配)
  user-C → worker-3  (不变)

只有部分用户需要重新计算推荐

移除 Worker

移除前:
  user-A → worker-1
  user-B → worker-2
  user-C → worker-3

移除 worker-2 后:
  user-A → worker-1  (不变)
  user-B → worker-3  (重新分配)
  user-C → worker-3  (不变)

被移除 Worker 的用户会自动分配给其他 Worker

模型同步机制

模型更新检测流程

1. Master 训练新模型
      ↓
2. Master 保存模型到 Blob Store
      ↓
3. Master 更新元数据中的 model_id
      ↓
4. Worker.Sync() 定期轮询 Master
      ↓
5. 比较 latestModelId vs modelId
      ↓
6. 如果不一致 → 发送信号到 syncedChan
      ↓
7. Worker.Pull() 被唤醒
      ↓
8. 从 Blob Store 下载模型
      ↓
9. 反序列化并加载到内存
      ↓
10. 发送信号到 pulledChan
      ↓
11. 主循环触发推荐重新计算

协同过滤模型的加载

// 1. 从 Blob Store 打开模型文件
r, err := w.blobStore.Open(strconv.Itoa(w.latestCollaborativeFilteringModelId))

// 2. 反序列化物品向量和 HNSW 索引
items := logics.NewMatrixFactorizationItems(time.Time{})
err = items.Unmarshal(r)
// items 包含:
//  - 物品向量矩阵([][]float32)
//  - HNSW 索引(用于快速 ANN 搜索)
//  - 物品 ID 映射

// 3. 反序列化用户向量
users := logics.NewMatrixFactorizationUsers()
err = users.Unmarshal(r)
// users 包含:
//  - map[userId][]float32(用户向量)

// 4. 更新 Pipeline
w.MatrixFactorizationItems = items
w.MatrixFactorizationUsers = users
w.collaborativeFilteringModelId = w.latestCollaborativeFilteringModelId

协同过滤推荐计算

func (p *Pipeline) updateCollaborativeRecommend(
    items *logics.MatrixFactorizationItems,
    userId string,
    userEmbedding []float32,
    excludeSet mapset.Set[string],
    itemCache *ItemCache,
) error {
    ctx := context.Background()
    
    // 1. 在物品向量索引中搜索 TopK
    scores := items.Search(userEmbedding, p.Config.Recommend.CacheSize+excludeSet.Cardinality())
    // Search 内部使用 HNSW 算法,时间复杂度 O(log N)
    
    // 2. 过滤已排除的物品
    itemsMap, err := itemCache.GetMap(...)
    recommend := make([]cache.Score, 0, len(scores))
    for i := range scores {
        if item, exist := itemsMap[scores[i].Id]; exist && !excludeSet.Contains(item.ItemId) {
            recommend = append(recommend, scores[i])
        }
    }
    
    // 3. 写入缓存
    err = p.CacheClient.AddScores(ctx, cache.CollaborativeFiltering, userId, recommend)
    err = p.CacheClient.Set(ctx,
        cache.Time(cache.Key(cache.CollaborativeFilteringUpdateTime, userId), localStartTime),
        cache.String(cache.Key(cache.CollaborativeFilteringDigest, userId), ...),
    )
    
    // 4. 删除过期缓存
    err = p.CacheClient.DeleteScores(ctx, []string{cache.CollaborativeFiltering},
        cache.ScoreCondition{Before: &localStartTime, Subset: proto.String(userId)})
    
    return nil
}

监控和健康检查

Prometheus Metrics

Worker 暴露以下指标(/metrics):

// 1. 更新的用户数
UpdateUserRecommendTotal = promauto.NewGauge(prometheus.GaugeOpts{
    Namespace: "gorse",
    Subsystem: "worker",
    Name:      "update_user_recommend_total",
})

// 2. 各推荐步骤耗时
OfflineRecommendStepSecondsVec = promauto.NewGaugeVec(prometheus.GaugeOpts{
    Namespace: "gorse",
    Subsystem: "worker",
    Name:      "offline_recommend_step_seconds",
}, []string{LabelStep})
// LabelStep 取值:
//  - collaborative_recommend
//  - item_based_recommend
//  - user_based_recommend
//  - latest_recommend
//  - popular_recommend

// 3. 总推荐耗时
OfflineRecommendTotalSeconds = promauto.NewGauge(prometheus.GaugeOpts{
    Namespace: "gorse",
    Subsystem: "worker",
    Name:      "offline_recommend_total_seconds",
})

// 4. 内存使用
MemoryInuseBytesVec = promauto.NewGaugeVec(prometheus.GaugeOpts{
    Namespace: "gorse",
    Subsystem: "worker",
    Name:      "memory_inuse_bytes",
}, []string{LabelData})

健康检查 API

1. Liveness Probe (/api/health/live)

func (w *Worker) checkLive(writer http.ResponseWriter, _ *http.Request) {
    healthStatus := w.checkHealth()
    writeJSON(writer, healthStatus)
}

返回示例:

{
  "Ready": false,
  "DataStoreError": null,
  "CacheStoreError": "connection refused",
  "DataStoreConnected": true,
  "CacheStoreConnected": false
}

2. Readiness Probe (/api/health/ready)

func (w *Worker) checkReady(writer http.ResponseWriter, _ *http.Request) {
    healthStatus := w.checkHealth()
    if healthStatus.Ready {
        writeJSON(writer, healthStatus)  // HTTP 200
    } else {
        writeError(writer, string(errReason), http.StatusServiceUnavailable)  // HTTP 503
    }
}

与 Master/Server 的关系

三者交互图

┌──────────────────────────────────────────────────────────────┐
│                         Master                               │
│  - 训练模型                                                   │
│  - 保存模型到 Blob Store                                      │
│  - 提供元数据(配置、模型版本、Worker 列表)                    │
│  - 接收 Worker 进度上报                                       │
└──────────────────────────────────────────────────────────────┘
         ↓ gRPC                              ↑ gRPC
         ↓ GetMeta()                         ↑ PushProgress()
         ↓                                   ↑
┌──────────────────────────────────────────────────────────────┐
│                        Worker                                │
│  - 同步配置和模型版本                                          │
│  - 下载模型                                                   │
│  - 生成用户推荐                                                │
│  - 写入 Cache Store                                           │
└──────────────────────────────────────────────────────────────┘
         ↓
         ↓ 写入推荐结果
         ↓
┌──────────────────────────────────────────────────────────────┐
│                     Cache Store                              │
│  - cache.Recommend (用户推荐列表)                             │
│  - cache.CollaborativeFiltering (协同过滤推荐)                │
│  - cache.RecommendUpdateTime (更新时间)                       │
└──────────────────────────────────────────────────────────────┘
         ↑
         ↑ 读取推荐结果
         ↑
┌──────────────────────────────────────────────────────────────┐
│                        Server                                │
│  - 提供推荐 API                                                │
│  - 从 Cache Store 读取预计算的推荐                             │
│  - 应用实时过滤和补充                                           │
└──────────────────────────────────────────────────────────────┘
         ↓
         ↓ HTTP API
         ↓
┌──────────────────────────────────────────────────────────────┐
│                      Client/User                             │
└──────────────────────────────────────────────────────────────┘

Worker 与 Master 的交互

1. Worker → Master: GetMeta()

// 请求
message NodeInfo {
  NodeType node_type = 1;        // NodeType_Worker
  string uuid = 2;               // Worker 名称
  string binary_version = 3;     // 版本号
  string hostname = 4;           // 主机名
}

// 响应
message Meta {
  string config = 1;                            // JSON 配置
  int64 collaborative_filtering_model_id = 2;   // 协同过滤模型版本
  int64 click_through_rate_model_id = 3;        // CTR 模型版本
  repeated string workers = 4;                   // Worker 列表
  string me = 5;                                // 当前 Worker 标识
}

2. Worker → Master: PushProgress()

w.masterClient.PushProgress(context.Background(), 
                            monitor.EncodeProgress(w.Tracer.List()))

上报的进度信息:

  • 当前任务名称(如 "Generate recommendation")
  • 总任务数
  • 已完成数
  • 进度百分比

Master 收到后会:

  • 更新仪表盘显示
  • 聚合多个 Worker 的进度

Worker 与 Server 的间接交互

Worker 和 Server 不直接通信,通过 Cache Store 间接交互:

Worker 写入:
  cache.Recommend/{user_id} → [item1:0.9, item2:0.8, ...]

Server 读取:
  GET /api/recommend/{user_id}
    ↓
  CacheClient.SearchScores(cache.Recommend, user_id, ...)
    ↓
  返回推荐列表

实战示例

示例 1:启动单个 Worker

# 启动 Worker
gorse-worker \
  --master-host 127.0.0.1 \
  --master-port 8086 \
  --http-host 0.0.0.0 \
  --http-port 8089 \
  --jobs 4 \
  --cache-path /tmp/worker_cache.data

# 参数说明:
# --master-host/port: Master 地址
# --http-host/port: HTTP 服务地址(Prometheus + 健康检查)
# --jobs: 并行任务数(推荐使用 CPU 核心数)
# --cache-path: 本地缓存文件路径(可选)

启动后的日志

[INFO] start meta sync meta_timeout=10s
[INFO] connect data store database=redis://127.0.0.1:6379/0
[INFO] connect cache store database=redis://127.0.0.1:6379/1
[INFO] new ranking model found old_version=0 new_version=1
[INFO] start pull collaborative filtering model
[INFO] synced collaborative filtering model id=1
[INFO] ranking recommendation n_working_users=1000 n_jobs=4 cache_size=100
[INFO] complete ranking recommendation used_time=2m30s

示例 2:启动 Worker 集群

# Worker 1
gorse-worker --master-host master.example.com --http-port 8089 --jobs 4

# Worker 2
gorse-worker --master-host master.example.com --http-port 8090 --jobs 4

# Worker 3
gorse-worker --master-host master.example.com --http-port 8091 --jobs 4

集群自动协调

  1. 每个 Worker 从 Master 获取 Worker 列表
  2. 通过一致性哈希自动分配用户
  3. Worker 1 处理用户 A、D、F...
  4. Worker 2 处理用户 B、E、G...
  5. Worker 3 处理用户 C、H、I...

示例 3:Kubernetes 部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gorse-worker
spec:
  replicas: 3  # 3 个 Worker
  selector:
    matchLabels:
      app: gorse-worker
  template:
    metadata:
      labels:
        app: gorse-worker
    spec:
      containers:
      - name: worker
        image: zhenghaoz/gorse-worker:latest
        args:
        - --master-host=gorse-master
        - --master-port=8086
        - --http-port=8089
        - --jobs=4
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
            cpu: "2000m"
        ports:
        - containerPort: 8089
          name: http
        livenessProbe:
          httpGet:
            path: /api/health/live
            port: 8089
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /api/health/ready
            port: 8089
          initialDelaySeconds: 10
          periodSeconds: 5

示例 4:监控 Worker 状态

查看 Prometheus Metrics

curl http://worker-host:8089/metrics

# 输出示例:
# gorse_worker_update_user_recommend_total 1523
# gorse_worker_offline_recommend_total_seconds 150.5
# gorse_worker_offline_recommend_step_seconds{step="collaborative_recommend"} 45.2
# gorse_worker_offline_recommend_step_seconds{step="item_based_recommend"} 30.1
# gorse_worker_offline_recommend_step_seconds{step="latest_recommend"} 15.8

查看健康状态

curl http://worker-host:8089/api/health/ready

# 正常输出:
{
  "Ready": true,
  "DataStoreError": null,
  "CacheStoreError": null,
  "DataStoreConnected": true,
  "CacheStoreConnected": true
}

# 异常输出(HTTP 503):
{
  "Ready": false,
  "DataStoreError": null,
  "CacheStoreError": "connection refused",
  "DataStoreConnected": true,
  "CacheStoreConnected": false
}

示例 5:调试推荐流程

查看用户推荐缓存

# Redis 示例
redis-cli

# 查看用户推荐列表
ZREVRANGE gorse:recommend:user123 0 9 WITHSCORES
# 输出:
# 1) "item456"
# 2) "0.95"
# 3) "item789"
# 4) "0.88"
# ...

# 查看推荐更新时间
GET gorse:recommend_update_time:user123
# 输出:2025-01-09T10:30:00Z

# 查看推荐配置摘要
GET gorse:recommend_digest:user123
# 输出:a3f5b8c9d2e1...

查看协同过滤推荐

# 查看协同过滤推荐
ZREVRANGE gorse:collaborative_filtering:user123 0 9 WITHSCORES

总结:Worker 核心要点

1. Worker 的三大职责

职责 说明 核心方法
配置同步 从 Master 同步配置和元数据 Sync()
模型同步 从 Blob Store 下载模型 Pull()
推荐计算 为用户生成推荐并缓存 Recommend()

2. Worker 的工作流程

启动 Worker
    ↓
连接 Master (gRPC)
    ↓
启动 Sync 协程:定期同步配置和元数据
    ↓
启动 Pull 协程:检测并下载新模型
    ↓
启动 HTTP 协程:提供 metrics 和健康检查
    ↓
主循环:
  ├─ Timer 触发(默认 1 分钟)
  └─ 模型更新触发
    ↓
  拉取用户(一致性哈希分配)
    ↓
  并行生成推荐
    ↓
  写入 Cache Store
    ↓
  上报进度到 Master

3. Worker 与其他组件的关系

Master
  ├─ 提供配置和元数据 (GetMeta)
  ├─ 提供模型版本号
  ├─ 接收进度上报 (PushProgress)
  └─ 存储模型到 Blob Store

Worker
  ├─ 从 Master 同步配置
  ├─ 从 Blob Store 下载模型
  ├─ 从 Data Store 读取用户/物品
  └─ 写推荐结果到 Cache Store

Server
  ├─ 从 Cache Store 读取推荐
  └─ 对外提供 API

4. Worker 的扩展性

  • 水平扩展:添加更多 Worker 实例
  • 自动负载均衡:一致性哈希自动分配用户
  • 容错性:单个 Worker 故障不影响其他 Worker
  • 无状态:Worker 可以随时重启,状态在数据库中

5. Worker 的性能优化

优化点 说明 配置参数
并行度 增加 --jobs 参数 --jobs=8
缓存大小 增加 cache_size recommend.cache_size
缓存过期 增加 cache_expire recommend.cache_expire
跳过不活跃用户 启用 active_user_ttl recommend.active_user_ttl
跳过冷启动用户 默认启用 无需配置

posted @ 2026-01-12 10:33  技术漫游  阅读(5)  评论(0)    收藏  举报