# TinyRPC **Repository Path**: CodeNoobPromising/tiny-rpc ## Basic Information - **Project Name**: TinyRPC - **Description**: TinyRPC是一款自写的建议RPC框架,基于Java+Etcd+Vert.x+自定义协议实现。开发者可以引入SpringBootStarter,通过注解和配置文件快速使用TinyRPC,实现简易版的远程调用服务,另外支持通过SPI机制动态扩展序列化器,负载均衡器,提供重试和容错策略等等。 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-03-03 - **Last Updated**: 2025-03-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: rpc ## README # TinyRPC ## 介绍 TinyRPC是一款较高性能的RPC框架,基于Java+Etcd+Vert.x+自定义协议实现。开发者可以引入对应的SpringBootStarter,通过注解和配置文件快速使用TinyRPC,实现简易版的远程调用服务,另外支持通过SPI机制动态扩展序列化器、负载均衡器、重试和容错策略等等。 ## 技术选型 * ⭐️ Vert.x 框架 * Vert.x 使用 Netty 作为其网络通信层的基础框架。Netty 提供了高性能的异步事件驱动模型,Vert.x 在此基础上构建了更上层的抽象和工具。 * ⭐️ Etcd 云原生存储中间件(jetcd 客户端) * ZooKeeper 分布式协调工具(curator 客户端)(暂时没写) * ⭐️ SPI 机制(读取配置加载用户自定义或者系统指定的工具) * ⭐️ 多种序列化器 * JSON 序列化 * Kryo 序列化 * Hessian 序列化 * ⭐️ 多种设计模式 * 双检锁单例模式 * 工厂模式 * 代理模式 * 装饰者模式 * ⭐️ Spring Boot Starter 开发 * 反射和注解驱动 * Guava Retrying 重试库 ## 项目目录介绍 * tiny-rpc-core:TinyRPC 框架正式代码(项目启动后扩展的核心代码) * tiny-rpc-proto:TinyRPC 框架初始版(项目启动阶段的简易实现) * tiny-rpc-example-common:示例代码公用模块 * tiny-rpc-example-consumer:示例服务消费者 * tiny-rpc-example-provider:示例服务提供者 * tiny-rpc-example-springboot-consumer:示例服务消费者(Spring Boot 框架) * tiny-rpc-example-springboot-provider:示例服务提供者(Spring Boot 框架) * tiny-rpc-spring-boot-starter:注解驱动的 RPC 框架,可在 Spring Boot 项目中快速使用 ## 注册中心 1. 数据分布式存储:集中的注册信息数据存储、读取和共享 2. 服务注册:服务提供者上报服务信息到注册中心 3. 服务发现:服务消费者从注册中心拉取服务信息 4. 心跳检测测:定期检查服务提供者的存活状态 5. 服务注销:手动剔除节点、或者自动剔除失效节点 6. 更多优化点:比如注册中心本身的容错、服务消费者缓存等。 ### Etcd介绍及启动 * etcd 本地启动占用2379和2380端口 * 2379:提供HTTP APi服务,直接和etcdctl客户端交互 * 2380:集群中节点间通讯 ![img_3.png](img/img_3.png) ### EtcdKeeper - Etcd的可视化控制界面 ![img_4.png](img/img_4.png) ### 设计Etcd的KV存储结构 使用 **层级结构**,将服务理解为文件夹,将服务对应的多个节点理解为文件夹下的文件,可以通过服务名称用**前缀** 查询的方式查询到某个服务的所有节点 键名规则: `/业务前缀/服务名/服务节点地址` eg: key=/service/localhost:8080 ### 心跳检测及续期机制 ![img_8.png](img/img_8.png) ### 服务节点下线机制 下线分主动下线和被动下线 * 被动下线即服务提供方异常退出,这一点可以直接由Etcd提供的过期机制实现(等到过期后自动删除注册信息) * 主动下线即服务主动退出,于是该在服务提供方主动退出之前将注册信息清除 核心是**主动下线**,交给JVM的*ShutdownHook*机制,**在JVM即将关闭之前执行工作** ```Java /** * 框架初始化,支持传入自定义配置 * todo protoss版本:我们设置了消费方和提供方与注册中心的SPI机制配置化方式的建立连接,在此需要注册中心的初始化操作 * * @param newRpcConfig */ public static void init(TinyRpcConfig newRpcConfig) { rpcConfig = newRpcConfig; log.info("TinyRPC init..., config: {}", newRpcConfig.toString()); // 通过RPC的配置获取注册中心的配置项,于是初始化注册中心 RegistryCenterConfig registryCenterConfig = rpcConfig.getRegistryCenterConfig(); RegistryCenter registryCenter = RegistryCenterFactory.getRegistryCenterByType( registryCenterConfig.getRegistryCenterType()); registryCenter.registryCenterInit(registryCenterConfig); log.info("RegistryCenter init..., config:{}", registryCenterConfig); // 创建并注册ShutdownHook,在JVM退出前执行注册中心下线流程(将对应服务节点及时清楚注册信息) Runtime.getRuntime().addShutdownHook(new Thread(registryCenter::destroyRegistryCenter)); } ``` ### Etcd的watch监听机制 框架的设计提供消费方拉取注册中心中的服务列表缓存信息,而服务列表缓存也应该根据服务的状态动态变化。这就靠Etcd的watch监听机制实现。 以下是监听机制的步骤简述:(需要防止重复监听同一个key代表的服务列表) - 服务消费方-监听-Etcd - 服务提供者-从Etcd下线 - Etcd-通知服务消费方【服务下线信息】 - 服务消费方-处理-清除目标服务缓存信息 ## 自定义协议 HTTP协议是RPC框架网络传输的一种可选方式,然而RPC框架一般注重性能,HTTP协议请求响应格式以及头部信息很重,严重影响RPC网络传输的性能。因此自拟一套RPC的协议,自己定义请求响应格式实现高性能、灵活、安全的RPC框架。 ### 网络传输 * HTTP协议本身无状态,每次请求都是独立的,请求响应前都需要**重新建立和关闭连接** * 即便HTTP协议后续版本引入了Keep-Alive的持久连接,但HTTP和RPC框架的协议都属于应用层协议,性能不如传输层协议TCP ### 消息结构 用最少的空间传递必要信息 请求头结构: * magic【1 byte】:安全证书(也像JVM对编译得到的.class字节码中的魔数)、 * version【1 byte】:版本号,保证请求和响应的一致性 * serializationId【1 byte】:序列化方式 * type【Req/Res 1 byte】:区别请求或响应的类型 * status【1 byte】:响应结果 * TinyRPC Request ID【8 byte】:TCP请求唯一标识 * Body Length【4 byte】:应对TCP可能出现的半包、粘包问题,根据请求体数据长度获取完整body内容 * TinyRPC Request Body:请求体(内容) 因为协议是我们自拟的,请求头信息总长 17 byte,按照我们定义的顺序组成一个字节数组,就可以对封装的数组做【编码】和【解码】操作 【编码】:从空的Buffer缓冲区中按顺序写入字节数组的内容 【解码】:从已写入Buffer缓冲区的内容顺序读取,还原编码数据 TinyRpc中的流程如下: 1. Client(消费方)发送请求TinyRpcProtocolMessage request 2. request通过【编码器】编码得到Buffer字节数组发送给Server(提供方) 3. Server将收到的Buffer字节数组通过【解码器】解码得到TinyRpcProtocolMessage request 4. Server处理request构造响应TinyRpcProtocolMessage response 5. response通过【编码器】编码得到Buffer字节数组发送给Client(消费方) 6. Client将收到的Buffer字节数组通过【解码器】解码得到TinyRpcProtocolMessage response 7. 如上一次完整的请求响应逻辑 ### 半包、粘包问题 半包:收到的数据变少 粘包:收到的数据在预期之外(更多一些) #### Vert.x解决半包、粘包问题 一开始只是基于bodyLength这个字段去判断,如果: 得到的数据长度 < bodyLength - 半包 得到的数据长度 > bodyLength - 粘包 Vert.x 框架中,使用内置的`RecordParser`完美解决半包粘包问题,保证下次读取到【特定长度】的字符 ## 负载均衡 同样类似于【注册中心】的实现方法,允许用户指定或者系统指定【负载均衡】逻辑,下面是系统实现的负载均衡逻辑 * 一致性哈希 * 轮询 * 加权轮询(静态权重) * 随机 * 加权随机(静态权重) ## 重试机制 ### 重试工作 重试要做的就是重复执行原本要执行的操作,如果重试超过上限次数(或者超过了最大的超时时间),往往还需要 * 通知告警 * 降级容错 ### 重试算法 选择主流的重试算法,Guava-Retrying提供的重试库 ### 重试等待策略 重试等待策略指的是重试时间隔的策略,这里实现了以下几种 * 固定重试时间 - 即每次重试之间用固定时间的间隔 * 指数退避 - 即每次重试间隔以指数级别增加,避免过于密集 * 随机延迟 - 每次重试之间用随机的时间间隔 * 不重试 ### 重试停止策略 如超过最大重试次数,超过最大重试时间则停止重试 ## 容错降级 容错指的是系统出现异常时,通过一些策略保证系统仍然稳定运行,而【容错】是一个比较宽泛的概念 ### 容错策略 * Fail-Over:故障转移,也算是一种重试 * Fail-Back:失败自动恢复,系统某个功能调用失败或者错误通过其他方法恢复该功能的正常,可以理解为降级 * Fail-Safe:静默处理,非重要功能异常时直接忽略不做任何处理,就好像错误没有发生 * Fail-Fast:快速失败,立刻报错交给外层调用发处理 ### 容错策略什么时候执行? 结合上面写的【重试机制】,本系统在**重试**仍然无法解决之后做**容错**处理 ## 启动机制 整体项目已经比较完善了,现在还差一个启动方式,使用注解驱动的机制去执行,便于开发者上手使用 目标: 1. 将所有启动相关的方法封装成一个专门启动的类或者方法,供消费方或者提供方使用 2. 注解驱动:使用类似Dubbo的`@DubboSercice`或者`@DubboRefference`注解,快速注册或者引用服务 3. 还可以封装SpringBoot Starter 实现【注解驱动】 1. 主动扫描:指定要扫描的路径,遍历所有类文件,针对有注解的类执行自定义操作 2. 监听Bean机制: Spring项目可以通过实现`BeanPostProcessor`接口,在Bean初始化后执行自定义的操作 心得:在完成"所有启动相关的方法封装成一个专门启动的类或者方法" 这一步其实就是为注解驱动埋下伏笔,因为就是靠Spring提供的机制,在【初始化】时执行操作,在Bean加载后执行操作,用这样的方式去调用我们封装好的初始化逻辑等等。搭配注解扫描的方式,【注册】或者【引入】相关的接口/实现,最后完成SpringBootStarter类似的易用性很高的启动机制。 ## 代码进度 ### Mock服务测试 ![img.png](img/img.png) ### Mock增强服务测试 ![img_1.png](img/img_1.png) ### SPI机制加载序列化器 配置自定义 hessian=com.york.tinyrpc.protoss.serializer.HessianSerializer json=com.york.tinyrpc.protoss.serializer.JsonSerializer Kryo=com.york.tinyrpc.protoss.serializer.KryoSerializer ![img_2.png](img/img_2.png) ### Etcd实现服务注册与发现 1. 单测 ![img_5.png](img/img_5.png) 2. 服务发现未获取到有效服务时见如下情况 ![img_6.png](img/img_6.png) 3. 服务发现获取到有效服务调用正常如下 ![img_7.png](img/img_7.png) ### Etcd监听示例 下方是消费方调用时【维护消费方从注册中心中已拉取到的服务列表缓存】的动态变化情况,基于Etcd的watch监听机制实现 【消费方】是【服务发现】的主要调用角色,消费方在第一次调用时触发了【服务发现】而把服务列表缓存,后续的调用都从缓存中获取该信息,且该缓存是可以信任的,因为是基于watch监听机制的,一切Key的动态变化都会触发在缓存中的状态 ![img_9.png](img/img_9.png) ### 测试基于Vert.x的TCP服务端和客户端之间的连接 ![img_10.png](img/img_10.png) ### 测试基于Vert.x的TCP协议消费提供方服务 ![img_12.png](img/img_12.png) ### 测试负载均衡器的加载并使用 ![img.png](img/img_13.png) ### 测试Spring Starter ![img_1.png](img/img_14.png)