# rpc框架 **Repository Path**: csyuwen/rpc-framework ## Basic Information - **Project Name**: rpc框架 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-29 - **Last Updated**: 2025-04-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RPC框架 ## 一、项目概述 1.项目结构 - example-common - example-consumer - example-provider - yuwen-rpc-easy 2.技术栈 - web服务器 运用Vertx [Vert.x Core | Eclipse Vert.x](https://vertx.io/docs/vertx-core/java/) > ``` > > io.vertx > vertx-core > 4.5.13 > > ``` > > 注意 使用的 example-provider也需要 引入依赖 - 本地服务注册器 > 使用线程安全的ConcurrentHashMap存储服务注册信息,key为服务名称、value为服务的实现类。之后就可以根据要调 > 用的服务名称获取到对应的实现类,然后通过反射进行方法调用了。 - 序列化 > serialize [interface] > > JdkSerialize implements serialize 实现 序列化和反序列化 ==被序列化的类应当实现Serializable接口== - 请求处理器 > 1.反序列化请求为对象,并从请求对象中获取参数。 > 2.根据服务名称从本地注册器中获取到对应的服务实现类。 > 3.通过反射机制调用方法,得到返回结果。 > 4.对返回结果进行封装和序列化,并写入到响应中。 - 静态代理 - 动态代理 > ```java > RpcRequest rpcRequest = RpcRequest > .builder() > .serviceName(method.getDeclaringClass().getName()) // 服务名 > .methodName(method.getName())//方法名 > .parameterTypes(new Class[] {method.getReturnType()}) //返回参数类型 > .parameters(args)//返回参数 > .build(); > > Parameter[] parameters = method.getParameters(); > ``` > > ### `method.getParameters()` > > - **目的**:`method.getParameters()` 方法返回一个 `Parameter` 数组,表示方法的参数。在此方法的上下文中,它提供了关于方法参数的信息,例如参数的类型、名称、修饰符等。 > - **返回内容**:返回的是一个 `java.lang.reflect.Parameter` 数组,数组的每个元素对应于被代理方法的一个参数。 > - 内容包含: > - 参数的类型(Type) > - 参数的名称(在编译时提供,使用 `-parameters` 选项编译时可用) > - 参数的修饰符(如是否是 final) > - ![image-20250330122853854](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250330122853854.png) > > ### `Object[] args` > > - **目的**:`args` 是代理方法调用时传递给目标方法的实际参数值数组。它包含了调用该方法时实际使用的参数值。 > > - **返回内容**:`Object[]` 数组,数组的长度与方法参数的数量相同,数组的每个元素对应方法调用时传递的实际参数值。 > > - **内容包含**:这是调用方法时的实际参数值,不包括任何关于参数的元数据或修饰符信息。 > > ![image-20250330122917953](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250330122917953.png) > > 此处应该使用args参数 ## 二、项目优化 ### 2.1、全局加载 > 1. 配置类 > > ​ RpcConfig > > 1. name 名称 > > 2. version 版本号 > > 3. serverHost 服务器主机名 > > 4. serverPort 服务器端口号 > > > > 2. 配置工具类 => 从配置文件自动装配配置类 > > ​ ConfigUtils > > 【RpcConfig】loadConfig > > |-- 无环境 > > |-- 环境 (test/dev/pro) > > > > 3. 配置常量类 > > RpcConstant > > |-- DEFAULT_CONFIG_PREFIX > > > > 4. 初始化 rpc配置 > > RpcApplication > > **使用双锁检查单例模式** ### 2.2接口Mock > 什么是 Mock? > RPC 框架的核心功能是调用其他远程服务。但是在实际开发和测试过程中,有时可能无法直接访问真实的远程服务,或者访问真实的远程服务可能会产生不可控的影响,例如网络延迟、服务不稳定等。在这种情况下,就需要使用 mock 服务来模拟远程服务的行为,以便进行接口的测试、开发和调试。 > mock 是指模拟对象,通常用于测试代码中,特别是在单元测试中,便于我们跑通业务流程。 1. 设置mock属性,并全局配置到配置文件 ### 2.3序列化和SPI机制 > 什么是 SPI? > SPl(Service Provider Interface)服务提供接口是Java 的机制,主要用于实现模块化开发和插件化扩展。 > SPI机制允许服务提供者通过特定的配置文件将自己的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。 > -个典型的 SPI 应用场景是JDBC(Java 数据库连接库),不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。 > 此外,我们使用的主流Java 开发框架中,几乎都使用到了 SP1机制,比如 Servlet 容器、日志框架、ORM 框架、Spring框架。所以这是 Java 开发者必须掌握的一个重要特性! ### 2.4注册中心 #### 注册中心概述 RPC 框架的一个核心模块是注册中心,目的是帮助服务消费者获取到服务提供者的调用地址,而不是将调用地址硬编码到项目中。 > **注册中心核心能力** > 我们先明确注册中心的几个实现关键(核心能力): > > 1.数据分布式存储:集中的注册信息数据存储、读取和共享 > > 2.服务注册:服务提供者上报服务信息到注册中心 > > 3.服务发现:服务消费者从注册中心拉取服务信息 > > 4.心跳检测:定期检查服务提供者的存活状态 > > 5.服务注销:手动剔除节点、或者自动剔除失效节点 > > 6.更多优化点:比如注册中心本身的容错、服务消费者缓存等。 技术选型:[etcd-io/etcd: Distributed reliable key-value store for the most critical data of a distributed system](https://github.com/etcd-io/etcd) **etcd**技术 Etcd 安装 进入Etcd 官方的下载页:https://github.com/etcd-io/etcd/releases 也可以在这里下载:https://etcd.io/docs/v3.2/install/ 1.etcd概述: Etcd 的核心数据结构包括: 1.Key(键):Etcd 中的基本数据单元,类似于文件系统中的文件名。每个键都唯一标识一个值,并且可以包含子键,形 成类似于路径的层次结构。 2.Value(值):与键关联的数据,可以是任意类型的数据,通常是字符串形式。 2.etcd特性 Etcd 有很多核心特性,其中,应用较多的特性是: 1.Lease(租约):用于对键值对进行TTL超时设置,即设置键值对的过期时间。当租约过期时,相关的键值对将被自动 删除。 2.Watch(监听):可以监视特定键的变化,当键的值发生变化时,会触发相应的通知。 此外,Etcd的一大优势就是能够保证数据的**强一致性**。 > Etcd如何保证数据一致性? > 从表层来看,Etcd支持事务操作,能够保证数据一致性。 > 从底层来看,Etcd使用Raft一致性算法来保证数据的一致性。 > Rat是一种分布式一致性算法,它确保了分布式系统中的所有节点在任何时间点都能达成一致的数据视图。 > 具体来说,Rat算法通过选举机制选举出一个领导者(Leader)节点,领导者负责接收客户端的写请求,并将写操作复制 > 到其他节点上。当客户端发送写请求时,领导者首先将写操作写入自己的日志中,并将写操作的日志条目分发给其他节 > 点,其他节点收到日志后也将其写入自己的日志中。一旦大多数节点(即半数以上的节点)都将该日志条目成功写入到 > 自己的日志中,该日志条目就被视为已提交,领导者会向客户端发送成功响应。在领导者发送成功响应后,该写操作就被 > 视为已提交,从而保证了数据的一致性。 > 如果领导者节点宕机或失去联系,Raft算法会在其他节点中选举出新的领导者,从而保证系统的可用性和一致性。新的 > 领导者会继续接收客户端的写请求,并负责将写操作复制到其他节点上,从而保持数据的一致性。 > 上面这段不理解也没关系,我们可以使用官方提供的Etcd Playground 来可视化操作Etcd,便于学习。 > > Playground 地址: http://play.etcd.io/play 3.etcd的增删改查操作 **Client** > 1.kvClient:用于对etcd 中的键值对进行操作。通过kvClient 可以进行设置值、获取值、删除值、列出目录等操作。 > 2.leaseClient:用于管理etcd 的租约机制。租约是etcd 中的一种时间片,用于为键值对分配生存时间,并在租约到期 > 时自动删除相关的键值对。通过leaseClient 可以创建、获取、续约和撤销租约。 > 3.watchClient:用于监视etcd 中键的变化,并在键的值发生变化时接收通知。 > 4.clusterClient:用于与 etcd 集群进行交互,包括添加、移除、列出成员、设置选举、获取集群的健康状态、获取成员 > 列表信息等操作。 > 5.authClient:用于管理etcd的身份验证和授权。通过 authClient可以添加、删除、列出用户、角色等身份信息,以及 > 授予或撤销用户或角色的权限。 > 6.maintenanceClient:用于执行etcd 的维护操作,如健康检查、数据库备份、成员维护、数据库快照、数据库压缩 > 等。 > 7.lockClient:用于实现分布式锁功能,通过lockClient 可以在 etcd上创建、获取、释放锁,能够轻松实现并发控制。 > 8.electionClient:用于实现分布式选举功能,可以在 etcd上创建选举、提交选票、监视选举结果等。 Etcd 可视化工具 一般情况下,我们使用数据存储中间件时,一定要有一个可视化工具,能够更直观清晰地管理已经存储的数据。比如Redis 的 Redis Desktop Manager。 同样的,Etcd也有一些可视化工具,比如: - etcdkeeper: https://github.com/evildecay/etcdkeeper/ - kstone: https://github.com/kstone-io/kstone/tree/master/charts ```java //Etcd Demo package com.yupi.yurpc.registry; import io.etcd.jetcd.ByteSequence; import io.etcd.jetcd.Client; import io.etcd.jetcd.KV; import io.etcd.jetcd.kv.GetResponse; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class EtcdRegistry { public static void main(String[] args) throws ExecutionException, InterruptedException { // 连接etcd Client client = Client.builder().endpoints("http://localhost:2379") .build(); //获取操作客户端 KV kvClient = client.getKVClient(); ByteSequence key = ByteSequence.from("test_key".getBytes()); ByteSequence value = ByteSequence.from("test_value".getBytes()); // 存放kv put kvClient.put(key, value).get(); // 获取kv get CompletableFuture getFuture = kvClient.get(key); // get the value from CompletableFuture GetResponse response = getFuture.get(); // 删除kv delete kvClient.delete(key).get(); } } ``` #### 实现注册中心(etcd) 1. 注册中心相关配置 > 1. 服务地址 localhost:2380 > 2. 服务类型 etcd ====>>>服务中心的实现方式 后续的**SPI机制** > 3. 服务用户名 > 4. 服务用户密码 > 5. 服务请求超时时间 timeout 2. 注册中心服务的相关配置 serviceMetaInfo (服务**元**信息) > 1. Host 域名 > 2. Port 端口 > 3. serviceName 服务名 > 4. Version 版本 > 5. Group 分组 3. 注册中心的 业务方法 ---SPI机制 Service Provide Interface > EtcdRegistry **implements** Registry > > 1. init 初始化方法 > 2. 注册服务方法 registry > 3. 注销服务 方法 unRegistry > 4. 获取(发现)服务方法 registryDiscovery > 5. 注销注册中心方法 destroy 4. 注册中心的创建工厂 > RegistryFactory > > 1. 默认的注册中心 (兜底方案) DEFAULT_SERVICE_REGISTRY = > 2. getRegistry(getInstance)创建对应的注册中心 5. SPI的实现 > META-INF/system 创建文件名称为 接口的全类名 > > etcd = 实现类的全类名 ClassName #### 注册中心遇到的问题 1. 类实例化失败 ![image-20250401184649309](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250401184649309.png) - 可能是META-INF 下的文件 写的是接口的全类名,而不是实现类的全类名 - 可能是serviceProxy 获取请求地址出现了错误,registryRecovery传入的参数应该为 serviceNodeKey ![image-20250401185124466](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250401185124466.png) 2. 无法加载xxxClass ![image-20250401184950451](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250401184950451.png) - 工厂未进行 静态代码块内的 load ,加载实现类 ![image-20250401185100971](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250401185100971.png) ### 2.5注册中心的优化 #### 1.目前注册中心存在的问题 1.数据一致性:服务提供者如果下线了,注册中心需要即时更新,剔除下线节点。否则消费者可能会调用到已经下线的节点。 2.性能优化:服务消费者每次都需要从注册中心获取服务,可以使用缓存进行优化。 3.高可用性:保证注册中心本身不会宕机。 4.可扩展性:实现更多其他种类的注册中心。 #### 2.优化点 1.心跳检测和续期机制 2.服务节点下线机制 3.消费端服务缓存 4.基于ZooKeeper的注册中心实现 > 1.心跳检测和续期机制 > > 通过hutool 的CronUtil 实现定时续期任务 > > CronUtil( ) > > 2.服务节点下线机制 > > - 主动下线:服务提供者项目正常退出时,主动从注册中心移除注册信息。 > - 被动下线:服务提供者项目异常推出时,利用 Etcd 的key 过期机制自动移除。 > > 被动下线已经可以利用Etcd的机制实现了,我们主要开发主动下线。 > 问题是,怎么在Java项目正常退出时,执行某个操作呢? > 其实,非常简单,利用JVM 的ShutdownHook就能实现。 > JVM的**ShutdownHook**是Java虚拟机提供的一种机制,允许开发者在JVM即将关闭之前执行一些清理工作或其他必要 > 的操作,例如关闭数据库连接、释放资源、保存临时数据等。 > > 3.消费端服务缓存 > 正常情况下,服务节点信息列表的更新频率是不高的,所以在服务消费者从注册中心获取到服务节点信息列表后,完全可 > 以缓存在本地,下次就不用再请求注册中心获取了,能够提高性能。 > > - 增加本地缓存 > 本地缓存的实现很简单,用一个列表来存储服务信息即可,提供操作列表的基本方法,包括:写缓存、读缓存、清空缓 > 存。 > > - 使用本地缓存 > > ![image-20250401214204401](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250401214204401.png) > > - 服务缓存更新,监听机制 ### 2.6自定义网络协议 ##### 2.6.1为什么需要自定义协议 目前的 RPC 框架,我们使用Vert.x的HttpServer 作为服务提供者的服务器,代码实现比较简单,其底层网络传输使用的 是 HTTP 协议。 很多同学会把HTTP和RPC理解为同一类技术,但HTTP只是RPC 框架网络传输的一种可选方式罢了。 问题来了,使用HTTP协议会有什么问题么?或者说,有没有更好的选择? 一般情况下,RPC 框架会比较注重性能,而 HTTP 协议中的头部信息、请求响应格式较“重",会影响网络传输性能。 举个例子,利用浏览器网络控制台随便查看一个请求,能看到大量的请求和响应标头: ##### 2.6.2设计方案 自定义RPC协议可以分为2大核心部分: - 自定义网络传输 - 自定义消息结构 ###### 网络传输 网络传输设计的目标是:选择一个能够高性能通信的网络协议和传输方式。 需求分析中已经提到了,HTTP 协议的头信息是比较大的,会影响传输性能。但其实除了这点外,HTTP本身属于无状态协 议,这意味着每个HTTP请求都是独立的,每次请求/响应都要重新建立和关闭连接,也会影响性能。 考虑到这点,在HTTP/1.1中引I入了**持久连接**(Keep-Alive),允许在单个TCP连接上发送多个HTTP请求和响应,避免了 每次请求都要重新建立和关闭连接的开销。 虽然如此,HTTP本身是应用层协议,我们现在设计的RPC协议也是应用层协议,性能肯定是不如底层(**传输层**)的TCP 协议要高的。所以我们想要追求更高的性能,还是选择使用TCP协议完成网络传输,有更多的自主设计空间。 ###### 消息结构 1)如何使用最少的空间呢? 大家之前接触到的数据类型可能都是整型、长整型、浮点数类型等等,这些类型其实都比较“重",占用的字节数较多。比 如整型要占用4个字节、32个bit位。 我们在自定义消息结构时,想要节省空间,就要尽可能使用更轻量的类型,比如byte字节类型,只占用1个字节、8个b it位。 需要注意的是,Java 中实现bit位运算拼接相对比较麻烦,所以权衡开发成本,我们设计消息结构时,尽量给每个数据凑 到整个字节。 2)消息内需要哪些信息呢? 目标肯定是能够完成请求嘛,那我们何不从之前的HTTP请求方式中,找到一些线索? 分析HTTP请求结构,我们能够得到RPC消息所需的信息: - 魔数:作用是安全校验,防止服务器处理了非框架发来的乱七八糟的消息 (类似HTTPS 的安全证书) - 版本号:保证请求和响应的一致性(类似HTTP协议有1.0/2.0等版本) - 序列化方式:来告诉服务端和客户端如何解析数据(类似 HTTP 的Content-Type 内容类型) - 类型:标识是请求还是响应?或者是心跳检测等其他用途。(类似 HTTP 有请求头和响应头) - 状态:如果是响应,记录响应的结果(类似HTTP的200状态代码) 此外,还需要有请求id,唯一标识某个请求,因为TCP是双向通信的,需要有个唯一标识来追踪每个请求。 最后,也是最重要的,要发送body内容数据。我们暂时称它为请求体,类似于我们之前 HTTP 请求中发送的RpcRequest 如果是 HTTP 这种协议,有专门的 key/ value 结构,很容易找到完整的 body 数据。但基于 TCP 协议,想要获取到完整的 body内容数据,就需要一些“小v心思”了,因为TCP 协议本身会存在**半包和粘包**问题,每次传输的数据可能是不完整的, 具体的后面会讲。 所以我们需要在消息头中新增一个字段 ==请求体数据长度==,保证能够完整地获取 body内容信息。 基于以上的思考,我们可以得到最终的消息结构设计,如下图: ![image-20250402192039916](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250402192039916.png) 实际上,这些数据应该是紧凑的,请求头信息总长17个字节。也就是说,上述消息结构,本质上就是拼接在一起的一个 字节数组。我们后续实现时,需要有消息编码器和消息解码器,编码器先new一个空的Buffer缓冲区,然后按照顺序 向缓冲区依次写入这些数据;解码器在读取时也按照顺序依次读取,就能还原出编码前的数据。 通过这种约定的方式,我们就不用记录头信息了。比如magic魔数,不用存储"magic"这个字符串,而是读取第一个字节 (前8 bit)就能获取到。 如果你学过Redis底层,会发现很多数据结构都是这种设计。 如果大家是第一次设计协议,或者经验不足,强烈建议大家先去学一下优秀开源框架的协议设计,这样不会说毫无头绪。 代码实现 **服务端** ```java public class VertxTcpServer implements HttpServer { private byte[] handleRequest(byte[] requestData){ return "@@@@@@@@@@@@@@@@@@@@hello,client".getBytes(); } @Override public void doStart(int port) { //1. 创建vert.x实例 Vertx vertx = Vertx.vertx(); //2. 创建TCP服务器 NetServer server = vertx.createNetServer(); //3. 处理请求 server.connectHandler( socket->{ //3.1处理连接 socket.handler(buffer -> { //3.1.1处理收到的字节数组 byte[] requestData = buffer.getBytes(); //3.1.2在这里自定义的字节数组处理逻辑,比如解析请求,调用服务,构造响应等等 byte[] responseData = handleRequest(requestData); //3.1.3 发送响应 socket.write(Buffer.buffer(responseData)); }); }); //4. 启动TCP服务器 server.listen(port,result->{ if(result.succeeded()){ System.out.println("TCP server started on port "+port); } else { System.err.println("Failed to start TCP server" + result.cause()); } }); } public static void main(String[] args) { new VertxHttpServer().doStart(8888); } } ``` **客户端** ```java /** * TCP vertx的客户端 */ public class VertxTcpClient { public void start (){ //创建Vert.x 实例 Vertx vertx = Vertx.vertx(); vertx.createNetClient().connect(8888,"localhost",result->{ if(result.succeeded()){ System.out.println("Connected to TCP server"); io.vertx.core.net.NetSocket socket = result.result(); //发送数据 socket.write("@@@@@@@@@@@@@@@@@@@@hello ,Server!"); // 接收响应 socket.handler(buffer -> { System.out.println("Received desponseData from server: " + buffer.toString()); }); } else{ System.err.println("Failed to connect to TCP server"); } }); } public static void main(String[] args) { new VertxTcpClient().start(); } } ``` ### 2.7负载均衡 #### 什么是负载均衡 > 让我们把这个词拆开来看: > 1)何为负载?可以把负载理解为要处理的工作和压力,比如网络请求、事务、数据处理任务等。 > 2)何为均衡?把工作和压力平均地分配给多个工作者,从而分摊每个工作者的压力,保证大家正常工作。 > > 用个比喻,假设餐厅里只有一个服务员,如果顾客非常多,他可能会忙不过来,没法及时上菜、忙中生乱;而且他的压力 > 会越来越大,最严重的情况下就累倒了无法继续工作。而如果有多个服务员,大家能够服务更多的顾客,即使有一个服务 > 员生病了,其他服务员也能帮忙顶上。 > 所以,负载均衡是一种用来分配网络或计算负载到多个资源上的技术。它的目的是确保每个资源都能够有效地处理负载、增加系统的并发量、避免某些资源过载而导致性能下降或服务不可用的情况。 > > 回归到我们的RPC框架,负载均衡的作用是从一组可用的服务提供者中选择一个进行调用。 #### 负载均衡的类型 1. 轮询 ![image-20250406133826323](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133826323.png) 2. 随机 ![image-20250406133726516](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133726516.png) 3. 一致性哈希 ![image-20250406133741381](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133741381.png) #### 出现的问题 1. 键名未对应 > key = roundRobin ![image-20250406133416939](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133416939.png) ### 2.8 重试机制 #### 为什么需要重试机制 > 目前,如果使用RPC框架的服务消费者调用接口失败,就会直接报错。 > > 调用接口失败可能有很多原因,有时可能是服务提供者返回了错误,但有时可能只是网络不稳定或服务提供者重启等临时 > 性问题。这种情况下,我们可能更希望服务消费者拥有自动重试的能力,提高系统的可用性。 > > #### 重试机制 我们需要掌握的是“如何设计重试机制",重试机制的核心是重试策略,一般来说,包含以下几个考虑点: 1.什么时候、什么条件下重试? 2.重试时间 (确定下一次的重试时间) 3.什么时候、什么条件下停止重试? 4.重试后要做什么? > 1. 重试条件 > > 首先是什么时候、什么条件下重试? > 这个比较好思考,如果我们希望提高系统的可用性,当由于网络等异常情况发生时,触发重试。 > > 2. 重试时间 > > 重试时间(也叫重试等待)的策略就比较丰富了,可能会用到一些算法,主流的重试时间算法有: > > - 固定重试间隔 > > ![image-20250406133519308](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133519308.png) > > - 指数退避重试 > > ![image-20250406133631281](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133631281.png) > > - 随机延迟重试 > - 可变延迟重试 > - 无重试 > > ![image-20250406133801230](C:\Users\钰玟\AppData\Roaming\Typora\typora-user-images\image-20250406133801230.png) > > 3. 停止重试 > > 一般来说,重试次数是有上限的,否则随着报错的增多,系统同时发生的重试也会越来越多,造成雪崩。 > 主流的停止重试策略有: > 1)最大尝试次数:一般重试当达到最大次数时不再重试。 > 2)超时停止:重试达到最大时间的时候,停止重试。 > > 4. 重试工作 > > 最后一点是重试后要做什么事情?一般来说就是重复执行原本要做的操作,比如发送请求失败了,那就再发一次请求。 > 需要注意的是,当重试次数超过上限时,往往还要进行其他的操作,比如: > 1.通知告警:让开发者人工介入 > 2.降级容错:改为调用其他接口、或者执行其他操作 ### 2.9 容错机制 我们给RPC 框架增加了重试机制,提升了服务消费端的可靠性和健壮性。 但如果重试超过了一定次数仍然失败,我们又该怎么处理呢? 或者说当调用出现失败时,我们一定要重试么?有没有其他的策略呢? > 1. 概述 > > 容错是指系统在出现异常情况时,可以通过一定的策略保证系统仍然稳定运行,从而提高系统的可靠性和健壮性。 > 在分布式系统中,容错机制尤为重要,因为分布式系统中的各个组件都可能存在网络故障、节点故障等各种异常情况。要 > 顾全大局,尽可能消除偶发/单点故障对系统带来的整体影响。 > 打个比方,将分布式系统类比为一家公司,如果公司某个优秀员工请假了,需要“触发容错",让另一个普通员工顶上,这 > 本质上是容错机制的一种降级 策略。 > 容错机制一般都是在系统出现错误时才触发的,这点没什么好讲的,我们需要重点学习的是容错策略和容错实现方式。 > > 2. 容错策略 > > 容错策略有很多种,常用的容错策略主要是以下几个: > 1)Fail-Over故障转移:一次调用失败后,切换一个其他节点再次进行调用,也算是一种重试。 > 2)Fail-Back失败自动恢复:系统的某个功能出现调用失败或错误时,通过其他的方法,恢复该功能的正常。可以理解为 > 降级,比如重试、调用其他服务等。 > 3)Fail-Safe静默处理:系统出现部分非重要功能的异常时,直接忽略掉,不做任何处理,就像错误没有发生过一样。 > 4)Fail-Fast快速失败:系统出现调用错误时,立刻报错,交给外层调用方处理。 > > 3. 实现方式 > > 容错其实是个比较广泛的概念,除了上面几种策略外,很多技术都可以起到容错的作用。 > > 1)重试:重试本质上也是一种容错的降级策略,系统错误后再试一次。 > 2)限流:当系统压力过大、已经出现部分错误时,通过限制执行操作(接受请求)的频率或数量,对系统进行保护。 > 3)降级:系统出现错误后,改为执行其他更稳定可用的操作。也可以叫做“兜底”或“有损服务",这种方式的本质是:即 > 使牺牲一定的服务质量,也要保证系统的部分功能可用,保证基本的功能需求得到满足。 > 4)熔断:系统出现故障或异常时,暂时中断对该服务的请求,而是执行其他操作,以避免连锁故障。 > 5)超时控制:如果请求或操作长时间没处理完成,就进行中断,防止阻塞和资源占用。 > 注意,在实际项目中,根据对系统可靠性的需求,我们通常会结合多种策略或方法实现容错机制。