title: 单体、SOA、微服务、Serverless:我们 4 次重构,第 3 次才没把团队拖垮
tags: 软件架构, 微服务, Serverless, 单体, 架构演进
description: 从我们团队 4 次架构重构的翻车与收敛出发,拆解单体、SOA、微服务、Serverless 四种模式的真实代价与适用边界,给出按团队规模选型的判断。


我们团队这五年把架构从单体拆到微服务,又从微服务回调一部分回单体,最后把一部分边缘能力放上 Serverless。四次大重构,前两次都付出了惨痛代价。这篇文章不堆定义,只讲每种架构真实的运维账和协作账,以及我为什么现在劝人"别急着拆"。

先说一句可能得罪人的话:大多数公司根本不需要微服务。微服务解决的是"组织沟通成本随代码量超线性增长"的问题,如果你团队不到 10 个人,微服务带来的分布式复杂度,会比它解决的问题多得多。

单体:被低估的"正确的起点"

很多人觉得单体是落后的代名词。不是。一个结构良好的模块化单体,在很长一段时间内都是性价比最高的选择。

// 模块化单体:用 package 边界做逻辑隔离,而不是物理隔离
@SpringBootApplication
public class ShopMonolith {
    public static void main(String[] args) {
        SpringApplication.run(ShopMonolith.class, args);
    }
}
// 通过 module 划分,但仍在同一个进程、同一个数据库
// com.shop.order / com.shop.user / com.shop.inventory
// 好处:本地事务 @Transactional 一把锁全解决,没有网络边界的分布式难题
@Service
public class OrderService {
    @Transactional  // 单体里跨模块调用仍是本地方法,ACID 天然保证
    public void createOrder(Order o) {
        inventoryService.deduct(o); // 同进程调用,零网络开销
        userPointsService.add(o);   // 出错直接回滚,不用写补偿
    }
}

第 13 行 @Transactional 是单体的隐藏优势:跨模块的操作在同一数据库事务里,没有分布式事务、没有最终一致性的补偿逻辑。我们后来拆微服务后,光"下单扣库存"这一个操作,就为了对付分布式事务写了三套补偿和对账定时任务。单体不是不能长大,是把"模块边界"靠纪律守好,别让 order 模块直接 new 了 user 的 DAO。

SOA:被微服务"盖过风头"的中间态

SOA 和微服务的区别常被模糊化。我的理解是:SOA 强调企业服务总线(ESB)做集中式编排,服务粒度较粗;微服务强调去中心化、服务粒度更细、数据自治。ESB 当年的问题是:总线成了单点瓶颈和"上帝组件",改一个路由全公司颤抖。

我们第二版架构就栽在这:引入了一个厚重的 ESB 做服务编排,结果所有跨系统调用都过总线,总线一抖全员抖,而且总线上的 XML 路由配置复杂到没人敢动。所以我不建议新团队上传统 SOA/ESB,它的中心化治理收益,抵不过中心化故障的风险。

微服务:拆得爽,运维哭

微服务的好处是真的:团队边界清晰、独立部署、技术栈可异构。代价也是真的,而且往往被低估。

// 微服务化后,本地方法调用变成跨网络调用,必须处理失败
@FeignClient(name = "inventory-service", fallback = InventoryFallback.class)
public interface InventoryClient {
    @PostMapping("/deduct")
    DeductResult deduct(@RequestBody DeductReq req);
}
// 关键变化:这次调用可能超时、可能丢、可能返回半成品
@Service
public class OrderService {
    public void createOrder(Order o) {
        try {
            DeductResult r = inventoryClient.deduct(...); // 网络调用,不是本地方法
            if (!r.ok()) throw new BizException("扣库存失败");
        } catch (FeignException e) {
            // 1. 必须考虑超时/降级,否则库存服务抖一下,下单全挂
            // 2. 这里还要写"库存扣减失败"的补偿或重试,单体时不需要
        }
    }
}

第 2 行 @FeignClient 背后是 HTTP 调用,第 11 行必须处理网络失败——这就是微服务最贵的那部分:每个原本免费的方法调用,现在都要考虑超时、重试、降级、幂等、分布式追踪。我们第一次微服务化时,8 个人拆出了 30 个服务,结果联调一次要起十几个本地实例,一个接口慢了要翻五个服务的日志才能定位。那次重构把团队效率拖了整整一个季度。

我的判断:微服务的真正触发条件是"康威定律"——当你的组织大到单体重构的沟通成本超过分布式的运维成本时,才该拆。团队 < 20 人,我建议先用模块化单体;20–50 人再考虑按业务域拆到 5–10 个服务;别一上来 30 个。

Serverless:事件驱动的真香,但冷启动是真痛

Serverless(函数计算)适合"事件触发、稀疏流量、短运行"的场景,比如图片处理、定时任务、Webhook 接收。你不用管服务器,按调用次数付费。

// 阿里云 FC / AWS Lambda 的 Java 函数入口,一次调用一个实例
public class ImageResizeHandler implements RequestHandler<S3Event, String> {
    // 1. 注意:handler 之外不要放"有状态"的东西,函数实例会被复用也会冷启
    private static final ObjectMapper MAPPER = new ObjectMapper(); // 静态变量可复用,省冷启动
    @Override
    public String handleRequest(S3Event event, Context context) {
        String key = event.getRecords().get(0).getS3().getObject().getKey();
        // 2. 处理完即销毁,没有常驻进程,所以没有"服务器要我养"的成本
        return resize(key);
    }
}

第 6 行把 ObjectMapper 放静态变量,是我们踩冷启动坑后的优化:Java 的函数冷启动本身就要 300–800ms(JVM 启动 + 类加载),如果每次还 new 一堆重对象,延迟直接破秒。我们曾把一个原本 50ms 的接口搬上函数计算,结果 P99 从 50ms 飙到 800ms,就是因为没处理好冷启动和依赖注入。Serverless + Java 的冷启动是天然短板,除非你用 GraalVM 原生镜像把启动压到几十毫秒,否则对延迟敏感的在线链路别轻易上。

我们的第四次重构:混合架构

最后我们收敛成这样:核心交易链路保持模块化单体(要强一致、要简单);按业务域拆出少量(不到 10 个)独立服务(要独立扩容、团队自治);边缘的异步任务(图片压缩、报表导出、Webhook 处理)放上 Serverless(要省服务器成本、流量稀疏)。

这不是"最佳实践",是我们踩了四次坑后,账算明白了的结果。核心链路用单体省掉了分布式事务的痛苦;少量服务承载真正的独立演进需求;Serverless 只接它擅长的事件型工作。

一张表算清四种模式的真实账

把四种模式的隐性成本摆出来,你就不会只盯着"能不能拆":

模式部署单元数据一致性主要隐性成本适合团队规模
模块化单体1 个进程本地事务(强)模块边界靠纪律守< 20 人
SOA/ESB粗粒度服务靠总线编排中心化总线是单点逐渐被弃
微服务多个小服务最终一致(要补偿)分布式复杂度、运维20–100+ 人
Serverless函数事件驱动冷启动、状态外置看场景,不限规模

表里的"主要隐性成本"才是选型时该盯的列。微服务那栏写着"分布式复杂度",翻译成人话就是:你要补服务发现、配置中心、链路追踪、分布式事务、容器编排这一整套,少一样就会在某个深夜爆雷。

隔离高频变更点:绞杀者模式

回到前面那个思考题——核心链路要单体,但"推荐"模块要每周发十几次、还想独立技术栈。硬拆成全量微服务没必要,用绞杀者模式(Strangler Fig)在网关层把这一小块流量分流出去就够:

// 用 Spring Cloud Gateway 把"推荐"从单体里绞杀出来,其余流量仍走单体
@Bean
public RouteLocator routes(RouteLocatorBuilder b) {
    return b.routes()
        .route("recommend", r -> r.path("/api/recommend/**")    // 1. 只有推荐流量被分流
            .uri("lb://recommend-service"))                     // 2. 转到独立部署的推荐服务
        .route("legacy", r -> r.path("/api/**")                 // 3. 其余流量默认回单体
            .uri("lb://shop-monolith"))
        .build();
}

逐行看:第 3 行 path("/api/recommend/**") 只把推荐这一条路径切走,第 6 行 legacy 路由兜底其余所有 /api/** 仍回单体。这样你只为一个真正高频变更的点付出了分布式的代价,而不是把整个系统推倒重来。等哪天推荐服务跑稳了、团队也习惯了微服务运维,再决定要不要继续绞杀下一个模块。这是我们第四次重构能平稳落地的关键心法:别一次性革命,要一点点把该拆的抽出来。

选型框架:先问团队,再问技术

  • 团队 < 10 人:模块化单体。别碰微服务,你耗不起运维。
  • 10–50 人,业务域清晰:按域拆 5–10 个服务,配好服务发现、配置中心、链路追踪再拆。
  • 需要异构技术栈 / 独立部署频率极高:微服务值得,但要配套 DevOps 和 SRE。
  • 事件驱动、稀疏流量、对冷启动不敏感:Serverless,但 Java 要注意冷启动,考虑原生镜像。
  • 别上传统 SOA/ESB:中心化总线的故障域太大。

我不建议任何人"为了架构而架构"。每次拆分都是在用运维复杂度和分布式难题去换组织扩展性和部署独立性。这笔账,得你自己的团队规模和数据特征来算,别人给不了标准答案。


思考题:如果你的核心交易链路用单体,但某个模块(比如推荐)需要每周发布十几次、还想要独立技术栈,你会怎么在不引入全量微服务复杂度的情况下,隔离这个"高频变更点"?

更多推荐