《深入理解 RPC 框架原理与实现》读书笔记

概念

RPC (Remote Procedure Call) 叫作远程过程调用,它是利用网络从远程计算机上请求服务:可以理解为把程序的一部分放到其他远程计算机上执行。通过网络通信将调用请求发送至远程计算机后,利用远程计算机的系统资源执行这部分程序,最终返回远程计算机上的执行结果。

将“远程过程调用”概念分解为“远程过程”和“过程调用”来理解更加直观:

远程过程:远程过程是相对于本地过程而言的,本地过程也可以认为是本地函数调用,发起调用的方法和被调用的方法都在同一个地址空间或者内存空间内。而远程过程是指把进程内的部分程序逻辑放到其他机器上,也就是现在常说的业务拆解,让每个服务仅对单个业务负责,让每个服务具备独立的可扩展性、可升级性,易维护。在每个机器上提供的服务被称为远程过程,这个概念使正确地构建分布式计算更加容易,为后续的服务化架构风格奠定了基础

过程调用:这个概念非常通俗易懂,它包含我们平时见到的方法调用、函数调用,并且用于程序的控制和数据的传输。而当“过程调用”遇到 “远程过程”时,意味着过程调用可以跨越机器、网络进行程序的控制和数据的传输

选型

RPC 选型的衡量角度

使用 RPC 框架无非三个选择

  1. 自研RPC框架,可以从投开始设计一款符合业务特征和场景的 RPC 框架,但是自研框架需要有足够的资金和人力支持
  2. 基于开源的 RPC框架进行改造,让改造后的 RPC 框架更加适合业务场景。这种做法相较于第一种做法,人力成本没有那么高。但是这种做法需要经常与开源社区保持同步更新,一旦不再和社区版本同步,也许到某一个版本后,公司内部改造的 RPC 框架再也不能合并社区最新版本的特性,这种现象最终会导致慢慢向第一种选择选择靠近
  3. 完全使用开源的 RPC框架,并且定期与社区版本进行同步。这种选择的好处在于要投人的人力威本最低,一些问题可以借助社区的力量进行解决。但是由于业务场景的不同直接将开源的 RPC 框架拿过来用,这种选择往往存在很多局限性。框架各部分的设计都是为了更加优雅地解决业务场景的问题,而不是反过来让业务场景去适应 RPC 框架。而且 RPC 框架有自己的定位及未来的规划,所以很多规模不是太小的公司都选择在 RPC 框架上做些许改造来适应自己的业务场景

Java 对 I/O 模型的封装

NIO

Java NIO 中最核心的就是 selector, 每当连接事件、接收连接事件、读事件和写事件中的一种事件就绪时,相关的事件处理器就会执行对应的逻辑,这种基于事件驱动的模式叫作 Reactor 模式。Reactor模式的核心思想就是减少线程的等待。当遇到需要等待的 IO 操作时,先释放资源,而在 IO 操作完成时,再通过事件驱动的方式,继续接下来的处理,这样从整体上减少了资源的消耗。

以下是 Reactor模式的五种重要角色:

  • Handle(在 Linux 下称为描述符):它是资源在操作系统层面上的一种抽象,表示一种由操作系统提供的资源,比如前面提到的网络编程中的 socket 描达符或者文件描达符。该资源与事件绑定在一起,也可用于表示一个个事件,比如前面提到的客户端的连接事件、服务端的接收连接事件、写数据事件等
  • Synchronous Event Demultiplexer(同步事件分离器):Handle 代表的事件会被班注册到同步事件分离器上,当事件就绪时,同步事件分离器会分发和处理这些事件。它的本质是一个系统调用,用于等待事件的发生。调用方在调用它的时候会被阻塞,一直到同步事件分离器上有时间就绪为止。在 Linux 中,同步事件分离器就是常用的 I/O 多路复用,比如 select, poll, epoll 等系统调用,用来等待一个或多个事件发生。在 Java NIO 领域中,同步事件分离器对应的组件就是 Selector,对应的阻塞方法就是 select 方法
  • Event Handler(事件处理器):它由多个回调方法构成,这些回调方法就是对某个事件的逻辑反馈,事件处理器一般都是抽象接口。比如当 Channel 被注册到 Selector 时的回调方法、连接事件发生时的回调方法、写事件发生时的回调方法等都是事件处理器,我们可以实现这些回调方法来达到对某一个事件进行特定反馈的目的。在 Java NIO 中,并没有提供事件处理器的抽象供我们使用
  • Concrete Event Handler(具体的事件处理器):它是事件处理器的实现。它本身实现了事件处理器所提供的各种回调方法,从而实现了特定的业务逻辑。比如针对连接事件需要打印一条日志,就可以在连接事件的回调方法里实现打印日志的逻辑
  • Initiation Dispatcher(初始分发器):可以把它看作 Reactor, 它规定了事件的调度策略,并且用于管理事件处理器,提供了事件处理器的注册、删除等方法,事件处理器需要注册到 Initiation Dispatcher 上才能生效。它是整个事件处理器的核心,Initiation Dispatcher 会通过 Synchronous Event Demultiplexer来等待事件的发生。一旦事件发生,Initiation Dispatcher 首先会分离出每一个事件,然后找到相应的事件处理器,最后调用相关的回调方法处理这些事件

Reactor 的三种模型

单 Reactor 单线程模型

单 Reactor 单线程模型就是在设计中只会有一个 Reactor,并且无论与 IO 相关的读/写,还 是与 IO 无关的编/解码或计算,都在一个 Handler 线程上完成。

单 Reactor 多线程模型

单 Reactor 多线程模型中的多线程指的是处理除了 IO 操作以外的逻辑通过多线程来执行,而IO 的读/写和 Reactor 的处理还是由一个线程执行。

相对于第一种模型来说,将业务逻辑交给线程池来处理,可以充分利用多核 CPU 的处理能 力,但是 Reactor 用一个线程处理了所有事件的监听和响应,在高并发应用场景下,容易出现性 能瓶颈,所以出现了主从 Rcactor 多线程模型。

主从 Reactor 多线程模型

当客户端连接数很多,IO 操作频繁时,单 Reactor 就会暴露问题,因为单 Reactor 只能同 步处理 IO 操作,连接事件往往没有读/写事件频繁,当 Reactor 角色在处理读/写事件时,新客 户端的连接事件就会被阻塞,所以它会影响新客户端建立连接的请求,导致连接超时等情况。

主从 Reactor 多线程模型中专门处理了这个问题。处理连接事件的线程与处理读/写事件的线程隔离,避免了在读/写事件发生较为频繁的情况下影响新客户端连接的问题。在主从 Reactor 多线程模型中存在多个 Reactor, Main Reactor 一般只有一个,它负责监听和处理连接请求,而 Sub Reactor 可以有多个, 用线程池进行管理,Sub Reactor 主要负责监听和处理读/写事件等。当然也可以将 Main Reactor 改为多个,通过线程池管理,但并不是Main Reactor 越多越好,Main Reactor 的数量主要取决 于客户端连接是否频繁。

AIO

AIO 是 Asynchronous I/O 的简称,是 JDK 1.7 之后引入的 Java I/O 新类库,它是 NIO 的升 级版本,提供了异步非阻塞的 IO 操作方式。异步 IO 是基于事件和回调机制实现的,也就是 应用程序发起请求之后会直接返回,不会阻塞,当后台处理完成时,操作系统会通知相应的线 程执行后续的操作。AIO 有两种使用方式:一种是简单的将来式,另一种是回调式。

  • 将来式:Java 用 Future 类实现将来式,将执行任务交给线程池执行后,执行任务的线程并不会阻塞,它会返回一个 Future 对象,在执行结束后,Future 对象的状态会变成完成。在 Future 对象中可以获得对应的返回值,但是需要调用 get 方法来获取结果。如果结果还没返回,则调用 get 方法的线程就会被阻塞,这无疑跟同步调用相差无几。如果需要及时得到结果,那么这种方式甚至可能比同步调用的效率更低
  • 回调式:Java 提供了 CompletionHandler 作为回调接口,在调用 read, write 等方法时,可以传入 CompletionHandler 的实现作为事件完成的回调接口,这种方式需要用户自行编写回调后的业务逻辑。

因为 Future 的 get 方法会阻塞线程,并且 Future 接口无法实现自动回调,所以在 Java 8 中提供了 CompletableFuture,它既支持原来的 Future 的功能,也支持回调式。除此之外,CompletableFuture 还支持不同 CompletableFuture 间的相互协调或组合,方便了异步 I/O 的开发。

协议

自定义协议

优势

在设计一个系统时,协议的选型或者设计是至关重要的,相对于 HTTP 这样应用广泛的标淮协议,自定义协议的优势主要在于:

  1. 第一个优势是自定义协议有良好的可扩展性。系统是需要演进和迭代的,一旦后续的演进计划涉及协议层面的变动,扩展性差的协议就会阻塞系统的演进。而自定义协议可以做一些扩展性设计,这样就可以满足根据业务需求和发展进行扩展的需求。像 HTTP 这样的标准协议并不容易扩展,因为它设计的初衷是为了让该协议更加通 用,能够适应各类应用场景
  2. 第二个优势是自自定义协议的安全性更高。因为整个传输数据格式都是自定义的,而标准协议的数据格式都是透明且公开的,所以自定义协议可以增强通信的安全性,甚至可以对数据做一些加密处理来保证传输数据的安全性
  3. 第三个优势是自定义协议的传输效率更高。协议本身就是数据格式的规则,它会影响一次请求所携带的数据包大小及传输的速度,自定义协议可以根据需要制定高效的且最适合系统本身的协议。当然有的时候可扩展性和高效性会有冲突,这时就需要根据实际场景来做权衡。总体来说自定义协议更具灵活性,但是一个协议的设计和实现的难度是比较大的,如果系统场景比较简单,那么自定义协议反而会增加系统设计的复杂度,延长系统设计和开发的周期,协议自定义的收益可能并不大

步骤

  1. 第一步需要明确在设计通信协议时,有两方面需要设计,分别是协议头和协议体,协议头可以认为是携带协议特殊字段的部分,协议体则是我们需要传输的数据实体部分
  2. 第二步是设计协议头的必要字段,在设计协议头的必要字段时,需要考虑以下几个必要的字段(命名不是重点,可以随意更换):
    • version:这里的版本号是指协议版本号,传输携带协议版本号是为了加强协议的兼容性,如果没有该字段,当数据包被解析后,服务端就无法知道客户端用的是哪个版本的协议也就无法实行一些兼容措施
    • upgrade:该字段是参考 HTTP 中的协议协商机制,该机制是在 HTTP 1.1 中被引入的, 这种机制的灵活性非常大,完美地解决了多版本的协议之间的前后兼容问题。举个例子,HTTP 除了标准的协议,还行生出了其他基于 HTTP 改造的协议,比如 WebSocket 协议等
  3. 第三步是确定协议头的编码方式,一种是二进制编码方式,一种是文本编码方式
  4. 第四步是确定各个数据位以及每个字段的排列顺序