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 做)
为什么这样设计?
✅ 无状态 → 易扩展
✅ 只读取 → 高性能
✅ 配置同步 → 灵活
✅ 缓存优先 → 低延迟

浙公网安备 33010602011771号