Unilearning · 架构

把 6 个微服务收进 1 个二进制之后,我们失去了什么

模块化单体不是「退回去写单体」。它用编译期约束换掉了运行期约束,代价和收益都真实存在。

旧系统是 6 个微服务,靠 HTTP 与 URL 配置互相调用。听起来很标准,但真实成本集中在两处:一是服务间调用的网络开销与失败处理,二是「改一个功能要动三个仓库、部署三个服务」。

把调用变回函数调用

模块化单体的核心不是「不拆」,而是「在进程内拆」。8 个业务域进程内聚合,域与域之间的调用是普通函数调用——没有网络、没有序列化、没有服务发现。

unile-go — 两层依赖图
01// 每个域拆成两个 module:
02// cms/api → 契约,只依赖 base
03// cms/internal → 实现,外部不可 import
04
05// edge 层做跨域编排
06func (h *Handler) SubmitAttempt(c *gin.Context) {
07 node := cms.MustGetNode(id) // 域内调用
08 result := quiz.Grade(node, answers) // 跨域:只经 api
09 cms.Records.Save(node, result) // 再调回 cms
10 c.JSON(200, result)
11}

边界由编译器守,不是靠 lint

Go 的 internal 机制让跨域 import 直接编译失败。这条约束比任何 lint 规则或架构文档都硬——文档会过期,编译器不会。

  • 域间只能 import 别人的 api(契约),不能碰 internal(实现)
  • api 只依赖 base,base 零外部依赖
  • 需要编排多个域的逻辑统一写在 edge,依赖图是一棵两层树
  • 新增域只需补 api 与 internal,边界规则不需要重新讨论

我们失去了什么

代价是真实的,不掩饰:

  • 独立扩缩容能力没了。哪个域压力大,只能整体扩容
  • 故障隔离变弱。一个域 panic 可能影响整个进程(靠 recover 与健康检查兜)
  • 发布粒度变粗。改一个域要重新发布整个二进制
  • 技术栈锁定。不能一个域用 Go、另一个用 Java

为什么这笔交易划算

因为这个系统的真实瓶颈不是单域吞吐,而是迭代速度与运维复杂度。6 个微服务意味着十余个容器、服务发现、配置中心、分布式追踪——这些基础设施的成本,远高于我们在扩缩容上损失的那点弹性。

交付形态对比
01之前:6 个服务 · 十余个容器 · 服务发现 · 配置中心
02现在:1 个二进制 · 1 个容器 · 启动时自动迁移
03
04$ make docker && docker run -p 8080:8080
05[migrate] cms v12 → v14 ✓
06[migrate] quiz v07 → v09 ✓
07[server] listening on :8080

架构决策的本质是比较成本,不是追求某个「更先进」的形态。如果哪天单域吞吐真的成了瓶颈,把那个域拆出去也只是复制一次 api 契约。

More Notes
Get in touch

把身份、Agent 与私域的复杂度,交给一套可治理的内核

无论你是要替换现有的 IAM、搭建 Agent 中台,还是想把企微私域真正运营起来——先从一次 30 分钟的架构沟通开始。我们会先判断问题是不是我们擅长解的那类,不合适会直接说。