AIHub

分布式与微服务:CAP、拆分与治理

进阶24 分钟读完2026-08-06#后端#微服务#分布式#CAP#服务治理
分布式与微服务:CAP、拆分与治理

一家公司刚创业时,整个系统是一个代码库、一个数据库、部署在一台机器上的单体应用——这是完全正确的起点。但随着团队膨胀到几十上百人,痛苦开始显现:改一行代码要全量回归测试,任何小功能上线都要整个系统一起发布,某个模块的内存泄漏拖垮整站。微服务不是银弹,它是为组织规模付出的代价。理解「为什么拆」比学会「怎么拆」重要得多。

一、从单体到微服务:拆分的动机与代价

拆分的真正收益有三个:独立发布(订单团队上线不用等支付团队)、独立扩容(大促时只给订单服务加机器)、故障隔离(推荐服务挂掉不影响下单)。

但代价清单同样长,而且很多人拆完才看见:

  • 本地一次函数调用变成了网络调用,延迟从纳秒级变成毫秒级,还会失败。
  • 一个请求横跨五六个服务,出问题像破案,没有链路追踪根本查不动。
  • 数据散在多个数据库里,「一个事务搞定」成为过去式。
  • 运维对象从 1 个应用变成 50 个,没有 CI/CD 和容器化根本玩不转。

所以业界有条朴素的共识:能单体就别微服务,先按模块把单体写干净。拆分的时机是组织协作成本超过了分布式的技术成本,而不是「微服务听起来更高级」。

二、CAP 与 BASE:分布式的第一性原理

分布式节点之间必须在一致性与可用性之间权衡

CAP 定理说,一个分布式系统面对网络分区(Partition tolerance,节点之间断网)时,只能在一致性(Consistency,所有节点读到同样的最新数据)和可用性(Availability,每个请求都能得到响应)之间二选一。

关键直觉:网络分区在互联网上必然发生(光缆被挖断、机房网络抖动),所以 P 不是可选项,真正的选择题是分区发生的那一刻你保 C 还是保 A。

  • 保 C(如 ZooKeeper、Etcd):分区期间宁可拒绝写入,也要保证数据不出错。适合注册中心、分布式锁这类「错不起」的场景。
  • 保 A(如 Cassandra、DNS):分区期间继续服务,允许读到旧数据,事后修复。适合购物车、动态流这类「可用性大于一切」的场景。

BASE 是 AP 路线的工程化表述:基本可用(Basically Available)、软状态(Soft state,允许中间状态)、最终一致(Eventually consistent)。翻译成人话:不追求任何时刻都绝对正确,只保证「给点时间,系统会收敛到正确状态」。绝大多数互联网业务走的都是这条路。

三、服务注册发现与配置中心

微服务的第一个现实问题:服务 A 怎么找到服务 B? 硬编码 IP 在容器时代毫无意义——实例每天扩缩容、重建,IP 一直在变。

服务注册发现的模型类似电话簿:每个服务实例启动时向注册中心(Nacos、Consul、Etcd)「登记」自己的名字和地址,并定期发心跳;调用方要找人时,先查电话簿拿到当前健康的地址列表,再自己选一个(客户端负载均衡)或经由网关转发。实例挂了,心跳超时,注册中心把它摘出去——这就是为什么注册中心通常选 AP 或最终一致,因为「暂时看到一个已死的地址」比「整个电话簿不可用」好接受。

配置中心(Nacos、Apollo)解决另一个问题:50 个服务的数据库密码、限流阈值、开关,不能散落在 50 个配置文件里靠改文件重启来维护。配置集中到一处,支持按环境分组、版本管理、推送生效(改了限流阈值,几秒内全网服务热更新,不用重启)。

对外还有一个网关(Gateway):客户端不直接调内部服务,所有流量先经过统一入口,鉴权、限流、路由、日志在这里集中处理。网关是系统的「前台」,内部服务怎么拆分重组,对外的 API 保持稳定。

四、熔断、限流、降级:系统的自我保护三板斧

分布式系统里,局部故障会像瘟疫一样蔓延:下游服务 C 响应变慢 → 调用它的 B 线程全部堵在等待上 → B 也变慢 → 调用 B 的 A 跟着挂。这叫雪崩效应。三板斧就是为打断这个链条:

  • 限流:在入口挡住超过处理能力的流量。常见算法是令牌桶(桶里按固定速率放令牌,请求拿到令牌才放行,允许一定突发)和漏桶(请求以恒定速率流出,强行削平峰值)。
  • 熔断:模式直接借自电路保险丝。对下游的调用失败率超过阈值,熔断器「跳闸」进入 Open 状态,短时间内直接失败、不再发起真实调用;过一阵进入半开状态试探几个请求,成功则恢复闭合。它保护的是调用方——与其线程全堵死,不如快速失败。
  • 降级:兜底预案。非核心功能主动牺牲:推荐服务挂了,首页返回一份静态的热门榜单;库存查询超时,先按「有货」展示、下单时再严格校验。

工程上常用 Sentinel、Resilience4j 这类库,用注解就能给方法加上规则:

@SentinelResource(value = "queryStock",
    fallback = "queryStockFallback",       // 降级逻辑
    blockHandler = "queryStockBlocked")    // 被限流/熔断时
public Stock queryStock(long itemId) { ... }

核心思想一句话:任何跨网络的调用都要假设它会失败、会变慢,并为这两种情况写好剧本

五、分布式事务:没有免费的 ACID

单体时代,转账就是数据库一个事务。微服务时代,钱在账户服务、积分在积分服务、订单在订单服务,三个库三个服务,怎么办?

主流思路是放弃强一致的 ACID,拥抱最终一致

  • 本地消息表 / 事务消息:A 服务在本地事务里同时写入业务数据和一条「待发送消息」,然后由后台任务可靠地把消息投递给消息队列,B 服务消费消息完成自己的部分。只要消息投递和消费的机制可靠,两边最终会一致。这是互联网场景最常用的方案。
  • TCC(Try-Confirm-Cancel):把操作拆成两步。Try 阶段各参与方预留资源(账户服务把 100 元冻结而不是扣掉);全部 Try 成功则 Confirm 真正执行;任何一个失败则全部 Cancel 释放冻结。它能在业务层达到接近强一致的效果,但每个操作都要实现三个接口,成本高,只适合钱、库存这类核心链路。
  • Saga:把长事务拆成一串本地事务,每一步都配一个补偿动作;第 N 步失败就逆序执行前 N-1 步的补偿(订机票失败 → 退酒店 → 退优惠券)。适合流程长、对实时一致性要求低的业务。

选型的直觉是:能用最终一致就不用 TCC,能用业务对账兜底就不过度设计——大多数「不一致」其实可以被一条对账任务在凌晨悄悄修复。

六、链路追踪:给请求发一张通行证

「用户说下单慢,是哪个服务的问题?」没有链路追踪(Distributed Tracing),这个问题要在五台机器的日志里人工拼图。有了它,每个请求入口生成一个全局唯一的 traceId,每跨一个服务生成一个 spanId,像接力棒一样在请求头里传递。所有日志带上 traceId,你就能在 Jaeger、SkyWalking 的界面里看到一棵完整的调用树:每个服务花了多久、哪次数据库查询最慢、错误发生在哪一环。

OpenTelemetry 已经成为这个领域的事实标准,埋点、上报、展示都有成熟生态。它和 Metrics(指标)、Logging(日志)并称可观测性三支柱,下一篇会展开。

常见误区

  • 为拆而拆:10 个人的团队维护 30 个服务,大部分精力花在服务间扯皮和运维上。拆分粒度应跟着团队边界走(康威定律:系统结构会复制组织结构)。
  • 共享数据库:两个服务连同一个库,等于用网络调用伪装成单体的分布式单体,改表结构牵一发动全身。一个服务一个库是底线。
  • 忽略重试的幂等性:网络超时后重试是标配,但如果接口不幂等(比如重复创建订单),重试就会制造脏数据。所有写接口都要回答「同一个请求来两次会怎样」。
  • 把分布式事务当默认解:先问业务能不能接受短暂不一致。能,就用最简单的最终一致方案。
  • 忽视接口契约:服务之间的接口就是契约。字段只增不删、语义变更走新版本,否则一次发布就能让上下游全线报错。

小结

微服务是组织规模逼出来的架构,代价是网络、数据和运维的复杂度;CAP 告诉你分区面前只能 C/A 二选一,BASE 的最终一致是互联网的主流答案;注册发现解决「找得到」,配置中心解决「改得动」,熔断限流降级解决「挂不透」;分布式事务优先最终一致,核心链路再上 TCC;链路追踪让跨服务排查从破案变成看图。下一篇是本系列的收官:把这些零件组装成高可用系统,并聊聊专家的成长路径。


系列导航:上一篇《Docker 与现代化部署:从容器到 CI/CD》(/content/backend-expert-10-docker-deploy) · 下一篇《架构实战与专家思维:高可用与系统设计》(/content/backend-expert-12-architecture)

相关教程

分布式与微服务:CAP、拆分与治理 | AIHub