改个配置要重启服务,灰度新功能要发版,都慢。配置热加载让改配置即时生效,特性开关让新功能按用户比例放开,回滚只是改个值。这一篇两件事都做。
1. 为什么热加载
日志级别、限流阈值、下游地址,这些运行时常调。每次改都重启,连接会闪断、正在处理的请求丢失(第 8 篇讲的优雅停机也救不了频繁重启)。
2. viper 监听变更
第 8 篇用 viper 读了配置,加 WatchConfig 就能在文件变化时回调:
v.WatchConfig() v.OnConfigChange(func(e fsnotify.Event) { v.Unmarshal(&cfg) log.Printf("配置已热加载: %s", e.Name) })
cfg 是全局指针,回调里重新 Unmarshal 覆盖。注意并发:读 cfg 的地方要用 atomic 或 rwlock,别让热加载和请求抢同一个字段。
3. 热加载要原子的
配置结构放 atomic.Value,回调整体替换,读方无锁:
var cfgAtomic atomic.Value func SetCfg(c *Config) { cfgAtomic.Store(c) } func GetCfg() *Config { return cfgAtomic.Load().(*Config) }
读配置 GetCfg() 永远拿到完整一份,不会出现改到一半读到半新半旧。
4. 特性开关控制新功能
新功能不敢全量放,先给内部用户开。开关放配置或独立存储:
func FeatureOn(name, uid string) bool { f, ok := GetCfg().Features[name] if !ok || !f.Enabled { return false } if f.Allowlist[uid] { return true } h := fnvHash(uid) % 100 return h < f.RolloutPct }
5. 中间件按开关分流
新接口旧逻辑并存,靠开关决定走哪条:
func FeatureGate(name string) gin.HandlerFunc { return func(c *gin.Context) { uid := c.GetString("uid") if !FeatureOn(name, uid) { c.JSON(404, gin.H{"error": "功能未开放"}) c.Abort() return } c.Next() } }
开关关了返回 404,对未放行用户无感;出了问题把 RolloutPct 调 0 立即全关,不用发版。
6. 开关放哪
简单场景放 viper 配置(跟着热加载走)。多服务共享、要运营后台实时改,放 Redis 或专用 flag 服务(如 Unleash),别散落在各服务配置文件里。
7. 上线清单
热加载的配置结构用 atomic 或锁保护,避免读到半更新状态。改配置后打日志,方便追溯。特性开关默认关,灰度从小比例起,回滚改值不发包。开关判断失败走保守路径(关),别因为开关服务挂了就全开。
下一篇讲灰度发布,把流量按规则分到新旧版本。