Loading

# Gorse Master 工作原理详解

Master 是整个 Gorse 系统的"大脑"和"指挥官"

📊 Master 在架构中的位置

┌──────────────────────────────────────────────────┐
│                  Master (大脑)                    │
│          端口: 8086 (gRPC) + 8088 (HTTP)         │
└────┬─────────────┬────────────────┬──────────────┘
     │             │                │
     │ 配置同步     │ 模型下发       │ 任务分配
     ▼             ▼                ▼
┌─────────┐  ┌──────────┐    ┌──────────┐
│ Server  │  │ Worker-1 │    │ Worker-2 │
│ (API层) │  │ (计算层) │    │ (计算层) │
└─────────┘  └──────────┘    └──────────┘
     │             │                │
     └─────────────┴────────────────┘
                   │
        ┌──────────┴──────────┐
        │                     │
   ┌────▼─────┐        ┌─────▼────┐
   │  MySQL   │        │  Redis   │
   │ (数据)   │        │ (缓存)   │
   └──────────┘        └──────────┘

🎯 Master 的六大职责

1. 任务调度 🕐

定期触发训练任务,控制整个推荐系统的生命周期

2. 数据加载 📦

从 MySQL 加载用户/物品/反馈数据,构建训练数据集

3. 模型训练 🧮

训练协同过滤模型、CTR 模型,保存模型参数

4. AutoML 优化 🔧

自动搜索最佳超参数,提升推荐质量

5. 配置分发 ⚙️

通过 gRPC 向 Server/Worker 分发配置和数据库连接信息

6. 统计监控 📊

收集系统指标,提供 Dashboard 和监控接口


🏗️ Master 的核心结构

// master.go:62-100
type Master struct {
    // gRPC 服务器(与 Worker/Server 通信)
    grpcServer *grpc.Server
    
    // REST API 服务器(Dashboard + 管理接口)
    RestServer server.RestServer
    
    // 数据库连接
    metaStore  meta.Database    // 元数据(SQLite)
    blobStore  blob.Store       // 模型文件存储
    DataClient  data.Database   // 用户数据(MySQL)
    CacheClient cache.Database  // 推荐缓存(Redis)
    
    // 协同过滤模型
    collaborativeFilteringModelMutex sync.RWMutex
    collaborativeFilteringMeta       meta.Model[cf.Score]
    
    // CTR 模型
    clickThroughRateModelMutex sync.RWMutex
    clickThroughRateMeta        meta.Model[ctr.Score]
    
    // 事件通道(触发训练)
    fitTicker    *time.Ticker              // 定时器
    importedChan *parallel.ConditionChannel // 数据导入事件
    triggerChan  *parallel.ConditionChannel // 手动触发事件
}

🔍 Master 启动流程详解

Step 1: 创建 Master 实例(master.go:103-144)

func NewMaster(cfg *config.Config, cacheFolder string, standalone bool) *Master {
    // 1. 设置追踪提供者(OpenTelemetry)
    tp, _ := cfg.Tracing.NewTracerProvider()
    otel.SetTracerProvider(tp)
    
    // 2. 设置 OpenAI 客户端(如果使用 LLM 排序)
    clientConfig := openai.DefaultConfig(cfg.OpenAI.AuthToken)
    openAIClient := openai.NewClientWithConfig(clientConfig)
    
    // 3. 创建定时器(默认每小时训练一次)
    duration := cfg.Recommend.Collaborative.FitPeriod  // 如 60m
    fitTicker := time.NewTicker(duration)
    
    // 4. 创建事件通道
    importedChan := parallel.NewConditionChannel()  // 数据导入通知
    triggerChan := parallel.NewConditionChannel()   // 手动触发通知
    
    m := &Master{
        cachePath:    cacheFolder,
        openAIClient: openAIClient,
        fitTicker:    fitTicker,
        importedChan: importedChan,
        triggerChan:  triggerChan,
        RestServer: server.RestServer{
            Config:     cfg,
            HttpHost:   cfg.Master.HttpHost,
            HttpPort:   cfg.Master.HttpPort,
        },
    }
    return m
}

关键配置

  • FitPeriod:训练周期(默认 60 分钟)
  • CacheFolder:存储模型和元数据的路径
  • Standalone:是否独立模式(包含 Worker 功能)

Step 2: 启动服务(master.go:147-283)⭐ 核心

func (m *Master) Serve() {
    var err error
    
    // 1️⃣ 连接 Blob Store(存储模型文件)
    if m.Config.S3.Endpoint != "" {
        m.blobStore, _ = blob.NewS3(m.Config.S3)  // AWS S3
    } else if m.Config.GCS.Bucket != "" {
        m.blobStore, _ = blob.NewGCS(m.Config.GCS)  // Google Cloud Storage
    } else {
        m.blobStore = blob.NewPOSIX(m.cachePath)  // 本地文件系统
    }
    
    // 2️⃣ 连接 Meta Database(SQLite,存储元数据)
    m.metaStore, _ = meta.Open(
        fmt.Sprintf("sqlite://%s/meta.sqlite3", m.cachePath),
        m.Config.Master.MetaTimeout,
    )
    m.metaStore.Init()
    
    // 3️⃣ 连接 Data Database(MySQL/PostgreSQL)
    m.DataClient, _ = data.Open(
        m.Config.Database.DataStore,  // "mysql://user:pass@host/db"
        m.Config.Database.DataTablePrefix,
    )
    m.DataClient.Init()
    
    // 4️⃣ 连接 Cache Database(Redis)
    m.CacheClient, _ = cache.Open(
        m.Config.Database.CacheStore,  // "redis://host:6379"
        m.Config.Database.CacheTablePrefix,
    )
    m.CacheClient.Init()
    
    // 5️⃣ 加载已保存的模型元数据
    metaStr, _ := m.metaStore.Get(meta.COLLABORATIVE_FILTERING_MODEL)
    if metaStr != nil {
        m.collaborativeFilteringMeta.FromJSON(*metaStr)
        log.Logger().Info("loaded collaborative filtering model",
            zap.String("type", m.collaborativeFilteringMeta.Type),
            zap.Any("params", m.collaborativeFilteringMeta.Params),
            zap.Any("score", m.collaborativeFilteringMeta.Score))
    }
    
    // 6️⃣ 启动任务调度循环(后台 goroutine)⭐⭐⭐
    go m.RunTasksLoop()
    
    // 7️⃣ 启动 gRPC 服务器(后台 goroutine)
    go func() {
        log.Logger().Info("start rpc server",
            zap.String("host", m.Config.Master.Host),
            zap.Int("port", m.Config.Master.Port))  // 默认 8086
        
        lis, _ := net.Listen("tcp", 
            fmt.Sprintf("%s:%d", m.Config.Master.Host, m.Config.Master.Port))
        
        m.grpcServer = grpc.NewServer()
        
        // 注册服务
        protocol.RegisterMasterServer(m.grpcServer, m)              // Master 服务
        protocol.RegisterCacheStoreServer(m.grpcServer, ...)        // Cache 代理
        protocol.RegisterDataStoreServer(m.grpcServer, ...)         // Data 代理
        protocol.RegisterBlobStoreServer(m.grpcServer, ...)         // Blob 存储
        
        m.grpcServer.Serve(lis)
    }()
    
    // 8️⃣ 启动 HTTP 服务器(Dashboard)
    m.StartHttpServer()  // 默认端口 8088
}

启动顺序图

Master 启动
    ↓
1. 连接 Blob Store (S3/GCS/本地)
    ↓
2. 连接 Meta Store (SQLite)
    ↓
3. 连接 Data Store (MySQL)
    ↓
4. 连接 Cache Store (Redis)
    ↓
5. 加载已保存的模型
    ↓
6. 启动任务调度循环 (goroutine)
    ↓
7. 启动 gRPC 服务器 (8086)
    ↓
8. 启动 HTTP 服务器 (8088)
    ↓
✅ Master 就绪!

🔄 任务调度循环(master.go:295-323)⭐⭐⭐ 最核心

func (m *Master) RunTasksLoop() {
    defer util.CheckPanic()
    
    // 后台监控数据导入
    go func() {
        m.importedChan.Signal()  // 首次立即触发
        for {
            if m.checkDataImported() {  // 检查是否有新数据导入
                m.importedChan.Signal()
            }
            time.Sleep(time.Second)  // 每秒检查一次
        }
    }()
    
    // 主循环:等待触发事件
    for {
        select {
        case <-m.fitTicker.C:      // 定时触发(如每小时)
        case <-m.importedChan.C:    // 数据导入触发
        case <-m.triggerChan.C:     // 手动触发
        }
        
        // 执行完整的训练流程
        err := m.runLoadDatasetTask()  // 1. 加载数据集
        if err != nil {
            log.Logger().Error("failed to load dataset", zap.Error(err))
            continue
        }
        
        // 后续还会调用(在实际代码中):
        // m.runFitCollaborativeFilteringTask()  // 2. 训练协同过滤模型
        // m.runFitClickModelTask()              // 3. 训练 CTR 模型
        // m.runCacheTask()                      // 4. 生成推荐缓存
    }
}

调度流程图

┌──────────────────────────────────────┐
│         主循环(永不停止)            │
└────────────┬─────────────────────────┘
             │
    ┌────────┴────────┐
    │  等待触发事件    │
    │  - 定时器         │
    │  - 数据导入       │
    │  - 手动触发       │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 1. 加载数据集   │  ← tasks.go:loadDataset()
    │    从 MySQL     │
    │    构建训练集   │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 2. 训练 CF 模型 │  ← tasks.go:fitCollaborativeFiltering()
    │    BPR/ALS      │
    │    评估性能     │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 3. AutoML 优化  │  ← tasks.go:optimizeCollaborativeFiltering()
    │    搜索最佳参数 │
    │    (可选)       │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 4. 训练 CTR 模型│  ← tasks.go:fitClickModel()
    │    FM/DeepFM    │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 5. 分发任务给   │  ← Worker 执行推荐计算
    │    Worker       │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 6. 等待下次触发 │
    └─────────────────┘

📦 任务 1:加载数据集(tasks.go:51-149)

func (m *Master) loadDataset() (datasets Datasets, err error) {
    ctx, span := m.tracer.Start(context.Background(), "Load Dataset", 1)
    defer span.End()
    
    // 1️⃣ 创建非个性化推荐器(流行、最新等)
    nonPersonalizedRecommenders := make([]*logics.NonPersonalized, 0)
    for _, cfg := range m.Config.Recommend.NonPersonalized {
        recommender, _ := logics.NewNonPersonalized(cfg, ...)
        nonPersonalizedRecommenders = append(nonPersonalizedRecommenders, recommender)
    }
    
    // 2️⃣ 从 MySQL 加载数据
    log.Logger().Info("load dataset",
        zap.Any("positive_feedback_types", m.Config.Recommend.DataSource.PositiveFeedbackTypes),
        zap.Uint("item_ttl", m.Config.Recommend.DataSource.ItemTTL))
    
    datasets.clickDataset, datasets.rankingDataset, err = m.LoadDataFromDatabase(
        ctx, 
        m.DataClient,
        m.Config.Recommend.DataSource.PositiveFeedbackTypes,  // ["purchase", "like"]
        m.Config.Recommend.DataSource.ReadFeedbackTypes,      // ["click", "view"]
        m.Config.Recommend.DataSource.ItemTTL,                // 物品过期时间
        m.Config.Recommend.DataSource.PositiveFeedbackTTL,    // 反馈过期时间
        evaluator,
        nonPersonalizedRecommenders,
    )
    
    // 3️⃣ 保存非个性化推荐结果到 Redis
    for i, recommender := range nonPersonalizedRecommenders {
        scores := recommender.PopAll()
        m.CacheClient.AddScores(ctx, cache.NonPersonalized, recommender.Name(), scores)
    }
    
    // 4️⃣ 写入统计数据到 Redis
    m.CacheClient.AddTimeSeriesPoints(ctx, []cache.TimeSeriesPoint{
        {Name: cache.NumUsers, Value: float64(datasets.rankingDataset.CountUsers())},
        {Name: cache.NumItems, Value: float64(datasets.rankingDataset.CountItems())},
        {Name: cache.NumFeedback, Value: float64(len(datasets.clickDataset.Target))},
    })
    
    // 5️⃣ 分割训练集和测试集
    datasets.rankingTrainSet, datasets.rankingTestSet = 
        datasets.rankingDataset.SplitCF(numTestUsers, seed)
    datasets.clickTrainSet, datasets.clickTestSet = 
        datasets.clickDataset.Split(testRatio, seed)
    
    return datasets, nil
}

加载数据流程

MySQL 数据库
    │
    │ SELECT * FROM users
    │ SELECT * FROM items  
    │ SELECT * FROM feedbacks WHERE feedback_type IN ('purchase', 'like')
    ↓
┌─────────────────────────┐
│  LoadDataFromDatabase   │
│  - 过滤过期数据         │
│  - 过滤隐藏物品         │
│  - 构建用户-物品矩阵    │
└──────────┬──────────────┘
           │
           ├──→ rankingDataset (协同过滤用)
           │    - 用户数: 10,000
           │    - 物品数: 50,000
           │    - 反馈数: 500,000
           │
           └──→ clickDataset (CTR 模型用)
                - 正样本: 300,000
                - 负样本: 200,000

🧮 任务 2:训练协同过滤模型

func (m *Master) fitCollaborativeFiltering(datasets Datasets) {
    // 1️⃣ 选择模型类型
    modelName := m.Config.Recommend.Collaborative.Model
    // 可选: bpr, als, item-cf, user-cf
    
    // 2️⃣ 设置超参数
    params := model.Params{
        model.NFactors:   50,      // 隐向量维度
        model.Lr:         0.01,    // 学习率
        model.Reg:        0.01,    // 正则化
        model.NEpochs:    100,     // 训练轮数
        model.InitMean:   0,
        model.InitStdDev: 0.1,
    }
    
    // 3️⃣ 创建并训练模型
    var cfModel cf.MatrixFactorization
    switch modelName {
    case "bpr":
        cfModel = cf.NewBPR(params)
    case "als":
        cfModel = cf.NewALS(params)
    }
    
    // 4️⃣ 训练
    log.Logger().Info("start training", 
        zap.String("model", modelName),
        zap.Any("params", params))
    
    cfModel.Fit(datasets.rankingTrainSet, datasets.rankingTestSet, ...)
    
    // 5️⃣ 评估模型
    scores := cfModel.Evaluate(datasets.rankingTestSet)
    log.Logger().Info("model trained",
        zap.Float32("Precision@10", scores.Precision),
        zap.Float32("Recall@10", scores.Recall),
        zap.Float32("NDCG@10", scores.NDCG))
    
    // 6️⃣ 保存模型
    modelBytes := cfModel.Marshal()
    m.blobStore.Put("models/cf_model.bin", modelBytes)
    
    // 7️⃣ 保存元数据
    m.collaborativeFilteringMeta = meta.Model[cf.Score]{
        Type:   modelName,
        Params: params,
        Score:  scores,
    }
    m.metaStore.Set(meta.COLLABORATIVE_FILTERING_MODEL, 
        m.collaborativeFilteringMeta.ToJSON())
}

🔧 任务 3:AutoML 超参数优化(可选)

func (m *Master) optimizeCollaborativeFiltering(datasets Datasets) {
    // 使用 TPE (Tree-structured Parzen Estimator) 算法
    
    study, _ := goptuna.CreateStudy("cf-optimize", 
        goptuna.StudyOptionSampler(tpe.NewSampler()))
    
    // 定义优化目标
    objective := func(trial goptuna.Trial) (float64, error) {
        // 1️⃣ 采样超参数
        nFactors := trial.SuggestInt("n_factors", 10, 100)
        lr := trial.SuggestFloat("lr", 0.001, 0.1)
        reg := trial.SuggestFloat("reg", 0.001, 0.1)
        
        // 2️⃣ 使用这组参数训练模型
        params := model.Params{
            model.NFactors: nFactors,
            model.Lr:       lr,
            model.Reg:      reg,
        }
        cfModel := cf.NewBPR(params)
        cfModel.Fit(datasets.rankingTrainSet, datasets.rankingTestSet, ...)
        
        // 3️⃣ 返回评估指标(越大越好)
        scores := cfModel.Evaluate(datasets.rankingTestSet)
        return float64(scores.NDCG), nil
    }
    
    // 4️⃣ 运行优化(默认 10 次试验)
    for i := 0; i < m.Config.Recommend.Collaborative.OptimizeTrials; i++ {
        study.Optimize(objective, 1)
        
        log.Logger().Info("optimization progress",
            zap.Int("trial", i+1),
            zap.Float64("best_ndcg", study.GetBestValue()))
    }
    
    // 5️⃣ 使用最佳参数重新训练
    bestParams := study.GetBestParams()
    log.Logger().Info("found best params", zap.Any("params", bestParams))
}

AutoML 搜索空间

超参数范围:
- n_factors:  [10, 20, 50, 100]        # 隐向量维度
- lr:         [0.001, 0.01, 0.05, 0.1] # 学习率
- reg:        [0.001, 0.01, 0.1]       # 正则化
- n_epochs:   [50, 100, 200]           # 训练轮数

目标:最大化 NDCG@10
方法:TPE (贝叶斯优化)
试验次数:10 次(默认)

📡 gRPC 服务:配置分发

Server/Worker 请求配置

// rpc.go
func (m *Master) GetMeta(ctx context.Context, nodeInfo *protocol.NodeInfo) (*protocol.Meta, error) {
    // 1️⃣ 记录节点信息
    log.Logger().Info("node connected",
        zap.String("node_type", nodeInfo.NodeType.String()),
        zap.String("uuid", nodeInfo.Uuid),
        zap.String("version", nodeInfo.BinaryVersion))
    
    // 2️⃣ 序列化配置为 JSON
    configJson, _ := json.Marshal(m.Config)
    
    // 3️⃣ 返回元数据
    return &protocol.Meta{
        Config:             string(configJson),
        ConfigVersion:      m.Config.Version,
        RankingModelName:   m.collaborativeFilteringMeta.Type,
        RankingModelVersion: m.collaborativeFilteringMeta.Version,
        // ... 其他元数据
    }, nil
}

Worker 请求模型

func (m *Master) GetRankingModel(ctx context.Context, req *protocol.VersionInfo) (*protocol.Model, error) {
    // 1️⃣ 读取模型元数据
    m.collaborativeFilteringModelMutex.RLock()
    meta := m.collaborativeFilteringMeta
    m.collaborativeFilteringModelMutex.RUnlock()
    
    // 2️⃣ 从 Blob Store 读取模型文件
    modelBytes, _ := m.blobStore.Get("models/cf_model.bin")
    
    // 3️⃣ 返回模型
    return &protocol.Model{
        Name:    meta.Type,
        Version: meta.Version,
        Data:    modelBytes,
    }, nil
}

🎨 Master 的数据流

┌────────────────────────────────────────────┐
│              Master 数据流                  │
└───────┬────────────────────────────────────┘
        │
   输入源 ↓
┌───────────────┐
│ MySQL         │
│ - users       │
│ - items       │
│ - feedbacks   │
└───────┬───────┘
        │ 加载
        ▼
┌──────────────────┐
│ Dataset          │
│ - 10K 用户       │
│ - 50K 物品       │
│ - 500K 反馈      │
└────────┬─────────┘
         │ 训练
         ▼
┌──────────────────┐
│ CF Model (BPR)   │
│ UserFactor: 10K×50│
│ ItemFactor: 50K×50│
└────────┬─────────┘
         │ 保存
         ├──→ Blob Store (模型文件)
         └──→ Meta Store (元数据)
                 │
                 │ 分发
                 ▼
        ┌────────────────┐
        │ Worker         │
        │ - 下载模型     │
        │ - 计算推荐     │
        │ - 写入 Redis   │
        └────────────────┘

⚙️ Master 的配置管理

配置热更新

// Master 监控配置文件变化
func (m *Master) watchConfig() {
    watcher, _ := fsnotify.NewWatcher()
    watcher.Add("config.toml")
    
    for {
        select {
        case event := <-watcher.Events:
            if event.Op&fsnotify.Write == fsnotify.Write {
                // 重新加载配置
                newConfig, _ := config.LoadConfig("config.toml")
                m.Config = newConfig
                
                log.Logger().Info("config reloaded")
                
                // 触发重新训练
                m.triggerChan.Signal()
            }
        }
    }
}

📊 Master 监控指标

Prometheus 指标

// metrics.go

// 用户和物品统计
UsersTotal = prometheus.NewGauge(...)
ItemsTotal = prometheus.NewGauge(...)
ImplicitFeedbacksTotal = prometheus.NewGauge(...)

// 模型性能
ModelPrecision = prometheus.NewGauge(...)
ModelRecall = prometheus.NewGauge(...)
ModelNDCG = prometheus.NewGauge(...)

// 训练时长
TrainingDuration = prometheus.NewHistogram(...)

// 内存使用
MemoryInUse = prometheus.NewGauge(...)

Dashboard 访问

# HTTP 接口
GET http://master:8088/api/dashboard/stats
# 返回:
{
  "num_users": 10000,
  "num_items": 50000,
  "num_feedbacks": 500000,
  "model_precision": 0.23,
  "model_recall": 0.45,
  "model_ndcg": 0.67
}

# Prometheus 指标
GET http://master:8088/metrics

🎯 Master vs Server 对比

特性 Master Server
作用 大脑(调度训练) 手(提供 API)
状态 有状态(存储模型) 无状态(只读缓存)
实例数 1 个(单例) N 个(可扩展)
端口 8086 (gRPC) + 8088 (HTTP) 8087 (HTTP)
主要工作 训练模型、分配任务 读取缓存、返回推荐
数据库 读写 MySQL/Redis 只读 Redis
CPU 使用 高(训练密集) 低(只读缓存)
内存使用 高(加载数据集) 低(无需存储)
故障影响 无法训练,但推荐继续 部分实例失败,其他继续

💡 核心要点总结

Master 的本质

Master = 任务调度器 + 模型训练器 + 配置管理器

核心工作:
1. 定期从 MySQL 加载数据
2. 训练协同过滤模型
3. 分配推荐计算任务给 Worker
4. 通过 gRPC 分发配置给 Server/Worker
5. 监控系统状态

NOT 做的事:
❌ 不提供推荐 API(Server 做)
❌ 不计算推荐结果(Worker 做)
❌ 不面向终端用户(只面向集群内部)

Master 的调度周期

默认配置:
- 训练周期:60 分钟(FitPeriod)
- AutoML 周期:360 分钟(OptimizePeriod)
- 数据检查:1 秒(检查是否有新数据导入)

可以通过以下方式触发训练:
1. 定时器到期(每小时)
2. 数据导入(API 插入大量反馈后)
3. 手动触发(Dashboard 点击按钮)
posted @ 2026-01-15 11:36  技术漫游  阅读(24)  评论(0)    收藏  举报