首页 > 其他分享 >MyPRC 框架设计与实现

MyPRC 框架设计与实现

时间:2024-06-01 14:32:01浏览次数:14  
标签:服务 MyPRC 协程 请求 框架 事件 设计 连接 客户端

MyPRC 框架设计与实现

框架概述

RPC(Remote Produce Call,远程过程调用)被封装成和本地调用一样的方式,但实际上,RPC需要通过网络通信来调用远端的服务接口。RPC框架在实现分布式微服务的过程中是必备的。下面介绍我所实现的RPC框架的特性:

  • I/O模型与并发:
    使用epoll网络模型,那么所有的网络I/O操作都是非阻塞的。为了得到高并发的服务处理能力,采用了进程池、Reactor-MS以及协程池。
  • 协程本地变量
  • 多协议支持
  • 超时管理
  • 服务路由管理
  • 连接池管理
  • 高效的客户端
  • 分布式调用追踪
  • 业务层开发

框架具体实现

MyRPC框架核心类关系简图

  • MyRPCService 类是RPC服务启动的入口。
  • EventDispatch 类通过 epoll 实现了事件的分发。
  • MyHandler 类实现了事务处理的核心逻辑。
  • Reactor 类通过 EventDispatch 成员变量实现了 Reactor-MS。
  • Timer 类实现了定时器功能。
  • CoroutineLocal 模板类封装了协程本地变量。
  • EpollCtl 类实现了事件的管理操作。
  • TimeStat 类实现了时间的统计。
  • DistributedTrace 类实现了分布式调用信息的管理。
    框架设计图

服务启动流程

服务启动流程在 MyRPCService 类中完成。

  • 首先,使用命令行解析代码完成命令行参数的解析,
  • 判断是否在后台运行。然后注册信号处理函数并开启 core dump。
  • 接下来,读取服务指定的配置文件并注册业务处理的 handler。
  • 如果服务按DeBug模式启动,则直接陷入Reactor事件监听循环;否则创建工作子进程,让工作子进程陷入Reactor事件监听循环。
  • 最后,主进程通过给工作子进程发送0号信号来监控工作子进程的状态。当监控到工作子进程异常时,主进程会创建新的工作子进程。当接收到退出相关信号时,无论服务是否按debug模式启动,都会执行对应的退出逻辑。
    请添加图片描述

Service.h

Service.cpp

信号处理相关代码:signalhandler.hpp

事件分发流程

事件的分发由Reactor-MS并发模型实现。每个工作子进程启动两个线程。

MainReactor

为了应对恶意连接(建立连接之后,不发送任何数据),在MainReactor线程的事件分发流程中,并不是一监听到新的客户端连接,就将客户端事件的监听迁移到SubReactor线程中,而是:

  • 先创建一个超时的定时器事件,如果在指定的时间内客户端连接上没有触发可读事件(认为这是恶意连接),就关闭这个客户端连接。
    请添加图片描述

SubReactor

  • 在SubReactor线程的事件分发流程中,先初始化协程池,在陷入监听客户端连接上读写事件的循环中。
  • 每次先处理I/O事件,当事件有关联的协程时,就恢复之前协程的执行,否则创建新的协程并执行。
  • 当执行权返回到主协程之后,接着处理协程池的batch任务,恢复batch任务相关协程的执行。
  • 最后,处理到期的定时事件,执行设置的超时回调函数。
    请添加图片描述

reactor.cpp

eventdispatch.hpp

coroutinelocal.hpp

timer.hpp

服务器端请求处理流程

事件的处理由 MyHandler 类的 HandlerEntry 函数完成。

  • HandlerEntry 首先从字节流中解析出请求的内存对象;
  • 然后对请求进行处理,获取应答的内存对象;
  • 接下来,对应答的内存对象进行序列化
  • 最后把应答数据返回给客户端;
  • 除了主分支,还有Fast-Resp 模式和 oneway 模式。Fast-Resp 模式在执行请求的业务处理逻辑之前,就把默认的应答数据返回给客户端。而oneway模式在处理完请求的业务逻辑之后,不需要写应答数据给客户端。
    请添加图片描述

在MyHandler类中,为了同时支持HTTP和MySvr,使用了MixedCodec对象来完成数据的解析。当请求通过HTTP发送时,先把请求数据转换成MySvr的请求对象,再使用MySvr得业务逻辑处理函数获取应答对象。接下来,把MySvr得应答对象转换成HTTP的应答对象。当请求通过MySvr发送时,和HTTP处理流程类似,只是少了协议对象转换的处理。在执行业务处理逻辑之前,必须对请求对象做参数校验(为了简化处理):

  1. 必须是服务支持的RPC接口
  2. HTTP请求格式是受限的,必须时POST请求
  3. URL只支持/index
  4. body必须是JSON格式

事件监听状态的迁移

HandlerEntry 函数根据不同的请求模式,在请求的不同阶段做事件监听状态的调整。事件监听的管理封装在 epoolctl.hpp。EpollCtl 类封装了5个函数,用于管理epoll实例上的事件监控,同时也提供了EventReadable 函数,用于完成事件标志易读化的转换。EventData 结构体是用于事件触发的回调数据,EventType 则是事件的逻辑分类。在不同的请求模式下,客户端连接上事件监听状态的迁移流程如下:
请添加图片描述

协程版读写函数的封装

在 HandlerEntry 函数中,并没有显式的协程切换调用。协程的切换被封装在CoRead函数、CoWrite函数和CoConnect函数中。当非阻塞的I/O暂不可用时,就主动让出执行权,切换到主协程中执行。考虑到可测试性,使用System类对read、write、connect函数进行了封装,并支持通过定义SocketIoMock的子类来实现对函数的装饰(mock),以方便进行单元测试。

客户端请求调用流程

服务发现

在客户端发起请求之前,需要先查询被调服务部署的实例信息(IP + Port),然后根据负载均衡策略选择调用的具体服务实例,最后将请求发送给对应的服务实例。通常,服务发现和服务注册一起使用。在这个实现中,选择简化服务发现,并且没有使用服务注册(后续考虑使用zk封装一个使用)。我们直接从配置文件中获取服务部署的实例信息。

连接管理

通过服务发现获取路由信息之后,就可以开始建立连接了。此时,我们面临长短连接的选择。

  • 短连接实现简单,每次请求前建立连接,请求结束后结束连接。虽然简单,但是成本高,每次请求都要进行三次握手和四次挥手!
  • 长连接在请求结束后保持连接,在下次请求时复用连接即可。但是长连接需要管理连接池(如何控制连接池的大小?如何维持连接的可用性?)。

客户端基类

MySvr客户端

Resp 客户端

分布式调用栈追踪

超时管理

本地协程变量管理

业务层的开发

标签:服务,MyPRC,协程,请求,框架,事件,设计,连接,客户端
From: https://blog.csdn.net/weixin_51332735/article/details/139334382

相关文章