diff --git a/cmd/s3-balance/main.go b/cmd/s3-balance/main.go index a303942..709c480 100644 --- a/cmd/s3-balance/main.go +++ b/cmd/s3-balance/main.go @@ -29,11 +29,15 @@ func main() { flag.StringVar(&configFile, "config", "config/config.yaml", "Path to configuration file") flag.Parse() - // 加载配置 - cfg, err := config.Load(configFile) + // 创建配置管理器 + configManager, err := config.NewManager(configFile) if err != nil { - log.Fatalf("Failed to load configuration: %v", err) + log.Fatalf("Failed to create config manager: %v", err) } + defer configManager.Close() + + // 获取初始配置 + cfg := configManager.GetConfig() // 初始化数据库 if err := database.Initialize(&cfg.Database); err != nil { @@ -88,6 +92,23 @@ func main() { cfg.S3API.ProxyMode, ) + // 注册配置热更新回调 + configManager.OnConfigChange(func(newConfig *config.Config) { + log.Println("Configuration changed, updating components...") + + // 更新bucket manager配置 + if err := bucketManager.UpdateConfig(newConfig); err != nil { + log.Printf("Failed to update bucket manager config: %v", err) + } + + // 更新负载均衡器配置 + if err := lb.UpdateStrategy(newConfig.Balancer.Strategy); err != nil { + log.Printf("Failed to update load balancer strategy: %v", err) + } + + log.Println("Components updated successfully") + }) + // 设置路由 router := mux.NewRouter() diff --git a/go.mod b/go.mod index 5979f00..585c66b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.31.1 github.com/aws/aws-sdk-go-v2/credentials v1.18.5 github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0 + github.com/fsnotify/fsnotify v1.9.0 github.com/gorilla/mux v1.8.1 github.com/prometheus/client_golang v1.23.2 gopkg.in/yaml.v3 v3.0.1 @@ -14,6 +15,7 @@ require ( gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.1 + modernc.org/sqlite v1.39.0 ) require ( @@ -34,7 +36,9 @@ require ( github.com/aws/smithy-go v1.22.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect @@ -42,16 +46,23 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect google.golang.org/protobuf v1.36.8 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 3d94c36..5e27317 100644 --- a/go.sum +++ b/go.sum @@ -44,10 +44,18 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -70,10 +78,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -84,6 +96,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -97,12 +111,19 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -119,3 +140,29 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= +modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/balancer/balancer.go b/internal/balancer/balancer.go index dc17a4f..bf4a4b6 100644 --- a/internal/balancer/balancer.go +++ b/internal/balancer/balancer.go @@ -87,7 +87,7 @@ func (b *Balancer) GetStrategy() string { // 允许在运行时更改负载均衡策略 func (b *Balancer) SetStrategy(strategyName string) error { var strategy Strategy - + switch strategyName { case "round-robin": strategy = NewRoundRobinStrategy() @@ -100,11 +100,16 @@ func (b *Balancer) SetStrategy(strategyName string) error { default: return fmt.Errorf("unknown balancer strategy: %s", strategyName) } - + b.strategy = strategy return nil } +// UpdateStrategy 更新负载均衡策略(热更新用) +func (b *Balancer) UpdateStrategy(strategyName string) error { + return b.SetStrategy(strategyName) +} + // SetMetrics 设置指标服务 func (b *Balancer) SetMetrics(metrics *metrics.Metrics) { b.metrics = metrics diff --git a/internal/bucket/manager.go b/internal/bucket/manager.go index ec61732..43a6eb8 100644 --- a/internal/bucket/manager.go +++ b/internal/bucket/manager.go @@ -3,6 +3,7 @@ package bucket import ( "context" "fmt" + "log" "sync" "time" @@ -280,7 +281,7 @@ func (m *Manager) GetVirtualBuckets() []*BucketInfo { func (m *Manager) GetRealBuckets() []*BucketInfo { m.mu.RLock() defer m.mu.RUnlock() - + var real []*BucketInfo for _, b := range m.buckets { if !b.IsVirtual() { @@ -289,3 +290,101 @@ func (m *Manager) GetRealBuckets() []*BucketInfo { } return real } + +// UpdateConfig 更新配置(支持热更新) +func (m *Manager) UpdateConfig(newConfig *config.Config) error { + m.mu.Lock() + defer m.mu.Unlock() + + log.Println("Updating bucket manager configuration...") + + // 更新配置引用 + oldConfig := m.config + m.config = newConfig + + // 检查是否需要重新创建存储桶 + needsRestart := m.checkIfRestartNeeded(oldConfig, newConfig) + if needsRestart { + log.Println("Bucket configuration changed significantly, recreating buckets...") + + // 停止现有的监控 + if m.healthMonitor != nil { + m.healthMonitor.Stop() + } + if m.statsMonitor != nil { + m.statsMonitor.Stop() + } + + // 重新创建bucket映射 + m.buckets = make(map[string]*BucketInfo) + + // 初始化所有存储桶客户端 + for _, bucketCfg := range newConfig.Buckets { + if !bucketCfg.Enabled { + continue + } + + client, err := createS3Client(bucketCfg) + if err != nil { + // 如果创建失败,恢复旧配置 + m.config = oldConfig + return fmt.Errorf("failed to create S3 client for bucket %s: %v", bucketCfg.Name, err) + } + + info := &BucketInfo{ + Config: bucketCfg, + Client: client, + Available: true, + LastChecked: time.Now(), + } + + m.buckets[bucketCfg.Name] = info + } + + // 重新初始化监控 + m.initHealthMonitoring() + } else { + // 只更新监控间隔(需要重启监控来改变间隔) + log.Println("Updating monitoring intervals...") + if m.healthMonitor != nil { + m.healthMonitor.Stop() + } + if m.statsMonitor != nil { + m.statsMonitor.Stop() + } + // 重新初始化监控以应用新的间隔 + m.initHealthMonitoring() + } + + log.Println("Bucket manager configuration updated successfully") + return nil +} + +// checkIfRestartNeeded 检查是否需要重启bucket manager +func (m *Manager) checkIfRestartNeeded(oldConfig, newConfig *config.Config) bool { + // 检查bucket数量变化 + if len(oldConfig.Buckets) != len(newConfig.Buckets) { + return true + } + + // 检查bucket配置变化 + for i, oldBucket := range oldConfig.Buckets { + if i >= len(newConfig.Buckets) { + return true + } + newBucket := newConfig.Buckets[i] + + // 检查关键配置项 + if oldBucket.Name != newBucket.Name || + oldBucket.Endpoint != newBucket.Endpoint || + oldBucket.AccessKeyID != newBucket.AccessKeyID || + oldBucket.SecretAccessKey != newBucket.SecretAccessKey || + oldBucket.Region != newBucket.Region || + oldBucket.Enabled != newBucket.Enabled || + oldBucket.Virtual != newBucket.Virtual { + return true + } + } + + return false +} diff --git a/internal/config/manager.go b/internal/config/manager.go new file mode 100644 index 0000000..3c46547 --- /dev/null +++ b/internal/config/manager.go @@ -0,0 +1,246 @@ +package config + +import ( + "log" + "os" + "sync" + "time" + + "github.com/fsnotify/fsnotify" +) + +// Manager 配置管理器,支持热更新 +type Manager struct { + configFile string + config *Config + mutex sync.RWMutex + watcher *fsnotify.Watcher + callbacks []func(*Config) + stopChan chan struct{} + lastModTime time.Time + pollingTicker *time.Ticker +} + +// NewManager 创建新的配置管理器 +func NewManager(configFile string) (*Manager, error) { + // 初始加载配置 + cfg, err := Load(configFile) + if err != nil { + return nil, err + } + + // 获取文件的初始修改时间 + fileInfo, err := os.Stat(configFile) + if err != nil { + return nil, err + } + + manager := &Manager{ + configFile: configFile, + config: cfg, + callbacks: make([]func(*Config), 0), + stopChan: make(chan struct{}), + lastModTime: fileInfo.ModTime(), + } + + // 同时启用fsnotify和轮询监听 + // 这样可以确保在Docker挂载等场景下也能正常工作 + manager.initWatching() + + return manager, nil +} + +// initWatching 初始化文件监听(同时使用fsnotify和轮询) +func (m *Manager) initWatching() { + // 尝试启用fsnotify + watcher, err := fsnotify.NewWatcher() + if err == nil { + if err := watcher.Add(m.configFile); err == nil { + m.watcher = watcher + log.Println("fsnotify watcher enabled for config file") + go m.watchConfig() + } else { + log.Printf("Failed to add file to fsnotify watcher: %v", err) + watcher.Close() + } + } else { + log.Printf("Failed to create fsnotify watcher: %v", err) + } + + // 同时启用轮询模式(作为备用和补充) + // 在Docker挂载等场景下,轮询更可靠 + m.pollingTicker = time.NewTicker(3 * time.Second) + log.Println("Config file polling enabled (3s interval)") + go m.pollConfig() +} + +// pollConfig 轮询检查配置文件变化 +func (m *Manager) pollConfig() { + for { + select { + case <-m.pollingTicker.C: + fileInfo, err := os.Stat(m.configFile) + if err != nil { + log.Printf("Failed to stat config file during polling: %v", err) + continue + } + + // 检查文件修改时间 + if fileInfo.ModTime().After(m.lastModTime) { + log.Printf("Config file %s modified (detected by polling), reloading...", m.configFile) + m.lastModTime = fileInfo.ModTime() + m.reloadConfig() + } + + case <-m.stopChan: + return + } + } +} + +// GetConfig 获取当前配置(线程安全) +func (m *Manager) GetConfig() *Config { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // 返回配置的副本以避免并发修改 + configCopy := *m.config + return &configCopy +} + +// OnConfigChange 注册配置变化回调 +func (m *Manager) OnConfigChange(callback func(*Config)) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.callbacks = append(m.callbacks, callback) +} + +// watchConfig 监听配置文件变化(fsnotify模式) +func (m *Manager) watchConfig() { + for { + select { + case event, ok := <-m.watcher.Events: + if !ok { + return + } + + // 只处理修改和重命名事件 + if event.Op&fsnotify.Write == fsnotify.Write || + event.Op&fsnotify.Rename == fsnotify.Rename { + log.Printf("Config file %s modified (detected by fsnotify), reloading...", m.configFile) + + // 更新最后修改时间以避免轮询重复触发 + if fileInfo, err := os.Stat(m.configFile); err == nil { + m.lastModTime = fileInfo.ModTime() + } + + m.reloadConfig() + } + + case err, ok := <-m.watcher.Errors: + if !ok { + return + } + log.Printf("Config watcher error: %v", err) + + case <-m.stopChan: + return + } + } +} + +// reloadConfig 重新加载配置 +func (m *Manager) reloadConfig() { + // 添加延迟以防止编辑器的多次写入事件 + time.Sleep(100 * time.Millisecond) + + // 加载新配置 + newConfig, err := Load(m.configFile) + if err != nil { + log.Printf("Failed to reload config: %v", err) + return + } + + // 更新配置 + m.mutex.Lock() + oldConfig := m.config + m.config = newConfig + callbacks := make([]func(*Config), len(m.callbacks)) + copy(callbacks, m.callbacks) + m.mutex.Unlock() + + log.Printf("Configuration reloaded successfully") + + // 异步调用回调函数 + go func() { + for _, callback := range callbacks { + func() { + defer func() { + if r := recover(); r != nil { + log.Printf("Config change callback panic: %v", r) + } + }() + callback(newConfig) + }() + } + }() + + // 记录重要配置变更 + m.logConfigChanges(oldConfig, newConfig) +} + +// logConfigChanges 记录配置变更 +func (m *Manager) logConfigChanges(oldConfig, newConfig *Config) { + // 检查服务器端口变化 + if oldConfig.Server.Port != newConfig.Server.Port { + log.Printf("Server port changed: %d -> %d (restart required)", + oldConfig.Server.Port, newConfig.Server.Port) + } + + // 检查数据库配置变化 + if oldConfig.Database.DSN != newConfig.Database.DSN { + log.Printf("Database DSN changed (restart required)") + } + + // 检查存储桶数量变化 + if len(oldConfig.Buckets) != len(newConfig.Buckets) { + log.Printf("Bucket count changed: %d -> %d", + len(oldConfig.Buckets), len(newConfig.Buckets)) + } + + // 检查负载均衡策略变化 + if oldConfig.Balancer.Strategy != newConfig.Balancer.Strategy { + log.Printf("Load balancer strategy changed: %s -> %s", + oldConfig.Balancer.Strategy, newConfig.Balancer.Strategy) + } + + // 检查代理模式变化 + if oldConfig.S3API.ProxyMode != newConfig.S3API.ProxyMode { + log.Printf("S3 API proxy mode changed: %t -> %t", + oldConfig.S3API.ProxyMode, newConfig.S3API.ProxyMode) + } + + // 检查指标配置变化 + if oldConfig.Metrics.Enabled != newConfig.Metrics.Enabled { + log.Printf("Metrics enabled changed: %t -> %t", + oldConfig.Metrics.Enabled, newConfig.Metrics.Enabled) + } +} + +// Close 关闭配置管理器 +func (m *Manager) Close() error { + // 停止监听协程 + close(m.stopChan) + + // 停止轮询 + if m.pollingTicker != nil { + m.pollingTicker.Stop() + } + + // 关闭fsnotify watcher + if m.watcher != nil { + return m.watcher.Close() + } + + return nil +} \ No newline at end of file