Gorse Worker 架构详解
目录
Worker 概述
Worker 是什么?
Worker 是 Gorse 的推荐计算引擎,负责为用户生成个性化推荐。它的核心职责是:
- 从 Master 同步配置和模型:获取最新的推荐策略和训练好的模型
- 生成离线推荐:为分配给自己的用户计算推荐结果
- 写入缓存:将推荐结果存储到 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_ModelId和latestXxx_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检测更新 - 集群感知:更新
peers和me,用于一致性哈希分配用户
协程 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
}
缓存过期条件总结:
- 缓存为空
- 配置变化(digest 不匹配)
- 缓存超时(CacheExpire)
- 用户有新行为(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
集群自动协调:
- 每个 Worker 从 Master 获取 Worker 列表
- 通过一致性哈希自动分配用户
- Worker 1 处理用户 A、D、F...
- Worker 2 处理用户 B、E、G...
- 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 |
| 跳过冷启动用户 | 默认启用 | 无需配置 |

浙公网安备 33010602011771号