feat: 梳理集群模式运行服务

This commit is contained in:
tbphp
2025-07-09 09:46:38 +08:00
parent 2126e30f21
commit 234731d826
9 changed files with 68 additions and 82 deletions

View File

@@ -22,7 +22,6 @@ ENABLE_GZIP=true
# 数据库配置 # 数据库配置
DATABASE_DSN=user:password@tcp(localhost:3306)/gpt_load?charset=utf8mb4&parseTime=True&loc=Local DATABASE_DSN=user:password@tcp(localhost:3306)/gpt_load?charset=utf8mb4&parseTime=True&loc=Local
DB_AUTO_MIGRATE=true
# Redis配置 # Redis配置
REDIS_DSN=redis://:password@localhost:6379/1 REDIS_DSN=redis://:password@localhost:6379/1

View File

@@ -77,27 +77,54 @@ func NewApp(params AppParams) *App {
// Start runs the application, it is a non-blocking call. // Start runs the application, it is a non-blocking call.
func (a *App) Start() error { func (a *App) Start() error {
// 1. 启动 Leader Service 并等待选举结果
if err := a.leaderService.Start(); err != nil { if err := a.leaderService.Start(); err != nil {
return fmt.Errorf("leader service failed to start: %w", err) return fmt.Errorf("leader service failed to start: %w", err)
} }
// 2. Leader 节点执行不依赖配置的“写”操作
if a.leaderService.IsLeader() { if a.leaderService.IsLeader() {
if err := a.settingsManager.InitializeSystemSettings(); err != nil { logrus.Info("Leader mode. Performing initial one-time tasks...")
// 2.1. 数据库迁移
if err := a.db.AutoMigrate(
&models.RequestLog{},
&models.APIKey{},
&models.SystemSetting{},
&models.Group{},
); err != nil {
return fmt.Errorf("database auto-migration failed: %w", err)
}
logrus.Info("Database auto-migration completed.")
// 2.2. 初始化系统设置
if err := a.settingsManager.EnsureSettingsInitialized(); err != nil {
return fmt.Errorf("failed to initialize system settings: %w", err) return fmt.Errorf("failed to initialize system settings: %w", err)
} }
logrus.Info("System settings initialized by leader.") logrus.Info("System settings initialized in DB.")
} else { } else {
logrus.Info("This node is not the leader. Skipping leader-only initialization tasks.") logrus.Info("Follower Mode. Skipping initial one-time tasks.")
} }
logrus.Debug("Loading API keys into the key pool...") // 3. 所有节点从数据库加载配置到内存
if err := a.settingsManager.LoadFromDatabase(); err != nil {
return fmt.Errorf("failed to load system settings from database: %w", err)
}
logrus.Info("System settings loaded into memory.")
// 4. Leader 节点执行依赖配置的“写”操作
if a.leaderService.IsLeader() {
// 4.1. 从数据库加载密钥到 Redis
if err := a.keyPoolProvider.LoadKeysFromDB(); err != nil { if err := a.keyPoolProvider.LoadKeysFromDB(); err != nil {
return fmt.Errorf("failed to load keys into key pool: %w", err) return fmt.Errorf("failed to load keys into key pool: %w", err)
} }
logrus.Info("API keys loaded into Redis cache by leader.")
}
// 5. 显示配置并启动所有后台服务
a.settingsManager.DisplayCurrentSettings() a.settingsManager.DisplayCurrentSettings()
a.configManager.DisplayConfig() a.configManager.DisplayConfig()
// Start background services
a.startRequestLogger() a.startRequestLogger()
a.logCleanupService.Start() a.logCleanupService.Start()
a.keyValidationPool.Start() a.keyValidationPool.Start()

View File

@@ -119,7 +119,6 @@ func (m *Manager) ReloadConfig() error {
}, },
Database: types.DatabaseConfig{ Database: types.DatabaseConfig{
DSN: os.Getenv("DATABASE_DSN"), DSN: os.Getenv("DATABASE_DSN"),
AutoMigrate: parseBoolean(os.Getenv("DB_AUTO_MIGRATE"), true),
}, },
RedisDSN: os.Getenv("REDIS_DSN"), RedisDSN: os.Getenv("REDIS_DSN"),
} }
@@ -318,7 +317,6 @@ func (s *SystemSettingsManager) GetInt(key string, defaultValue int) int {
return defaultValue return defaultValue
} }
// SetupLogger configures the logging system based on the provided configuration. // SetupLogger configures the logging system based on the provided configuration.
func SetupLogger(configManager types.ConfigManager) { func SetupLogger(configManager types.ConfigManager) {
logConfig := configManager.GetLogConfig() logConfig := configManager.GetLogConfig()

View File

@@ -125,8 +125,8 @@ func NewSystemSettingsManager() *SystemSettingsManager {
return globalSystemSettings return globalSystemSettings
} }
// InitializeSystemSettings 初始化系统配置到数据库 // EnsureSettingsInitialized 确保数据库中存在所有系统设置的记录。
func (sm *SystemSettingsManager) InitializeSystemSettings() error { func (sm *SystemSettingsManager) EnsureSettingsInitialized() error {
if db.DB == nil { if db.DB == nil {
return fmt.Errorf("database not initialized") return fmt.Errorf("database not initialized")
} }
@@ -168,8 +168,7 @@ func (sm *SystemSettingsManager) InitializeSystemSettings() error {
} }
} }
// 加载配置到内存 return nil
return sm.LoadFromDatabase()
} }
// LoadFromDatabase 从数据库加载系统配置到内存 // LoadFromDatabase 从数据库加载系统配置到内存

View File

@@ -24,6 +24,9 @@ func BuildContainer() (*dig.Container, error) {
if err := container.Provide(config.NewManager); err != nil { if err := container.Provide(config.NewManager); err != nil {
return nil, err return nil, err
} }
if err := container.Provide(services.NewLeaderService); err != nil {
return nil, err
}
if err := container.Provide(db.NewDB); err != nil { if err := container.Provide(db.NewDB); err != nil {
return nil, err return nil, err
} }
@@ -56,9 +59,6 @@ func BuildContainer() (*dig.Container, error) {
if err := container.Provide(services.NewLogCleanupService); err != nil { if err := container.Provide(services.NewLogCleanupService); err != nil {
return nil, err return nil, err
} }
if err := container.Provide(services.NewLeaderService); err != nil {
return nil, err
}
if err := container.Provide(keypool.NewProvider); err != nil { if err := container.Provide(keypool.NewProvider); err != nil {
return nil, err return nil, err
} }

View File

@@ -2,7 +2,6 @@ package db
import ( import (
"fmt" "fmt"
"gpt-load/internal/models"
"gpt-load/internal/types" "gpt-load/internal/types"
"log" "log"
"os" "os"
@@ -21,7 +20,9 @@ func NewDB(configManager types.ConfigManager) (*gorm.DB, error) {
return nil, fmt.Errorf("DATABASE_DSN is not configured") return nil, fmt.Errorf("DATABASE_DSN is not configured")
} }
newLogger := logger.New( var newLogger logger.Interface
if configManager.GetLogConfig().Level == "debug" {
newLogger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{ logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold SlowThreshold: time.Second, // Slow SQL threshold
@@ -30,6 +31,7 @@ func NewDB(configManager types.ConfigManager) (*gorm.DB, error) {
Colorful: true, // Disable color Colorful: true, // Disable color
}, },
) )
}
var err error var err error
DB, err = gorm.Open(mysql.Open(dbConfig.DSN), &gorm.Config{ DB, err = gorm.Open(mysql.Open(dbConfig.DSN), &gorm.Config{
@@ -49,18 +51,6 @@ func NewDB(configManager types.ConfigManager) (*gorm.DB, error) {
sqlDB.SetMaxOpenConns(100) sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour) sqlDB.SetConnMaxLifetime(time.Hour)
if dbConfig.AutoMigrate { fmt.Println("Database connection initialized.")
err = DB.AutoMigrate(
&models.SystemSetting{},
&models.Group{},
&models.APIKey{},
&models.RequestLog{},
)
if err != nil {
return nil, fmt.Errorf("failed to auto-migrate database: %w", err)
}
}
fmt.Println("Database connection initialized and models migrated.")
return DB, nil return DB, nil
} }

View File

@@ -14,11 +14,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const (
keypoolInitializedKey = "keypool:initialized"
keypoolLoadingKey = "keypool:loading"
)
type KeyProvider struct { type KeyProvider struct {
db *gorm.DB db *gorm.DB
store store.Store store store.Store
@@ -192,36 +187,14 @@ func (p *KeyProvider) handleFailure(keyID uint, keyHashKey, activeKeysListKey st
// LoadKeysFromDB 从数据库加载所有分组和密钥,并填充到 Store 中。 // LoadKeysFromDB 从数据库加载所有分组和密钥,并填充到 Store 中。
func (p *KeyProvider) LoadKeysFromDB() error { func (p *KeyProvider) LoadKeysFromDB() error {
// 1. 检查是否已初始化
initialized, err := p.store.Exists(keypoolInitializedKey)
if err != nil {
return fmt.Errorf("failed to check for keypool initialization flag: %w", err)
}
if initialized {
logrus.Info("Key pool already initialized, skipping database load.")
return nil
}
// 2. 设置加载锁,防止集群中多个节点同时加载 // 1. 分批从数据库加载并使用 Pipeline 写入 Redis
lockAcquired, err := p.store.SetNX(keypoolLoadingKey, []byte("1"), 10*time.Minute)
if err != nil {
return fmt.Errorf("failed to acquire loading lock: %w", err)
}
if !lockAcquired {
logrus.Info("Another instance is already loading the key pool. Skipping.")
return nil
}
defer p.store.Delete(keypoolLoadingKey)
logrus.Info("Acquired loading lock. Starting first-time initialization of key pool...")
// 3. 分批从数据库加载并使用 Pipeline 写入 Redis
allActiveKeyIDs := make(map[uint][]any) allActiveKeyIDs := make(map[uint][]any)
batchSize := 1000 batchSize := 1000
var batchKeys []*models.APIKey var batchKeys []*models.APIKey
err = p.db.Model(&models.APIKey{}).FindInBatches(&batchKeys, batchSize, func(tx *gorm.DB, batch int) error { err := p.db.Model(&models.APIKey{}).FindInBatches(&batchKeys, batchSize, func(tx *gorm.DB, batch int) error {
logrus.Infof("Processing batch %d with %d keys...", batch, len(batchKeys)) logrus.Debugf("Processing batch %d with %d keys...", batch, len(batchKeys))
var pipeline store.Pipeliner var pipeline store.Pipeliner
if redisStore, ok := p.store.(store.RedisPipeliner); ok { if redisStore, ok := p.store.(store.RedisPipeliner); ok {
@@ -257,24 +230,18 @@ func (p *KeyProvider) LoadKeysFromDB() error {
return fmt.Errorf("failed during batch processing of keys: %w", err) return fmt.Errorf("failed during batch processing of keys: %w", err)
} }
// 4. 更新所有分组的 active_keys 列表 // 2. 更新所有分组的 active_keys 列表
logrus.Info("Updating active key lists for all groups...") logrus.Info("Updating active key lists for all groups...")
for groupID, activeIDs := range allActiveKeyIDs { for groupID, activeIDs := range allActiveKeyIDs {
if len(activeIDs) > 0 { if len(activeIDs) > 0 {
activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID) activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID)
p.store.Delete(activeKeysListKey) // Clean slate p.store.Delete(activeKeysListKey)
if err := p.store.LPush(activeKeysListKey, activeIDs...); err != nil { if err := p.store.LPush(activeKeysListKey, activeIDs...); err != nil {
logrus.WithFields(logrus.Fields{"groupID": groupID, "error": err}).Error("Failed to LPush active keys for group") logrus.WithFields(logrus.Fields{"groupID": groupID, "error": err}).Error("Failed to LPush active keys for group")
} }
} }
} }
// 5. 设置最终的初始化成功标志
logrus.Info("Key pool loaded successfully. Setting initialization flag.")
if err := p.store.Set(keypoolInitializedKey, []byte("1"), 0); err != nil {
logrus.WithError(err).Error("Critical: Failed to set final initialization flag. Next startup might re-run initialization.")
}
return nil return nil
} }

View File

@@ -13,14 +13,16 @@ import (
type LogCleanupService struct { type LogCleanupService struct {
db *gorm.DB db *gorm.DB
settingsManager *config.SystemSettingsManager settingsManager *config.SystemSettingsManager
leaderService *LeaderService
stopCh chan struct{} stopCh chan struct{}
} }
// NewLogCleanupService 创建新的日志清理服务 // NewLogCleanupService 创建新的日志清理服务
func NewLogCleanupService(db *gorm.DB, settingsManager *config.SystemSettingsManager) *LogCleanupService { func NewLogCleanupService(db *gorm.DB, settingsManager *config.SystemSettingsManager, leaderService *LeaderService) *LogCleanupService {
return &LogCleanupService{ return &LogCleanupService{
db: db, db: db,
settingsManager: settingsManager, settingsManager: settingsManager,
leaderService: leaderService,
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
} }
} }
@@ -58,6 +60,11 @@ func (s *LogCleanupService) run() {
// cleanupExpiredLogs 清理过期的请求日志 // cleanupExpiredLogs 清理过期的请求日志
func (s *LogCleanupService) cleanupExpiredLogs() { func (s *LogCleanupService) cleanupExpiredLogs() {
if !s.leaderService.IsLeader() {
logrus.Debug("Not the leader, skipping log cleanup.")
return
}
// 获取日志保留天数配置 // 获取日志保留天数配置
settings := s.settingsManager.GetSettings() settings := s.settingsManager.GetSettings()
retentionDays := settings.RequestLogRetentionDays retentionDays := settings.RequestLogRetentionDays

View File

@@ -78,5 +78,4 @@ type LogConfig struct {
// DatabaseConfig represents database configuration // DatabaseConfig represents database configuration
type DatabaseConfig struct { type DatabaseConfig struct {
DSN string `json:"dsn"` DSN string `json:"dsn"`
AutoMigrate bool `json:"autoMigrate"`
} }