分布式、集群与微服务:三个词分别在说什么

用同一套订单系统说明集群、分布式与微服务关注的层次,解释它们怎样组合,以及服务拆分后新增的失败边界。

“我们已经上了微服务,所以是分布式系统。”这句话常常把三件不同的事揉在一起:把同一程序多部署几份、让多台机器协作、按业务边界拆开代码与团队。它们可以同时成立,也可以只发生其中一件。

这篇文章只回答一个问题:面对同一套订单系统,什么时候应该说它是集群,什么时候应该说它进入了分布式问题,什么时候才能称得上微服务。

文中的定义主要参照 Fowler 与 Lewis 对微服务的说明、Microsoft 的微服务架构指南和 Kubernetes 的 Deployment 模型。示例是合成系统,目的是标出责任边界,不对应任何具体公司的架构。

先把结论放在一起

词 关注对象 它解决的主要问题 它新增的主要问题
集群 同类节点的副本与调度 容量、单点故障、弹性伸缩 流量分配、健康检查、会话与副本一致性
分布式系统 跨进程、跨机器或跨故障域的协作 规模、隔离、容错与地理分布 网络不可靠、部分失败、时间与状态协调
微服务 代码、数据和团队的业务边界 独立演进、独立发布、局部伸缩 接口契约、跨服务事务、可观测性与运营复杂度

集群是部署拓扑,微服务是架构与组织方式,分布式系统描述运行时的协作事实。它们并不处于同一层级。

一个单体应用可以部署成十个副本,成为集群,也要面对负载均衡、会话粘滞和共享缓存等分布式问题。一个微服务系统可以在开发环境的同一台机器上运行,业务边界依然存在;到了生产环境,它通常会以多个副本跨机器部署,于是同时成为集群和分布式系统。

同一订单系统的三层变化

这张图的读法是从左往右。第一列只改变副本数,第二列改变业务边界,第三列把每个边界放进多节点运行时。每往右走一步,都增加一种能力,也增加一类需要由程序和运维共同承担的失败。

一、集群先解决“同一个程序不够用”

设想一个单体订单应用,里面有登录、商品、下单、支付状态查询和后台管理。它连接一套数据库,运行在一台应用服务器上。

最先遇到的常常是容量或单点问题:促销时 CPU 打满,机器重启时所有请求中断,单实例内存泄漏会影响全部用户。常见的第一步是把同一个构建产物运行多份:

客户端
  ↓
负载均衡器
  ├── order-app #1
  ├── order-app #2
  └── order-app #3
        ↓
      同一套数据库

这就是应用集群。节点执行同一份代码、承接同一类请求,负载均衡器把流量分给健康副本。Kubernetes 的 Deployment 也是这一类模型:它维护指定数量的 Pod 副本,并在副本失效时尝试恢复数量。

集群能提高吞吐和单实例故障下的可用性,却不会自动解决所有状态问题。订单应用若把登录 Session 放在本机内存,用户第一次请求命中 #1,下一次命中 #2 时就会“失忆”。常见选择是把 Session 迁到共享存储、使用可验证的无状态令牌,或通过粘滞会话把同一用户固定路由到一个副本。每种选择都在一致性、故障切换和扩缩容之间交换条件。

另一个容易漏掉的边界是后台任务。三个副本都启动“每分钟扫描超时订单”的定时器,就会执行三次。集群解决的是副本冗余,不会自动选出某一台负责执行。后续的分布式锁、Leader 选举和任务调度文章处理的正是这类问题。

因此,“部署了多台机器”可以准确称为集群。它已经是分布式运行环境的一部分,但还没有说明代码是否按业务拆分。

二、分布式系统关心“协作时谁能相信什么”

只要一个动作需要跨独立进程或节点传递信息,系统就要面对分布式的不确定性。调用方看不到远端进程的内存,也无法凭超时判断远端操作有没有发生。

例如,订单应用的三个副本同时使用 Redis 维护库存预占。#1 向 Redis 发送扣减请求后网络超时。对 #1 而言,至少有三种可能:请求没有到达、Redis 已扣减但响应丢失、请求仍在路上。直接重试可能多扣,直接报失败又可能留下已扣未确认的库存。

单体多副本、数据库主从、缓存集群、消息队列消费者组都会遇到它。分布式系统讨论的核心是:状态保存在哪里,谁有权修改,成功响应在什么条件下发出,节点失联后如何恢复,以及客户端怎样处理结果未知。

把系统叫作分布式,并不等于它必须有几十个服务。两台互相复制状态的数据库节点已经是分布式系统;一个跨地域的对象存储也是;多个 Agent Worker 从队列领取任务、写入同一任务状态库,同样是。

前一篇《分布式系统入门》从故障模型、一致性、共识和事务展开了这些问题。这里保留一个实用判断:只要两个独立故障域必须对同一件事达成可验证的结论,就进入了分布式系统的设计范围。

三、微服务解决的是业务边界和独立演进

微服务不是把类、Controller 或 Maven 模块拆得很小。较常用的判断是:一个服务围绕相对完整的业务能力组织,能作为独立部署单元演进,并且对自己的数据或外部状态负责。Fowler 的定义也把“按业务能力组织”和“独立部署”放在核心位置。

回到订单系统。商品检索在大促期间需要大量扩容,支付接入有更严格的发布与审计要求,后台报表又主要消耗离线计算资源。团队可以把它们拆成三个服务:

商品服务:商品目录、价格读取、搜索索引
订单服务:订单状态、下单校验、订单查询
支付服务:支付请求、渠道回调、支付账本

拆分带来的实际收益,是每个边界能以不同节奏发布、按不同负载扩容,并让团队对一段明确的业务能力负责。Microsoft 的架构指南同样强调服务应在一个 bounded context 内实现业务能力,并通过定义明确的 API 隔离内部实现。

但“单独仓库 + HTTP 调用”还不足以证明独立演进。若订单、商品和支付仍共享一张数据库表,任一表结构变更都要协调发布;若下单流程每次都要求三个服务同步升级,部署也仍然绑在一起。形式上拆成了进程,耦合只是从函数调用搬到了网络和共享数据层。

独立部署也不表示完全没有依赖。支付服务依赖订单号,订单服务依赖商品价格,这些依赖需要通过版本化 API、事件契约、超时策略和兼容性演进来管理。所谓独立,是指一个服务替换、扩容或发布时,不需要把整套系统作为同一个构建产物重新发布。

四、一次演进会跨过三道不同的门槛

很多系统的合理演进顺序是:先保持单体内的模块边界,再增加无状态副本,最后只为明确的业务和组织压力拆出服务。

第一步:模块化单体

订单、库存和支付先在一个代码仓库中,但使用明确模块、接口和数据库访问边界。一次本地下单仍然可以用数据库本地事务完成,调试和部署也比较直接。

这一阶段应先验证边界是否真实存在:支付模块是否能只经由公开接口访问订单数据,库存规则是否散落在多个 Controller,是否所有改动都必须触碰同一张巨型表。没有模块边界就直接拆服务,通常只是把混乱变成更多仓库。

第二步:单体集群化

当请求量或单点风险成为瓶颈,把单体做成尽量无状态的多个副本。这里新增的工程清单包括:健康检查、优雅下线、连接池容量、Session 策略、缓存失效、定时任务唯一执行和灰度发布。

这一步已经需要后端和全栈开发者理解接口的失败语义。前端发出同一个“创建订单”请求两次,可能是用户双击,也可能是网关重试;服务端要以业务幂等键区分“同一次动作的重试”和“两笔不同订单”。

第三步:为独立变化的部分拆服务

只有当某块能力的发布频率、容量模型、可靠性等级、数据边界或团队所有权明显不同,拆分才会带来净收益。支付渠道改造不应迫使商品搜索重新部署,搜索流量暴涨不应把订单写库一同扩容,这些都是有效信号。

服务拆开后,原来的函数调用变成 RPC 或消息。单库事务可能变成 Saga、Outbox 或其他跨服务协议;本地日志变成跨服务追踪;一个进程内的异常处理变成超时、重试、限流和降级。微服务没有消灭复杂度,它把复杂度移到边界处,要求团队把边界显式管理起来。

五、用一个下单请求检验是否真的需要拆

假设用户点击“提交订单”,订单服务需要校验价格、预占库存、创建订单并发起支付。

在模块化单体里,这些调用可能都是进程内函数,订单与库存也在同一个数据库事务中。失败时回滚路径短,调用栈和数据库日志通常已经足够排查。

把单体部署成集群后,用户请求会落到任一副本。下单接口必须具备幂等性,后台扫单任务只能由一个持有者执行,缓存和会话不能依赖某一个实例的内存。业务边界没有变化,运行时边界已经变化。

再把库存拆成服务后,reserveStock(orderId, sku, quantity) 变成远程调用。订单服务收到超时时,不能仅凭异常决定库存是否已经预占。接口需要让调用方查询预占状态,库存服务需要以 orderId 去重,订单状态机需要容纳 PENDING_STOCK 这样的中间状态。

这段流程说明拆分的判断应落在责任上:

现象 优先动作 原因
单体 CPU 或连接数到瓶颈 先增加副本、做缓存或优化热点 这是容量问题,不必先引入跨服务调用
某个模块改动频繁拖累全站发布 先检查模块边界,再考虑拆服务 部署耦合可能来自代码和数据耦合
搜索流量远高于订单写入 将搜索读模型独立扩容 两类负载的容量模型不同
支付与订单必须长期协调发布 暂缓拆分,先收敛契约与数据所有权 服务数量不会消除同步发布
单节点故障导致定时任务重复执行 设计任务幂等与唯一领取协议 这是集群化带来的协作问题

六、几个常见误判

多副本单体就是微服务

不是。把同一 WAR 或容器镜像运行十份,解决的是吞吐和单实例故障。它仍然可能是边界清晰、可长期维护的单体,也可能是难以拆分的巨石应用;副本数不能判断架构风格。

微服务一定要很多机器

生产环境的微服务通常会跨节点运行,但“是否微服务”先看业务边界和独立部署能力。开发或测试环境中多个服务可以同机运行。反过来,十台机器上的单体副本依然不是微服务。

服务拆得越细,扩展性越好

过细拆分会让一次用户请求经过更多网络跳转,增加尾延迟、故障点、版本兼容和追踪成本。服务粒度应该由业务能力、数据所有权、发布节奏和负载差异决定,而不是由目录层级或团队人数机械决定。

有 API Gateway 就完成微服务改造

Gateway 能处理入口路由、鉴权、限流和聚合,但它不能替服务建立数据所有权,也不能替跨服务操作定义事务与补偿。所有服务若仍经由 Gateway 调同一张共享数据库,关键耦合依然存在。

七、不要按名词选方案,按压力选动作

做技术决策时,可以把三个词暂时放下,先问三个更具体的问题。

第一个问题是:瓶颈是否只是同一能力的容量或可用性? 如果答案是肯定的,优先考虑让应用无状态化、增加副本、接入负载均衡、缓存热点和处理连接池。这是集群化的工作。它会带来会话、定时任务和缓存一致性的细节,但不要求先把商品、订单和支付拆成独立服务。

第二个问题是:是否存在一个长期独立变化的业务能力? 一个候选边界应当同时有相对独立的数据、发布节奏和责任人。例如搜索索引的写入策略与订单写入完全不同,支付渠道的审计要求也不应被普通商品页改版绑住。若只能说“代码文件太大”或“想用 RPC”,通常还不是可靠的拆分依据;先把模块接口和数据访问收住,能更低成本地验证边界。

第三个问题是:这些部件跨故障域协作时,结果未知由谁处理? 一旦下单依赖远程库存、异步消息或多个副本,设计就要明确超时、重试、幂等键、状态查询和人工补偿。这个问题无论系统叫单体集群还是微服务都会出现。把它写进接口契约和监控指标,远比在架构图上多画几个方框重要。

这三问给出的答案可以不同:一家团队完全可能保留模块化单体,同时以集群方式解决流量;也可能只把搜索拆出服务,其他交易链路仍留在同一进程。架构不是一次性身份选择,而是持续把复杂度放到最值得承担的位置。

八、Agent 和全栈开发者应该怎样使用这组概念

Agent 应用很容易同时踩到三层。一个 Web Agent 服务部署多个副本是集群;多个 Worker 从队列领取任务、写入状态库是分布式协作;把模型调用、工具执行、检索和账单拆给独立团队维护,才接近微服务架构。

全栈开发者不一定负责 Raft 或服务网格,但需要把接口设计成能跨副本运行:前端重试会不会重复创建数据,轮询任务状态时会不会读到旧状态,用户在灰度发布期间能否完成一条流程。后端开发者还要继续回答数据归属、调用超时、幂等与跨服务一致性。

一个实用的顺序是:先让单体模块边界清晰,再让它能以多个无状态副本稳定运行,最后只拆出确实需要独立演进的能力。这样做不会让系统永远停在单体,也不会因为追逐“微服务”标签过早付出分布式协调成本。

参考资料