feat: 梳理集群模式运行服务
This commit is contained in:
@@ -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
|
||||||
|
@@ -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.keyPoolProvider.LoadKeysFromDB(); err != nil {
|
if err := a.settingsManager.LoadFromDatabase(); err != nil {
|
||||||
return fmt.Errorf("failed to load keys into key pool: %w", err)
|
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 {
|
||||||
|
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()
|
||||||
|
@@ -118,8 +118,7 @@ func (m *Manager) ReloadConfig() error {
|
|||||||
EnableRequest: parseBoolean(os.Getenv("LOG_ENABLE_REQUEST"), true),
|
EnableRequest: parseBoolean(os.Getenv("LOG_ENABLE_REQUEST"), true),
|
||||||
},
|
},
|
||||||
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()
|
||||||
|
@@ -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 从数据库加载系统配置到内存
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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,15 +20,18 @@ 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
|
||||||
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
|
if configManager.GetLogConfig().Level == "debug" {
|
||||||
logger.Config{
|
newLogger = logger.New(
|
||||||
SlowThreshold: time.Second, // Slow SQL threshold
|
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
|
||||||
LogLevel: logger.Info, // Log level
|
logger.Config{
|
||||||
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
SlowThreshold: time.Second, // Slow SQL threshold
|
||||||
Colorful: true, // Disable color
|
LogLevel: logger.Info, // Log level
|
||||||
},
|
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||||
)
|
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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -77,6 +77,5 @@ 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"`
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user