Java面试——RPC
一、RPC 服务的原理
【1】Socket 套接字:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个链接的一端称为 Socket。可以实现不同计算机之间的通信,是网络编程接口的具体实现。Socket 套接字是客户端/服务端网络结构程序的基本组成部分。
【2】RPC 的调用过程:实现透明的远程过程调用的重点是创建客户存根(client stub),存根(stub)就像代理(agent)模式里的代理。在生成代理代码后,代理的代码就能与远程服务端通信了,通信的过程都是由 RPC 框架实现,而调用者就像调用本地代码一样方便。在客户端看来,存根函数就像普通的本地函数一样,但实际上包含了通过网络发送和接收消息的代码。
● 第1步:客户端调用本地的客户端存根方法(client stub)。客户端存根的方法会将参数打包并封装成一个或多个网络消息体并发送到服务端。将参数封装到网络消息中的过程被称为编程(encode),它会将所有数据序列化为字节数组格式。
● 第2步:客户端存根(client stub)通过系统调用,使用操作系统内核提供的 Socket 套接字接口来向远程服务发送我们编码的网络消息。
● 第3步:网络消息由内核通过某种协议(无连接协议:UDP,面向连接协议:TCP)传输到远程服务端。
● 第4步:服务端存根(server stub)接受客户端发送的消息,并对参数消息进行解码(decode),通常它会将参数从标准的网络格式转化成特定的语言格式。
● 第5步:服务端存根调用服务端,并将从客户端接收的参数传递给该方法,它来运行具体的功能并返回,对客户端来说这部分代码的执行就是远程过程调用。
● 第6步:将返回值返回到服务端存根代码中。
● 第7步:服务端存根在将该返回值进行编码并序列化后,通过一个或多个网络消息发送给客户端。
● 第8步:消息通过网络发送到客户端存根中。
● 第9步:客户端存根从本地 Socket 接口中读取结果消息。
● 第10步:客户端存根再将结果返回给客户端函数,并且消息从网络二进制形式转化成本低语言格式,这样就完成了远程服务调用。
二、gRPC 你了解多少
gRPC 是谷歌旗下的一款 RPC 框架,基于 HTTP/2 协议标准,使用 ProtoBuf 作为序列化工具和接口定义语言(IDL),支持多语言开发。使用 gRPC后,客户端应用就像调用本地对象一样直接调用另一台机器上服务端的方法,使我们能够更容易地创建分布式应用和服务。gRPC 和其它 RPC框架类似,也通过定义一个服务接口并指定其能够被远程调用的方法,然后在服务端实现这个接口,并运行 gRPC服务器来处理客户端调用。在客户端拥有一个存根,该存根提供了和服务端相同的方法。gRPC 客户端和服务端可以在不同的环境下进行和交互,并且可以用 gRPC支持的任意语言来开发。
【1】服务定义:默认使用 protocol buffers作为接口定义语言,来描述服务接口和定义数据结构,示例代码如下:
【2】gRPC 中的四种服务类型:①、单项的 RPC:它是最简单的,和其他 RPC 框架中的用法一样。客户端通过调用该方法发送请求到服务端,等待服务器的响应,和本地方法调用一样,示例代码如下:
②、服务端流式 RPC:服务端发送一个请求给客户端,可获取一个数据流来读取一些列消息。客户端从返回的数据流中一直读取直到没有更小消息为止。
③、客户端流式 RPC:既客户端可以通过一个数据流写入并发送一系列消息给服务端。
如果使用的是单机模式的 Dubbo 服务,如果服务消费者(Consumer)发出一次调用请求,恰好这次由于网络问题调用失败,我们便可以配置服务消息者的重试策略,可能消费者的第二次重试调用是成功的(重试策略只需要配置即可,重试过程是透明的)但是服务提供者发布服务所在的节点发生故障,那么消费者怎么重试都是失败的,所以需要采取集群容错模式,这样如果单个服务节点因故障无法提供服务,则可以根据配置的集群容错模式,调用其它可用的服务节点,这就提高了服务的可用性。
【1】集群容错的配置方法:通过 cluster 属性进行配置,支持6种集群模式。
【2】 六种集群容错模式:①、Failover Cluster 模式:默认选择模式,在调用失败时会自动切换,重新尝试调用其它节点上可用的服务。适用幂等性(任意多次执行对资源本身所产生的影响均与一次执行的影响相同)操作,例如读操作。可通过 retries 属性来设置重试次数(不含第一次);
②、Failfast Cluster 模式:快速失败模式,调用只执行一次,若失败则立即报错。这种操作适合非幂等性操作,每次调用的副作用是不同的,比如数据库的写操作,或者交易系统中的订单操作等,如果一次失败就应该让它直接失败,不需要重试。
③、Failsafe Cluster 模式:失败安全模式,如果调用失败,则直接忽略失败的调用,记录失败的调用到日志文件中,以便后续审计。
④、Failback Cluster 模式:失败后自动恢复,后台记录失败的请求,定时重发,通常用于消息通知操作。
⑤、Forking Cluster 模式:并行调用多个服务器,只要一个成功便返回。通常用于实时性要求比较高的读操作,但需要浪费更多的服务资源。可通过 forks 属性(forks="2")来这只最大并行数。
⑥、Broadcast Cluster 模式:广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存和日志等本地资源信息。
四、Dubbo 负载均衡
Dubbo 框架内置了负载均衡的功能及扩展接口,我们可以透明地扩展一个服务或服务集群,根据需要能非常容易地增加或移除节点,提高服务的可伸缩性。Dubbo内置了4种负载均衡策略。
【1】集群的负载均衡配置方法:
【2】集群的四种负载均衡策略:①、随机模式:指按权重设置随机概率。调用量大时分布越均匀。
②、轮询模式:按公约后的权重设置轮询比率。该模式存在响应慢的提供者会累积请求的问题。
③、最少活跃调用数:指响应慢的提供者收到更少的请求的一种调用方式,如果活跃数相同的则随机。活跃数值调用前后的计数差,响应越慢的提供者调用前后的计数差越大。
④、一致性Hash:指带有相同参数的请求总是被发给同一提供者。在某台机器挂掉时,原本发往该机器的请求会基于虚拟节点平摊到其他提供者上,不会引起剧烈变动。
五、Dubbo 服务降级
服务降级则是在服务器压力剧增的情况下,根据当前的业务情况及流量对一些服务和页面有策略地进行降级,以释放服务器资源并保证核心任务的正常运行。Hystrix 是 Netflix 的开源框架,主要用于解决分布式系统交互时的超时处理和容错,具有保护系统稳定性的功能,是目前最流行的容错系统。Hystrix 的设计主要包括资源隔离、熔断器和命令模式。Dubbo 通过使用 mock 配置来实现服务降级。mock 在出现非业务异常(比如超时、提供者全部挂掉或者网络异常)时执行。
六、自己实现一个简单的 RPC
【详细博客链接】:链接
七、RPC 和 HTTP的区别
【详细博客链接】:链接
八、Dubbo是如何暴露服务的
【详细博客链接】:链接
【1】进入ServiceBean 的 onApplicationEvent 方法,判断是否是延迟暴露服务,如果不是则调用 export 暴露服务;
【2】先进行“前置工作”检查配置和组装 URL;
【3】多协议多注册中心暴露服务,doExportUrls 遍历所有的协议,并组装 URL;
【4】组装 URL,URL 是 Dubbo 配置的载体,通过 URL 可让 Dubbo 的各种配置在各个模块之间传递,包含版本、时间戳、方法名、上下文路径、主机名以及端口号等信息。URL 之于 Dubbo,犹如水之于鱼,非常重要。这里出现的 URL 并非 java.net.URL,而是 com.alibaba.dubbo.common.URL。
【5】暴露服务,首先将服务实现封装成一个 invoke(代理类),将 invoke封装成 Export,并缓存起来,使用 invoke的 url作为 key。 先暴露本地服务,再暴露远程服务;
【6】启动 Netty,创建 ServerBootstrap 绑定端口;
【7】连接 Zookeeper,将服务注册到 Zookeeper,用于服务发现;
【8】监听 Zookeeper;
九、Dubbo 支持哪些协议,分别有什么特点
【1】dubbo 默认协议:单一 TCP长连接,Hessian 二进制序列化和 NIO异步通讯。适合于小数据包大并发的服务调用和服务消费者数远大于服务提供者数的情况。不适合传送大数据包的服务;
【2】rmi 协议:采用 JDK 标准的 java.rmi.* 实现,采用阻塞式短连接和 JDK 标准序列化方式。如果服务接口继承了 java.rmi.Remote 接口,可以和原生 RMI 互操作。因反序列化漏洞,需升级 commons-collections3 到 3.2.2版本或 commons-collections4 到 4.1 版本。对传输数据包不限,消费者和传输者个数相当;
【3】hessian 协议:底层 Http 通讯,Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。可与原生 Hessian 服务互操作。通讯效率高于 WebService 和 Java 自带的序列化。参数及返回值需实现 Serializable 接口,自定义实现 List、Map、Number、Date、Calendar 等接口。适用于传输数据包较大,提供者比消费者个数多,提供者压力较大。
【4】http 协议:基于 http 表单的远程调用协议,短连接,json 序列化.对传输数据包不限,不支持传文件。适用于同时给应用程序和浏览器 JS 使用的服务;
【5】webservice 协议:基于 Apache CXF 的 frontend-simple 和 transports-http 实现,短连接,SOAP文本序列化。可与原生 WebService 服务互操作。适用于系统集成、跨语言调用;
【6】thrift 协议:对 thrift 原生协议 [2] 的扩展添加了额外的头信息,使用较少,不支持传 null 值;
十、Dubbo 的总体架构
【详细博客链接】:链接
十一、服务引入的流程
服务的引入时机有两种,第一种是饿汉式,第二种是懒汉式。
饿汉式就是加载完毕就会引入,懒汉式是只有当这个服务被注入到其他类中时启动引入流程,默认是懒汉式。
会先根据配置参数组装成 URL ,一般而言我们都会配置注册中心,所以会构建 RegistryDirectory 。向注册中心注册消费者的信息,并且订阅提供者、配置、路由等节点。得知提供者的信息之后会进入 Dubbo 协议的引入,会创建 Invoker ,期间会包含 NettyClient,来进行远程通信,最后通过 Cluster 来包装 Invoker,默认是 FailoverCluster,最终返回代理类。
十二、服务调用的流程
调用某个接口的方法会调用之前生成的代理类,然后会从 cluster 中经过路由的过滤、负载均衡机制选择一个 invoker 发起远程调用,此时会记录此请求和请求的 ID 等待服务端的响应。服务端接受请求之后会通过参数找到之前暴露存储的 map,得到相应的 exporter ,然后最终调用真正的实现类,再组装好结果返回,这个响应会带上之前请求的 ID。
消费者收到这个响应之后会通过 ID 去找之前记录的请求,然后找到请求之后将响应塞到对应的 Future 中,唤醒等待的线程,最后消费者得到响应,一个流程完毕。
关键的就是 cluster、路由、负载均衡,然后 Dubbo 默认是异步的,所以请求和响应是如何对应上的。
十三、什么是 SPI
这又是一个方向了,从上面的回答中,不论是从 Dubbo 协议,还是 cluster ,什么 export 方法等等无处不是 SPI 的影子,所以如果是问 Dubbo 方面的问题,问 SPI 是毋庸置疑的,因为源码里 SPI 无处不在,而且 SPI 也是 Dubbo 可扩展性的基石。
SPI 是 Service Provider Interface,主要用于框架中,框架定义好接口,不同的使用者有不同的需求,因此需要有不同的实现,而 SPI 就通过定义一个特定的位置,Java SPI 约定在 Classpath下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。
所以就可以通过接口找到对应的文件,获取具体的实现类然后加载即可,做到了灵活的替换具体的实现类。
十四、Dubbo 不用 JDK 的 SPI,而要自己实现
因为 Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。因此 Dubbo 就自己实现了一个 SPI,给每个实现类配了个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化,按需加载。
十五、Dubbo 为什么默认用 Javassist
Dubbo 用 Javassist动态代理,所以很可能会问你为什么要用这个代理,可能还会引申出 JDK 的动态代理、ASM、CGLIB。
Javassist 快,且字节码生成方便。ASM 比 Javassist更快,但是没有快一个数量级,而 Javassist只需用字符串拼接就可以生成字节码,而 ASM需要手工生成,成本较高,比较麻烦。