点赞收藏和评论,功德+1你我他,今天又是开心快乐的一天啊!
RPC知识体系相关请看
前言
上一篇讲了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的流程如下
- FusionService注解内含@Component,交给spring管理,因此从beanfactory中获取类上存在注解FusionService的bean就行
- 注册实现FusionServerInterceptorRegistry的拦截器
- 遍历bean构建自定义的FusionServiceDefinition对象,存储在serviceDefinitionMap中,以Class为key。
- 注册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
优化及拓展
优化
- 汇总精简配置类GlobalFusionRpcConfig--以前有FusionRpcGlobalConfiguration、FusionRpcClientSelector和FusionRpcServerSelector,因为用不上extends WebApplicationObjectSupport,所以直接注入普通的ApplicationContext
- 服务端接口多实现时避免覆盖,增加了服务启动时的重复检查机制--初始化时遍历注释了@FusionService的Bean时,检查是否存在同个接口下的重复实现,如果重复则报错,之前存在先后顺序的实现覆盖问题。
- 针对fusion.rpc.client.service-facades.remote-services配置的接口(极低概率出现),如果出现自定义beanName或者首字母连续大写的情况,需要特殊处理。SpringBean的命名规范有两种,非@Bean注解的,有自定义就用自定义,没有则按照首字母小写(连续首字母大写则保留原状)的规则默认生成beanName,用Java自带的java.beans.Introspector#decapitalize(String name)即可获取转换后的beanName。如果@Bean注解,优先用自定义,没有则通过方法名生成。
- 默认实现com..insurance.fusion.rpc.client.DefaultClientInvoker--重用httpclient,避免重复创建,增加实现Closeable接口,应用结束自动回收资源,减少可能的资源泄露
拓展展望
- 服务多实现时指定BeanName进行代码处理-但是也没什么大的必要,新建一个别的bean就行,主要应对同一个接口多个实现的问题,可能当前项目应用了复杂的设计模式处理,极小概率出现
- 支持多个协议,目前只有Http,因为对接网关,开别的需要容器开端口支持,比如netty之类的,所以暂时没必要。
- 提供版本号和分组管理功能
Q&A:
注解@FusionService相比@FusionRpcClient多了@Component,为什么要交给Spring自动注入?
比自己手动注入方便快捷,@FusionService是注释在类上,@FusionRpcClient是注释在类变量上因此不能加@Component,当然也没必要用@Bean,因为注入的是我们自定义的代理对象,Spring找不到对应实现,无法自动装配。
fusion.rpc.client.service-facades.hosts一定需要配置吗?
是的,一定需要配置,目前fusion-rpc不支持服务发现功能。后续有必要的话,调研从泰山平台获取服务注册信息是否提供了公开API,做成定时刷新本地服务列表的逻辑,目前也没有节点探活和服务重试功能,因此负载均衡选择的节点失效,当前请求就会失败(优化点)。
RPC的服务注册发现在哪里实现的?
目前没有,服务注册发现依托于科技网关,从配置文件也可以看出,只有一个目标调用地址也就是网关。所以框架中负载均衡实际上没用,因为本质是依赖于科技网关背后的负载均衡。
serviceName定义成这种格式f-rpc://trade.service有啥意义,而且还专门校验了?
目前无特殊意义,仅仅是规范一个通用的格式,不重复即可。目的是后续根据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 渡劫大阵能否重塑?且看牛头王如何用代码逆转天劫。—— 预知后事如何?且听下文分解!