当前位置: 首页 > 科技观察

你管这叫多宝?

时间:2023-03-15 15:42:14 科技观察

RPC框架实现又到了年初,大家又要开始准备面试了。为了方便大家,我写几篇面试相关的文章。这次是Dubbo。相信很多小伙伴都看过不少Dubbo的八卦文章。比如Dubbo支持哪些序列化框架,支持哪些注册中心,支持哪些集群容错策略,是否支持服务降级等。但是你知道Dubbo服务导出和服务引入的流程吗?服务降级是如何实现的?等等,本文从源码的角度来分享一下Dubbo的整个调用过程(不用急,图为主,辅以一小部分源码)》的实现RPC框架基本上就是下面的架构。”RPC调用的过程如下。调用者发送请求后,代理类将调用的方法和参数组装成可以在网络上传输的消息体。调用者代理类将消息体发送给提供者。提供者代理类对消息进行解码并获取调用的方法和参数。提供者代理类执行相应的方法,并返回结果“协议、编解码、序列化不是本文的重点,就不分析了,有兴趣的可以看我之前的文章。"先写一个简化版的RPC框架,让大家对上面的过程有更深入的理解。手写一个简单的PRC框架,封装网络请求对象@Data@Builder@NoArgsConstructor@AllArgsConstructorpublicclassRpcRequestimplementsSerializable{privateStringinterfaceName;privateStringmethodName;privateClass[]paramTypes;privateObject[]parameters;}根据interfaceName可以确定要调用的接口,methodName和paramTypes可以确定要调用的接口的方法名调用,定位到具体方法,传入参数即可调用方法封装调用接口封装接口给api模块,生产者端写实现逻辑,消费者端写调用逻辑publicinterfaceHelloService{StringsayHello(Stringcontent);}publicinterfaceUpperCaseService{StringtoUpperCase(Stringcontent);}开发生产者端publicclassHelloServiceImpl实现HelloService{@OverridepublicStringsayHello(Stringcontent){return"hello"+content;}}publicclassUpperCaseServiceImplimplementsUpperCaseService{@OverridepublicStringtoUpperCase(Stringcontent){returncontent.toUpperCase}}ServiceMap保存了生产端接口名和接口实现类的映射关系,使得对应的实现类publiccla可以根据请求对象的接口名找到ssServiceMap{//接口名称->接口实现类privatestaticMapserviceMap=newHashMap<>();publicstaticvoidregisterService(StringserviceKey,Objectservice){serviceMap.put(serviceKey,service);}publicstaticObjectlookupService(StringserviceKey){returnserviceMap.get(serviceKey);}}为了提高服务端的并发性,我们将每个请求的处理都放在线程池中@Slf4jpublicclassRequestHandlerimplementsRunnable{privateSocketsocket;publicRequestHandler(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){try(ObjectInputStreaminputStream=newObjectInputStream(socket.getInputStream());ObjectOutputStreamoutputStream=newObjectOutputStream(socket.get)(Output)){RpcRequestrpcRequest=(RpcRequest)inputStream.readObject();对象服务=ServiceMap.lookupService(rpcRequest.getInterfaceName());方法method=service.getClass().getMethod(rpcRequest.getMethodName(),rpcRequest.getParamTypes());Objectresult=method.invoke(service,rpcRequest.getParameters());outputStream.writeObject(结果);}catch(Exceptione){log.error("调用方法错误",e);thrownewRuntimeException("调用方法错误");}}}启动服务端publicclassRpcProviderMain{privatestaticfinalExecutorServiceexecutorService=Executors.newCachedThreadPool();publicstaticvoidmain(String[]args)throwsException{HelloServicehelloService=newHelloServiceImpl();UpperCaseServiceupperCaseService=newUpperCaseServiceImpl();//将需要暴露的接口注册到serviceMap中ServiceMap.registerService(HelloService.class.getName(),helloService);ServiceMap.registerService(UpperCaseService.class.getName(),upperCaseService);ServerSocketserverSocket=newServerSocket(8080);而(真){//获取一个socket(阻塞),所以为了并行化一个请求,开一个线程去处理//为了复用线程,threadPoolfinalSocketsocket=serverSocket.accept();executorService.execute(newRequestHandler(socket));}}}开发消费端前面提到,我们需要通过动态代理对象来解耦方法调用和网络调用,所以接下来我们来编写动态代理对象的实现逻辑。生成代理对象的过程非常简单。实现InvocationHandler接口,在invoke方法中添加代理逻辑,调用Proxy.newProxyInstance方法生成代理对象。三个参数分别是ClassLoader,代理对象需要实现的接口数组,InvocationHandler接口实现类。当代理执行实现的接口方法时,它会被调用。InvocationHandler#invoke,这个方法中加入了代理逻辑。公共类ConsumerProxy{publicstaticTgetProxy(finalClassinterfaceClass,finalStringhost,finalintport){return(T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),newClass[]{interfaceClass},newConsumerInvocationHandler(host,port));}}可以看到代理对象的主要作用是组装请求参数,然后发起网络调用@Slf4jpublicclassConsumerInvocationHandlerimplementsInvocationHandler{privateStringhost;私有整数端口;publicConsumerInvocationHandler(Stringhost,Integerport){this.host=host;this.port=端口;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{try(Socketsocket=newSocket(host,port);ObjectOutputStreamoutputStream=newObjectOutputStream(socket.getOutputStream());ObjectInputStreaminputStream=newObjectInputStream(socket.getInputStream())){RpcRequestrpcRequest=RpcRequest.builder().interfaceName(method.getDeclaringClass().getName()).methodName(method.getName()).paramTypes(method.getParameterTypes()).parameters(args).build();输出流。写对象(rpcRequest);对象结果=inputStream。读取对象();返回结果;}赶上(异常e){日志。error("消费者调用错误",e);thrownewRuntimeException("消费者调用错误");}}}此时我们只需要通过ConsumerProxy#getProxy方法即可轻松获取代理对象通过代理对象调用远程方法和调用本地方法一样方便network在本地调用HelloServiceImpl而不是调用HelloServicehelloService=ConsumerProxy.getProxy(HelloService.class,"127.0.0.1",8080);//你好世界System.out.println(helloService.sayHello("world"));UpperCaseServiceupperCaseService=ConsumerProxy.getProxy(UpperCaseService.class,"127.0.0.1",8080);//这是内容System.out.println(upperCaseService.toUpperCase("这是内容"));}}至此我们就把一个RPC框架的核心功能实现了,是不是很简单。“其实Dubbo的源码也很简单,只是增加了很多扩展功能,所以有时候会让人觉得比较难。”那么让我们分析一下核心扩展功能。比如Filter、服务降级、集群容错等是如何实现的?其他的扩展功能,比如支持多注册中心,支持多序列化框架,支持多协议,基本不知道怎么处理,就别浪费时间了。从前面的图解我们知道,代理类在服务调用和响应的过程中扮演着重要的角色。“在Dubbo中,代理类有一个专有名词叫Invoker,在Dubbo中,通过不断代理这个Invoker来增加各种新的功能。”Dubbo服务导出《当第三方框架想和Spring集成时,有哪些方式?》?《实现BeanFactoryPostProcessor接口(扩展BeanFactory)实现BeanPostProcessor接口(扩展Bean的生成过程),Dubbo也不例外,当Dubbo和Spring集成时,会在容器中注入两个BeanPostProcessor,功能如下。ServiceAnnotationBeanPostProcessor将被@Service注解的类封装到ServiceBean注入容器ReferenceAnnotationBeanPostProcessor中,@Reference注解的接口封装到ReferenceBean注入容器中,所以服务导出和服务导入必然与ServiceBean和ReferenceBean的生命周期相关。”ServiceBean实现ApplicationListener接口。当接收到ContextRefreshedEvent事件(即启动Spring容器),开始服务导出。”服务导出的两个重要步骤是将服务注册到zk(我们后面的分析,注册中心是基于zk的哈)将服务对象包装成Invoker保存在map中,key为服务名,value为Invoker对象,返回结果。”这里有个小问题,反射执行的效率会很低,那么Dubbo还有哪些解决方案呢?从图中我们可以看出,AbstractProxyInvoker是被其他Invoker代理的,而这些Invoker是用来执行Filter的,一个Invoker代理类执行一个Filter,层层代理。“下图是Dubbo逐层接收请求的过程。”在引入Dubbo服务之前,我们已经推断服务导出与ReferenceBean相关。我们来看看具体阶段?ReferenceBean实现FactoryBean接口,重写getObject方法,在该方法中导出服务。因此,我们推断服务导出的时机是在ReferenceBean被其他对象注入时基于Invoker对象构建动态代理类,并赋值给接口。最终能够发起网络调用的是DubboInvoker,这个Invoker被很多层代理来实现各种扩展功能。服务降级第一个是服务降级。什么是服务降级?“当服务不可用时,我们不想抛出异常,而是返回一个特定的值(友好提示等),这时我们可以使用服务降级。》dubbo中有很多服务降级策略,这里举几个例子:force:表示强制使用Mock行为。此时远程调用不会失败:只有在远程调用失败时才使用mock行为.如果有下面的controller,调用DemoService获取值,但是DemoService没有启动hello")publicStringhello(@RequestParam("msg")Stringmsg){returndemoService.hello(msg);}}可以看到直接返回了mock字符串(不会发生网络调用)。更改mock@Reference的属性如下,再次调用@RestControllerpublic类"msg")Stringmsg){returndemoService.hello(msg);}}将发起网络调用,调用失败,然后返回fail。“dubbo中的服务降级只是使用MockClusterInvoker类实现的,所以相对于Hystrix等功能非常简单,实现起来也非常简单,如下图所示。”当Reference没有配置mock属性或者属性为false时,表示不降级,可以直接调用代理对象。当该属性以force开头时,表示直接降级,不会发生网络调用。其他请求只有在网络出现故障后才会降级。对不起。dubbo中有很多集群容错策略容错策略解释代理类AvailableCluster找到一个可用的节点,直接发起调用AbstractClusterInvoker匿名内部类FailoverCluster失败重试(默认)FailoverClusterInvokerFailfastCluster快速失败FailfastClusterInvokerFailsafeCluster安全失败FailsafeClusterInvokerFailbackCluster失败自动恢复FailbackClusterInvokerForkingCluster并行调用ForkingClusterInvokerBroadcastCluster广播调用BroadcastClusterInvokerFailoverCluster:Automaticswitchingonfailure,whenafailureoccurs,retryotherservers.Usuallyusedforreadoperations,butretriescanintroducelongerdelays.FailfastCluster:Fastfailure,onlyonecallismade,andanerrorisreportedimmediatelyifitfails.Usuallyusedfornon-idempotentwriteoperations,suchasaddingnewrecords.FailsafeCluster:Failsafe,whenanexceptionoccurs,ignoreitdirectly.Typicallyusedforoperationssuchaswritingtoauditlogs.FailbackCluster:Automaticallyrecoverfromfailures,recordfailedrequestsinthebackground,andresendthematregularintervals.Typicallyusedformessagenotificationoperations.ForkingCluster:Callmultipleserversinparallel,andreturnaslongasonesucceeds.Itisusuallyusedforreadoperationswithhighreal-timerequirements,butmoreserviceresourcesneedtobewasted.Themaximumnumberofparallelismcanbesetbyforks="2".BroadcastCluster:Broadcastcallstoallproviders,onebyone,ifanyonereportsanerror,itwillreportanerror.Usuallyusedtonotifyallproviderstoupdatelocalresourceinformationsuchascacheorlogs."ItisrecommendedtouseFailovertoautomaticallyswitchonfailureofthereadoperation,andretrytootherserverstwicebydefault.ItisrecommendedtouseFailfasttofailquicklyforthewriteoperation,andanerrorwillbereportedimmediatelyaftersendingacallfailure."Idon'tknowifyoufoundit?"ChangingtheclusterfaulttolerancestrategyistochangeDubboInvokerProxyclass"ProxyclassesrelatedtoclusterfaulttoleranceallhaveacommonattributeRegistryDirectory,whichisaveryimportantcomponent.ItusesListtosaveallInvokerscorrespondingtotheserviceprovider.”更神奇的是,这个List是动态变化的,当服务提供者下线时,会触发相应的事件,调用者会监听这个事件,删除相应的Invoker,这样后续调用就不会下线了。有新的服务提供者,就会触发新的Invoker的生成。”当一个服务的多个Invoker摆在我们面前时,我们应该选择调用哪个呢?这就不得不提到负载均衡策略了负载均衡策略实现类讲解RandomLoadBalance随机策略(默认)RoundRobinLoadBalance轮询策略LeastActiveLoadBalance最小活跃数调用ConsistentHashLoadBalance一致性哈希策略“我们只需要选择合适的负载均衡策略”类似于服务器,最终可以发送网络请求的Invoker也会被Filter对应的Invoker类代理,一个filter一个proxy类,逐层代理,下图展示了Dubbo发送请求时的逐层调用流程,分享了一些Dubbo比较重要的扩展点,也链接了整个请求响应的基本流程!