文章目录
17 | 异步RPC:压榨单机吞吐量
阻塞-非阻塞-异步-同步-的理解
一次 RPC 调用的本质就是调用端向服务端发送一条请求消息,服务端收到消息后进行处理,处理之后响应给调用端一条响应消息,调用端收到响应消息之后再进行处理,最后将最终的返回值返回给动态代理。
这里我们可以看到,对于调用端来说,向服务端发送请求消息与接收服务端发送过来的响应消息,这两个处理过程是两个完全独立的过程,这两个过程甚至在大多数情况下都不在一个线程中进行。那么是不是说 RPC 框架的调用端,对于 RPC 调用的处理逻辑,内部实现就是异步的呢?
不错,对于 RPC 框架,无论是同步调用还是异步调用,调用端的内部实现都是异步的。
调用端发送的每条消息都一个唯一的消息标识
- 实际上调用端向服务端发送请求消息之前会先创建一个 Future,
- 并会存储这个消息标识与这个 Future 的映射
- 动态代理所获得的返回值最终就是从这个 Future 中获取的
- 当收到服务端响应的消息时,调用端会根据响应消息的唯一标识,通过之前存储的映射找到对应的 Future,
- 将结果注入给那个 Future
- 再进行一系列的处理逻辑,
- 最后动态代理从 Future 中获得到正确的返回值。
所谓的同步调用,不过是 RPC 框架在调用端的处理逻辑中主动执行了这个 Future 的 get 方法,让动态代理等待返回值;
而异步调用则是 RPC 框架没有主动执行这个 Future 的 get 方法,用户可以从请求上下文中得到这个 Future,自己决定什么时候执行这个 Future 的 get 方法。
这是客户端的异步,那么服务端怎么做异步呐?能做吗?
能做,其实就是服务端自身业务逻辑处理的太慢了,需要优化。
RPC 框架提供一种回调方式,让业务逻辑可以异步处理,处理完之后调用RPC 框架的回调接口,将最终的结果通过回调的方式响应给调用端。
可以让 RPC 框架支持 CompletableFuture,实现 RPC 调用在调用端与服务端之间完全异步。
18 | 安全体系:如何建立可靠的安全体系?(暂略)
19 | 分布式环境下如何快速定位问题?
- 为了在分布式环境下能够快速地定位问题,RPC 框架应该对框架自身的异常进行详细地封装,每类异常都要有明确的异常标识码,并将其整理成一份简明的文档,异常信息中要尽量包含服务接口名、服务分组、调用端与服务端的 IP,以及产生异常的原因等信息,这样对于使用方来说就非常便捷了。
- 分布式链路跟踪:见:天机阁——全链路跟踪系统设计与实现
20 | 详解时钟轮在RPC中的应用
无论是同步调用还是异步调用,调用端内部实行的都是异步,而调用端在向服务端发送消息之前会创建一个 Future,并存储这个消息标识与这个 Future 的映射,当服务端收到消息并且处理完毕后向调用端发送响应消息,调用端在接收到消息后会根据消息的唯一标识找到这个 Future,并将结果注入给这个 Future。
那在这个过程中,如果服务端没有及时响应消息给调用端呢?调用端该如何处理超时的请求?
没错,就是可以利用定时任务。每次创建一个 Future,我们都记录这个 Future 的创建时间与这个 Future 的超时时间,并且有一个定时任务进行检测,当这个 Future 到达超时时间并且没有被处理时,我们就对这个 Future 执行超时逻辑。
那么由谁来进行超时检测?(等价于 Redis 中的过期)一种方式就是每创建一个 Future 我们都启动一个线程,之后 sleep,到达超时时间就触发请求超时的处理逻辑。另一种就是:用一个线程来处理所有的定时任务,还以刚才那个 Future 超时处理的例子为例。假设我们要启动一个线程,这个线程每隔 100 毫秒会扫描一遍所有的处理 Future 超时的任务,当发现一个 Future 超时了,我们就执行这个任务,对这个 Future 执行超时逻辑。
但是:同样是高并发的请求,那么扫描任务的线程每隔 100 毫秒要扫描多少个定时任务呢?如果调用端刚好在 1 秒内发送了 1 万次请求,这 1 万次请求要在 5 秒后才会超时,那么那个扫描的线程在这个 5 秒内就会不停地对这 1 万个任务进行扫描遍历,要额外扫描 40 多次(每 100 毫秒扫描一次,5 秒内要扫描近 50 次),很浪费 CPU。
什么是时钟轮?
网上查阅资料即可~
时钟轮在 RPC 中的应用
- 调用端请求超时处理,这里我们就可以应用到时钟轮,我们每发一次请求,都创建一个处理请求超时的定时任务放到时钟轮里,在高并发、高访问量的情况下,时钟轮每次只轮询一个时间槽位中的任务,这样会节省大量的 CPU。
- 调用端与服务端启动超时也可以应用到时钟轮,以调用端为例,假设我们想要让应用可以快速地部署,例如 1 分钟内启动,如果超过 1 分钟则启动失败。我们可以在调用端启动时创建一个处理启动超时的定时任务,放到时钟轮里。
- 心跳检测 是要定时重复执行的,在定时任务的执行逻辑的最后,我们可以重设这个任务的执行时间,把它重新丢回到时钟轮里即可。
注意点
- 时间槽位的单位时间越短,时间轮触发任务的时间就越精确。例如时间槽位的单位时间是 10 毫秒,那么执行定时任务的时间误差就在 10 毫秒内,如果是 100 毫秒,那么误差就在 100 毫秒内。
21 | 流量回放:保障业务技术升级的神器
RPC 怎么支持流量回放?
- 所有的请求都会经过 RPC,那么我们在 RPC 里面是不是就可以很方便地拿到每次请求的出入参数?拿到这些出入参数后,我们只要把这些出入参数旁录下来,并把这些旁录结果用异步的方式发送到一个固定的地方保存起来,这样就完成了流量回放里面的录制功能。
- 有了真实的请求入参之后,剩下的就是怎么把这些请求参数转发到我们要回归测试的应用里面。在 RPC 中,我们把能够接收请求的应用叫做服务提供方,那就是说我们只需要模拟一个应用调用方,把刚才收到的请求参数重新发送一遍到要回归测试的应用里面,然后比对录制拿到的请求结果和新请求的结果,就可以完成请求回放的效果。整个过程如下图所示:
22 | 动态分组:超高效实现秒级扩缩容
23 | 如何在没有接口的情况下进行RPC调用?(泛化调用)
- 场景一:
- 场景二:
如何解决?
我们可以定义一个统一的接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,而 GenericService 接口的 $invoke 方法的入参就是方法名以及参数信息。
这样我们传递给服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等都可以通过调用 GenericService 代理的 $invoke 方法来传递。具体的接口定义如下:
class GenericService {
Object $invoke(String methodName,
String[] paramTypes, Object[] params);
}
这个通过统一的 GenericService 接口类生成的动态代理,来实现在没有接口的情况下进行 RPC 调用的功能,我们就称之为泛化调用。