手写 RPC 框架项目教程 - 鱼友项目笔记 笔记
(负载均衡策略、重试策略、容错策略,都在服务消费端配置)
负载均衡策略:轮询:原子类下标自增取模。随机:随机下标。一致性哈希:有序Map (TreeMap、SortedMap) 构建哈希环,key为服务实例哈希值,value为服务实例,每个服务实例构建N个虚拟节点添加到哈希环上,对请求计算哈希值,取出哈希环上第一个哈希值大于等于请求哈希值的节点 (ceilingEntry、tailMap),或者第一个节点。
重试策略:重试时机 (发生指定异常),等待策略 (固定间隔、指数退避),停止策略 (尝试指定次数后、指定延迟时间后),参数为Callable (包装执行方法,结构为无参,返回一个值),响应为Callable返回值。
容错策略:快速失败:重新抛出异常。安全失败:不抛出异常,静默处理。
序列化器类型:JDK、JSON、Kryo、Hessian。
注册中心类型:Etcd、Zookeeper。
客户端请求封装 (http、tcp协议):构造请求,发送请求,接收响应。
服务器请求处理器封装 (http、tcp协议):接收请求,处理请求,返回响应。
服务代理工厂。
可配置的自定义SPI机制 (SPI配置加载工厂、SPI实例加载工厂、SPI配置文件、服务接口、各服务接口实现类):获取指定服务接口下指定key的服务接口实例:
- SPI配置加载:配置文件名为服务接口全类名,内容每行为key=服务接口实例全类名,扫描指定路径下的所有配置文件并构建<服务接口全类名,<key,服务接口实例全类名>>映射
- 获取指定服务接口实例:根据服务接口全类名和key找到对应的服务接口实例全类名,反射实例化并存入<服务接口实例全类名,服务接口实例对象>映射中
- 服务工厂:每个服务接口都可以配置一个对应的加载工厂,类静态初始化时加载服务配置信息,并提供根据key加载对应服务接口实例对象的方法,使用时,读取配置key,根据key获取指定服务接口实例对象并使用
配置对象:提供各种服务实现的配置,提供默认值,项目启动时从指定配置文件中加载配置信息,使用时从配置对象中获取配置信息并从对应工厂中获取指定服务实例。
编解码 (在这里针对消息结构的转换,在消息编解码中对消息体进行序列化和反序列化):针对消息结构整体,包括消息头和消息体,通常是字节对字节,或字节对基本类型数据的转换。关注数据的格式转换,将数据从一种格式转换为另一种格式。
序列化反序列化:针对字节数组和java对象的转换,通常针对消息体数据的转换。关注对象的状态和结构,将对象转换为可以存储或网络传输的格式。
装饰器模式 (调用的并不是目标对象本身,而是一个装饰器对象,装饰器对象中引用了目标对象,在装饰器对象执行时可以控制目标对象的执行) 解决TCP半包粘包问题:请求处理器执行时需要指定一个Handler (消息到来时执行),这里实现两个Handler,第一个Handler用于解决半包粘包问题,在其handle方法中,通过RecordParser,第一次先读取消息头长度 (固定配置) 的字节数据,从中获取消息体长度,第二次读取消息体长度的字节数据,并拼接到消息头后面,此时才调用第二个Handler的handle方法具体去执行请求 (第一个Handler以第二个Handler作为构造参数,如此便可控制第二个Handler的实际调用),并传入完整的消息对象。
(服务提供者有个本地服务注册器,为了根据请求信息获取对应服务实例并调用执行;服务消费者有个本地服务缓存,为了避免频繁访问注册中心)
服务提供者启动和执行流程:
-
初始化应用:读取配置信息,初始化注册中心 (启用心跳续约机制),注册JVM-ShutdownHook (关闭时调用注册中心的销毁方法,注销所有已注册的服务实例)
-
注册服务:
-
得到需要注册的服务列表,注册到本地服务注册器中 (存储服务名到服务实现实例的映射,这里可以通过反射实例化服务实例然后存储服务实例对象,或者存服务实例类型,然后执行时再反射调用) (注册到本地注册器上,是为了执行调用)
-
获取注册中心实例,将服务实例注册到注册中心上 (注册到注册中心上,是为了服务发现)
-
启动服务器:获取服务器实例,启动服务器,等待请求,请求来了,交由请求处理器执行
-
请求处理器执行:
-
通过装饰器模式解决TCP半包粘包问题,先拼接得到完整消息对象,再具体执行请求处理逻辑
-
具体请求处理执行:
-
根据协议类型进行不同处理,如tcp自定义协议:解码消息 (Buffer转为消息对象),反序列化消息体 (消息体字节数组转为请求对象)
-
根据请求信息,获取服务名、方法信息,从本地服务注册器中获取对应服务实现实例,调用指定方法获取结果对象
-
反序列化响应对象 (响应对象转为字节数组),构造响应消息,编码响应消息 (消息对象转为Buffer),响应给客户端
服务消费者启动和执行流程:
-
初始化应用:加载配置信息
-
获取服务接口代理对象:
-
若启用mock,则返回mock代理MockProxy
-
否则,返回服务代理ServiceProxy
-
执行服务接口代理对象方法,会执行服务代理对象的invoke方法
-
服务发现:根据调用的服务信息,从注册中心获取服务实例列表 (会先走本地服务缓存查询)
-
负载均衡:根据配置的负载均衡策略,从服务实例列表中选择一个服务实例
-
请求客户端处理:
-
构建消息对象,将请求对象反序列化为字节数组并设置为消息体,将消息对象编码为Buffer
-
发送请求
-
接收响应Buffer,将Buffer解码为消息对象,将消息体反序列化为响应对象并返回
-
重试策略:用重试策略包装请求客户端处理逻辑
-
容错策略:捕获请求处理过程中的异常 (在重试之外),在异常处理中执行容错策略
注册中心实现:
-
初始化:根据配置信息,与注册中心建立连接,获取客户端操作对象,启用心跳续约机制
-
注册服务:将服务实例注册到注册中心上,key为/前缀/服务全类名/ip:port,value为服务实例对象的JSON字符串表示,并设置过期时间,将key添加到已注册key集合中
-
注销服务:将服务实例从注册中心上移除,并从已注册key集合中删除该key
-
销毁注册信息 (应用主动停机时需要执行,此时服务已不可用,可通过JVM的shuedown钩子实现):遍历已注册的key集合,将所有注册的服务实例从注册中心上移除
-
心跳续约机制:遍历已注册的key集合,重置其过期时间
-
服务发现 (针对服务消费者):先从本地服务缓存中读取服务实例列表。不存在则以服务全类名为前缀到注册中心查询服务实例列表 (获取其值列表并通过JSON解析为服务实例对象),并存入本地服务缓存中,本地服务缓存格式为<服务全类名,<服务实例key,服务实例对象>>,同时监听
/前缀/服务全类名前缀节点 (懒执行机制,在远程调用时才缓存服务实例并监听节点变更) -
监听节点变更 (针对服务消费者):(监听的是针对服务全类名的前缀节点)
-
当该节点下有服务实例新增时:将该服务实例加入本地服务缓存中
-
当该节点下有服务实例删除时:将该服务实例从本地服务缓存中删除
-
当该节点下有服务实例更新时:将旧的服务实例从本地服务缓存中删除,将新的服务实例加入本地服务缓存中
服务实例对象:服务接口全类名、ip、port。
注解驱动自动装配实现:
- @RpcReference (为bean中标有@RpcReference注解的属性注入代理对象):通过
BeanPostProcessor#postProcessAfterInitialization,遍历bean所有属性,若其标注有@RpcReference注解,则根据其类型从服务代理工厂中获取其代理对象实例并设置为该属性的值 - @RpcService (将标注有@RpcService的bean作为服务注册到本地注册表和注册中心上):通过
BeanPostProcessor#postProcessAfterInitialization,若bean标注有@RpcService注解,则根据其类型,将其注册到本地服务注册表和注册中心上 - @EnableRpc (若项目中有配置类标注有@EnableRpc注解,则启动Rpc框架,初始化配置,再看情况初始化注册中心和启用服务器):通过
ImportBeanDefinitionRegistrar#registerBeanDefinitions,在@Import该类的配置类实例化前,会执行该方法,在该方法中启动Rpc框架 (@EnableRpc组合@Import注解导入该类,则在项目中任意一个配置类上使用@EnableRpc时,都会导入该类并执行初始化逻辑)

