【分布式】面试官问 RPC,该怎么答?
RPC(Remote Procedure Call,远程过程调用)可谓是分布式微服务系统里最重要的组件之一,它是各服务间跨进程通信的桥梁,也是微服务、大型分布式系统的基石。
其作用是:让客户端(调用方)调用远程服务器(被调用方)上的方法,像调用本地方法一样简单。
1、什么是 RPC,项目中具体做什么用?
RPC 是一种跨进程通信协议,它允许客户端(调用方)像调用本地函数一样,调用远程服务器(被调用方)上的函数或方法,无需手动处理网络通信、数据序列化 / 反序列化等底层细节。
举个生活例子:你(客户端,比如是微服务中的一个订单服务)想喝奶茶,直接打电话给奶茶店(服务端,比如是微服务中的一个支付服务)下单(调用方法),无需知道奶茶店的制作流程(底层逻辑),只需等待奶茶送到(接收返回结果)——RPC 就像 “电话 + 配送” 的组合,让远程调用变得和 “自己做奶茶” 一样简单。
前面文章讲过为什么要构建微服务,而 RPC 就是不同服务之间的桥梁。
- 如果项目不是微服务架构,那么订单、库存,支付这一系列的功能都是在一个库里面的(相当于在一个屋子里,低头不见抬头见),他们之间的通信其实就是在本地内存中互相调用;
- 如果项目是微服务项目,那么这些不同的服务可能部署在不同的服务器(相当于在不同的城市),他们之间需要知道其他服务的 IP、端口号才能知道对方在哪,然后通过 HTTP 等协议在网络中通信。而 RPC 就是帮助我们屏蔽掉这些网络通信细节,像调用本地方法一样调用其他服务里的方法。
2、RPC 解决了什么问题?用 RPC 的优势是什么?为什么不直接用HTTP
- 解耦服务调用,屏蔽网络细节:开发者无需关注 “如何建立 TCP 连接”,“如何处理数据包丢失”,“如何解析二进制数据”,只需专注业务逻辑。
- 标准化通信协议:RPC 框架定义了统一的请求 / 响应格式、序列化规则,避免不同服务间因通信格式不统一导致的兼容问题。
- 提升分布式开发效率:将复杂的分布式调用简化为 “本地函数调用”,降低开发门槛,减少重复编码(如手动封装网络请求)。
- 支持跨语言通信:主流 RPC 框架(如 gRPC)支持多语言调用(Java 调用 Go、Python 调用 C++),满足异构系统需求。
- 性能更好,传输速度快:RPC 可以基于二进制协议(Protobuf/Thrift),体积更小、性能更高。
3、RPC 底层是如何实现的?如何设计一个 RPC ?
介绍 RPC 底层,其实就是把其使用过程中屏蔽掉的网络通信细节描述清楚。
这里只简单讲解下通用的 RPC 通信全过程(面试一般也够用了),具体底层细节感兴趣的同学可以去深入学习,比如可以去看 gRPC,Dubbo 这些主流 RPC 框架的源码。
客户端存根 Stub / 服务端骨架 Skeleton
RPC 中的 Stub / Skeleton 相当于一个代理人,帮助我们完成 构造 Req, Resp,序列化,反序列化 等操作。
- 客户端存根负责:把用户调用翻译成 RPC 请求(构造请求对象、序列化、设置元数据/header、调用连接发送、等待/处理返回值并反序列化)。
- 构造 Request -> 将请求的函数名,参数等信息序列化 -> 发起网络请求
- 接收 Response -> 将结果反序列化 -> 拿到返回值
- 服务端骨架负责:把到达的网络请求反序列化,映射到具体的服务实现(handler),并把返回值序列化回去。
- 接收 Request -> 将请求反序列化为函数名,参数等信息 -> 执行函数
- 将执行结果序列化 -> 发起网络请求将 Response 返回客户端
- 典型实现:使用 IDL(接口定义语言,如 protobuf / Thrift / IDL)生成 stub 与 skeleton 的代码。
序列化 / 反序列化
一个对象 User(name = "张三", age = 20, gender = "男") 这种形式的数据是无法在网络中传输的,需要给它转换成可跨语言/跨平台的 二进制流 或者 JSON 等格式才可以在网络中传输。这个过程就是序列化和反序列化。
- 序列化:把对象的状态信息转换为可存储或传输的形式,如二进制流或文本。
- 反序列化:对调一下,把可存储或传输的形式转换为对象的状态信息
- 常见工程做法:二进制协议 (Protobuf) 用于高性能;文本协议 (JSON) 用于调试和弱类型场景
传输协议,连接管理
- 连接复用与多路复用(比如 gRPC 的 HTTP/2):避免每次请求都重建 TCP/TLS 连接,降低延迟。
- Keepalive/heartbeat:检测死亡连接并尽快回收资源。
- TLS / mTLS:传输加密和双向证书验证(微服务间常用 mTLS)。
- 连接池:客户端维护到后端实例的长连接池以复用连接。
服务发现
前文 分布式架构演进 我们讨论过,各服务之间通过 Consul, Nacos, Eureka 等中间件来实现服务注册与发现,每个服务将自己的IP 端口号 等信息注册到这些中间件,这样各个服务之间就可以通过统一的注册中心找到彼此了。
服务发现有两种模式(后面会出文档详细讲服务发现):
(1) 客户端发现(Client-side discovery)
- 客户端在调用服务时,直接向 服务注册中心 查询目标服务实例列表。
- 客户端自己选择一个实例(通常有负载均衡策略,如轮询、最少连接、哈希一致性等)。
- 代表实现:Eureka(Netflix)、Consul、Zookeeper。
流程:
客户端 -> 注册中心:获取服务实例列表
客户端 -> 服务实例:发起 RPC 调用
(2) 服务端发现(Server-side discovery)
- 客户端只知道一个 统一网关或负载均衡器 的地址。
- 网关/负载均衡器在接收到请求后,再去查注册中心,挑选一个真实实例。
- 代表实现:Kubernetes Service + kube-proxy / Envoy / Istio。
流程:客户端 -> 负载均衡器(LB) -> 注册中心查找 -> 服务实例
可靠性策略
- 超时(Timeout):调用层设定 RPC 超时,防止无限等待。
- 重试(Retry):当调用失败时重试,但必须考虑幂等性,避免副作用。
- 指数退避(Backoff):避免对短暂故障的洪泛重试。
- 熔断器(Circuit Breaker):对连续失败的后端短路调用,保护本地资源。
- 限流(Rate limiting):保护后端不被突发流量压垮。
4、RPC 如何实现跨语言调用?Java 调用 Golang 函数
一句话总结:通过 统一的接口描述(IDL) + 语言无关的序列化格式 + 各语言的代码生成器 / 运行时库,把「方法签名 + 数据结构」固定成一种“契约(contract)”,客户端与服务端各自用本地语言的 stub/实现来与这个契约交互,网络上交换的是与语言无关的二进制/文本消息。
IDL(接口描述语言)
定义服务方法、消息类型与字段编号/名称(对齐不同语言的类型系统)。例如:Protobuf,Thrift
给个例子:通过这个 接口描述语言,RPC 会生成不同语言的 Stub, Skeleton,它们会帮助我们屏蔽掉网络通信细节,这样我们就可以专注于业务逻辑,不用担心语言的差异。
syntax = "proto3"; package order; message OrderItem { int64 sku = 1; int32 qty = 2; } message GetOrderRequest { int64 order_id = 1; } message GetOrderResponse { int64 order_id = 1; string status = 2; repeated OrderItem items = 3; } service OrderService { rpc GetOrder(GetOrderRequest) returns (GetOrderResponse); }
代码生成 (codegen)
用上面的 IDL 生成客户端存根(stub)和服务端桩(skeleton/handler scaffold),生成的代码是目标语言的“胶水”,负责序列化/反序列化、发送/接收网络请求。
序列化后传输
在网络上传输的是与语言无关的字节流(Protobuf)或 JSON 文本
传输到两端后,各语言的 Stub,Skeleton 会将这些字节流或JSON文本,统一转换成本语言的格式
#微服务架构##Java##后端##面试题#