1. 先泼一盆冷水:这五个词根本不在一个维度上
RESTful、SOAP、RPC、SOA、微服务,这五个词几乎每年都会被拿出来重新讲一遍,但真正让人头疼的不是它们各自有多复杂,而是大量资料把它们并列摆在一起做"对比",仿佛它们是五个可以互相替代的选项。我带过几个新人,几乎每个人在第一次做接口联调时都会问出同一个问题:我们这个系统到底是 REST 还是 RPC,是 SOA 还是微服务,能不能四选一?答案是根本没法选,因为这句话就像在问"我这套房到底是钢筋混凝土、是中式装修、是两室一厅、还是小区物业模式"——它们分属完全不同的层级,混着说必然说不清。
先把坐标轴摆正。RPC 和 SOAP 描述的是一次调用"长什么样",属于通信协议与消息格式层面;RESTful 描述的是一组接口应该遵循什么样的约束,属于架构风格层面;SOA 和微服务描述的是整个系统被拆成什么样、服务之间怎么治理,属于架构组织层面。一个系统完全可以是"用 RPC 做通信、内部按微服务拆分"的,也完全可以是"用 RESTful 风格暴露接口、底下是一个巨大的单体应用"。你说它是 RPC 还是微服务,两个答案都对,因为它们回答的不是同一个问题。
这篇内容适合谁看?如果你是刚入行、被这些缩写轮番轰炸到麻木的开发,它能帮你把脑子里那团乱麻理顺,以后跟人讨论时不会再答非所问;如果你是要做技术选型的负责人,它能给你一套可落地的判断依据,而不是背概念;如果你是面试准备中的人,它能让你在"说说 RPC 和 REST 的区别"这类问题上答到根子上,而不是背三条网上的标准答案。下面我会按"通信层 → 架构层"的顺序逐个拆,每个概念都讲清楚它为什么出现、解决了什么、代价是什么,最后再做横向对照和选型决策。
1.1 三个坐标轴:协议、风格、组织方式
第一个轴是通信协议与编码格式。这一层关心的是:两台机器之间那串字节怎么组织、怎么解析、出错怎么表达。RPC 和 SOAP 都在这条轴上。RPC 是一个思想——让远程调用看起来像本地函数调用;SOAP 是一套具体的规定——用 XML 信封包裹消息、用契约文件描述接口。它们不是互斥的,因为 RPC 思想完全可以用 SOAP 格式承载,历史上也确实这么干过。
第二个轴是接口设计风格。这一层关心的是:接口该怎么命名、用什么语义、资源怎么表达、状态怎么传递。RESTful 在这条轴上,它是一组约束条件,不规定你用什么传输协议(虽然实践中几乎都用 HTTP),也不规定你用什么数据格式(JSON 和 XML 都行)。所以"RESTful 接口"和"RPC 调用"的对立,本质上是"面向资源的设计"和"面向动作的设计"的对立,而不是两种技术的对立。
第三个轴是系统组织与服务治理。这一层关心的是:整个业务被切成几个部署单元、服务之间怎么发现彼此、怎么容错、怎么发布、谁负责。SOA 和微服务在这条轴上,它们是同一条演进路线上的两个阶段。SOA 是较早的那次尝试,强调服务复用和企业级总线;微服务是后来的修正,强调去中心化、独立部署和轻量通信。这一层跟上面两层是正交的——微服务可以用 REST 通信,也可以用 RPC 通信,甚至可以混用。
把这三个轴想明白,很多争论就自动消解了。比如"微服务是不是一定要用 RESTful",答案显然是否定的,通信方式的选择要看性能、契约强度、团队熟悉度,跟"服务是不是微的"没有必然关系。再比如"用了 RPC 是不是就不能叫微服务",这更是无稽之谈。我在实际项目里见过最舒服的组合就是:内部服务间用二进制 RPC 通信保证性能,对外网关暴露 RESTful 接口方便第三方接入,整个系统按业务域拆成十几个独立部署的服务。三个轴各取所需,互不干扰。
1.2 一张表把五者的定位摊开说
概念定位不清的时候,最好的办法是做减法——把每个词强行归到某一层,看它到底解决什么问题。
| 概念 | 所属层级 | 核心要解决的问题 | 典型形态 | 是否规定实现 |
|---|---|---|---|---|
| RPC | 通信方式 | 让远程调用像本地调用一样简单 | 客户端桩 + 序列化 + 网络传输 + 服务端骨架 | 否,只是一个思想 |
| SOAP | 消息格式与协议规范 | 跨异构系统的强契约通信 | XML 信封 + 接口契约描述文件 | 是,规定得很死 |
| RESTful | 接口架构风格 | 用统一的方式表达资源和操作 | 资源路径 + 标准方法语义 + 状态码 | 否,是约束集合 |
| SOA | 系统架构组织 | 企业内服务复用与统一集成 | 服务注册 + 企业服务总线 + 粗粒度服务 | 部分规定,偏方法论 |
| 微服务 | 系统架构组织 | 让大型系统能独立演进和弹性伸缩 | 按业务域拆分 + 独立部署 + 轻量通信 | 否,强调原则 |
看这张表有个细节值得注意:只有 SOAP 是"规定实现"的,它把消息结构、错误表达、接口描述方式全都定死了,好处是互操作性极强,坏处是任何一点小改动都得动整个契约。而 RPC、RESTful、微服务都是"给原则不给实现",所以你会看到同一个名词下衍生出五花八门的做法,这也是这些词被用滥的根源——每个人嘴里的"微服务"可能完全不是一回事。
1.3 为什么这几个词总被混着叫
混用的根源有三个。第一是历史包袱。这几个概念的流行期有重叠,早期资料经常在同一篇文章里同时出现,读者很容易把它们当成并列选项。第二是厂商与培训话术的简化。为了讲清楚卖点,很多材料会把复杂的分层压缩成"新技术替代老技术"的线性叙事,久而久之就变成了"微服务取代 SOA、REST 取代 SOAP"这种过度简化的说法。第三是实际项目里的混搭。真实系统很少纯粹,经常是 SOA 时代留下的几个老服务,套了一层 RESTful 网关,内部又用 RPC 调用,然后被贴上"微服务架构"的标签。在这种环境里待久了,概念边界自然就模糊了。
我个人的建议是:讨论任何架构问题时,先明确你在说哪一层。听到"你们用微服务吗"这种问法,先反问一句"你指的是部署单元拆分还是通信方式",能省掉半小时的无效沟通。这个习惯看着小,实际能避免非常多鸡同鸭讲的会议。
2. RPC:把网络调用伪装成本地函数的老祖宗
2.1 RPC 到底解决了什么问题
在没有 RPC 的年代,两台机器协作要手工拼报文、处理字节序、自己写解析逻辑,写一个远程调用比写十个本地函数还累。RPC 的核心主张极其朴素:既然本地函数调用这么顺手,那就让远程调用看起来也一样。你写userService.getById(1001),代码层面看不出这是在跨网络,序列化、寻址、传输、反序列化这些脏活全被框架吞掉。
这个抽象的价值在于降低了分布式开发的心智负担。但它也埋下了一个持续几十年的陷阱——它故意隐藏了网络的不可靠性。本地函数调用要么成功要么抛异常,不会"调用到一半超时"。远程调用则可能成功、可能失败、可能成功但你没收到响应(最恶心的情况)。所以 RPC 的所有工程实践,本质上都在跟这个"伪装"作斗争:超时怎么设、重试怎么做、幂等怎么保证、连接怎么复用。你把这些想明白了,RPC 就算真懂了。
2.2 一次 RPC 调用的完整旅程
把一次调用拆开看,大概是这么几步。客户端拿到一个"桩"对象,这个桩长得跟真实服务接口一模一样,但里面没有业务逻辑,只负责把方法名和参数按约定编码成字节流。编码方式分两大类:文本型(JSON、XML)可读性好但体积大、解析慢;二进制型(Protobuf 这类 IDL 驱动的方案)体积小、解析快,但需要先写接口定义文件并生成代码。
编码完的字节流通过网络传输层发出去,传输层可能是裸 TCP、HTTP/1.1、HTTP/2 或者更底层的方案。服务端收到后按同样的约定解码,找到对应的方法执行,再把返回值编码回来。整个过程里,序列化格式和传输协议是两个独立的决策点,很多人会把它们混为一谈。比如"用 HTTP/2 承载 Protobuf"和"用 HTTP/1.1 承载 JSON",都是 RPC,但性能差异能到几倍甚至十几倍。
一个极简的接口定义大概长这样,用 IDL 描述的好处是契约先行,客户端和服务端从同一份文件生成代码,字段类型对不上编译期就能发现:
// 接口定义示例(Protobuf 风格) syntax = "proto3"; service OrderService { // 单次查询 rpc GetOrder (OrderQuery) returns (OrderReply); // 批量查询,适合列表页 rpc BatchGetOrder (BatchQuery) returns (BatchReply); } message OrderQuery { int64 order_id = 1; string trace_id = 2; // 链路追踪用,建议每个请求都带 } message OrderReply { int64 order_id = 1; int32 status = 2; int64 amount_cent = 3; // 金额统一用分,绝不用浮点 }这里有个很实际的细节:金额字段我从来不用浮点数。二进制序列化和 JSON 序列化对浮点的处理都有精度隐患,跨语言传输时出问题是迟早的事,统一用整数分或者字符串是行业里踩过太多坑之后的共识。
2.3 主流实现路线的对比与选型
RPC 的实现路线大致分三派,选型时对照着看会清晰很多。
| 路线 | 序列化特点 | 传输承载 | 性能量级 | 适合场景 |
|---|---|---|---|---|
| 文本型 HTTP RPC | JSON/XML,可读 | HTTP/1.1 | 中等 | 调试频繁、跨团队、开放接口 |
| 二进制 IDL RPC | 紧凑二进制,需生成代码 | HTTP/2 或私有协议 | 高 | 内部高频调用、强类型契约 |
| 私有协议 RPC | 自定义编解码 | 裸 TCP | 最高 | 超高频、低延迟、团队有能力自维护 |
我一般这么判断:内部调用量大、对延迟敏感的走二进制 IDL 路线;对外暴露、需要第三方或前端直接调用的走文本型;私有协议这条路,除非团队里有专门做中间件的人,否则不要碰,因为一旦出问题,排查工具链、抓包解析、跨语言支持全得自己补,投入产出比很低。
2.4 RPC 上最容易翻车的三个地方
第一个是超时设置。见过太多项目直接用一个全局默认超时,比如 3 秒,然后所有接口共用。结果是慢接口拖垮调用方线程池,快接口又因为超时太长导致故障恢复慢。正确做法是按接口的 P99 耗时分别设定,后端服务之间的调用超时应该比下游的实际耗时长一些但不能长太多,一般取 P99 的 1.5 到 2 倍。
第二个是重试与幂等。RPC 框架普遍支持失败重试,但重试必须建立在幂等前提上。查询类接口天然幂等,重试问题不大;写操作如果没做幂等设计就开重试,等于给系统埋了一个数据重复的定时炸弹。我的习惯是:所有写接口的请求参数里强制带一个业务唯一键,服务端用这个键做去重,这样重试就安全了。
第三个是连接管理。很多人只关注序列化性能,忽略了连接复用。每次调用新建 TCP 连接,握手开销加上 TIME_WAIT 堆积,在几千 QPS 的场景下足以把机器拖垮。连接池大小怎么定,后文会给出具体算法。
3. SOAP:被嫌弃的老将,契约精神其实没过时
3.1 SOAP 的构成:信封、头、体、契约
SOAP 的形态非常规范,一条消息就是一个 XML 文档,结构固定分三块:外层是信封(Envelope),里面装着消息头(Header)和消息体(Body)。消息头放的是与业务无关的横切信息,比如认证凭据、事务标识、路由标记;消息体放真正的业务数据。配套的还有一份接口契约描述文件,把服务有哪些方法、每个方法的入参出参结构、数据类型全写清楚,客户端可以据此自动生成代码。
<!-- SOAP 消息结构示例 --> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header> <auth:Token xmlns:auth="urn:example:auth">abc123</auth:Token> <trace:Id xmlns:trace="urn:example:trace">t-20240101-001</trace:Id> </soap:Header> <soap:Body> <order:GetOrder xmlns:order="urn:example:order"> <order:orderId>1001</order:orderId> </order:GetOrder> </soap:Body> </soap:Envelope>这套设计的优点是契约极其严格。数据类型精确到小数位数、日期格式、枚举取值,全都写死在契约里,两边生成的代码天然对齐。在跨组织、跨技术栈、跨年代的集成场景里,这种严格性是巨大的优势——你不会遇到"对方传了个字符串过来你到底该按数字还是按时间解析"这种破事。
3.2 SOAP 为什么被 REST 挤下神坛
原因不复杂,主要是三个字:太重了。
消息体积上,一个简单的查询请求,JSON 版本可能只有几十字节,XML 信封版本轻松几百字节,加上命名空间声明翻几倍。移动互联网起来之后,流量和解析开销都是实打实的成本。解析性能上,XML 解析比 JSON 慢一个数量级,在高并发场景下这是不可忽视的。开发体验上,改一个字段要先改契约、重新生成代码、两端重新发布,迭代速度被拖得很慢。相比之下,REST 风格改个字段可能只是加一行代码的事。
还有一层是工具链的生态转移。前端、移动端、脚本语言生态全面倒向 JSON 之后,XML 系的工具支持逐渐边缘化,新人学习意愿也低。技术选型从来不只是技术问题,生态和人力的权重往往更大。
3.3 今天还值得用 SOAP 的场景
我不建议新项目用 SOAP,但要说它彻底没用了,那是不客观的。有三类场景它依然有生命力。一是对接老系统。某些行业的历史系统只提供 SOAP 接口,你没得选,只能适配。二是需要严格契约保障的跨组织集成。当双方分属不同组织、无法通过代码评审和联调来对齐字段语义时,一份严格的契约描述文件能省掉大量扯皮。三是需要消息级安全与可靠传输的场景。SOAP 的消息头天然支持签名、加密、可靠投递这些规范,虽然这些能力在 REST 生态里也能拼出来,但需要额外搭轮子。
我实际操作中的做法是:如果必须对接 SOAP 服务,一定在中间加一层适配层,把 SOAP 转成内部统一的 JSON 接口,让业务代码只面对一种调用方式。这层适配代码大概一两百行,但能避免 SOAP 的复杂度污染整个代码库。
4. RESTful:一种架构风格,不是协议也不是框架
4.1 REST 的六条约束,真正落地的是哪几条
REST 原教旨定义了六条约束:客户端服务端分离、无状态、可缓存、统一接口、分层系统、按需代码。真正在工程里被严格执行的其实是前四条里的三条——统一接口、无状态、分层系统。所谓统一接口,落到实操就是三件事:用 URI 标识资源、用标准方法表达操作、用标准状态码表达结果。
"资源"这个词被误解得最厉害。资源不是数据库表,也不是实体类,而是业务概念。订单是资源,支付记录是资源,用户会话也可以是资源。判断一个设计是不是 RESTful,最简单的标准是看 URI 里有没有动词——/getOrderById、/createUser、/deleteItem这类命名,本质还是 RPC 思维套了层 HTTP 的壳。
4.2 RESTful 接口设计的实操规范
我总结了一套在自己项目里反复用的规范,直接抄作业基本不会出问题。
资源用名词复数,层级不超过两层:GET /orders/1001是查单个订单,GET /orders?status=paid&page=2是列表。超过两层的嵌套,比如/users/1/orders/2/items/3,可读性差且难维护,一般改成/order-items?orderId=2平铺出来更好。方法语义严格对应:GET 只读不改,POST 创建,PUT 全量更新,PATCH 局部更新,DELETE 删除。这里最容易犯错的是用 GET 做有副作用的操作,一旦被爬虫或预加载请求扫到,就是批量数据事故。
状态码要真的用起来,而不是全返回 200 再在 body 里塞错误码。这一点争议很大,我知道有些团队坚持业务错误返回 200,理由是前端处理统一。但从可观测性角度看,HTTP 状态码是网关、监控、日志系统能直接识别的信号,200 和 500 混在一个统计里,故障发现会慢半拍。
一个标准的响应结构大概是这样,注意错误码分两层:HTTP 状态码表达传输与语义层面的结果,业务码表达具体业务原因:
{ "code": 0, "message": "ok", "data": { "orderId": 1001, "status": "PAID", "amountCent": 12900 }, "traceId": "t-20240101-001" }分页参数我固定用page+pageSize或cursor+limit两套。前者用于后台管理这类需要跳页的场景,后者用于信息流这类无限下滑的场景。用游标分页能避免深翻页性能崩坏,这个坑我在数据量到千万级时踩过一次,LIMIT 1000000, 20这种查询直接把数据库拖到超时。
4.3 REST 和 RPC 的根本分歧在哪
分歧不在传输协议,两者都能跑在 HTTP 上。真正的分歧在于抽象对象是什么。RPC 抽象的是"动作",客户端想的是"我要执行什么操作",接口名是动词,参数是动作所需的输入。REST 抽象的是"资源",客户端想的是"我要对哪个资源做什么",接口路径是名词,方法语义表达动作。
这个差异在实际系统里会表现为不同的接口粒度。RPC 天然倾向于细粒度、面向用例,一个具体业务场景一个接口,参数只传这个场景需要的字段。REST 天然倾向于粗粒度、面向资源,同一份资源可以被多种场景以不同的方法访问。所以做后台管理类系统,RPC 风格往往更省事;做开放平台、需要被多方以各种方式访问的,RESTful 的资源模型更合适。
4.4 那些年见过的伪 REST
见过最典型的伪 REST 有三种。第一种是 URL 里放动词,前面提过了,/api/queryUserList这种一抓一大把。第二种是只用 POST,所有接口一律 POST,理由是"POST 能传大 body 而且没有缓存问题",这等于放弃了 HTTP 语义带来的所有好处。第三种是忽略状态码分层,所有的失败都返回 200 加{"success": false},让网关的限流、熔断、可观测体系全部失效。
这三种做法都不是"错",它们是 RPC 风格套了个 REST 的壳。我的态度是:要么老老实实按 REST 约束做,要么坦率承认这是 HTTP 上的 RPC,别叫 RESTful。命名不准确本身不致命,但它会让后来的人按错误的预期去理解和使用接口,这才是成本。
5. SOA:企业服务化的第一次集体尝试
5.1 SOA 的核心主张
SOA 出现的背景是:企业内部系统越建越多,每个系统各有一套用户体系、一套权限、一套订单逻辑,重复建设严重,跨系统的业务流程(比如下单要同时调库存、财务、物流)靠点对点集成,接口数量随系统数量呈平方级增长。SOA 的主张是把可复用的业务能力抽象成服务,通过统一的服务总线来集成,让系统之间不再直连,而是都接到总线上。
这个思路在当时是革命性的。服务总线承担了路由、协议转换、消息转换、安全认证、监控统计等职责,业务系统只需要暴露标准化服务,剩下的集成复杂度全被总线吞掉。服务的粒度通常比较粗,一个"客户管理服务"可能囊括了几十个操作,因为它对应的是完整的业务域能力。
5.2 重型 SOA 为什么在实战中翻车
理想很丰满,落地之后问题集中爆发在几个地方。
总线成了单点瓶颈和变更瓶颈。所有流量都过总线,性能压力巨大;更麻烦的是,任何新服务上线、任何接口改动,都要在总线上配置、测试、走审批。总线的配置复杂度很快就超过了业务本身,团队里必须专门养几个人维护它。我见过一个项目,总线的规则配置文件有上万行,没人敢动,因为改错一行可能影响全公司的接口。
服务粒度过粗导致复用变成伪复用。名义上一个服务被十个系统调用,实际上每个系统只用其中一两个操作,还得忍受其他操作带来的契约约束。想改任何一个操作,都要评估十个调用方的影响,最终谁都不敢改。
契约与实现强耦合。SOA 时代大量使用 SOAP 作为服务的对外形态,前面说过它的严格性,好处是对齐,坏处是每次变更成本极高。
治理是集中式的。服务的注册、版本、路由都归一个团队管,业务团队想快速迭代,得排期等治理团队配合。这种组织摩擦在小步快跑的业务节奏下是致命的。
5.3 SOA 留下了什么遗产
说 SOA 失败其实不公平,它留下的东西今天还在用。服务注册与发现的思想来自它,服务契约先行的理念来自它,通过中间层解耦系统的思路也来自它——今天网关、服务网格干的事情,本质上还是服务总线那套,只是把重的东西拆轻了、把集中式改成了分布式。
更重要的遗产是一个教训:集成层的复杂度不会消失,只会转移。SOA 把复杂度堆在总线上,微服务把复杂度分给每个服务自己承担。哪个更好取决于团队规模和能力,而不是架构本身的先进程度。这一点我在后面讲微服务代价时还会展开。
6. 微服务:SOA 的进化,更是对它的反叛
6.1 微服务的四个硬性特征
微服务不是"把单体拆小"这么简单,它有四个绕不开的特征。一是按业务能力拆分,每个服务对应一个可以独立演进的业务域,而不是按技术分层拆。二是独立部署,改一个服务不需要重新发布整个系统,这是微服务最核心的价值来源。三是去中心化的数据管理,每个服务管自己的数据,不共享数据库。四是轻量通信,服务之间用简单的协议交互,不走重型总线。
第四条经常被忽略但极其关键:如果服务之间还共享一个数据库,那无论拆成多少个部署单元都不算微服务,因为你没有获得独立演进的能力,schema 一改全挂。我在评审时看到"微服务"项目共享一个库,基本可以直接判定这次拆分大概率会失败。
6.2 微服务和 SOA 的六处关键差异
| 维度 | SOA | 微服务 |
|---|---|---|
| 服务粒度 | 粗,按业务域甚至按系统 | 细,按业务能力 |
| 集成方式 | 集中式服务总线 | 轻量点对点 + 服务发现 |
| 通信协议 | 重型,多为 SOAP | 轻量,REST 或 RPC 皆可 |
| 数据管理 | 倾向共享 | 严格私有 |
| 治理模式 | 集中治理 | 分散治理,团队自治 |
| 部署单元 | 常常整体部署 | 独立部署,独立伸缩 |
看这张表最该关注的是治理模式这一行。很多人以为微服务和 SOA 的区别在技术,其实最大的区别在组织。微服务要求团队对服务有完整的 ownership——从设计、开发、部署到线上值班全都归这个团队。没有这个前提,微服务就退化成了"拆得很碎但依然要排队发布的 SOA",复杂度上去了,收益一点没拿到。
6.3 拆分的边界怎么定:一个实操判断法
服务边界的划分是微服务落地里最难的一步,我用的判断法比较土但有效,就三条。
第一条,看变更频率是否一致。两个功能如果总是一起改,那就不该拆开,拆了之后每次都要双发;如果它们的变更节奏完全不同,一个每周发三次一个每季度发一次,那就该拆。
第二条,看数据是否需要强一致。如果两个功能必须在同一个事务里完成,拆开就要引入分布式事务,成本陡增。这种情况我倾向于先不拆,用一个服务扛着,等业务成熟了再考虑用最终一致性方案重构。
第三条,看团队规模。经典的"两个披萨能喂饱"的团队规模是有道理的——一个服务最好由一个团队完全负责。如果你是五个人的团队,拆出二十个服务,那维护成本会直接压垮你。服务数量应该跟团队数量大致匹配,而不是跟想象中的架构蓝图匹配。
6.4 微服务的账单:你必须承受的代价
这一节是我最想强调的,因为讲微服务好处的文章太多了,讲代价的太少。
第一笔账是分布式复杂度。一次本地方法调用变成一个网络调用,你就得处理超时、重试、熔断、降级、链路追踪、日志聚合。这些都是基础设施,不做就是线上事故,做了就是几个月的投入。再加上分布式事务、数据一致性、幂等设计,复杂度是数量级的跃升。
第二笔账是运维成本。二十个服务意味着二十套部署流水线、二十份监控面板、二十组告警规则。容器化和编排系统能缓解一部分,但配置管理、版本管理、依赖管理的工作量还是实实在在地涨了。
第三笔账是调试难度。一个请求穿过六个服务,出问题时你要在六个服务的日志里拼出完整链路。没有统一的 traceId 贯穿,排查基本靠猜。所以链路追踪在微服务里不是可选项,是必需品,这一点必须在项目启动前就定下来。
第四笔账是团队能力要求。微服务对团队的工程素养要求高很多,尤其是自动化测试、持续集成、可观测性这些基本功。基本功不到位的团队上微服务,结果往往是发布更慢、故障更多。我见过不止一个团队拆完之后后悔,又花半年往回合并。所以如果团队还在手工发布、没有自动化测试覆盖,我的建议是先做单体模块化,把内部边界划清楚,等工程能力上来了再拆。
7. 横向对比与选型:什么场景走什么路
7.1 五个概念的决策对照
| 你的问题 | 该看哪个概念 | 判断依据 |
|---|---|---|
| 接口怎么设计 | RESTful / RPC | 面向资源还是面向动作;调用方是谁 |
| 消息怎么编码 | SOAP / JSON / 二进制 | 契约严格度、性能要求、对接方能力 |
| 系统怎么拆 | SOA / 微服务 / 单体模块化 | 团队规模、变更频率、一致性要求 |
| 服务间怎么通信 | RPC / REST | 性能敏感度、是否跨团队、调试便利性 |
| 要不要上总线 | SOA / 网关 | 集成系统数量、治理集中度需求 |
这张表的价值在于把"选架构"这个模糊问题拆成了五个具体问题。实际项目里这五个决策是分别做的,做完之后自然形成一个组合。比如一个电商后台:对外接口用 RESTful,内部高频调用用二进制 RPC,系统按订单、库存、支付拆成三个服务,不引入重型总线,用一个轻量网关做统一入口。这个组合里每个词都用上了,但没有任何冲突。
7.2 超时、重试、连接池的参数怎么算
这部分是纯工程细节,也是最能体现经验的地方。假设你的服务峰值 QPS 是 1000,下游接口的 P99 耗时是 50 毫秒,怎么配?
并发数估算:并发 = QPS × 平均耗时(秒)。按 1000 × 0.05 = 50,也就是平均有 50 个请求在处理中。但峰值不可能这么平滑,实际要留余量,一般按 1.5 到 2 倍估,取 75 到 100。
连接池大小:不能小于并发数,否则请求会排队等连接。我一般设成并发估算值的 1.2 倍左右,也就是 90 到 120。同时要设最大等待时间,超过就直接快速失败,而不是无限排队——无限排队在故障时会引发雪崩。
超时时间:取 P99 的 1.5 到 2 倍。50 毫秒的 P99,超时设 100 毫秒比较合适。设成 3 秒这种默认值,等于让故障请求占用线程 3 秒,线程池会被迅速打满。
重试次数:只对幂等接口开重试,次数不超过 2 次,并且必须配合退避策略,比如首次重试间隔 50 毫秒、二次 200 毫秒,而不是立刻重试——立刻重试在下游刚出现抖动时等于加压。另外要设置重试预算,比如重试请求不超过总请求的 10%,超过就整体熔断,避免重试风暴。
这四组参数不是配完就完事,要随业务量定期回顾。我一般每季度看一次监控,如果发现大量超时集中在某个接口,那就是参数该调或者该优化下游了。
7.3 从单体到微服务的演进路线
我不建议一步到位。比较稳妥的路径是四步。
第一步,单体内部模块化。代码层面先把业务边界划清楚,模块之间只通过明确定义的接口交互,禁止跨模块直接访问数据表。这一步不改部署形态,风险最低,收益是把边界摸清楚了。
第二步,把无状态、变更频繁的模块抽出来。比如鉴权、消息推送、文件处理,这些模块跟核心业务耦合弱,抽出来风险小,还能立刻获得独立伸缩的好处。
第三步,拆分数据。这是最难的一步,要把共享库按模块切开。做法一般是先加一层数据访问接口,让业务代码不直接碰其他模块的表,然后再物理分库。这一步通常要几个月,急不得。
第四步,补齐基础设施。链路追踪、统一日志、配置中心、服务发现、熔断限流,这些要在服务数量还不多的时候建好,等拆到十几个再补,成本会高很多。
每一步都可以停下来,也都可以回退。这种渐进式路径最大的好处是每一步都能验证收益,如果某一步拆完发现收益不明显,那就停在那儿,不必勉强走到终点。
8. 踩坑实录与问题速查
8.1 高频问题速查表
| 现象 | 常见原因 | 排查方向 | 处理办法 |
|---|---|---|---|
| 接口偶发超时 | 连接池耗尽或下游慢查询 | 看连接池等待时间、下游 P99 | 扩池 + 加超时上限 |
| 重复数据 | 重试未做幂等 | 查请求唯一键是否落库去重 | 补幂等表或唯一索引 |
| 错误码混乱 | 全返回 200 | 看网关错误率统计 | 分层状态码 + 业务码 |
| 服务拆分后发布更慢 | 流水线未独立 | 查每次发布影响的模块数 | 拆分流水线,独立部署 |
| 跨服务查询变慢 | 循环调用或 N+1 | 看单请求的跨服务调用次数 | 批量接口 + 缓存 |
| 契约变更引发故障 | 未做版本兼容 | 查字段增删是否破坏旧客户端 | 只增不删,废弃字段保留 |
8.2 几条不太好写进文档的经验
接口契约只增不删。字段想废弃,标记为 deprecated 之后至少保留两个大版本,因为永远有你不知道的调用方在用。这个规则在对外开放接口上尤其重要,内部接口可以激进一些但要提前通知。
跨服务的批量查询一定要设计。单体时代for循环查库没人管,拆成服务之后每个循环都是一次网络调用,N+1 问题会被放大十倍。我在设计任何列表接口时,都会同步设计一个批量查询接口,哪怕一开始用不上。
链路追踪的 ID 必须从入口贯穿到底。网关生成,所有服务透传,日志里打印。这件事的价值只有在一次跨六个服务的故障排查中才能真切体会到,那时候你会庆幸当初多写了几行代码。
不要为了架构而架构。我见过最离谱的一个项目,三个人维护十五个微服务,每天一半时间在处理服务间的兼容问题,业务需求排期排到下个季度。判断标准很简单:如果拆完之后你的发布频率没有提升、故障恢复时间没有缩短,那这次拆分就是失败的,不管架构图画得多漂亮。
SOA 时代的教训要记住。集成复杂度不会凭空消失。你今天省下的治理工作,明天会以另一种形式出现在别的地方。所以在引入任何"中间层"的时候,先问一句:这个东西三五年后会不会变成没人敢动的黑盒?如果答案是会,那就要在设计之初就给它留好拆解的口子。
最后分享一个我自己的习惯:每接手一个新系统,我会先画一张图,横轴是部署单元,纵轴是通信方式,把每个服务填进去。这张图画完,架构的真实面貌就出来了——很多时候它跟文档里写的完全不是一回事,而这张图才是你后续做任何决策的真实起点。