Update 分布式.md

补充 RPC 的调用过程、服务发现、与 HTTP 的区别、超时与重试,并配图。
This commit is contained in:
CyC2018
2026-10-26 01:17:35 +08:00
parent 5d46cfa1eb
commit 2fddf5c4a2
3 changed files with 181 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+104
View File
@@ -28,6 +28,11 @@
* [单个 Candidate 的竞选](#单个-candidate-的竞选)
* [多个 Candidate 竞选](#多个-candidate-竞选)
* [日志复制](#日志复制)
* [七、RPC](#七rpc)
* [调用过程](#调用过程)
* [服务发现](#服务发现)
* [RPC 与 HTTP](#rpc-与-http)
* [超时与重试](#超时与重试)
* [参考](#参考)
<!-- GFM-TOC -->
@@ -416,8 +421,107 @@ Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了
发生网络分区时,少数派一侧的旧 Leader 得不到大多数节点的确认,无法提交日志,多数派一侧会选出新的 Leader。分区恢复后,旧 Leader 发现更大的任期号就会退回 Follower,它未提交的日志被覆盖,因此不会出现脑裂。
## 七、RPC
远程过程调用(Remote Procedure Call, RPC)指的是像调用本地方法一样调用另一台机器上的服务。例如订单服务调用库存服务扣减库存:
```java
boolean ok = inventoryService.deduct(10086, 2);
```
### 调用过程
调用方写的是普通的方法调用,RPC 框架在背后完成了网络通信:
<div align="center"> <img src="pics/rpc-call.png" width="740px"/> </div><br>
1. inventoryService 是框架用动态代理生成的对象,调用它的方法时,代理拿到接口名、方法名和参数;
2. 把这些信息序列化成字节,加上请求 ID 等首部组成请求帧,从注册中心获取的地址列表中按负载均衡选出一个实例,通过长连接发送;
3. 服务方解码请求,找到对应的实现类,通过反射调用 deduct(),再把返回值按同样的方式发回;
4. 一个连接上可能同时有多个请求在等待响应,调用方根据响应中的请求 ID 把结果交给对应的调用。
去掉工程上的细节,核心只有两部分:调用方用动态代理把方法调用转成网络请求,服务方用反射把网络请求转成方法调用。
```java
// 调用方:生成接口的代理对象,调用方法时把方法名和参数发给服务方
@SuppressWarnings("unchecked")
static <T> T refer(Class<T> api, String host, int port) {
return (T) Proxy.newProxyInstance(api.getClassLoader(), new Class<?>[]{api}, (proxy, method, args) -> {
try (Socket socket = new Socket(host, port)) {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeUTF(method.getName());
out.writeObject(method.getParameterTypes());
out.writeObject(args);
out.flush();
return new ObjectInputStream(socket.getInputStream()).readObject();
}
});
}
// 服务方:读出方法名和参数,通过反射调用本地的实现
static void export(Object impl, int port) throws Exception {
try (ServerSocket server = new ServerSocket(port)) {
while (true) {
try (Socket socket = server.accept()) {
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Method method = impl.getClass().getMethod(in.readUTF(), (Class<?>[]) in.readObject());
Object result = method.invoke(impl, (Object[]) in.readObject());
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(result);
out.flush();
}
}
}
}
```
```java
// 服务方
export(new InventoryServiceImpl(), 20880);
// 调用方
InventoryService inventoryService = refer(InventoryService.class, "10.0.0.8", 20880);
inventoryService.deduct(10086, 2); // 实际执行的是服务方的 InventoryServiceImpl.deduct()
```
这段代码每次调用都新建连接,使用 Java 自带的序列化,也没有超时处理。Dubbo、gRPC 等框架在此基础上实现了连接复用、高效的序列化、服务发现、负载均衡、超时重试和熔断,设计一个 RPC 框架时也是从这几个模块考虑。
### 服务发现
服务的实例会扩容、缩容或者宕机,地址不能写死在调用方。注册中心(例如 Nacos、ZooKeeper)保存每个服务当前可用的实例:
- 服务方启动时把自己的地址注册到注册中心,之后定期发送心跳,长时间没有心跳的实例会被删除;
- 调用方订阅需要的服务,把地址列表缓存在本地,实例变化时由注册中心推送通知;
- 每次调用都从本地缓存的列表中选择实例,注册中心不转发请求。
因此注册中心全部宕机时,调用方仍然可以按缓存的地址继续调用,只是新上线的实例无法被发现。某个实例宕机后、被注册中心删除之前,发往它的请求会失败,需要调用方重试其它实例。
### RPC 与 HTTP
HTTP 是一种协议,RPC 是一种调用方式,两者不是同一层面的概念:RPC 框架可以在 TCP 上使用自定义的协议,例如 Dubbo 协议;也可以基于 HTTP,例如 gRPC 基于 HTTP/2,Spring Cloud 的 OpenFeign 用动态代理把接口调用转换成 HTTP 请求。
面试中通常问的是:为什么对外接口常用 HTTP + JSON,内部服务之间却常用 RPC 框架。这是因为内部调用量大、对延迟敏感,RPC 框架在这些方面做得更好:
- 序列化更紧凑:例如参数 {"skuId":10086,"count":2},JSON 需要 25 字节;Protobuf 不传字段名,只传字段编号和值,编码为 08 E6 4E 10 02,共 5 字节,其中 08 表示字段 1,E6 4E 是 10086 的变长编码,10 02 表示字段 2 的值为 2;
- 连接复用:在少量长连接上同时发送多个请求,用请求 ID 区分响应,省去频繁建立连接的开销;
- 服务治理:内置服务发现、负载均衡、超时重试和熔断;
- 接口约定:用 Java 接口或者 IDL 文件定义服务,自动生成调用方的代码,参数类型在编译期就能检查。
HTTP + JSON 的优势在于通用:浏览器和各种语言都能直接调用,报文可读,便于调试,适合对外开放的接口。
### 超时与重试
- 每次调用都要设置超时时间,否则下游变慢时,调用方的线程会一直等待,最终被耗尽;
- 调用方超时不代表服务方没有执行:请求可能在网络中延迟,或者在服务方的线程池中排队、遇到 GC 停顿,这些时间都计入了调用方的耗时,却不在服务方记录的处理时间里,于是调用方超时了,服务方却执行成功了;
- 因此只有幂等的接口才能自动重试,例如查询;扣库存这类写操作要带上请求 ID,由服务方去重,否则重试会扣减两次;
- 调用链较长时,下游的超时时间应该比上游短。gRPC 会把截止时间(deadline)随请求传给服务方,超时后调用方收到 DEADLINE_EXCEEDED,服务方也可以检查调用是否已经超时,从而停止处理。
## 参考
- [Apache Dubbo: Architecture](https://dubbo.apache.org/docs/v2.7/user/preface/architecture/)
- [Protocol Buffers: Encoding](https://protobuf.dev/programming-guides/encoding/)
- [gRPC: Core concepts](https://grpc.io/docs/what-is-grpc/core-concepts/)
- [gRPC on HTTP/2](https://grpc.io/blog/grpc-on-http2/)
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- [Designing Data-Intensive Applications 中文版](https://github.com/vonng/ddia)
- [Distributed locks with Redis](https://redis.io/topics/distlock)
+77
View File
@@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" width="740" height="400" viewBox="0 0 740 400">
<style>
text { font-family: "PingFang SC", "Microsoft YaHei", Helvetica, Arial, sans-serif; fill: #666; font-size: 12px; }
.title { font-weight: bold; font-size: 15px; fill: #777; text-anchor: middle; }
.b { font-weight: bold; }
.c { text-anchor: middle; }
.e { text-anchor: end; }
.note { fill: #C0605A; }
.ok { fill: #6A9F68; }
.sub { font-size: 11px; fill: #888; }
.mono { font-family: Menlo, Consolas, monospace; }
.arrow { stroke: #666; stroke-width: 1.3; fill: none; marker-end: url(#a); }
.red { stroke: #C0605A; marker-end: url(#r); }
.green { stroke: #6A9F68; marker-end: url(#g); }
.dash { stroke-dasharray: 5 4; }
.line { stroke: #666; stroke-width: 1.3; fill: none; }
.frame { fill: none; stroke: #999; stroke-width: 1; stroke-dasharray: 4 3; }
.shadow { fill: #DADADA; }
</style>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#666" stroke-width="1.6"/>
</marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#C0605A" stroke-width="1.6"/>
</marker>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M1,1 L9,5 L1,9" fill="none" stroke="#6A9F68" stroke-width="1.6"/>
</marker>
</defs>
<rect width="100%" height="100%" fill="#FFFFFF"/>
<text x="370" y="28" class="title">一次 RPC 调用</text>
<rect x="282" y="48" width="180" height="48" rx="6" class="shadow"/>
<rect x="280" y="46" width="180" height="48" rx="6" fill="#E6E6E6"/>
<text x="370" y="68" class="b c">注册中心</text>
<text x="370" y="83" class="sub c">库存服务:10.0.0.8、10.0.0.9</text>
<line x1="330" y1="98" x2="160" y2="158" class="arrow dash"/>
<text x="128" y="126" class="sub">① 订阅地址,缓存在本地</text>
<line x1="590" y1="158" x2="410" y2="98" class="arrow dash"/>
<text x="500" y="126" class="sub">启动时注册,定期发送心跳</text>
<rect x="20" y="164" width="270" height="196" rx="8" class="frame"/>
<text x="30" y="182" class="b">订单服务(调用方)</text>
<rect x="36" y="194" width="238" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="155" y="215.5" class="c sub mono">inventoryService.deduct(10086, 2)</text>
<rect x="38" y="248" width="238" height="40" rx="6" class="shadow"/>
<rect x="36" y="246" width="238" height="40" rx="6" fill="#DDD7F6"/>
<text x="155" y="264" class="b c">代理</text>
<text x="155" y="279" class="sub c">取得方法名和参数</text>
<rect x="38" y="306" width="238" height="40" rx="6" class="shadow"/>
<rect x="36" y="304" width="238" height="40" rx="6" fill="#B1DDF0"/>
<text x="155" y="322" class="b c">序列化、选择实例</text>
<text x="155" y="337" class="sub c">组成带请求 ID 的请求帧</text>
<line x1="155" y1="231" x2="155" y2="243" class="arrow "/>
<line x1="155" y1="289" x2="155" y2="301" class="arrow "/>
<rect x="450" y="164" width="270" height="196" rx="8" class="frame"/>
<text x="460" y="182" class="b">库存服务(服务方)</text>
<rect x="468" y="306" width="238" height="40" rx="6" class="shadow"/>
<rect x="466" y="304" width="238" height="40" rx="6" fill="#B1DDF0"/>
<text x="585" y="322" class="b c">解码请求</text>
<text x="585" y="337" class="sub c">得到方法名和参数</text>
<rect x="468" y="248" width="238" height="40" rx="6" class="shadow"/>
<rect x="466" y="246" width="238" height="40" rx="6" fill="#DDD7F6"/>
<text x="585" y="264" class="b c">反射调用</text>
<text x="585" y="279" class="sub c">InventoryServiceImpl.deduct()</text>
<rect x="466" y="194" width="238" height="34" rx="4" fill="#FFFFFF" stroke="#C8C8C8"/>
<text x="585" y="215.5" class="c sub mono">返回 true</text>
<line x1="585" y1="301" x2="585" y2="289" class="arrow "/>
<line x1="585" y1="243" x2="585" y2="231" class="arrow "/>
<line x1="277" y1="324" x2="462" y2="324" class="arrow "/>
<text x="370" y="316" class="c sub">② 请求帧</text>
<text x="370" y="342" class="c sub">请求 ID 7 + 方法 + 参数</text>
<line x1="462" y1="211" x2="277" y2="211" class="arrow dash"/>
<text x="370" y="203" class="c sub">③ 响应帧</text>
<text x="370" y="229" class="c sub">请求 ID 7 + 结果</text>
<text x="370" y="386" class="c sub">注册中心只负责注册和发现,不转发请求;调用方按请求 ID 把响应交给对应的等待线程</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB