你的微服务是不是拆过头了?从代码改动频次、独立部署率和认知边界三个维度自检
很多团队在把单体拆成微服务之后,反而陷入了一种更隐蔽的泥潭——服务数量膨胀到几十甚至上百个,但大部分“微服务”只有一个人在维护,改一个字段要动三个代码仓库。问题不出在微服务架构本身,而是拆分粒度失控。怎么判断你的服务是不是拆过头了?我通常会从三个维度交叉验证:代码改动频次、独立部署率和认知边界。下面逐一拆开说。
维度一:代码改动频次——低频变更的服务是“僵尸服务”
如果一个服务在过去 6 个月内没有任何业务逻辑变更,只有依赖升级和安全补丁,那它大概率不该作为一个独立服务存在。我见过一个支付网关项目,团队把“商户资质审核”单独拆了一个服务,上线后两年内只改过 3 次代码,其中两次是 Spring Boot 版本升级。这种服务就应该合并回上游的商户管理服务里。
量化标准:统计每个服务最近 180 天的 commit 数量(排除依赖升级和配置文件修改)。如果某个服务的业务相关 commit 少于 5 个,且每次改动只涉及单个文件,标记为“低活跃服务”。进一步看,如果这个服务的代码行数小于 200 行,直接启动合并评估。
Git 命令可以这样抽取:
# 统计指定目录下最近180天的业务代码commit数(排除pom.xml/build.gradle等构建文件)
git log --since="2024-10-01" --until="2025-03-30" \
--pretty=format:"%h" --name-only -- 'src/main/java/' \
':!**/pom.xml' ':!**/build.gradle' | grep -c "commit"
这个数字低于 5,就要认真问一句:这个服务真的有独立演进的必要吗?
另一个关键信号是“联动修改率”。如果你改了服务 A 的某个接口,有超过 40% 的情况需要同步修改服务 B 和服务 C,这三个服务大概率应该合并。高耦合的微服务比单体还糟糕——你只是把编译期的耦合变成了运行时的分布式耦合,调试成本反而更高。用 git log 跨仓库对比 commit 时间窗口,如果两个服务在同一个 2 小时窗口内频繁成对出现,耦合度已经高到不适合拆分了。
维度二:独立部署率——不能独立部署的服务不是微服务
微服务的核心价值在于独立部署。如果一个服务每次上线都必须跟其他服务“搭车”,那拆分就没有意义。我 2023 年参与过一个电商项目的架构审计,40 多个微服务里,有 12 个服务的生产部署记录显示:它们在过去一年里从未单独部署过,每次都是跟着订单服务一起发版。
量化标准:检查 CI/CD 流水线的触发记录,统计每个服务过去 3 个月的部署次数,并计算“独立部署占比”——即该服务单独触发部署(不与其他服务在同一发布窗口)的比例。如果这个比例低于 30%,说明这个服务实际上没有独立发布能力。
具体做法是查部署系统的历史记录。以 Jenkins 为例,拉取每个 job 的 build history,对比时间戳。如果服务 A 和服务 B 的部署时间总是落在同一个 10 分钟窗口内,且超过 70% 的部署都是如此,那这两个服务的“独立部署率”得分基本为零。
还有一个更直接的检查点:数据库是否共享。如果一个“微服务”跟另一个服务共用同一个数据库 schema,甚至同一个表,这就不是微服务,只是把代码分成了两个 jar 包。真正独立的服务应该拥有自己的数据存储,通过 API 而不是共享表来交换数据。我见过太多项目把“拆服务”理解成“拆代码”,数据层还是那个单体数据库,结果服务 A 改了表结构,服务 B 直接报错——拆了比不拆还痛苦。
维度三:认知边界——一个人能完整理解的服务才是合理的粒度
微服务的大小不应该用代码行数来衡量,而应该用“认知负荷”。一个好的微服务,应该能让一个开发者在半天内完整理解它的业务逻辑、数据模型和外部依赖。如果一个服务需要翻阅大量文档、跨多个团队问人才能搞明白,那它要么太大,要么边界切错了。
量化标准:做一次“新人测试”。找一个入职不到 3 个月的开发者,让他独立阅读服务代码和文档,然后在 4 小时内画出该服务的业务流程图和数据流转图。如果画不出来,或者画出来的跟实际差距很大,说明这个服务的认知负荷超出了单人能掌控的范围。
反过来,如果一个开发者同时“拥有”6 个以上的微服务,且能轻松应对所有 on-call,那这些服务的粒度很可能过细了。2024 年 ThoughtWorks 的技术雷达里提到“微服务税”(microservice tax)的概念——每多拆一个服务,就多一份基础设施成本、监控成本、协调成本。当一个开发者同时维护 8 个代码量都不到 500 行的服务时,他的时间大部分花在了服务间的通信调试和 CI 配置上,而不是业务逻辑本身。
我自己的经验法则是:一个服务应该对应一个明确的业务能力(business capability),而不是一个技术功能。比如“用户认证”是一个业务能力,可以独立为一个服务;“生成 JWT Token”只是一个技术功能,不应该单独拆出来。如果你的服务命名里大量出现类似 *-util、*-common、*-helper 这种技术导向的名字,说明你是按技术分层而不是按业务能力拆分的,这种拆分方式几乎必然导致粒度过细。
三个维度交叉验证
单看一个维度可能会误判,三个维度结合起来看就比较准了。我的实操方法是给每个服务打三个维度的红灯/绿灯:
- 代码改动频次:近 6 个月业务 commit < 5 → 红灯
- 独立部署率:近 3 个月独立部署占比 < 30% → 红灯
- 认知边界:单个开发者同时维护的服务数 > 6 → 红灯
如果一个服务三个维度全红,别犹豫,合并回去。如果两个维度过红,启动合并评估。如果只有一个维度红灯,可以再观察一个季度。
合并的时候也有讲究。不要直接把两个服务的代码 copy-paste 到一起,而是选择那个调用量更大、演进更快的服务作为“主服务”,把另一个服务的业务逻辑迁移进去,旧服务保留 2-3 个月的转发层,确认没有遗漏的调用方后再下线。这个过程本身也是重新审视业务边界的好机会。
常见问题
什么样的服务即使代码改动很少,也应该保持独立?
核心判断标准是“变更风险的隔离需求”。如果一个服务的 bug 会导致核心业务中断(比如支付扣款),即使它改动频率很低,也应该独立部署,因为独立意味着更严格的变更管控、更窄的爆炸半径。类似的还有合规强隔离的服务——金融行业的风控模型、医疗行业的患者数据处理,这类服务即使一年不改一次,拆分也是合理的,目的是隔离法律责任和审计范围,而不是为了开发效率。
我们已经拆出 50 多个服务了,怎么低成本地评估哪些该合并?
不用一开始就全量分析。先拉出过去一个季度的部署记录,筛选出“从未独立部署过”的服务,这批优先级最高。然后跑一遍代码仓库的 commit 日志,把 6 个月内业务 commit 少于 5 个的标记出来。这两个条件一交叉,通常能筛掉 30%-40% 的候选合并服务。实际动手合并之前,先用静态分析工具扫一下服务间的调用关系,确认没有遗漏的调用方。整个过程一个资深开发花 2-3 天就能完成初步评估。
合并服务之后,之前拆分的 Git 仓库历史怎么处理?
不建议用 git merge 直接合并仓库,会丢失文件路径信息,后续排查问题很痛苦。推荐的做法是用 git filter-repo 把旧仓库的代码按子目录迁移到新仓库,保留完整的 commit 历史。命令大概长这样:
git clone old-service old
cd old
git filter-repo --to-subdirectory-filter services/old-service
cd ../main-service
git remote add old ../old
git fetch old
git merge --allow-unrelated-histories old/main
这样新仓库里 services/old-service/ 路径下的文件会带着完整的提交历史,不会丢失 blame 信息。