RPC实战与核心原理

RPC全称是Remote Procedure Call,即远程过程调用。
RPC的作用:

  • 屏蔽远程调用跟本地调用的区别,让我们感受就是调用项目内的方法;
  • 隐藏底层网络通信的复杂性,专注于业务逻辑。

网络传输的数据必须是二进制数据。

整体协议就变成三部分内容:固定部分、协议头部分、协议体内容。在协议头部中有一个固定的部分,指定头部长度,然后在指定协议体的部分。

序列化的就是将对象转换成二进制数据的过程,反序列化就是反过来二进制转换为对象的过程。

常用的序列化

JDK原生序列化

序列化具体的实现是有ObjectOutputStream完成的,反序列化的具体实现是有ObjectInputStream完成的。

JSON

JSON序列化需要注意的事项:

  • JSON序列化时额外开销比较大,对于大数据量服务这意味着需要巨大的内容和磁盘开销;
  • JSON没有类型,性能不会太好。

Hessian

Hessian是动态类型、二进制、紧凑的,并且可跨语言一直的一种序列化框架。
Hessian要比JDK、JSON更加紧凑,性能上要好,生成的字节数也要小。
Hessian本身也有问题,Java常见对象的类型不支持,比如:

  • Linked系列,LinkedHashMap、LinkedHashSet等,可以通过扩展CollectionDeserializer类修复。
  • Local类,可以通过ContextSerializerFactory类修复;
  • Byte/Short反序列化的时候变成Integer。

ProtoBuf

ProtoBuf是一种轻便、搞笑的结构化数据存储格式,可以用于结构化数据序列化,支持Java、Python、C++、Go等语言。
ProtoBuf使用的时候需要定义IDL, 然后使用不同语言的IDL编译器,生成序列化工具类,优点是:

  • 序列化后体积相比JSON、Hessian小很多;
  • IDL能清晰地描述语义,足以帮助并保证应用程序之间的类型不会丢失,无需类似XML解析器;
  • 序列化反序列化速度很快,不需要通过反射获取类型;
  • 消息格式升级和兼容性不错,可以做到向后兼容。

ProtoBuf不支持的类型:

  • 不支持null
  • 不支持单纯的Map、List集合对象,需要包在对象里面。

RPC框架中网络IO模型

阻塞IO

应用进程发起IO系统调用后,应用进程被阻塞,赚到内核空间处理。内核开始等待数据,等待到数据之后,在将内核中的数据拷贝到用户内存中,
整个IO处理完毕后返回进程。最后应用的进程接触阻塞状态,运行业务逻辑。等待数据和拷贝数据操作时线程会一直处于阻塞状态。

IO多路服用

多路就是指多个通道,也就是多个网络连接的IO,复用是指多个通道复用在一个复用器上。

IO多路复用更适合高并发的场景。

零拷贝

所谓零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,都可以通过一种方式,让应用进程向用户空间写入或者读取数据,
就如同直接向内核空间写入或者读取数据一样,在通过dma将内核中的数据拷贝到网卡,或者将网卡中的数据copy到内核。
零拷贝的两种实现方式:mmap+write方式,sendFile方式。

mmap+write

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
这样就可以省掉原来内核read缓冲区copy数据到用户缓冲区,但是还是需要内核read缓冲区将数据copy到内核socket缓冲区。

sendFile

数据被 DMA 引擎从文件复制到内核缓冲区,然后调用 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为都在内核空间。

mmap 和 sendFile 的区别

  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
    在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

Netty的零拷贝是完全站在用户空间上,也就是JVM上,它的零拷贝主要是偏向于数据操作的优化上。

Netty对数据操作进行的优化

  • Netty 提供了CompositeByteBuf类,可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。
  • ByteBuffer支持slice操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。
  • 通过wrap操作,可以将byte[] 数组、ByteBuf、ByteBuffer等包装盛一个Netty ByteBuf对象,进而避免拷贝操作。
  • Netty 的ByteBuf可以采用Direct Buffers,使用堆外直接内存进行socket的读写操作。
  • Netty还提供了FileRegion中保证NIO的FileChannel.transferTo方法实现了零拷贝。

面向接口编程,屏蔽RPC处理流程

使用反射来进行处理。

可扩展的框架:

会使用spi方式,在springboot中使用condition接口也可以实现。

服务发现:到底是要CP还是AP

选择AP。
在大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群件各节点间需要同步大量服务节点数据,
到最如下问题:

  • 注册中心负载过高;
  • 各节点数据不一致;
  • 服务下发不及时或者下发错误的服务节点列表。

RPC框架依赖的注册中心的服务数据的一致性其实并不满足CP,只要满足AP即可。可以使用消息总线的通知机制,来保证注册中心数据的最终一致性,来解决这些问题。

当节点达到一定数量时,集中进行注册操作,会导致cpu持续升高,最终宕机。

健康监测:这个节点都挂了,为啥还要疯狂发请求

要保证业务正常:不同区域、不同机架去检测,保证业务正常。

负载均衡:节点收到的流量不一样

dubbo的负载均衡方法:

  • 基于权重随机算法:将请求按照权重进行分配,权重越大,分配的越多。
  • 基于最小活跃调用数算法:活跃少越少,表明该服务提供者效率越高,单位时间内可处理更多的请求。
  • 基于hash一致性:适用于服务有状态的场景。
  • 基于加权轮询算法:权重越大,节点选中的更多。

异常重试:在约定时间内安全重试

异常重试需要关注的点:

  • 保证被重试的业务服务是具有幂等性的;
  • 超时重试前重置计时;
  • 针对业务返回的异常,设置重试是异常名单;
  • 重试时负载均衡选取节点时要剔除前一次访问的节点

优雅关闭:如何避免服务停机带来的业务损失

服务对象在关闭过程中,会拒绝的新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。
Runtime.getRuntime().addShutdownHook(this);,注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:

  • 程序正常退出
  • 使用System.exit()
  • 终端使用Ctrl+C触发的中断
  • 系统关闭
  • 使用Kill pid命令干掉进程(不要带-9)

优雅启动

在服务启动之后,要对系统进行预热,可以执行一些测试数据,在整个系统启动并且预热之后,然后在进行服务注册。

熔断限流

可以使用Guava RateLimiter、Lua+Redis、Sentinel进行限流,hystrix、resilience4j进行熔断。

异步RPC

使用CompletableFuture、Future、ThreadPoolExecutor来进行请求。

参考文献

Spring Boot 2.0 之优雅停机
Spring boot 2.0 之优雅停机

你可能感兴趣的:(RPC实战与核心原理)