|

Aimee

Write the Code. Change the World.

服务通信:RPC、服务发现与注册 —— 拆成微服务后,"调用"怎么变成了一道难题

· 分享镜

服务通信:RPC、服务发现与注册 —— 拆成微服务后,"调用"怎么变成了一道难题

单体里调一个方法,orderService.create(),进程内瞬间返回。服务拆开之后,这行"调用"就得跨网络走一趟——用什么协议、对方在哪、有好几台调哪台、对方挂了怎么办,一串新问题全冒出来了。这篇按这条线走:怎么调(RPC vs REST)→ 怎么找到对方(注册发现)→ 调哪个实例(负载均衡)→ 调挂了怎么办(超时 / 重试 / 熔断)


一、服务怎么调:RPC vs REST/HTTP

是什么。 RPC 全称 Remote Procedure Call(远程过程调用),核心理念一句话:让你像调本地方法一样去调一个远程服务的方法。理想状态下,业务代码里写的还是 inventoryClient.deduct(orderId, 10),看起来和调本地函数没区别;但这行代码背后,框架帮你把参数打包(序列化)、通过网络发到对方机器、对方执行完再把结果打包传回来——这一整套"把远程调用伪装成本地调用"的活,就是 RPC 框架在干的。而 REST/HTTP 则不藏:它就是大大方方地发一个 HTTP 请求(POST /inventory/deduct),body 里塞 JSON,对方返回 JSON,你自己解析。

为什么要分这两条路。 关键差异在通用性性能这对取舍上,可以摆开对比:

维度REST / HTTPRPC(如 gRPC)
协议HTTP/1.1 + JSON,人类可读多走 HTTP/2 + 二进制(如 Protobuf)
通用性极强——浏览器、curl、任何语言开箱即用需要框架/桩代码,跨端调用没那么随手
性能文本传输 + 解析,开销较大二进制紧凑、体积小、序列化快,吞吐高
类型约束弱,字段全靠文档约定强类型,靠 IDL 契约,编译期就能查错
可调试性好,浏览器/Postman 直接看差,二进制流肉眼看不懂,要专门工具

一句话点破:REST 拿通用性和可调试性换性能,RPC 拿性能和强类型换通用性。 没有谁绝对更好,看你这个调用发生在哪。

实际业务场景。 这两者在真实系统里通常是分工而非二选一:

  • 对内(服务间)调用走 RPC。订单服务调库存服务、调用户服务,这些都是公司内部、高频、对延迟敏感的调用,一次下单可能扇出十几个内部调用,每个都省几毫秒、序列化体积小一点,累积起来很可观;而且内部服务都是自己人,强类型契约能在编译期挡住"字段名打错、类型对不上"这类低级错误。
  • 对外(开放给浏览器/App/第三方)走 REST。前端、移动端、合作方接你的接口,你不可能要求人家先装一套 RPC 框架、生成桩代码——HTTP + JSON 谁都能调,这是开放接口的"普通话"。这层往往由 API 网关统一承接(见《API 网关》篇),网关再在内部转成 RPC 去调后端服务。

所以一个典型系统的样子是:外圈 REST(对世界开放)、内圈 RPC(自己人之间高效互调),网关是这两层之间的翻译。

业界怎么做。 主流的 RPC 框架/方案,客观列几个常被提到的(非推荐,选型看团队技术栈):

  • gRPC:Google 开源,跨语言,基于 HTTP/2,默认用 Protobuf 序列化,支持流式调用,是目前云原生生态里最通用的 RPC 选择之一。
  • Apache Dubbo:阿里开源、后捐给 Apache,在 Java 微服务生态里用得很广,自带服务治理(注册发现、负载均衡、容错)一整套。
  • Apache Thrift:Facebook 开源、后进 Apache,特点是支持的语言非常多,IDL + 多语言代码生成成熟。

序列化协议这一层也值得单独看,它直接决定了 RPC 的体积和速度:Protobuf(二进制、紧凑、需先定义 .proto)、JSON(文本、可读、通用但偏大)、还有 Thrift 自带的二进制格式、以及 Apache Avro 等。RPC 之所以快,很大程度就是因为多用了 Protobuf 这类二进制序列化,而不是 JSON。

注意事项。

  • 选型先看场景,别盲目追性能。 内部高频核心链路上 RPC 收益明显;但如果只是个低频的后台管理接口,用 REST 省心可调试,硬上 RPC 反而徒增复杂度。
  • 序列化方式要和团队对齐。 Protobuf 性能好但要维护 .proto 文件、生成代码,多了一道构建步骤;JSON 省事但体积和解析开销大。这是一笔实打实的工程成本账。
  • 版本兼容是 RPC 的长期痛点。 服务接口一旦被多个调用方依赖,改字段就得小心:Protobuf 靠"字段编号不复用、只增不改不删、新增字段给默认值"来保证前后兼容,这套纪律一旦破坏,老调用方就会解析失败。契约即接口,改契约等于改公共 API,要当成破坏性变更来对待。

二、怎么找到对方:服务注册与发现

是什么。 假设你已经决定用 RPC 调库存服务了,下一个问题马上来了:库存服务在哪?它的 IP 和端口是多少? 单体时代没这问题——都在一个进程里,方法名就是地址。拆成微服务后,调用方必须先知道"被调方此刻在哪台机器上",才能把请求发过去。服务注册与发现就是解决这个"找地址"的机制:

  • 注册:每个服务实例启动后,主动把自己的地址(IP + 端口 + 服务名)上报给一个中心化的注册中心,相当于"我上线了,我在这儿"。
  • 发现:调用方要调某个服务时,先去注册中心查"库存服务现在有哪些可用实例、地址各是多少",拿到一份实例列表,再从中挑一个发起调用。

为什么需要它(为什么不能写死 IP)。 这是整节的要害。微服务时代,服务实例的地址是动态的、随时在变的:

  • 扩缩容:大促前库存服务从 4 个实例扩到 40 个,新增的 36 个 IP 是临时分配的,你根本无法提前写进配置;大促完又缩回去。
  • 上下线 / 故障:某个实例所在的机器宕了、或者发版滚动重启,它的地址就该立刻从"可用列表"里摘掉,否则请求发过去就是一片超时。
  • 容器化更夸张:在 Kubernetes 这类环境里,Pod 重建一次 IP 就可能变一次,容器调度本就是"地址天天换"的常态。

如果把下游 IP 硬编码在配置里,上面任何一种情况发生,你都得手动改配置、重启服务——在动辄几十上百个服务、成百上千个实例的系统里,这是不可能用人力维护的。注册发现的本质,就是把"谁在线、在哪"这件随时变化的事,交给一个中心自动维护,调用方永远查到的是当下最新的可用列表。

实际业务场景。 一次完整的链路是这样:库存服务的每个实例启动 → 向注册中心注册自己 → 注册中心持续对它做健康检查(定时心跳或探活),不健康就摘除 → 订单服务要调库存时,从注册中心(或本地缓存的实例列表)拿到当前健康实例 → 挑一个发起 RPC。整个过程业务代码无感,框架/SDK 在背后完成。所以你扩容时只管把新实例拉起来,它自己会"汇报上岗",流量自然就分过去了——这正是微服务弹性伸缩的前提。

业界怎么做。 几个主流注册中心(客观列举):

  • Nacos:阿里开源,注册中心 + 配置中心二合一,在国内 Spring Cloud / Dubbo 生态里很常见。
  • Consul:HashiCorp 出品,自带健康检查、KV 存储、多数据中心支持,功能完整。
  • etcd:CNCF 项目,强一致的分布式键值存储,Kubernetes 自己的服务信息就存在 etcd 里,也常被直接用作注册中心。
  • Eureka:Netflix 开源,早期 Spring Cloud 的经典选择,设计上偏向"可用性优先"(下一节注意事项会提到这背后的取舍)。

注意事项。

  • 注册中心自己必须高可用。 它是所有服务"找路"的总枢纽——它一旦挂了,等于整个系统集体"失明",谁也找不到谁。所以注册中心绝不能是单点,必须自身集群化部署、多副本。
  • CAP 取舍要心里有数。 注册中心本身是个分布式系统,逃不开 CAP 权衡:像 etcd / Consul 偏 CP(强一致,但分区时可能暂时不可写);Eureka 偏 AP(优先保证还能查到地址,哪怕数据短暂不一致)。对注册发现这个场景,业界常见的观点是"宁可拿到一份稍旧但能用的实例列表,也好过因为追求强一致而查不到"——但这取决于你的业务对一致性的容忍度,没有标准答案。
  • 健康检查不能少,且要靠谱。 实例其实已经挂了、注册中心却还没摘除,流量就会持续打到死实例上(俗称"僵尸实例")。健康检查的灵敏度是个平衡:太迟钝则故障摘除慢,太敏感又可能因一次网络抖动误摘健康实例。

三、调用时选哪个实例:客户端负载均衡

是什么。 上一节查注册中心,拿到的是一份实例列表(库存服务有 10 个健康实例)。可一次调用只能发给其中一个——到底发给哪个? 这就是负载均衡要回答的。注意这里有个和传统 Web 不一样的点:微服务里很常见的是客户端负载均衡——不是在调用方和被调方中间架一台专门的负载均衡器,而是调用方自己(借助框架/SDK)拿着完整实例列表,直接按某种策略挑一个、点对点发过去。少了中间那一跳,延迟更低。

为什么。 目的就是把请求均匀(或按权重)摊到所有实例上,既不让某台被打爆、也不让某台闲着,从而既扛住总流量,又避免单点过载。常见的挑选策略有几种:

策略怎么挑适合场景
轮询(Round Robin)一个一个轮着来,雨露均沾各实例配置相近、请求开销均匀
加权轮询(Weighted)按权重分,配置高的实例多分点实例规格不一(有的 8 核有的 4 核)
随机(Random)随机挑一个简单,实例多时也接近均匀
最少连接 / 最少请求谁当前手头活最少给谁请求耗时差异大,防止慢请求堆在一台上
一致性哈希按 key 哈希到固定实例想让"同一用户/同一 key"尽量落同一台(利于缓存命中)

一句话点破:轮询/随机是"机会均等",最少连接是"能者多劳的反面——闲者多劳",一致性哈希是"认人不认机会"。 选哪个看你的请求是不是均匀、实例是不是同构、要不要会话/缓存亲和。

实际业务场景。 各实例规格相同、请求开销也差不多时,轮询随机就够用,简单可靠。如果实例混了不同规格(老机器 + 新机器),用加权让强机器多担一点。如果某些请求特别慢(比如带复杂查询),用最少连接能防止慢请求全堆在某一台上把它拖垮。如果下游每台实例各自维护了本地缓存,用一致性哈希让同一个 key 总打到同一台,缓存命中率会高很多。

业界怎么做。 客户端负载均衡常常内置在 RPC 框架里——Dubbo、gRPC(配合 xDS / 负载均衡策略)、Spring Cloud 生态(早期的 Ribbon、现在的 Spring Cloud LoadBalancer)都自带可选策略,你配置一下选哪种即可。再往上,服务网格(Service Mesh,如 Istio + Envoy)把负载均衡、重试、熔断这些能力从业务代码里下沉到 sidecar 代理,业务进程只管发请求,选实例、容错都由旁边的代理统一处理——这是云原生时代把服务治理"基础设施化"的方向。

注意事项。

  • 策略要配合健康状态。 负载均衡只该在"健康实例"里挑——它必须和上一节的健康检查联动,把已摘除的实例剔出候选,否则再好的策略也会往死实例上分流量。
  • 轮询不等于真均衡。 如果请求耗时差异很大,机械轮询照样可能让某台慢实例越积越多,这时"最少连接"类策略更稳。
  • 有状态/缓存亲和要专门处理。 默认的轮询/随机会打散同一用户的请求,如果下游依赖本地会话或本地缓存,就需要一致性哈希这类"亲和"策略,否则命中率上不去。

四、调用出问题怎么办:超时、重试、熔断

是什么。 跨网络的调用,失败是常态而非意外:网络会抖、下游会慢、实例会挂。所以每个远程调用都得自带一套"出问题怎么办"的预案。最基础的三件套是超时、重试、熔断——这正是《高并发三板斧》篇里讲过的同一套思想,这里看它们在服务间调用里具体怎么用:

  • 超时(Timeout):调用必须设一个最长等待时间,过了就果断放弃、报错返回,绝不无限等。
  • 重试(Retry):对偶发的、可能是临时抖动导致的失败,自动再试一两次,提高成功率。
  • 熔断(Circuit Breaker):当某个下游持续大面积失败时,主动掐断对它的调用一段时间,快速失败、不再傻等,给它喘息恢复的机会。

为什么——防的是"服务雪崩"。 这是整节、也是整篇最该记住的概念。设想:库存服务变慢了(每个调用从 5ms 涨到 5s)。订单服务每次调库存都得干等 5 秒,而这期间订单服务自己的线程/连接被这些"卡住的调用"一个个占满、无法释放;很快订单服务的资源池被耗尽,它自己也开始无法响应;接着调用订单服务的网关、上游服务又跟着被拖住……一个下游的慢,顺着调用链一层层往上把整条链路全部拖垮——这就是服务雪崩:局部故障被调用链放大成全局崩溃。

超时、重试、熔断就是用来打断这个传导链的:

  • 超时让订单服务不会被卡 5 秒,1 秒等不到就放手,线程立刻释放——这是第一道也是最关键的闸(没有超时,后面都白搭)。
  • 熔断更进一步:发现库存服务已经大面积超时/出错,干脆暂时不再调它了,直接快速失败(或走降级逻辑,比如返回"稍后重试"),等它缓过来再试探性放量——既保护了上游不被拖垮,也给了下游恢复的空间。

实际业务场景。 几个要点:

  • 重试要小心,它是把双刃剑。 重试能救偶发抖动,但如果下游是因为过载才失败,你还重试,等于火上浇油——本来就扛不住,你又多发了几倍请求,直接把它彻底打死。所以重试通常要配合退避(backoff)、限制重试次数,且只对幂等操作重试(详见下方注意事项)。
  • 降级是熔断的搭档。 熔断掐断调用后,不能让用户看到一片报错——常配合降级:返回一个兜底结果(默认值、缓存的旧数据、或友好的"稍后再试"),保住核心体验。
  • 超时时间要分层设置且收敛。 整条调用链上,上游的超时应当大于它所等待的下游超时之和,否则会出现"下游还在重试、上游已经超时放弃"的浪费。

业界怎么做。 这套容错能力,业界有成熟的库和基础设施(客观列举):Resilience4j(Java 生态目前主流的容错库,提供熔断、限流、重试、舱壁等)、Netflix 的 Hystrix(早期经典,现已停止新功能开发但影响深远)、阿里的 Sentinel(熔断 + 限流 + 流量整形)。再往上,服务网格(Istio/Envoy) 同样能在 sidecar 层统一配置超时、重试、熔断,做到业务代码零侵入。具体的熔断状态机(关闭/打开/半开)和限流算法,《高并发三板斧》篇有展开,这里不再重复。

注意事项。

  • 没有超时的远程调用是定时炸弹。 这是最常见也最致命的疏忽——只要有一个调用没设超时,雪崩的引信就留在那。每一个跨网络调用都必须有超时,没有例外。
  • 重试务必只对幂等操作开。 一个"扣库存"的调用如果失败了,你不知道它到底是"没执行"还是"执行了但响应丢了",盲目重试可能重复扣减。非幂等操作的重试,要靠唯一请求 id + 幂等设计兜底(这点和《消息队列》篇里"at-least-once 必须配幂等"是同一个道理)。
  • 熔断阈值要调。 太灵敏会因一次小波动就熔断、误伤正常流量;太迟钝又起不到保护作用。阈值(错误率/慢调用比例、统计窗口、半开放量)需要结合监控数据持续调优,不是设一次就一劳永逸。

五、一张表

把这篇收进两张表。

RPC vs REST,内圈外圈各司其职:

对比项REST / HTTPRPC
典型用途对外开放接口(浏览器/App/第三方)对内服务间高频调用
协议 / 序列化HTTP/1.1 + JSON(文本)多 HTTP/2 + Protobuf 等(二进制)
性能一般高(紧凑、序列化快)
类型约束弱,靠文档强,靠 IDL 契约
可调试性好(肉眼可读)差(需专门工具)
代表方案标准 HTTP / OpenAPIgRPC、Dubbo、Thrift

主流注册中心横向看:

注册中心出身CAP 倾向一句话特点
Nacos阿里开源可选 AP/CP注册 + 配置中心二合一,国内生态常见
ConsulHashiCorp偏 CP自带健康检查、KV、多数据中心
etcdCNCFCP(强一致)K8s 自身就用它,也可直接当注册中心
EurekaNetflixAP(可用优先)早期 Spring Cloud 经典,分区时仍可读

名词解释

  • RPC(Remote Procedure Call,远程过程调用):让你像调本地方法一样调远程服务的方法;框架在背后完成参数序列化、网络传输、结果回传。
  • REST / HTTP:基于 HTTP + JSON 的通用接口风格,人类可读、任何语言开箱即用,常用于对外开放接口。
  • gRPC:Google 开源的跨语言 RPC 框架,基于 HTTP/2,默认用 Protobuf 序列化,支持流式调用。
  • Protobuf(Protocol Buffers):Google 的二进制序列化格式,紧凑、解析快,需先用 .proto 文件定义契约并生成代码;靠"字段编号只增不改"保证版本兼容。
  • 序列化(Serialization):把内存里的对象转成可在网络上传输的二进制数据(及反向的反序列化);RPC 快不快,很大程度取决于用 Protobuf(二进制)还是 JSON(文本)。
  • IDL(接口定义语言):用来定义服务接口和数据结构的契约文件(如 Protobuf 的 .proto、Thrift 的 .thrift),据此生成多语言桩代码。
  • 服务注册与发现:服务实例启动时把自己的地址上报给注册中心(注册),调用方动态查询某服务当前可用实例列表(发现),从而不必把下游 IP 写死。
  • 注册中心:集中维护"哪些服务、哪些实例、在哪、是否健康"的中心组件,是服务找路的总枢纽,自身必须高可用;如 Nacos、Consul、etcd、Eureka。
  • 健康检查:注册中心通过心跳或探活定期判断实例是否存活,不健康就从可用列表摘除,避免流量打到死实例。
  • 客户端负载均衡:调用方自己持有实例列表,按某种策略(轮询、加权、最少连接、一致性哈希等)直接挑一个实例点对点调用,省去中间负载均衡器那一跳。
  • 服务网格(Service Mesh):把负载均衡、重试、熔断等服务治理能力从业务代码下沉到 sidecar 代理(如 Istio + Envoy),业务对治理逻辑零侵入。
  • 超时 / 重试 / 熔断:远程调用的容错三件套——超时果断放弃慢调用,重试救偶发抖动,熔断在下游大面积失败时主动掐断调用、快速失败。
  • 服务雪崩:一个下游服务变慢或故障,顺着调用链把上游服务的资源逐层耗尽,导致局部故障扩散成全局崩溃;靠超时 + 熔断 + 降级来阻断。
  • 降级:在下游不可用或被熔断时,返回兜底结果(默认值、旧缓存、"稍后再试")以保住核心体验。

本文是《研发都要懂的事》·服务端架构设计系列的一篇——承接《微服务》篇(为什么拆),解决拆开之后"服务之间怎么互相调、怎么找到彼此"的问题;其中超时、重试、熔断与服务雪崩的细节(熔断状态机、限流算法),见《高并发三板斧》篇。下一步往"服务多了之后怎么统一管"走:API 网关、链路追踪、容器与编排。

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部