干微服务这行时间久了,经常被新同事问:设计模式到底用几个才够?每次听到这个问题我都想笑。微服务设计模式不是拿来凑数的,也不是学了就得全铺上去。微服务架构里真正有价值的,来来回回就那么几个模式,能把它们选对、用透,比盲目堆一堆"看起来很高级"的架构要实在得多。这篇文章我从实战角度聊聊,哪些微服务设计模式是绝大多数业务系统真正需要的,哪些只是听起来厉害,以及我在落地过程中踩过的那些坑。适合从单体拆微服务、或者刚接触分布式设计模式的朋友参考,也欢迎老手一起拍砖。
1. 微服务设计模式,先搞清楚它要解决什么问题
1.1 分布式环境下的四类核心问题
在单体时代,模块之间都在一个进程内,调用是内存地址级别的,数据是共享的。一拆成微服务,很多原本不存在的麻烦就冒出来了。我归纳下来,微服务设计模式主要解决四类问题。
第一类是服务之间怎么互相找到对方。原来在单体里,A模块调B模块就是一次普通的方法调用,拆成微服务之后,服务实例可能有多台,IP和端口还会漂移,这就催生了服务发现这类模式。
第二类是调用链路上出错了怎么办。单体环境里一个方法挂了,顶多是这个请求报错,进程还在。但微服务是一堆进程通过网络互相调用,网络超时、服务重启、资源争抢都会引发故障。更麻烦的是故障会顺着调用链扩散,A挂了带垮B,B又带垮C。这催生了断路器、超时、重试、舱壁隔离等容错模式。
第三类是数据从一份变成几十份,怎么保证一致性。一个服务一个库,跨服务的事务不再支持,原来单体里一个事务能搞定的事,现在要拆成多个本地事务加补偿。这催生了事件驱动、Saga、CQRS、事件溯源等数据一致性模式。
第四类是横切能力怎么统一处理。鉴权、限流、日志、链路追踪这种公共能力,不能每个服务自己搞一套,否则有人用Java写、有人用Go写,治理逻辑各不相同,运维就完了。这催生了网关模式、Sidecar模式。
搞明白这一点特别重要。很多朋友上来就问“要用哪些模式”,其实是跳过了问题定义。模式是药,不是保健品,得先有病再吃药。我见过很多项目,还没遇到高并发问题就先上了一堆容错组件,结果系统复杂度反而被这些组件搞崩了。
1.2 按解决问题分类,而不是按名字分类
网上流传的微服务设计模式名单,少说有二三十个,但很多其实是同一个思想的不同变体。我的经验是,按“解决什么问题”来分更能帮我们做出选择。
接入与发现类,包括网关模式、注册中心模式、客户端发现模式。容错与稳定性类,包括断路器模式、舱壁隔离模式、重试与退避模式。数据一致性类,包括事件驱动、Saga模式、CQRS模式、事件溯源。部署与治理类,包括Sidecar模式、配置中心模式。
这样分完之后你会发现,真正每个微服务项目都避不开的,其实只有网关、注册发现、配置管理这三样。其他模式要看业务场景按需引入。我常说一句话:模式清单是别人的,问题清单是自己的。你只需要解决自己问题的那几个,剩下的可以放在工具箱里备用。
2. 网关模式与服务发现:微服务架构的门和门牌
2.1 API网关:把横切关注点集中起来
API网关是我个人最推荐优先落地的模式。它本质上是所有外部请求的入口,把原来写在业务代码里的鉴权、限流、黑白名单、协议转换、日志审计这些横切能力集中在一个层里完成。
这样做的好处特别直接。第一,客户端不用关心后端被拆成了几个服务,它只需要跟网关约定一套聚合接口或者统一路由规则,由网关背后的服务发现能力去分发请求。第二,安全策略、流控策略这些只能在一处配置,不用改几十个服务,审计和整改也方便。
落地时要注意几个很现实的问题。网关只做路由和横切操作,不能把业务逻辑塞进去。我见过有的团队把下单逻辑直接写在网关里,最后网关成了新单体,完全背离了微服务的初衷。再一个,路由规则应该来自注册中心,而不是网关进程内的本地配置文件,否则每次加服务都要重启网关,根本没法做到服务动态扩展。还有一个容易忽略的点:网关的线程池和网络连接池要单独规划容量。它承载所有请求,稍微一卡就是全局故障,必须单独压测、单独上监控。
给大家一个参数参考。网关对业务后端的反向代理连接池,建议按“预估峰值QPS × 单请求平均耗时”来估算并发连接数,再留出至少一倍余量。如果峰值QPS是5000,后端平均响应时间200ms,那就是5000 × 0.2 = 1000个并发连接,实际配置至少2000。很多团队不压测网关,结果线上一个热点活动就把连接池打爆了。
2.2 服务注册与发现:动态拓扑的基石
服务注册发现解决的是“客户端怎么找到提供方”的问题。在虚拟机时代我用过客户端发现,服务实例把地址注册到注册中心,调用方自己查列表再选一个。好处是省一跳,坏处是每个客户端都要维护发现逻辑,对语言绑定强。到了容器和编排平台普及之后,服务发现很多时候会被平台接管,业务侧只需要把探针配好,让平台知道服务状态。
但如果你是自己搭的注册中心,还是要重点看这几个点。
第一,健康检查的频率和阈值。心跳间隔通常设成3到5秒,超时时间设成心跳间隔的3倍左右比较合理。太频繁会把注册中心压垮,太迟钝又会影响故障切换速度。第二,客户端缓存时长。客户端从注册中心拉下服务列表后,不能每个请求都实时穿透注册中心,那会把注册中心拖死。一般缓存10到30秒,牺牲一点感知延迟换取性能和可用性。第三,多机房场景下的容灾。要提前想清楚是本地机房优先还是全局一致性优先,否则跨机房调用延迟会把小故障放大成大事故。
这里我踩过一个坑:某个服务重启频率很高,注册中心里实例总数其实没变,但客户端的健康检查老是误报,导致流量倾斜到几台已经超标的机器上。后来查下来是健康检查的失败阈值设得太严,稍微慢一点就被踢掉,然后又立刻重新注册,列表反复震荡。从那以后我基本都用“连续N次失败才下线”的策略,而不是单次失败就立刻摘除。
3. 容错模式:断路器和舱壁,别让雪崩发生
3.1 断路器:给远端服务一个恢复期
断路器模式的思路来自于家里的电闸:当调用远端服务失败的次数超过阈值,就打开断路器,干脆不让请求到达故障服务,让故障服务有时间恢复,同时快速返回降级结果。它有三个状态:关闭、打开、半开。
关闭状态下,正常请求照常发出,但实时统计失败率。一旦失败率超过阈值,状态切换到打开,所有请求快速失败。打开状态持续一段时间后进入半开状态,放一小部分试探请求,如果试探请求成功率达到阈值,断路器关闭;否则继续打开。这个状态机很容易理解,但在生产里配置参数是最容易翻车的点。
我给出的经验值是这些:失败率阈值通常设在50%左右。太低会误伤波动,太高了保护力度不够。打开状态持续时间要结合后端恢复时间,一般给5到10秒。如果你的下游服务重启一次要30秒,那断路器设5秒就不太够,要在恢复前的快速失败阶段兜住流量。半开状态试探请求不要太多,10个左右就够,成功率达到80%再关闭断路器。
还需要说一个误区。有的团队把断路器当成唯一救星,却忽略了下游服务的慢调用。如果请求都慢吞吞地超时,断路器其实救不了,因为连接池先被打满了。超时、连接池限流、断路器这三层要配合使用,缺一不可。顺序是:首先设一个合理的超时时间,比如几百毫秒;其次用连接池或信号量限制并发;最后用断路器统计失败比例做全局熔断。只有这样,才是一套完整的自我保护体系。
3.2 舱壁隔离:让一个故障不要拖垮全船
舱壁模式的道理很直白,船要分多个水密隔舱,一个舱进水不至于让整条船沉下去。对应到服务调用上,就是给不同的下游服务分配独立的线程池或信号量,某个下游卡死时,只耗尽它自己的那份资源,不去影响其他调用。
我最常用的做法是按两条维度做隔离。一条是核心链路和非核心链路分开。比如订单主流程用一个线程池,而广告推荐这种可降级流程用另一个线程池,避免一个非核心接口拖垮主线。另一条是按下游服务分组。一个上游服务往往要调好几个下游,如果它们共用线程池,某个下游的故障会间接拖垮对其他下游的正常调用。
线程池大小不能拍脑袋。可以这么算:假设一个下游服务的TP99是300毫秒,单机需要支撑100 QPS,那么并发承载力至少要达到100 × 0.3 = 30个并发。再留一倍余量给抖动,取60比较合理。这个数字只是起步,上线后要根据实际监控调整。很多团队要么把线程池设得特别大,跟没隔离一样;要么设得特别小,正常流量一来就不断拒绝。两种我都见过,都很难受。
舱壁隔离的代价是线程资源变多,每个服务消耗更多内存。我的原则是只对关键的、有真实并发风险的下游做隔离,不要每个下游都搞一个线程池,那会把系统整体资源撑爆。如果你用的是协程模型,资源代价会小一些,隔离思路依然成立。
4. 数据一致性:CQRS、事件溯源与Saga模式
4.1 分布式事务不能靠数据库
微服务之后最头疼的就是数据。每个服务独享自己的数据库,跨多个库的事务不能再依赖传统数据库事务了。我早期踩过大坑,想用分布式事务中间件去硬保证跨服务强一致性,结果锁冲突严重、性能断崖式下跌,最后还是老老实实改成最终一致性方案。
分布式数据这块,有两个模式经常被放在一起讨论:CQRS和事件溯源。CQRS,命令查询职责分离,把写请求和读请求拆成两条链路。写侧使用领域模型和事件模型,读侧用专门优化过的读模型或者独立的查询库。这么做的好处是复杂查询和写入负载互不干扰,写模型保持纯粹,读模型可以根据查询场景做宽表、缓存甚至全文索引。
事件溯源则把状态变化当成连续事件记录保存下来,任何状态都可以通过重放事件流重建。它的核心优势是完整留痕,出了问题时可以追溯每一步变更。这个模式不是所有业务都适合,它更适合审计要求高、历史状态变化需要完整留痕的场景,比如资金账户、订单状态机、供应链轨迹。用在简单CRUD系统上是给自己找事。我曾经在一个内部管理系统里头脑发热引入事件溯源,后来发现查询历史事件的需求几乎没有,反而存了一大堆无用事件,维护成本直线上升。经验告诉我:不知道为什么要用事件溯源,就别用。
CQRS也不一定非要绑定事件溯源。CQRS和事件溯源是独立的,可以单独使用CQRS,也可以单独用事件溯源,也可以一起用。我现在的策略是:如果读写性能冲突明显,先上CQRS;如果审计和追溯需求强烈,再考虑事件溯源。不要因为是热门模式就强行捆绑。
4.2 Saga模式是最终一致性的主力方案
Saga解决的是跨服务的长事务问题。业务流程横跨多个服务,Saga把它拆成一连串本地事务,每个本地事务完成就发布事件或调用下一个服务,如果某一步失败,就反向执行补偿操作。
这里的关键是要理解“补偿”不是回滚。已经提交的本地事务不能撤销,只能通过一个新的本地事务去修正前面操作的影响。所以Saga的每一步都要设计对应的补偿逻辑,比如扣库存扣了,补偿是回补库存;创建了订单,补偿是取消订单并标记状态。补偿操作本身也要考虑幂等,因为分布式环境下同一个补偿动作可能被执行多次。
Saga有两种编排方式。编排模式,Choreography,每个服务通过监听事件来驱动下一步,好处是耦合低,坏处是流程分散在事件里,出了问题不好排查。协同模式,Orchestration,有一个专门的协调器服务负责通知各个参与方执行步骤和补偿步骤,好处是流程集中、好维护,坏处是协调器本身成了新的单点,需要额外做好高可用。
我的实践选择是:简单的两三个服务参与的事务用编排模式,利用消息中间件把事件广播出去,省掉一个专门服务。涉及四个以上服务、或者有严格记账要求的时候,倾向于用协同模式,由协调器统一编舞,让流程整体可见、可重试。
举一个典型场景。一个下单流程涉及订单服务、库存服务、支付服务。用协同模式实现时,协调器先调用订单服务创建订单,再调用库存服务扣库存,接着调用支付服务付款。如果支付失败,协调器依次调用库存服务回补库存、订单服务取消订单。每一步都要记录状态,协调器记录当前流程走到了第几步,方便重试和恢复。只有这样,Saga才能在故障后安全收尾。
5. Sidecar模式与配置管理,稳定性的隐形支撑
5.1 Sidecar模式:把通用能力挪到独立进程
Sidecar模式很有意思。它不是把功能做成类库打进业务进程,而是把通用能力拆成一个独立进程,以“边车”的形式和业务进程一起部署。这个进程负责网络转发、限流、鉴权、可观测性采集等旁路能力,业务代码只需要关心自己的逻辑。
好处有三点。第一,语言无关。业务团队可以用不同语言写服务,而治理能力完全一致。第二,升级独立。升级限流组件或链路追踪组件时,只需要重启Sidecar,不需要改业务代码,也不影响业务进程运行。第三,权限隔离。如果跨部门、跨团队治理,Sidecar可以把公共策略收敛到独立进程,避免业务开发者改到公共代码。
但它也有代价。每个服务实例多了成倍的进程数,资源开销会增加。排查问题链条变长了,一个请求要穿透多个进程才能看到全貌,日志也要跨进程聚合。更重要的是,Sidecar本身也可能故障,相当于系统里多了一层必须处理的依赖。
要不要用Sidecar,我的判断标准很简单:如果你已经是多语言团队,或者治理能力需要在很多服务里统一升级,Sidecar值得考虑。如果团队就一种语言,那用类库实现一套统一封装可能更划算。毕竟少部署一层就少一层故障点。做技术选型时,不要觉得Sidecar“更先进”就选它,要看它解决的是不是你的痛点。
5.2 配置管理,小事情里的大学问
配置管理看起来不算设计模式,但微服务环境里配置散落在几十个服务里,如果没有集中管理,发一次配置要改几十个地方,基线直接乱掉。配置中心解决的不仅仅是集中存储,更重要的是动态刷新和变更灰度。
我建议所有非敏感配置都收拢到配置中心,不同环境用不同的profile隔离,本地开发连本地配置,线上读线上配置,不能混。配置变更不能一把梭,要走灰度分组。先放一台机器观察指标,再逐步放量到5%、20%、50%、100%,每一步都盯告警。这个流程看起来麻烦,但救过我太多次了。
配置中心出故障时的本地缓存兜底一定要做。我遇到过配置中心短暂不可用导致一批服务回读默认值的故障,幸好当时预留了本地快照,服务用本地快照继续跑,等配置中心恢复后再重新同步。如果没做这个兜底,几十个服务的线程池大小、限流阈值全部变成默认值,那一刻的系统完全是失控的。这个教训我一直记得。
6. 模式不要贪多,按场景选型最重要
6.1 我的微服务设计模式使用清单
回到标题的问题:微服务的设计模式,你用了几个?我直接把我这些年常用的清单列出来。
一定先用:API网关模式、服务注册与发现、配置管理。这三个属于微服务架构的地基,没有它们谈不上真正的微服务。高并发稳定优先:断路器、舱壁隔离、重试与退避。这些是在业务量和复杂度起来以后逐步加的,用于防雪崩。分布式数据强依赖:事件驱动、Saga模式。只要业务跨了服务做数据修改,基本绕不开。特殊审计场景才用:CQRS、事件溯源。这两样不是为了炫技,是为了解决具体问题。个别场景按需:Sidecar模式、聚合模式、客户端发现模式。
所以我的真实答案其实是:用得最多的大概三五个,其他都在不同项目里用过,但很少同时全部上。有一位前辈跟我说过一句话,原话是“模式是帮你减少问题的,不是帮你增加代码的”。这句话后来成了我的原则。当你想引入一个新模式时,先问自己它到底减少了哪个已知问题,如果答不上来,就不要加。
6.2 从单体到微服务的落地顺序参考
如果你的系统还在单体阶段,想往微服务演进,我建议按下面这个顺序来,而不是一天之内把十几种模式全部铺上去。
第一步,先把模块边界和数据库拆分规划好。边界没划清,上任何模式都只是给混乱加装饰。第二步,引入配置中心和网关,把横切关注点收口,让服务能独立部署。第三步,用服务发现替代手工域名配置,让拓扑可以动态变化。第四步,打通链路追踪和日志聚合,再谈精细化容错。没有可观测性之前做熔断,等于蒙着眼睛救火,出了故障连根因都找不到。第五步,最后再根据业务特性决定是否引入事件驱动和Saga。
每一步都要有可量化的验收条件。比如引入服务发现后,要求新增一个服务实例能做到1分钟内自动注册、被调用方感知;引入断路器后,要求下游故障时5秒内触发熔断、上游请求不堆积。只有卡着这些指标滚动,架构演进才不会失控。
把顺序记好,比多记几个模式名字有用得多。我个人在实际操作中的体会是:大部分系统走到第三步其实已经能稳定运行了,真正的噩梦往往出现在跳过步骤、直接上数据一致性模式的时候。微服务设计模式不是越全越好,能解决你真实问题的那几个,才是真正有价值的。