1.RPC(Remote Procedure Call)概述
RPC是一种通过网络从远程计算机上调用程序的技术,使得构建分布式计算更加容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性,提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用;
1.1 RPC优点
- RPC框架一般使用长连接,不必每次通信都三次握手,减少网络开销;
- RPC框架一般都有注册中心,有丰富的监控管理、发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作,协议私密安全性较高;
- RPC协议简单内容小效率高,服务化架构、服务化治理;
- RPC可以基于TCP实现(Dubbo、Thrift)也可以基于HTTP2(gRPC)
1.2 RPC框架
- Dubbo:阿里巴巴开发的开源RPC框架,支持Java语言 ;
- Thrift:FaceBook开发的跨语言RPC框架,支持多种语言;
- gRPC:Google开发的跨语言RPC框架,支持多种语言;
- SpringCloud:Pivotal开发的RPC框架,提供了丰富的生态组件
1.3 RPC调用流程
所涉及的技术:
- 动态代理:生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到java动态代理技术。
- 序列化:在网络中,所有的数据都将会被转化为字节进行传送,需要对这些参数进行序列化和反序列化操作;目前主流高效的开源序列化框架有Kryo、fastjson、Hessian、Protobuf等。
- NIO通信:Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以采用Netty或者mina框架来解决NIO数据传输的问题。开源的RPC框架Dubbo就是采用NIO通信,集成支持netty、mina、grizzly。
- 服务注册中心:通过注册中心,让客户端连接调用服务端所发布的服务。主流的注册中心组件:Redis、Nacos、Zookeeper、Consul 、Etcd。Dubbo采用的是ZooKeeper提供服务注册与发现功能。
- 负载均衡:在高并发的场景下,需要多个节点或集群来提升整体吞吐能力。
- 健康检查:健康检查包括,客户端心跳和服务端主动探测两种方式。
2.序列化技术
网络传输中,数据必须采用二进制形式,序列化技术就负责对数据进行序列化(对象转成二进制数据)和反序列化(二进制数据转回对象);
2.1 常用的序列化技术
2.1.1 JDK原生序列化
public static void main(String[] args) throws IOException,
ClassNotFoundException {
String basePath = "D:/TestCode";
FileOutputStream fos = new FileOutputStream(basePath +
"tradeUser.clazz");
TradeUser tradeUser = new TradeUser();
tradeUser.setName("Mirson");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(tradeUser);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream(basePath +
"tradeUser.clazz");
ObjectInputStream ois = new ObjectInputStream(fis);
TradeUser deStudent = (TradeUser) ois.readObject();
ois.close();
System.out.println(deStudent);
}
- 首先序列化的对象必须实现java.io.Serializable接口;
- 通过ObjectOutputStream和ObjectInputStream的读和写来对对象进行序列化和反序列化;
- 对象的序列化id须一致才能反序列化(private static final long serialVersionUID);
- 序列化不会保存静态变量,变量前加Transient关键字可以不序列化该变量
2.1.2 JSON序列化
如常用的fastjson
JSON序列化具有较好的扩展性、可读性和通用性,但占用空间多、效率低;
2.1.3 Hessian2序列化
Hessian 是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。
Hessian 性能上要比 JDK、JSON 序列化高效很多,并且生成的字节数也更小。有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。
TradeUser tradeUser = new TradeUser();
tradeUser.setName("Mirson");
//tradeUser对象序列化处理
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bos);
output.writeObject(tradeUser);
output.flushBuffer();
byte[] data = bos.toByteArray();
bos.close();
//tradeUser对象反序列化处理
ByteArrayInputStream bis = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(bis);
TradeUser deTradeUser = (TradeUser) input.readObject();
input.close();
- 不支持Linked对象,如LInkedHashMap、LinkeHashSet等,但可以通过CollectionSerializer类修复
- 不支持Locale类,可以通过扩展ContextSerializerFactory类修复
- Byte/Short在反序列化的时候会转成Integer
2.1.4 Protobuf序列化
google推出的开源序列库,序列化后的体积小、序列化速度快;
3.动态代理
通过运行时动态创建代理对象,进行额外处理(如增强功能、日志记录、事务管理、权限控制等),再将调用传递给实际的目标对象;
3.1 常用的动态代理技术
3.1.1 JDK动态代理
JDK动态代理为Java标准库的一部分,不需要引入外部依赖;
public class JdkProxyTest {
/**
* 定义用户的接口
*/
public interface User {
String job();
}
/**
* 实际的调用对象
*/
public static class Teacher {
public String invoke(){
return "i'm Teacher";
}
}
/**
* 创建JDK动态代理类
*/
public static class JDKProxy implements InvocationHandler {
private Object target;
JDKProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[]
paramValues) {
return ((Teacher)target).invoke();
}
}
public static void main(String[] args){
// 构建代理器
JDKProxy proxy = new JDKProxy(new Teacher());
ClassLoader classLoader = ClassLoaderUtils.getClassLoader();
// 生成代理类
User user = (User) Proxy.newProxyInstance(classLoader, new
Class[]{User.class}, proxy);
// 接口调用
System.out.println(user.job());
}
}
3.1.2 Cglib动态代理
Cglib是一个强大的、高性能代码生成包,广泛被许多AOP框架使用,支持方法级别的拦截。
3.1.3 Javassist动态代理
一个开源的分析、编辑和创建Java字节码的类库。javassist是jboss的一个子项目,它直接使用java编码的形式,不需要了解虚拟机指令,可以动态改变类的结构,或者动态生成类。Javassist 的定位是能够操纵底层字节码,所以使用起来并不简单,Dubbo 框架的设计者为了追求性能花费了不少精力去适配javassist。
3.1.4 Byte Buddy 字节码增强库
Byte Buddy是致力于解决字节码操作和 简化操作复杂性的开源框架。Byte Buddy 目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后。它属于后起之秀,在很多优秀的项目中,像Spring、Jackson 都用到了 Byte Buddy 来完成底层代理。相比 Javassist,Byte Buddy 提供了更容易操作的 API,编写的代码可读性更高。
3.2 不同动态代理技术对比
Byte Buddy > CGLIB > Javassist> JDK
- 数据来自 Blog | JRebel & XRebel by Perforce
4.服务注册发现
在服务较多的项目中使得客户端能够及时感知服务端的变化,及时获取服务节点的连接信息;
目前用的比较多的就是Nacos和ZooKeeper;
5.网络IO模型
包括同步阻塞IO(BIO)、同步非阻塞IO(NIO)、IO多路复用、信号驱动IO、异步非阻塞IO(AIO)
阻塞:请求了之后线程一直等待回复;
非阻塞:请求之后立刻相应,线程可以干别的,并且轮询read是否完成;
5.1 IO多路复用
IO多路复用(I/O Multiplexing)是一种允许单个线程管理多个输入输出(I/O)操作的技术。它通过将多个 I/O 操作注册到一个选择器(Selector)上,然后阻塞等待其中任何一个 I/O 操作就绪,从而实现高效的 I/O 管理。IO多路复用在高并发服务器中非常有用,因为它可以显著减少系统资源的消耗,提高系统的吞吐量。
使用select进行IO请求与同步阻塞模式类似,甚至单条速度由于添加了监视socket效率更低,但是用户可以在一个线程内同事处理多个socket的IO请求,实现类似多线程的同步阻塞;
5.1.1 IO多路复用的实现方式
select | poll | epoll(仅Linux) | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | bitmap | 数组 | 红黑树 |
IO效率 | 每次调用都线性遍历,O(n)时间复杂度 | 每次调用都线性遍历,O(n)时间复杂度 | 每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,O(1)时间复杂度 |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
5.1.2 IO多路复用与阻塞IO对比
IO多路复用更适合高并发的场景,可以用较少的线程处理较多的socket的IO请求;阻塞IO每处理一个socket的IO请求都会阻塞线程,适合并发量低的场景,不需要发起大量select调用,这种场景下阻塞IO开销比IO多路复用低;
RPC调用大多情况下是高并发的场景,所以RPC框架一般会选择IO多路复用的方式,而且在linux环境下要使用epoll方式;
5.2 零拷贝
系统内核处理IO操作包含两个阶段:等待数据、拷贝数据
- 等待数据:系统内核在等待网卡接收到数据后,把数据写到内核中;
- 拷贝数据:系统内核在获取到数据后,将数据拷贝到用户进程的空间中;
进程的每一次写操作 都会把数据写到用户空间的缓冲区内,再由CPU将数据拷贝到系统内核的缓冲区,之后再由DMA将数据拷贝到网卡中,最后由网卡发送出去;
零拷贝指的就是取消用户空间与内核空间之间的数据拷贝操作;
Netty中的零拷贝,与此处的操作系统的零拷贝有一定区别,Netty的零拷贝实际上是对用户空间中数据操作的优化,Netty的接收和发送ByteBuffer采用DIRECTBUFFERS,使用堆外的直接内存(内存对象分配在JVM中堆以外的内存)进行Socket读写,不需要进行字节缓冲区的二次拷贝;