在上一篇文章中,我记录了自己彻底抛弃 go-kit、亲手实现 Golang 微服务架构的过程。只是简单的抛弃 go-kit 远远不够。
但真正进入工程实践后,我发现一个比“框架选型”更本质的问题:
如果配置不可治理,微服务架构几乎一定会失控。
于是,这一次重构,我没有从网关、RPC、链路追踪开始,而是反其道而行之:
从配置治理出发,倒推整个项目结构、启动流程与服务演进方式。
EasyMS 的项目结构被全面调整,配置体系完成一次“工程级升级”,也为后续所有能力(服务发现、网关、灰度、扩展)打下了真正可持续的基础。(由于时间因素,外围的文件结构虽然调整好了,但是内部代码很多还存在不合理的地方,准备在后续的改进中一步步迭代优化)
一、为什么我决定:必须“推翻重来”
上一篇文章发布后,我自己在继续写代码的过程中,产生了一个强烈的不适感:
- 配置读取逻辑散落在启动流程中
- Local / Consul 行为不统一
- 热更新难以保证一致性
- 配置变更无法追溯、无法回滚
- 每加一个服务,配置复杂度指数上升
这让我意识到一个事实:
如果配置体系不先稳定,微服务架构只是“代码拆分”,而不是工程体系。
因此,这次我做了一个明确的取舍(修改整体项目的文件结构对于很多朋友来说是可怕的,我从不喜欢堆砌,要敢于推倒重来):
- ❌ 暂停“更多业务能力”
- ✅ 先把配置治理做到工程级别
二、一次“反常规”的架构演进路径
很多微服务实现的路径是:
网关 → RPC → 注册中心 → 配置中心
而在 EasyMS 中,我走的是:
配置治理 → 项目结构 → 服务启动模型 → 微服务扩展能力
原因很简单:
配置是所有服务的“第一输入”。
三、项目结构的全面调整:为配置治理让出“中枢位置”
这次调整不是简单的目录美化,也绝非是简单的代码优化,而是一次职责重构。
重构后的核心思想
- 服务只关心业务
- 基础能力全部下沉
- 配置是 shared 层的核心模块,而不是工具代码
当前关键结构(篇幅原因,详细的可以在源码库中查看)
internal/
├── services/
│ └── auth/
│ └── main.go
│ └── internal/
│ ├── handles
│ ├── middleware
│ ├── service
│ └── storage
└── shared/
├── config ← 本次演进的核心
├── discovery
├── db
├── logger
└── entities
一个重要变化:
config不再是“读取 YAML 的工具”,而是一个具备状态、版本、治理能力的模块。
四、配置治理的核心抽象:我只做了一件事,但影响了整个架构
AppConfigProvider:配置来源的唯一抽象
type AppConfigProvider interface {
// 加载配置
LoadAppConfig() error
// 修改配置的回调
OnChange() func(*entities.AppConfig)
}
这一接口的意义在于:
- 服务启动流程只依赖 配置行为
- 不关心 配置来自哪里
- 本地 / Consul / 未来 Nacos 行为完全一致
这一步,直接解耦了“服务生命周期”和“配置实现”。 扩展性完全交给了使用者。
五、ConfigurationManager:真正的配置“治理中枢”
这是本次升级中最关键、也是最容易被忽略的模块。
//ConfigurationManager 配置管理器,统一管理配置的加载、合并和更新
type ConfigurationManager struct {
appConfig *entities.AppConfig
configLock sync.RWMutex
provider AppConfigProvider
watcher ConfigWatcherInterface
}
它解决的不是“读配置”,而是:
- 配置的生命周期
- 配置的一致性
- 配置的演进与回滚
1️⃣ 原子配置快照
func (cm *ConfigurationManager) UpdateConfig(newConfig *entities.AppConfig)
每次更新:
- 不是修改字段
- 而是整体替换配置快照
- 配合读写锁,避免并发污染
2️⃣ 深度合并,而非覆盖
mergo.Merge(target, source, mergo.WithOverride)
这让配置可以天然支持:
- 全局配置
- 服务配置
- 环境覆盖
- 局部覆盖
而不会出现“某个服务丢字段”的问题。
3️⃣ 配置版本化与回滚(工程级)
SaveConfigVersion(...)
RollbackToVersion(...)
配置版本不是“顺手存一下”,而是:
- 每次保存都是完整快照
- 存储在 Consul
- 带描述、带时间
- 回滚 = 正常更新路径
这意味着:
配置终于具备“变更审计能力”。
六、ConfigWatcher:让“热更新”不再是危险操作
Watcher 的设计目标只有一个:
配置变更 ≠ 服务不稳定
// ConfigWatcher 配置监听器
// 用于监听Consul中配置的变化并触发更新
type ConfigWatcher struct {
client *discovery.Discovery
keyPath string
serverName string
env string
onChange func(*entities.AppConfig)
stopCh chan struct{}
ticker *time.Ticker
mu sync.RWMutex
lastConfigs map[string]string // 存储上次配置值,用于比较变化
configMgr *ConfigurationManager
}
核心机制:
- 记录 Key → Value 快照
- 精确对比变化
- 变化才触发 reload
- reload 永远走完整加载流程
cw.onChange(newConfig)
没有“局部更新”、“临时修补”这种危险路径。
七、本地配置不是“降级方案”,而是同一套模型
LocalConfig 并不是“简单读文件”。
它完整复用了:
- 深度合并
- 校验逻辑
- 基础配置兜底
EnsureBasicConfig(...)
Validate(...)
这意味着:
开发环境与生产环境,配置行为是一致的。
八、配置治理如何真正落地到 auth-svc
auth-svc 是基于oauth2 授权的服务,因为涉及一些个人更新的解决方案在里面,后期会在优化后单独来聊一聊;今天我们只讲配置的治理 在 auth-svc/main.go 中 的应用,你可以看到一个高度标准化的启动流程:
- 初始化 AppConfigStore
- 决定配置来源
- 加载配置
- 启动 Watcher
- 获取配置快照
- 初始化日志 / DB / 服务
- 注册服务
- 启动 HTTP
appConfig := config.GetAppConfig()
...
// 使用Consul配置提供者
provider = config.NewConsulConfig(discoveryClient, serverName, cfgStore.Consul.KeyPath, cfgStore.Env)
err := provider.LoadAppConfig()
if err != nil {
logger.Error(err, "Failed to load app config", serverName, nil)
panic(err)
}
// 动态监听配置文件并更新服务
watch := config.NewConfigWatcher(discoveryClient, cfgStore.Consul.KeyPath, serverName, cfgStore.Env, provider.OnChange())
go watch.Start()
业务代码只关心一件事:
“当前配置是什么”
九、这次升级,真正改变了什么?
不是多了几个文件,而是:
- 微服务第一次具备“配置演进能力”
- 项目结构第一次为“长期维护”设计
- 启动流程第一次可复制、可扩展
- 架构演进有了清晰主线
十、EasyMS 现在处在什么阶段?
它已经不再是:
“一个微服务示例项目”
而是:
一个以配置治理为起点、可以持续生长的 Golang 微服务工程骨架。
项目地址
GitHub
github.com/louis-xie-p…
Gitee
gitee.com/louis_xie/e…
写在最后
如果上一篇解决的是:
“为什么我要抛弃 go-kit”
那么这一篇解决的是:
“抛弃之后,我如何构建一套真正可长期演进的微服务工程体系”
而配置治理,是这条路上绕不开的第一步。