分布式系统进程间通信:从RPC到gRPC的演化与选型 话说之前接手过一个项目一开始是单体应用所有模块都在一个进程里跑互相调用就是普通的函数调用代码写得飞起。后来业务量上来老板说要拆服务把用户模块、订单模块、支付模块拆成独立的服务进程。这一拆原来最简单的userService.getUserById(id)就成了跨进程调用。进程和进程之间怎么沟通数据怎么传这背后的技术就是标题里写的——进程间通信IPC里的RPC。这几年RPC相关的技术演化非常快从早期的RPC到SOAP再到REST再到gRPC、Thrift、GraphQL每个阶段解决的核心问题都不一样踩过的坑也各有各的精彩。这篇文章我不打算按照教科书的方式给你念概念我打算按照一条真实的技术演化线把RPC、SOAP、REST、gRPC/Thrift/GraphQL这五六个关键词串起来讲明白讲清楚每一代技术为什么会出现、解决了什么问题、又因为什么瓶颈被下一代替代。最后会聊一聊当下的技术选型站在2024年往后看什么场景该用哪种方式。1. RPC到底在解决什么问题从本地函数到远程调用的桥接1.1 一个拆分服务后的困惑先回到最开始的场景。单体应用拆成多个服务之后用户服务和订单服务分开了它们可能跑在不同机器上甚至不同机房。这时订单服务要查用户信息不能再直接调userService.getUserById()了因为那不是同一个进程。摆在面前的第一反应是那我用 HTTP 接口呗。订单服务发一个 HTTP 请求给用户服务用户服务处理完返回 JSON。这确实是最朴素的方式很多团队现在也是这么干的。但这里有一个问题HTTP 接口的写法跟本地函数调用差距很大。每次调用都要自己去拼 URL、设置请求头、序列化参数、解析响应、处理各种网络异常。代码里到处散落着HttpClient的调用逻辑业务代码被网络细节搅得乱七八糟。1.2 所谓透明调用远端函数像本地函数一样用RPC 的核心诉求说白了就四个字透明调用。我希望调用远端服务的过程体验上跟调用本地函数一样——我传入参数拿到返回值至于这个函数到底在哪个机器上执行网络层怎么传数据序列化怎么搞这些细节全部被框架隐藏掉。这个诉求非常好理解你看 Java 里早年的 RMIRemote Method Invocation就是干这个的。客户端定义一个接口实现类在服务端客户端拿着接口的代理对象直接调用框架通过动态代理把方法名、参数打包通过网络发送到服务端服务端执行完再把结果传回来。整个过程对调用方来说确实是透明的。这种透明调用最大的价值在于代码的调用语义得到保留。业务代码从“我要发起一个网络请求地址是xxx方法是xxx参数是xxx”变成了“我要调一个函数它的名字叫 getUserById参数是 id”。这样业务逻辑的抽象层次一下就上来了代码的可读性和可维护性都更好。1.3 让 RPC 跑起来的四个基础环节一个 RPC 框架要实现透明调用绕不开四个环节代理Proxy客户端持有一个接口的代理对象。调用代理对象的方法时框架拦截这次调用把方法名、参数类型、参数值等信息抓取出来。序列化Serialization把抓取到的调用信息转换成字节流。Java 原生序列化、JSON、XML、Protobuf 都可以干这个事。网络传输Transport把序列化后的字节流通过 TCP、HTTP 等协议发送到服务端。同步阻塞也好异步非阻塞也罢框架要解决连接管理、超时、重试等问题。服务端处理Server Stub服务端收到字节流后反序列化找到对应的实现类和他的方法反射调用拿到结果再序列化传回客户端。这四个环节的选型不同直接决定了 RPC 框架的性能和易用性。早期 RPC 大多基于 TCP 直连和二进制序列化性能不错但跨语言就很吃力了。Java RMI 只能 Java 和 Java 通信C 想调 Java 的服务门都没有。这类私有协议能力越强往往封闭性也越强这是第一代 RPC 最大的矛盾。2. SOAP与XML-RPC时代为了标准化付出的重量级代价2.1 跨语言的需求催生了标准化协议本地进程时代的函数调用都是编译器在管而分布式环境里的调用必须面对一个残酷现实不同服务可能由不同团队用不同语言开发。Java 团队提供服务PHP 团队要用C# 团队也要用。如果每个 RPC 框架都靠自家私有协议那跨语言协作就是天方夜谭。所以 90 年代末到 2000 年初行业开始折腾“标准化”这件事。思路是这样的既然平台上五花八门那我干脆定一个所有平台都能理解和处理的“通用语言”把所有调用信息用这个通用语言描述出来通过网络传输对方再用这个通用语言解析。这个通用语言就是 XML。基于 XML 的 RPC 协议最出名的就是 XML-RPC 和 SOAP。2.2 SOAP 的设计逻辑SOAPSimple Object Access Protocol简单对象访问协议从名字看挺“简单”实际用起来是另一回事。它的消息结构是 Envelope信封、Header头、Body体。信封标明这是 SOAP 消息Header 放事务、认证等元信息Body 放真正的调用参数和返回值。SOAP 解决跨语言问题的关键不只是消息格式而是配套的整套规格WSDLWeb Services Description Language用来描述服务接口的 XML 文档。它列出了服务有哪些方法、每个方法的参数类型、返回类型。客户端根据 WSDL 就能生成调用代码。UDDIUniversal Description, Discovery, and Integration服务注册和发现中心想找某个服务就上 UDDI 查。SOAP 本身对传输层不挑剔可以用 HTTP也可以用 SMTP邮件传输协议。大部分场景走 HTTP POST。这里的核心思路是“契约优先”。先有 WSDL 描述接口契约再根据契约生成客户端和服务端代码。跨语言通过标准契约 标准消息格式来解决理论上我给你一个 WSDL你用工具自动生成调用代码就无需关心服务端是什么语言写的了。2.3 SOAP 为什么让开发者又爱又恨说句公道话SOAP 在跨语言互操作、复杂企业级事务、安全性这些方面做得其实挺扎实。银行、电信、航空这类大型企业系统不少老系统之间至今仍然在用 SOAP 通信。但问题也同样明显消息体极其臃肿。一个简单的getUserById(123)调用SOAP 需要包裹一大堆 XML 标签里面塞上命名空间、方法名、参数类型、参数值一个手指头就能数清楚的操作消息体积能到几 KB 甚至更大。解析 XML 非常消耗 CPU 和内存。性能上跟二进制序列化完全没法比。高并发场景下光序列化/反序列化就得吃掉大量资源。学习曲线陡峭。WSDL 的 schema 规则、SOAP 的扩展规范、WS-* 那一大堆安全、寻址、事务规范新手光是把环境跑通就得翻好几篇文档。调试不方便。现代开发习惯用浏览器直接看网络请求SOAP 的请求都要用专门的工具去构造肉眼排查问题特别费劲。SOAP 这套体系对应的时代背景是企业应用集成强调可靠性强调标准化性能反而是其次。但当互联网业务爆发需要的是轻量、灵活、高吞吐的通信方式SOAP 自然而然就成了阻碍。3. REST是如何重构分布式调用思路的3.1 资源思维从动词到名词REST 出来的时候很多人都把它当成“不用 SOAP 换个轻量点的写法”但这种理解浅了。REST 最大的贡献是改变了对远程调用的思考方式。之前的 RPC 思路是什么是“我要调用一个方法”。方法天然是动词getUser、createOrder、deleteItem。REST 的思路是远程调用不是“调用方法”而是“操作资源”。用户是一个资源订单是一个资源评论是一个资源。资源用 URI 来标识URI 是名词/users/{id}、/orders/{id}。对资源的操作统一收敛到 HTTP 方法上GET 查、POST 增、PUT 改、DELETE 删。这种转变不是文字游戏。它带来几个实质性的好处接口语义统一。以前两个 RPC 接口可能一个叫getUser一个叫queryUserInfo实现的功能差不多的但你不看文档就不知道。REST 下这个资源就是 GET/users/{id}不会再有第二个写法。无状态性。REST 要求每次请求携带全部信息服务端不保存客户端上下文。这意味着服务可以随意横向扩展任意节点都能独立处理请求这是微服务架构最喜欢的性质。利用 HTTP 本身就是一种成熟的分布式基础设施。缓存GET 天然可缓存、代理、负载均衡、认证授权这些 HTTP 生态里已有的能力直接复用不需要自己造轮子。3.2 REST 不是 RPC 的反对而是对接口设计的降维很多人把 REST 和 RPC 放在对立面这其实是个误解。REST 同样是远程调用本质上还是请求-响应、调用-返回的流程只是它在协议语义层做了重新设计。REST 没有取消序列化没有取消网络传输它只是把“方法调用”变成了“资源表达”。举个例子同样是查询用户信息RPC 风格可能是User getUserById(long id);REST 风格是GET /api/v1/users/123服务端一样要接收参数、查询数据库、返回结果。区别在于RPC 风格里调用方脑子里想的是“哪个函数”REST 风格里调用方脑子里想的是“哪个资源要做什么操作”。这里就引出一个很重要的话题REST 相对于 SOAP 更简单但相对于早期私有 RPC它也不是银弹。REST 的优点在于简洁、直观、生态好。缺点在于它把很多东西“留给约定而不是规范”。比如资源怎么命名、错误怎么表达、批量操作怎么办、复杂查询怎么设计——REST 没有统一答案。于是大家看到同一个业务一千个团队有一千种 REST API 设计。有人把 GET 当 RPC 用路径上直接写GET /api/getUserById也有人把所有逻辑都塞进 POST。REST 的宽松既是它的成功原因也是它的混乱根源。3.3 REST 在实际项目里的边界实践下来你会发现REST 特别适合面向外部系统提供标准化接口也适合浏览器直接发起的请求。因为浏览器天然支持 HTTP你的 API 用 REST 风格设计前端工程师开箱即用甚至直接在地址栏里输入 URL 就能看到 JSON 返回。但 REST 在服务内部的密集调用场景也有尴尬的地方。最典型的是“接口爆炸”和“数据过度获取”。订单列表页需要展示订单数据以及每个订单关联的商品概要。REST 的做法是先GET /api/orders拿到订单列表后对每个订单再GET /api/orders/{id}/products如果一页有 20 个订单前端要发 21 次 HTTP 请求。这在浏览器端还能接受但在服务端内部的高频调用链路上就很蠢。于是出现了 BFFBackend For Frontend这种设计安排一个聚合层在前端和服务之间做一个定制化的 API帮前端把那些细碎的资源调用聚合起来。BFF 层本身做的事情其实就是一种“自定义的远程调用聚合逻辑”某种意义上说它是在用 RPC 的方式在 REST 外面套了一层皮。4. gRPC与Thrift二进制协议和高性能IDL重新定义RPC4.1 服务化框架带来的硬核需求进入微服务时代之后服务节点之间的大规模高频调用成了日常。一个请求进来可能要在服务网格里经过几十个服务节点接力处理。这个时候HTTP JSON 的方式在性能和连接管理上都开始吃不消。JSON 虽然人类可读但解析性能天然比不过二进制格式而且 HTTP/1.x 每个请求一个连接频繁建连开销很大尽管有 HTTP 连接池但头部开销依然不小。所以行业对这个阶段的需求很直接我要一个更高效的 RPC 框架序列化要快包体要小连接要复用最好还能支持请求-响应之外的流式通信模式。gRPC 和 Thrift 就是在这个背景下被大规模采用的。4.2 Protobuf HTTP/2gRPC 的核心杠杆gRPC 的设计有一个很明显的技术杠杆组合IDL 用 Protobuf传输走 HTTP/2服务模型支持四类方法。先说 Protobuf。它定义了一个.proto文件里面写清楚 service 有哪些 rpc 方法每个方法的请求消息和响应消息长什么样。写完.proto之后用 protoc 编译工具自动生成各语言的客户端和服务端代码。这个模式跟 SOAP WSDL 有点像都是“契约优先代码生成”但 Protobuf 的编码效率和体积远非 XML 能比。一个整数字段可能只占几个字节字段名在传输中根本不出现取而代之的是字段编号。这样的设计大大压缩了传输包体。再说 HTTP/2。HTTP/2 的多路复用允许在同一个 TCP 连接上并发交叠多个请求和响应彻底解决了 HTTP/1.x 的队头阻塞问题。头部压缩HPACK也让每次请求的头部体积变小。这些特性让 gRPC 在高并发、多请求场景下有很明显的优势。然后是四类方法这是 gRPC 区别于传统请求-响应模式的地方一元调用Unary一个请求对应一个响应跟普通 RPC 一样。服务端流式Server Streaming客户端发一个请求服务端持续返回多个消息。比如订阅一个数据源服务端有更新就推过来。客户端流式Client Streaming客户端持续发送多个消息服务端最后返回一个响应。比如上传一个大文件分片发送最后服务端确认。双向流式Bidirectional Streaming两边同时持续发送消息像聊天一样。这四种模式让 gRPC 的处理能力远超单纯的“调用-返回”。对实时性要求高、数据是持续产生的场景流式调用是天然匹配的。4.3 Thrift 的分层设计Thrift 是另一个流传很广的高性能 RPC 框架它出身于大型社交平台的技术栈后来捐给了开源社区。Thrift 的设计思路跟 gRPC 很像——用 IDL 定义接口代码生成器生成多语言代码二进制序列化。Thrift 的一个重要特点是分层架构清晰传输层Transport负责数据的读写支持内存缓冲、文件、网络 Socket 等。协议层Protocol负责序列化和反序列化支持二进制协议TBinaryProtocol、压缩二进制协议TCompactProtocol、JSON 协议TJSONProtocol等。处理器层Processor接收协议层解析出的调用请求根据方法名找到具体的处理器实现。Thrift 的分层让它可以灵活组合同样的 IDL 定义你想用二进制协议就走 TBinaryProtocol你想调试就走 TJSONProtocol不需要改业务代码。这个灵活性在实际运维中很实用毕竟复杂系统里“能调试”本身就是一种救命能力。4.4 gRPC 和 Thrift 的对比与选型差异两者都是现代高性能 RPC 框架的主流选择但使用体验上有明显区别这不只是技术对比其实是不同哲学的对决维度gRPCThrift传输层强制依赖 HTTP/2自定义通常走 TCP也可选 HTTP序列化Protobuf必需多种协议可选默认二进制语言支持官方多语言生态丰富多语言C/Java/Python 出身流式支持原生支持四类方法需要根据传输层自行设计流式支持相对弱生态工具跟云原生、服务网格深度集成传统 RPC 场景更易部署学习门槛需要理解 HTTP/2调试要配合工具协议清晰直接容易上手我在实际项目里的体感是如果系统跟容器化、K8s、Istio 这类云原生基础设施绑定很深gRPC 基本是默认最优解因为服务网格对 gRPC 的支持最好。如果只是 Java/Python/C 微服务之间需要高性能通信不想引入 HTTP/2 这一层额外复杂度Thrift 也能干得很好。不过这两个框架都有一个共通的劣势对人类不友好。你没法在浏览器里直接发起一个 gRPC 调用看返回也没法用一个文本编辑器打开 Thrift 的请求体去数字段。调试和排查问题都需要专门工具链这跟 REST 的“拿浏览器就能试”相比是实实在在的成本。5. GraphQL从API设计角度杀回RPC家族的异类5.1 客户端驱动的接口语言GraphQL 严格来说不算 RPC 框架但它解决的问题跟 RPC 的“接口调用”高度重合而且它的设计思路跟 REST 和 gRPC 都不一样属于 API 设计谱系中绕不开的一支。GraphQL 的核心特点是客户端决定要什么。客户端通过查询语句声明式地描述所需的数据结构服务端负责返回完全匹配的数据形状。REST 模式下资源结构是服务端定的。比如GET /api/user/123返回的字段是固定的多一个还是少一个都是后端说了算。前端想要的字段不在这份响应里就得额外再发请求或者干脆指望后端加字段。GraphQL 把选择权交到了客户端手里query { user(id: 123) { name email posts { title createdAt } } }这一个查询同时拿到了用户信息和用户发过的帖子标题列表且只拿需要的字段。一段查询替代了多次 REST 请求网络往返次数大幅降低响应体也刚好是前端想要的大小。5.2 为什么说 GraphQL 是“RPC 家族”的一员有人会问GraphQL 不是跟 REST 对着干的吗怎么又跟 RPC 有关系了其实从本质上看GraphQL 的架构和 RPC 框架非常接近。GraphQL 服务有一个统一的端点客户端发送查询语句服务端解析查询、执行解析器resolver、返回结果。查询语句的语义就是“我要调用一个类型系统里定义的操作”。GraphQL Schema 相当于 IDLResolver 相当于服务端的方法实现。客户端按需查询相当于“按需调用特定的数据聚合逻辑”。而且 GraphQL 通常替代的正是 REST 那一层接口定义。一旦部署了 GraphQL前端的“资源请求”变成了“声明式查询”服务端的方法变成可组合的 resolver——这跟 RPC 的“接口-实现-代理”模型是相同的骨架区别只在于表达层的设计。所以我觉得把 GraphQL 当成 RPC/API 演化谱系的一道分叉比单纯拿它跟 REST 对比要更准确。它补的是 REST 在“灵活取数”和“多资源聚合”上的短板代价是引入复杂的查询层和缓存难题。5.3 GraphQL 在实际落地时的甜头和暗坑我深度用过一段时间的 GraphQL甜头是真的明显。前端需求变化的时候后端不需要频繁改接口只要 Schema 允许前端自己改查询语句就行了。这对于前后端并行开发和接口迭代快的业务电商、内容类产品特别友好。暗坑也一个接一个N1 查询问题。你在 GraphQL 里查 10 个用户每个用户要查自己的帖子如果你的 resolver 写得粗糙服务端会执行 1 次用户查询 10 次帖子查询。要解决这个得引入 DataLoader 之类的批处理方案把 N1 合并成 2 次查询。缓存复杂。REST 的 GET 请求天然可以配合 HTTP 缓存GraphQL 因为整个查询都打同一个端点缓存粒度很难控制通常得在产品层做定制缓存。查询复杂度不可控。客户端理论上可以写一个查询把整个 Schema 都拉一遍服务端不堪重负。所以必须做深度限制、复杂度校验、超时控制这些额外工程。GraphQL 适合的场景是“数据需求多变、前端强驱动的应用”。如果业务接口非常稳定或者服务之间是内部密集型调用GraphQL 带来的灵活性反而会成为性能开销和运维负担。6. 技术选型实战站在今天我到底该用哪种6.1 从通信距离看选型模型写到这里核心的技术演化线已经梳理完了。最后聊聊一个非常实际的问题现在手里有个新项目到底选哪种我的经验是把“调用双方的距离”当作第一层判断标准进程内不用谈 RPC直接函数调用。进程间但同属一个应用系统比如微服务之间内部调用。首选 gRPC 或 Thrift。进程间但跨应用、面向外部比如开放 API、第三方系统对接。首选 REST若前端的数据需求极其多变考虑 GraphQL。进程间但需要遗留系统互操作、强事务/安全合规如金融、电信领域的异构系统SOAP 仍有一席之地但新项目不建议从零搭 SOAP。6.2 微服务内部通信的推荐组合内部服务间通信现在的推荐组合我比较倾向 gRPC原因不只是性能而是整个云原生基础设施的集成度。Envoy、Istio 这类服务网格对 HTTP/2 和 gRPC 做了深度优化链路追踪、负载均衡、健康检查都是开箱即用的。如果你们公司已经上了 K8sgRPC 几乎是顺理成章的选择。但有几点需要提前确认团队的语言生态是否都支持 gRPC 官方库。大多数现代语言都支持但如果团队里有纯 C 或老旧的嵌入式环境还是得评估成本。调试工具链是否齐全。gRPC 不像 REST 一样浏览器直接访问需要引入 grpcurl 和 grpcui 这类辅助工具。服务端是否要处理浏览器请求。如果服务既要服务内部又要接浏览器长连接那内部用 gRPC对外暴露 REST/HTTP 网关是常见的做法。6.3 高并发场景下的一些性能细节性能永远是 RPC 选型的核心话题。很多人在 gRPC 和 REST 之间纠结时一上来就比 JSON 和 Protobuf 的解析速度这个方向没错但不全面。我实测下来在高并发场景下瓶颈往往不在序列化本身而在连接管理和协议开销HTTP/1.x 的 REST每个请求通常独占一个连接虽然可以用连接池但多个请求要在应用层排队。gRPC 的 HTTP/2 多路复用能在同一连接上并发交叠多个请求延迟和连接资源开销都更低。Protobuf 的包体比 JSON 小很多意味着相同带宽下能承载更多的请求量。不过如果你的服务本身处理逻辑很重比如一次调用要查一堆数据库表、做一堆复杂计算那序列化省下的那点时间几乎可以忽略不计这时的选型重点反而应该放在开发效率和可维护性上。性能优化要放在整个链路的视角去看单点优化不等于链路优化。6.4 给新手的实操建议顺序如果你是第一次接触这块内容我不建议一下子把 SOAP、REST、gRPC、Thrift、GraphQL 全学一遍。最务实的路径是先把 REST 吃透多写几个接口搞清楚 HTTP 方法语义、状态码、缓存、认证等基础知识。再学 Protobuf 和 gRPC理解它为什么比 JSONHTTP 高效用官方的示例跑通一个服务端和客户端的完整链路。然后抽时间研究 GraphQL 的 Schema 设计思路它的类型系统和 resolver 机制会让你对接口设计有更深的理解。最后有精力再去了解 SOAP 的历史演进和底层思想不是为了用而是为了理解标准化协议的设计逻辑。在技术选型上还有一个建议同一个系统里不要同时出现太多种通信方式。有的团队内部服务用 gRPC对外用 REST网关再挂个 GraphQL为了一个简单功能把三种协议串在一起调试起来非常痛苦。通信方式的多样性本身是一种复杂度能收敛就收敛。最后分享一个个人经验。有一次我在一个项目里图省事内外统一用 REST JSON结果到了峰值内部调用链路延迟明显变大排查以后发现大部分时间耗在 JSON 序列化和频繁建连上。后来把内部调用迁到 gRPC对外接口保持 REST整体延迟降了一个量级连接数也下来了。反观另一个项目上来就全面拥抱 GraphQL前端倒是爽了但服务端查询优化和数据加载器折腾了好几轮才稳定。技术选型没有放之四海而皆准的答案核心还是要回到调用双方的距离、性能诉求和团队维护成本这三个维度上去权衡。