# 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 点击按钮)

浙公网安备 33010602011771号