源码级分析实战RPC框架-一文通透(含Q&A)

172 阅读13分钟

点赞收藏和评论,功德+1你我他,今天又是开心快乐的一天啊!

RPC知识体系相关请看

juejin.cn/post/748043…

前言

上一篇讲了RPC的概念性知识,搭配一个DEMO详细介绍了如何开发RPC框架,这里就直接上正菜,我们项目中真实开发的一个RPC框架。原本这里贴了一篇同事写的需求和开发逻辑讲解,但是因为内部文章,所以这里就简单讲讲原因。

这个RPC呢,比较特别,因为常规的项目是依托于公司底座,这是由于合规的原因,服务必须部署在外网,因此需要一个RPC系统做桥接。其实用HTTP也OK,但是测试是部署在内网的,为了避免开发两套代码,所以还是决定开发新的RPC来兼容公司RPC框架。

开发思路

这里结合上一篇文章的如何写一个RPC框架模块来说明一下这个实战的是怎么搭起来的

资源定义

需要注解+bean注册类

服务端在类上的服务方定义@FusionService

客户端在类变量上定义的资源定义@FusionRpcClient

注册类,因为@FusionService内嵌了@Component,所以用不着主动注入了。@FusionRpcClient是在FusionClientRpcProcessor主动对类中变量上定义了@FusionRpcClient进行了注入。

这里还有个区别是本地对象缓存类,Map<String(rpcServiceName), Object(服务端对象)> serviceMap,他是在ServerLifecycle中做的,逻辑简单来说就是从bean中获取所有类上有没有注解@FusionService,如果有,就定义rpcServiceName塞入Map

服务治理

因为蹭了科技网关来做对外的调用(内网调外网京东云-业务需求),所以服务治理相关就依赖于内网提供的基础中间件,提供注册中心、网关、负载均衡等功能。为啥要蹭呢?因为自己写太麻烦了(从头搭建网关、注册中心等等),并且科技网关提供对外的支持,而且服务端的开发可以基于内部提供的框架,大大减少了开发成本,比如监控、缓存、数据库等各种中间件的搭建和运维。

网络传输

网络传输采用的是HTTP,在科技网关上开了一个固定的接口传输数据,客户端传递RpcRequest,在包装为RpcMessage后传到服务端。服务端解析出rpcServiceName,从serviceMap中获取真实的对象执行方法,最后返回给客户端结果。

以上虽然说起来简单,但是要实现其实也挺麻烦的,比如最基础的bean生命周期处理.....真的很折磨人,顺带发现了BeanPostProcessor、Lifecycle和 Spring-Test环境的一些关系和坑点。如果亲自开发完一个rpc框架的话,剩下的就是在上面添砖加瓦了,大体框架基本是差不多的。

fusion-rpc流程

通过对代码的梳理,描述一下整体流程,此处最好结合需求大背景来看,详情看文首的两篇文章。

初始化

服务端和客户端独立初始化,有一定依赖顺序

总配置入口-GlobalFusionRpcConfig

通过@ConditionalOnProperty(value = "fusion.rpc.global.enable", havingValue = "true")控制总开关,fusion.rpc.server.enable和fusion.rpc.client.enable两个小开关,两边资源隔离互不影响

构造初始化,此时都是注入bean,大多并没有特殊的构造方法会被调用

  • ServerLifecycle、ClientLifecycle、FusionRpcFacadeDelegate、FusionClientFactory、NameResolverProvider、FusionClientRpcProcessor、IDynamicProxy、DefaultFusionDeserializer、DefaultFusionSerializer、RandomLoadBalancer、RsaCipherService
  • DefaultClientInvoker-构造方法中同时初始化线程池

Client端实例初始化-FusionClientRpcProcessor

FusionClientRpcProcessor implements BeanPostProcessor应用了Spring的生命周期,重写了postProcessAfterInitialization。处理逻辑是当前class的field上annotation如果有FusionRpcClient,则主动生成FusionRpcClient注解的Bean并注册,最后再注入到这个变量里。

这个类里冗余了一个init方法,会从applicationContext中重新拿到所有的bean,再次重复上面的处理逻辑,查漏补缺(考量是spring的三级缓存机制带来的依赖注入问题),触发时机在后面的ClientLifecycle类中。

注意这里为什么一定要implements BeanPostProcessor,原因是在Spring测试环境启动的时候会重新创建一个应用上下文并再次调用BeanPostProcessor,如果测试类中直接注解了FusionRpcClient,则可以通过postProcessAfterInitialization再次对测试类中的变量注入。

注入代理对象

FusionClientFactory#initializeInstance方法生成代理对象

/**
 * 获取指定接口的代理对象
 *
 * @param interfaceClass 要代理的接口类
 * @param invoker        用于处理方法调用的 Invoker 对象
 * @return 代理对象
 */
<T> T acquireProxy(Class<T> interfaceClass, Invoker invoker);
/**
 * 通过给定的代理对象和方法调用来执行方法。
 *
 * @param proxy  要使用的代理对象。
 * @param method 要调用的方法。
 * @param args   调用方法时使用的参数。
 * @return 方法的返回值。
 * @throws Throwable 如果在方法调用过程中发生异常。
 */
Object invoke(Object proxy, Method method, Object[] args);

通过上面两个接口定义,以及Invoke实现类

Client端实例注入-ClientLifecycle

ClientLifecycle implements SmartLifecycle,SmartLifecycle的start方法是在spring容器完全初始化之后调用,因此对于bean的操作,更推荐使用SmartLifecycle而非BeanPostProcessor。

该方法内部先注册了所有配置的拦截器,然后调用FusionClientRpcProcessor.init方法。

Server端注册Mapping

关联类:ServerLifecycle、FusionRpcFacadeDelegate

ServerLifecycle implements SmartLifecycle只干了一件事,start阶段调用了FusionRpcFacadeDelegate.startFusionServer。

startFusionServer的流程如下

  1. FusionService注解内含@Component,交给spring管理,因此从beanfactory中获取类上存在注解FusionService的bean就行
  2. 注册实现FusionServerInterceptorRegistry的拦截器
  3. 遍历bean构建自定义的FusionServiceDefinition对象,存储在serviceDefinitionMap中,以Class为key。
  4. 注册RequestMappingHandlerMapping,以配置文件中的serverProperties.getDefaultInvokeUri()为路径

初始化流程细节

c.j.i.f.rpc.init.GlobalFusionRpcConfig   : GlobalFusionRpcConfig Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : IDynamicProxy Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : NameResolverProvider Bean构造
c.j.i.f.r.c.p.NameResolverProvider       : 名称解析器初始化,填充bean工厂以及服务映射表
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : RsaCipherService Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : FusionClientFactory Bean构造
c.j.i.f.rpc.client.FusionClientFactory   : FusionClientFactory初始化
c.j.i.f.rpc.client.FusionClientFactory   : 注册远程服务: f-rpc://trade.service,个数:1
c.j.i.f.rpc.client.FusionClientFactory   : 获取远程服务的实例代理,serviceUri=f-rpc://trade.service, remoteService={"clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","injectType":"interface com..insurance.fusion.rpc.server.GreetingService","socketTimeout":5000,"connectTimeout":5000}
c.j.i.f.rpc.client.FusionClientFactory   : 从FusionRpcClient注解中获取所有的ClientInterceptor实例
c.j.i.f.rpc.client.FusionClientFactory   : 解析服务地址: f-rpc://trade.service
c.j.i.f.rpc.client.FusionClientFactory   : 解析服务地址完成: f-rpc://trade.service
c.j.i.f.rpc.client.FusionClientFactory   : 初始化实例并执行远程服务调用,injectionType=GreetingService,  finalAddress=f-rpc://trade.service, serviceFacade={"authenticate":{"serviceWhiteList":[],"allowAll":true,"publicKey":"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJAKdX2M-hvk-p9CnZUWXY_8EJSwnH-8MJE5rAT9v5BmB0KhbF_8NoUo4NIabeUfWNIOAAQy3Zy3WJ7_iKbXyB8CAwEAAQ","enable":true,"appKey":"platform"},"globalPath":"/fusion/rpc/request","hosts":[{"address":"127.0.0.1:8080","weight":20},{"address":"127.0.0.1:8080","weight":20}],"deserializer":"class com..insurance.fusion.rpc.global.DefaultFusionDeserializer","serializer":"class com..insurance.fusion.rpc.global.DefaultFusionSerializer","serviceName":"f-rpc://trade.service","clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","loadBalancer":"class com..insurance.fusion.rpc.client.protocol.balancer.impl.RandomLoadBalancer","socketTimeout":5000,"connectTimeout":5000,"remoteServices":[{"clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","injectType":"interface com..insurance.fusion.rpc.server.GreetingService","socketTimeout":5000,"connectTimeout":5000}],"properties":{}}
c.j.i.f.rpc.client.FusionClientFactory   : 注册客户端实例: f-rpc://trade.service -> GreetingService
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : FusionClientRpcProcessor Bean构造
o.e.j.s.h.ContextHandler.application     : Initializing Spring embedded WebApplicationContext
w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 502 ms

c.j.i.f.rpc.init.GlobalFusionRpcConfig   : FusionRpcFacadeDelegate Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : ServerLifecycle Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : ClientLifecycle Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : DefaultClientInvoker Bean构造
c.j.i.f.rpc.client.DefaultClientInvoker  : 默认客户端调用实现初始化
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : DefaultFusionDeserializer Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : DefaultFusionSerializer Bean构造
c.j.i.f.rpc.init.GlobalFusionRpcConfig   : RandomLoadBalancer Bean构造
o.e.j.s.h.ContextHandler.application     : Initializing Spring DispatcherServlet 'dispatcherServlet'
o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
o.e.jetty.server.AbstractConnector       : Started ServerConnector@46e64760{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8080 (http/1.1) with context path '/'
c.j.i.fusion.rpc.init.ServerLifecycle    : ServerLifecycle 开始初始化
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 初始化并注册所有的FusionService为bean
c.j.i.f.r.init.FusionRpcFacadeDelegate   : Had found fusion rpc service: FusionServiceDefinition(beanName=greetingServiceImpl, fusionServiceAnnotation=@com..insurance.fusion.rpc.annotation.FusionService(value=, interceptors=[]), fusionService=com..insurance.fusion.rpc.server.impl.GreetingServiceImpl@2ee92e7d), bean: greetingServiceImpl, class: com..insurance.fusion.rpc.server.impl.GreetingServiceImpl
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 注册requestMapping,地址为:/fusion/rpc/request
c.j.i.fusion.rpc.init.ClientLifecycle    : ClientLifecycle 开始初始化
c.j.i.f.rpc.client.FusionClientFactory   : 从注册表中获取所有的FusionClientInterceptorRegistry客户端拦截器
c.j.i.f.r.init.FusionClientRpcProcessor  : FusionClientRpcProcessor主动初始化
c.j.i.f.rpc.client.GreetServiceTest      : Started GreetServiceTest in 1.107 seconds (JVM running for 1.602)
c.j.i.f.r.init.FusionClientRpcProcessor  : com..insurance.fusion.rpc.client.GreetServiceTest#fGreetingService存在FusionRpcClient注解,主动注入变量
c.j.i.f.rpc.client.FusionClientFactory   : 获取远程服务的实例代理,serviceUri=, remoteService={"clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","injectType":"interface com..insurance.fusion.rpc.server.GreetingService","socketTimeout":-1,"connectTimeout":-1,"interceptors":[]}
c.j.i.f.r.init.FusionClientRpcProcessor  : com..insurance.fusion.rpc.client.GreetServiceTest#fAddressGreetingService存在FusionRpcClient注解,主动注入变量
c.j.i.f.rpc.client.FusionClientFactory   : 获取远程服务的实例代理,serviceUri=f-rpc://trade.service, remoteService={"clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","injectType":"interface com..insurance.fusion.rpc.server.GreetingService","socketTimeout":-1,"connectTimeout":-1,"interceptors":[]}
c.j.i.f.rpc.client.FusionClientFactory   : 从FusionRpcClient注解中获取所有的ClientInterceptor实例
c.j.i.f.rpc.client.FusionClientFactory   : 解析服务地址: f-rpc://trade.service
c.j.i.f.rpc.client.FusionClientFactory   : 解析服务地址完成: f-rpc://trade.service
c.j.i.f.rpc.client.FusionClientFactory   : 初始化实例,injectionType=GreetingService,  finalAddress=f-rpc://trade.service, serviceFacade={"authenticate":{"serviceWhiteList":[],"allowAll":true,"publicKey":"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJAKdX2M-hvk-p9CnZUWXY_8EJSwnH-8MJE5rAT9v5BmB0KhbF_8NoUo4NIabeUfWNIOAAQy3Zy3WJ7_iKbXyB8CAwEAAQ","enable":true,"appKey":"platform"},"globalPath":"/fusion/rpc/request","hosts":[{"address":"127.0.0.1:8080","weight":20},{"address":"127.0.0.1:8080","weight":20}],"deserializer":"class com..insurance.fusion.rpc.global.DefaultFusionDeserializer","serializer":"class com..insurance.fusion.rpc.global.DefaultFusionSerializer","serviceName":"f-rpc://trade.service","clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","loadBalancer":"class com..insurance.fusion.rpc.client.protocol.balancer.impl.RandomLoadBalancer","socketTimeout":5000,"connectTimeout":5000,"remoteServices":[{"clientInvoker":"class com..insurance.fusion.rpc.client.DefaultClientInvoker","injectType":"interface com..insurance.fusion.rpc.server.GreetingService","socketTimeout":5000,"connectTimeout":5000}],"properties":{}}
c.j.i.f.rpc.client.FusionClientFactory   : 注册客户端实例: f-rpc://trade.service -> GreetingService

初始化流程图

调用

调用流程图

前文提到FusionClientFactory#initializeInstance注入的代理类执行逻辑如下

自定义拦截器如下,实现即可

调用细节

c.j.i.f.rpc.client.FusionClientFactory   : 执行远程服务调用,injectionType=GreetingService,  method=sayHello, args=["tom"]
c.j.i.f.r.c.p.NameResolverProvider       : 解析服务地址,服务名称:f-rpc://trade.service,请求上下文:RequestContext(serviceInstance=null, clazz=com..insurance.fusion.rpc.server.GreetingService, methodName=sayHello, args=[tom], argsClasses=[java.lang.String], attributes={}, connectTimeout=5000, socketTimeout=5000)
c.j.i.f.r.c.p.b.impl.RandomLoadBalancer  : 负载均衡当前host列表:[ServiceInstance(address=127.0.0.1:8080, path=null, weight=20, properties=null), ServiceInstance(address=127.0.0.1:8080, path=null, weight=20, properties=null)]
c.j.i.f.rpc.client.DefaultClientInvoker  : RPC请求响应默认处理--发送HTTP POST请求并获取响应体
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 请求处理开始:/fusion/rpc/request
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 反序列化请求方式,获取实际传送的对象
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 反序列化成功,类名com..insurance.fusion.rpc.server.GreetingService
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 获取执行类:greetingServiceImpl,执行方法名:sayHello
c.j.i.f.r.init.FusionRpcFacadeDelegate   : 序列化执行结果,返回数据Hello tom!
c.j.i.f.rpc.client.FusionClientFactory   : 客户端调用成功,类名:com..insurance.fusion.rpc.server.GreetingService,方法名:sayHello,用时108ms

因为变量里注入的是FusionClientFactory#initializeInstance预生成的代理对象,所以执行逻辑就是代理对象的内部逻辑,即上面的调用流程图

FusionRpcFacadeDelegate这部分实际上是服务端的处理过程,在当前RPC中,也就是通过网关调用了服务端的固定路径/fusion/rpc/request,对应方法是com.insurance.fusion.rpc.init.FusionRpcFacadeDelegate#handleRequest

优化及拓展

优化

  1. 汇总精简配置类GlobalFusionRpcConfig--以前有FusionRpcGlobalConfiguration、FusionRpcClientSelector和FusionRpcServerSelector,因为用不上extends WebApplicationObjectSupport,所以直接注入普通的ApplicationContext
  2. 服务端接口多实现时避免覆盖,增加了服务启动时的重复检查机制--初始化时遍历注释了@FusionService的Bean时,检查是否存在同个接口下的重复实现,如果重复则报错,之前存在先后顺序的实现覆盖问题。
  3. 针对fusion.rpc.client.service-facades.remote-services配置的接口(极低概率出现),如果出现自定义beanName或者首字母连续大写的情况,需要特殊处理。SpringBean的命名规范有两种,非@Bean注解的,有自定义就用自定义,没有则按照首字母小写(连续首字母大写则保留原状)的规则默认生成beanName,用Java自带的java.beans.Introspector#decapitalize(String name)即可获取转换后的beanName。如果@Bean注解,优先用自定义,没有则通过方法名生成。
  4. 默认实现com..insurance.fusion.rpc.client.DefaultClientInvoker--重用httpclient,避免重复创建,增加实现Closeable接口,应用结束自动回收资源,减少可能的资源泄露

拓展展望

  1. 服务多实现时指定BeanName进行代码处理-但是也没什么大的必要,新建一个别的bean就行,主要应对同一个接口多个实现的问题,可能当前项目应用了复杂的设计模式处理,极小概率出现
  2. 支持多个协议,目前只有Http,因为对接网关,开别的需要容器开端口支持,比如netty之类的,所以暂时没必要。
  3. 提供版本号和分组管理功能

Q&A:

注解@FusionService相比@FusionRpcClient多了@Component,为什么要交给Spring自动注入?

比自己手动注入方便快捷,@FusionService是注释在类上,@FusionRpcClient是注释在类变量上因此不能加@Component,当然也没必要用@Bean,因为注入的是我们自定义的代理对象,Spring找不到对应实现,无法自动装配。

fusion.rpc.client.service-facades.hosts一定需要配置吗?

是的,一定需要配置,目前fusion-rpc不支持服务发现功能。后续有必要的话,调研从泰山平台获取服务注册信息是否提供了公开API,做成定时刷新本地服务列表的逻辑,目前也没有节点探活和服务重试功能,因此负载均衡选择的节点失效,当前请求就会失败(优化点)。

image.png

RPC的服务注册发现在哪里实现的?

目前没有,服务注册发现依托于科技网关,从配置文件也可以看出,只有一个目标调用地址也就是网关。所以框架中负载均衡实际上没用,因为本质是依赖于科技网关背后的负载均衡。

serviceName定义成这种格式f-rpc://trade.service有啥意义,而且还专门校验了?

image.png

目前无特殊意义,仅仅是规范一个通用的格式,不重复即可。目的是后续根据uri拓展协议和各种规则,适配服务发现。

举例来说,f-rpc 是自定义的协议前缀,用于标识这是一个特定的 RPC 框架调用地址,比如用f-rpc指代http2。 platform.service 是服务的标识符,可以在框架内部解析并进行服务发现。

fusion.rpc.client.service-facades. remote-services配置有什么用?怎么配?为什么只能配接口?

常规来说通过@FusionRpcClient注释类变量注入代理对象即可,这是扩展的额外配置,因此配置文件里可以配置全路径达到和@FusionRpcClient相同的结果。

极端情况下,比如SDK-A中定义了接口j,在服务端A中实现了接口j的实现类k上注释了@fusionservice,在客户端B引入了SDK-A和SDK-B,SDK-B中有引入接口j,如果不加这个配置,此时j找不到bean,就会在调用的时候报空指针

注意这里只能配置接口,如果配置com..insurance.fusion.rpc.server.impl.GreetingServiceImpl,注入到beanFactory里的key就是greetingServiceImpl,后续就在服务端找不到对应的KEY,会报错,无法找到对应服务。

这里牵出第二个问题点,为什么只能配接口?因为服务端代码里如上图,存入的服务映射信息的Key是取的接口,而不是具体的实现类,因此客户端必须和服务端的Key保持一致。同时也跟出一个新的问题,为什么服务端要存接口而不是具体的实现类?答案是如果出现多实现的情况下,暂时还没有开发相关找具体实现的功能,当前情况下因为存的接口,会存在实现类先后顺序的覆盖问题。

在实际应用中,非测试@Test环境中,因为处理的是代理对象,因此上级接口必然出现重复,日志截图如下

因此处理时先将代理对象转换成原本的对象,再获取其上级接口

写在最后

家人们,谁懂啊,我发到公司论坛里的上一篇数据比在掘金还好看。本周好像没什么特别的事情发生,主要是筹备下一篇文章,以下是下篇文章的预告。

故事要从东胜神州的XX路说起,某不知名的X姓牛头精在牛棚(啊不,工位!)品味刚冲好的温水,指尖在待办事项槽里划拉着今日份草料。突然!隔壁同槽传来紧急呼救:"道友!这季度的KPI渡劫大阵要塌!雷劫都劈到发际线了!速来支援"。 这X姓大侠一拍隔壁马腿(划掉),大喝一声:"键来!!!"(此时应有风雨雷电齐上阵 —— 然而工位上只有风扇在嗡嗡响),且看左手道法IDEA,右手神通Chrome,脚踏风火轮(工椅),嘴里口诀“好的呢”。天不生我社畜牛,键道万古如长夜,我辈行侠仗义,助人为乐,功德+3。 欲知这 KPI 渡劫大阵能否重塑?且看牛头王如何用代码逆转天劫。—— 预知后事如何?且听下文分解!