java架构师简书-架构专栏

RPC框架(技术总结)

2021-05-26  本文已影响0人  _code_x

什么是 RPC ?

rpc解决了什么问题

简单的说:

最终解决的问题:让分布式或者微服务系统中不同服务之间的调用(远程调用)像本地调用一样简单!调用者感知不到远程调用的逻辑。为此rpc需要解决三个问题(实现的关键):

RPC调用流程:

  1. 服务消费方(client)以本地调用方式调用服务;
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;找到服务地址,并将消息发送到服务端;(client stub封装、发送)
  3. server stub收到消息后进行解码,根据解码结果调用本地的服务;本地服务执行并将结果返回给server stub;server stub将返回结果打包成消息并发送至消费方。(server stub解码、调用、返回与发送)

4.client stub接收到消息,并进行解码。服务消费方接收返回结果;

一个完整的RPC架构里面包含了四个核心的组件,分别是Client ,Server,Client Stub以及Server Stub,这个Stub大家可以理解为存根(调用与返回)。分别说说这几个组件:

小结:RPC 的目标就是封装调用过程,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。

要实现一个RPC不算难,难的是实现一个高性能、高可靠、高可用的RPC框架(需要考虑的问题)

  1. 如何解决获取实例的问题?既然系统采用分布式架构,那一个服务势必会有多个实例,所以需要一个服务注册中心,比如在Dubbo中,就可以使用Zookeeper作为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用。也可以同Nacos做服务注册中心;
  2. 如何选择实例?就要考虑负载均衡,例如dubbo提供了4种负载均衡策略;
  3. 如果每次都去注册中心查询列表,效率很低,那么就要加缓存
  4. 客户端总不能每次调用完都等着服务端返回数据,所以就要支持异步调用
  5. 服务端的接口修改了,老的接口还有人在用,这就需要版本控制
  6. 服务端总不能每次接到请求都马上启动一个线程去处理,于是就需要线程池

常用的RPC框架

数据交互为什么用 RPC,不用 HTTP?

除 RPC 之外,常见的多系统数据交互方案还有分布式消息队列、HTTP 请求调用、数据库和分布式缓存等。RPC 和 HTTP 调用是没有经过中间件的,它们是端到端系统的直接数据交互。

首先需要指正,这两个并不是并行概念。RPC 是一种设计,就是为了解决不同服务之间的调用问题,完整的 RPC 实现一般会包含有 传输协议序列化协议 这两个。

而 HTTP 是一种传输协议,RPC 框架完全可以使用 HTTP 作为传输协议,也可以直接使用 TCP,使用不同的协议一般也是为了适应不同的场景使用 TCP 和使用 HTTP 各有优势:

(1)传输效率

(2)性能消耗,主要在于序列化和反序列化的耗时

(3)跨平台

总结

Java知识点

调用如何实现客户端无感(动态代理技术)? 与静态代理的区别。

静态代理:每个代理类只能为一个接口服务,这样会产生很多代理类。普通代理模式,代理类Proxy的Java代码在JVM运行时就已经确定了,也就是静态代理在编码编译阶段就确定了Proxy类的代码。而动态代理是指在JVM运行过程中,动态的创建一个类的代理类,并实例化代理对象。

JDK 动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用业务方法前调用InvocationHandler 处理。代理类必须实现 InvocationHandler 接口,并且,JDK 动态代理只能代理实现了接口的类。

JDK 动态代理类基本步骤,如果想代理没有实现接口的对象?

JDK 动态代理类基本步骤:

CGLIB 框架实现了对无接口的对象进行代理的方式。JDK 动态代理是基于接口实现的,而 CGLIB 是基于继承实现的。它会对目标类产生一个代理子类,通过方法拦截技术过滤父类的方法调用。代理子类需要实现 MethodInterceptor 接口。

CGLIB 底层是通过 asm 字节码框架实时生成类的字节码,达到动态创建类的目的,效率较 JDK 动态代理低。Spring 中的 AOP 就是基于动态代理的,如果被代理类实现了某个接口,Spring 会采用 JDK 动态代理,否则会采用 CGLIB。

写一个动态代理的例子

利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(也称“动态代理类”)及其实例(对象),代理的是接口(Interfaces),不是类(Class),也不是抽象类。在运行时才知道具体的实现,spring aop就是此原理。

// 1、创建代理对象的接口
interface DemoInterface {
    String hello(String msg);
}

// 2、创建具体被代理对象的实现类
class DemoImpl implements DemoInterface {
    @Override
    public String hello(String msg) {
        System.out.println("msg = " + msg);
        return "hello";
    }
}

// 3、创建一个InvocationHandler实现类,持有被代理对象的引用,invoke方法中:利用反射调用被代理对象的方法
class DemoProxy implements InvocationHandler {

    private DemoInterface service;

    public DemoProxy(DemoInterface service) {
        this.service = service;
    }

    @Override
    public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
        System.out.println("调用方法前...");
        Object returnValue = method.invoke(service, args);
        System.out.println("调用方法后...");
        return returnValue;
    }

}

public class Solution {
    public static void main(String[] args) {
        DemoProxy proxy = new DemoProxy(new DemoImpl());
        //4.使用Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)动态创建代理类对象,通过代理类对象调用业务方法。
        DemoInterface service = (DemoInterface)Proxy.newProxyInstance(
            DemoInterface.class.getClassLoader(),
            new Class<?>[]{DemoInterface.class},
            proxy
        );
        System.out.println(service.hello("呀哈喽!"));
    }
}

输出:

调用方法前...
msg = 呀哈喽!
调用方法后...
hello

拓展:

newProxyInstance:

invoke三个参数:

序列化与反序列化

对象是怎么在网络中传输的?

通过将对象序列化成字节数组,即可将对象发送到网络中。在 Java 中,想要序列化一个对象:

序列化对象主要由两种用途:

在 Java 序列化期间,哪些变量未序列化?

序列化期间,静态变量(static修饰)和瞬态变量(transient修饰)未被序列化:

如果有一个属性(字段)不想被序列化的,则该属性必须被声明为 transient

如何实现对象的序列化和反序列化?

将需要序列化的类实现Serializable接口就可以了,Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以序列化。JDK 中提供了 ObjectOutStream 类来对对象进行序列化。对象序列化(将对象转化为字节序列)包括如下步骤:

ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));
out.writeObject(“Hello”);

对象的反序列化(将字节序列重建成一个对象的过程)步骤如下:

ObjectInputStream in = new ObjectINputStream(new fileInputStream(“D:\\objectfile.obj”));
String obj1 = (String)in.readObject();

区别可外部接口:

Externalizable 给我们提供 writeExternal() 和 readExternal() 方法, 这让我们灵活地控制 Java 序列化机制, 而不是依赖于 Java 的默认序列化。正确实现 Externalizable 接口可以显著提高应用程序的性能。

框架中实现了哪几种序列化方式,介绍一下?

实现了 JSON、Kryo、Hessian 和 Protobuf 的序列化。后三种都是基于字节的序列化。

serialVersionUID的作用

Java的序列化机制是通过 在运行时 判断类的serialVersionUID来验证版本一致性的(用于实现对象的版本控制)。

serialVersionUID 是一个 private static final long 型 ID, 当它被印在对象上时, 它通常是对象的哈希码,你可以使用 serialver 这个 JDK 工具来查看序列化对象的 serialVersionUID。

在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值

显式地定义serialVersionUID有两种用途:

网络传输(基于Netty)

简单介绍Netty

Netty概述:

特点和优势:

为什么Netty性质高?

简单说一下IO模型,BIO、NIO与AIO

Java的IO阻塞和非阻塞以及同步和异步的问题:

(1)BIO(同步阻塞):

(2)NIO(同步非阻塞):

(3)AIO(异步非阻塞):

Netty线性模型?

Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池, boss 线程池和 worker 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 worker 线程池,其中 worker 线程池负责请求的 read 和 write 事件,由对应的Handler 处理。

如何解决 TCP 的粘包拆包问题

TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送;也可能把小的封装成一个大的数据包发送(多个小的数据包合并发送),对于接收端的应用程序拿到缓冲区的数据不知如何拆分。

TCP 粘包/拆包的原因:应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上(将发送端的缓冲区填满一次性发送),这将会发生粘包现象;总之:出现TCP 粘包/拆包的关键:套接字缓冲区大小限制与应用程序写入数据大小的关系。

Netty 自带解决方式,Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:

基于Netty进行网络读写的程序,可以直接使用这些Decoder来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LenghtFieldBasedFrameDecode更适合这样的场景。

常见的解决方案

说下 Netty 零拷贝

Netty 的零拷贝主要包含三个方面:

说下 Netty 重要组件

核心组件:

三者关系

与用户逻辑与数据流密切相关:

Netty 是如何保持长连接的(心跳机制)

首先 TCP 协议的实现中也提供了 keepalive 报文用来探测对端是否可用。TCP 层将在定时时间到后发送相应的 KeepAlive 探针以确定连接可用性。打开该设置:

ChannelOption.SO_KEEPALIVE, true

TCP 心跳的问题:

考虑一种情况,某台服务器因为某些原因导致负载超高,CPU 100%,无法响应任何业务请求,但是使用 TCP 探针则仍旧能够确定连接状态,这就是典型的连接活着但业务提供方已死的状态,对客户端而言,这时的最好选择就是断线后重新连接其他服务器,而不是一直认为当前服务器是可用状态一直向当前服务器发送些必然会失败的请求。

Netty 中提供了 IdleStateHandler 类专门用于处理心跳。IdleStateHandler 的构造函数如下:

public IdleStateHandler(long readerIdleTime, long writerIdleTime, 
                        long allIdleTime,TimeUnit unit){
}

所以这里可以分别控制读,写,读写超时的时间,单位为秒,如果是0表示不检测,所以如果全是0,则相当于没添加这个 IdleStateHandler,连接是个普通的短连接。

Nacos注册中心

待补充。。。

设计模式

责任链模式与在netty中的应用

适用场景:对于一个请求来说,如果有个对象都有机会处理它,而且不明确到底是哪个对象会处理请求时,我们可以考虑使用责任链模式实现它,让请求从链的头部往后移动,直到链上的一个节点成功处理了它为止

优点:

缺点:

所有的请求都从链的头部开始遍历,对性能有损耗
极差的情况,不保证请求一定会被处理

巨人的肩膀:

https://www.nowcoder.com/discuss/588903?channel=-1&source_id=profile_follow_post_nctrack

https://www.jianshu.com/p/28e48e5f9c73

https://blog.csdn.net/qq_21125183/article/details/86161620

https://andyoung.blog.csdn.net/article/details/113632855
https://www.cnblogs.com/ZhuChangwu/p/11241304.html

上一篇下一篇

猜你喜欢

热点阅读